首页 / 资讯中心 / 文章详情

Android API 21下载文件:DownloadManager实现与避坑指南

Android API 21下载文件:DownloadManager实现与避坑指南 ★ FEATURED ARTICLE
简介OpenJDK 21 完整压缩包zip 格式面向 Java 开发者、运维人员及学习者提供开箱即用的标准版 JDK 环境。包内共 484 个文件核心组件包括 87 个 DLL 动态链接库、36 个 EXE 可执行程序如 java、javac、70 个 jmod 模块文件、jar 与 classlist 等运行支撑文件另有 license、md、properties 等文档说明整体 188.28MB适合离线安装与多平台部署。压缩包内含 JRE、JVM 及标准类库集成新版语言特性、性能改进与安全更新包含完整的编译、运行与调试工具链关注新特性的开发者可据此快速搭建基于 OpenJDK 21 的开发调试环境。文件内含多份许可与附加许可信息便于合规使用目录结构清晰涵盖 bin、conf、lib 等核心目录。已有 206 人学习下载适合需要完整、开源版 JDK 21 工具链的实践者。1. open sdk 21 下载文件这个需求到底在问什么「open sdk 21 下载文件」是我排查应用内升级问题时被问得最多的一句话。它有两种读法一是想给本地 SDK 装上 API 21对应 Android 5.0的开发平台包这类问题一条命令就能收尾二是真正让人头大的那种——你的 App 还活在 Android 5.0 的设备上代码里要把 APK、离线包、加密文档从服务器拉下来。大部分搜索者是后者这篇按后者写透。为什么单独把 API 21 拎出来讲因为它是两个分水岭的交汇点它前面的版本不需要考虑运行时权限它后面的版本开始强制动态申请存储权限它的路径 API 写法沿用到了 28但从 24 开始暴露 file:// 又会被系统直接掀桌。网上随便抄的下载代码在 5.0 真机上要么没有断点续传要么下载完打不开要么在高版本手机上闪退。这篇文章就是把这些边界、参数和坑一次讲清楚适合给门店终端、车载屏、老旧平板写下载功能的人也适合做应用内自动升级的维护型项目。2. API 21 下载选型为什么默认交给 DownloadManager而不是自己写网络代码2.1 三条下载路线系统下载管理器、HttpURLConnection、第三方库在 API 21 上做文件下载主流路线就三条先看对比再选别上来就抄。方案优点缺点什么时候选系统 DownloadManager任务跑在系统进程、自动重试、自带通知栏、网络切换自动暂停定制弱进度要查 Cursor 取回应用内升级、普通文档/APK 下载HttpURLConnection 文件流完全可控能边下边解密、边下边校验线程、通知、断点、重试全得自己写加密下载、流式处理场景第三方网络库 文件流语法现代、生态成熟依赖体积、断点逻辑仍要自己实现项目已有网络库且要自定义下载逻辑我一般默认选 DownloadManager核心理由不是「省代码」而是「进程」。自己写在 Service 里的下载线程命跟着 App 进程走Android 5.0 之后的后台回收策略对长任务很不客气锁屏半小时回来下载线程连渣都不剩。DownloadManager 的任务由系统下载进程托管App 被杀了任务照常跑重新打开后拿 downloadId 一查就能接管进度。API 21 是 DownloadManager 行为最稳定的阶段之一它从 API 9 就有了在 5.0 上属于成熟功能踩坑样本也最多遇到异常你能搜到大量真实案例。注意如果你只是想给本地 SDK 安装 API 21 的开发平台包常见做法是命令行执行sdkmanager platforms;android-21装完在项目的 compileSdkVersion 里指定 21 即可。下面默认你是要在 App 代码里实现「下载文件」这是两件事。2.2 最小可跑通的一版enqueue 一个下载任务要写哪些参数直接用系统服务入队一个下载任务代码量比想象中少但参数每个都有讲究。DownloadManager.Request request new DownloadManager.Request(Uri.parse(downloadUrl)); request.setAllowedOverMetered(false); // 移动网络下不允许下载防止流量超支 request.setAllowedOverRoaming(false); // 漫游时暂停费用太不可控 request.setVisibleInDownloadsUi(true); // 在系统下载App 里可见方便用户翻查 request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); // 写到应用专属外部目录Android/data/包名/files/Download/ request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, update.apk); DownloadManager dm (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); long downloadId dm.enqueue(request);这段代码的逻辑是先构造一个下载请求对象约束好网络条件和通知策略再指定落盘位置最后交给系统排队。enqueue 返回的downloadId是后续所有操作的主键查进度、查状态、取消任务都靠它务必存到 SharedPreferences。参数上有三个点最容易改错setAllowedOverMetered只对移动网络生效Wi-Fi 下这个开关不影响setVisibleInDownloadsUi只影响系统下载列表的可见性不影响通知setDestinationInExternalFilesDir的第一个参数必须是 Context第二个参数是目录类型枚举写错路径层级系统不会报错但文件会落在你不期望的地方。setNotificationVisibility有四个取值这里用的是「下载中可见 完成时通知」最常用。2.3 DownloadManager 的边界哪些场景必须绕开它DownloadManager 不是万能药它把很多细节封装进了黑匣子需要自定义的地方反而会别扭。以下场景我建议绕开它自己写网络层。第一下载内容是加密流必须边下边解密才能落盘。DownloadManager 只会把字节原样写文件没有解密钩子你拿到的是密文文件还得二次处理。第二服务器要求动态签名 Header且每次请求都要重新计算。Request 在入队时就固定了 Header下载过程中换不了遇到时效性签名直接放弃。第三需要精确到字节的断点续传且任务调度完全由 App 控制。DownloadManager 的续传是系统自己判断的你无法控制从第几个偏移开始。第四需要边下载边播放或边预览的场景这必须走流式读取下载完成才可用文件的模型天然不合适。还有一点必须记住API 21 上主线程联网会直接抛 NetworkOnMainThreadException这是自写网络层时必踩的坑。无论用 HttpURLConnection 还是第三方库网络调用都要放到子线程。第三方库在 5.0 上的 TLS 兼容也比系统组件更挑配置这块在第 4 章的 4.4 里细说。3. 把「下载文件」做成完整功能路径、权限、状态跟踪三件套3.1 目标路径三兄弟缓存目录、私有外部目录、公共下载目录下载的落盘位置决定了你要不要申请权限、用户卸载后会不会残留、文件管理器里能不能看见。API 21 上常用的就三个目录。// 一内部缓存应用专属系统空间不足时可能被自动清理 File cacheFile new File(context.getCacheDir(), update.apk); // 二外部私有目录应用专属无需任何存储权限 File appFile new File(context.getExternalFilesDir( Environment.DIRECTORY_DOWNLOADS), update.apk); // 三外部公共目录所有应用可见需要 WRITE_EXTERNAL_STORAGE File publicFile new File( Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS), update.apk);三个目录的选择逻辑要拎清楚。getCacheDir适合临时文件比如一次性的升级包下次启动还要重新校验的短命数据getExternalFilesDir是应用专属目录卸载 App 时系统连目录一起删不会给用户留垃圾而且从 API 19 开始写它就不需要存储权限这一条直接帮你省掉权限申请的麻烦getExternalStoragePublicDirectory是用户能在文件管理器里直接看到的公共目录适合下载完要长期保留、用户可能手动拷贝走的文件但它在 API 21 上需要存储权限而且卸载后文件会残留。注意一个容易忽略的点getExternalStoragePublicDirectory在 API 29 被标记废弃但在 API 21 上随便用不会报错也不会警告。不过为了不给自己埋坑我一般只在 minSdk 21 到 28 的项目里用这个 API新项目一律走getExternalFilesDir或 MediaStore。还要检查外部存储状态SD 卡被拔出或设备处于 USB 存储模式时外部目录写入会失败调用前用Environment.getExternalStorageState()确认等于MEDIA_MOUNTED。3.2 用 Query Cursor 查状态8 个状态值里只有两个需要你关心DownloadManager 把进度和结果写在一张系统表里你的 App 通过 Query 加 Cursor 去读。查状态的代码很固定。DownloadManager.Query query new DownloadManager.Query(); query.setFilterById(downloadId); Cursor c dm.query(query); if (c ! null c.moveToFirst()) { int status c.getInt(c.getColumnIndexOrThrow( DownloadManager.COLUMN_STATUS)); long done c.getLong(c.getColumnIndexOrThrow( DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); long total c.getLong(c.getColumnIndexOrThrow( DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); String localUri c.getString(c.getColumnIndexOrThrow( DownloadManager.COLUMN_LOCAL_URI)); c.close(); }这段代码的逻辑是按 downloadId 过滤查询条件取结果集第一行把状态、已下载字节、总字节、本地 URI 一次读出来。Cursor 用完必须 close这是老开发者看见就皱眉的经典泄漏点。状态值的含义一定要背熟查错状态会导致你拿临时文件路径去打开文件。常量值含义STATUS_PENDING1排队中还没开始STATUS_RUNNING2下载中STATUS_PAUSED4暂停常见原因是网络不可用或流量限制STATUS_SUCCESSFUL8下载完成可以取文件了STATUS_FAILED16下载失败要继续查 COLUMN_REASONSTATUS_PAUSED 时别急着报错它可能是用户切了网络或系统资源紧张导致的临时暂停后续会自动恢复。STATUS_FAILED 时一定再读一列COLUMN_REASON拿错误码区分具体原因。我踩过最典型的坑是 ERROR_FILE_ALREADY_EXISTS原因是我没清理同名旧文件就重新入队系统认为目标文件冲突直接判失败解决方法是入队前先删掉目标路径的旧文件。3.3 用 ContentObserver 监听进度别开定时器空转轮询 Cursor 是基础能力但如果下载任务两三分钟每 500 毫秒轮询一次就是在浪费电量。更好的做法是监听系统下载表表有变动时系统回调你主动去查一次即可。contentResolver.registerContentObserver( DownloadManager.CONTENT_URI, true, new DownloadObserver(handler)); class DownloadObserver extends ContentObserver { private final long downloadId; DownloadObserver(Handler handler, long downloadId) { super(handler); this.downloadId downloadId; } Override public void onChange(boolean selfChange, Uri uri) { // 表里有任何下载相关的变化都会走到这里 queryAndUpdate(downloadId); } }逻辑上你在界面注册一个 ContentObserver系统下载表每次有行变化进度更新、状态切换都会触发 onChange你在回调里重新查询 downloadId 对应的行把进度推到 UI 上。比起固定频率轮询这个方案的请求次数大幅下降而且状态切换比如从 RUNNING 变 SUCCESSFUL一定不会漏掉。有两个细节必须处理onChange 回调跑在 Binder 线程不能直接更新 UI要么用 Handler 切主线程要么用 runOnUiThread注册的 ContentObserver 要在页面销毁或下载结束时反注册否则会泄漏。API 21 上没有 WorkManager 这类现成调度器用 ContentObserver 是成本最低的实时进度方案。3.4 权限兼容minSdk 21 和 targetSdk 23 的配置怎么共存minSdk 21 不代表你只处理 21 的行为。现在应用商店对 targetSdk 有硬性要求你的项目很可能最终 targetSdk 是 30 甚至更高这时候同一条下载代码要同时伺候 API 21 和 API 23 两套权限模型。uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE /清单里这两个权限都要声明。API 21 的安装流程会把 WRITE_EXTERNAL_STORAGE 在安装时直接授予不用你管API 23 的设备上这个权限必须在运行时向用户动态申请。if (Build.VERSION.SDK_INT Build.VERSION_CODES.M checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQ_WRITE_STORAGE); }这段代码的逻辑是只有系统版本大于等于 23 时才检查权限21 到 22 的设备直接跳过运行时申请以清单声明为准。如果你选择getExternalFilesDir作为下载目录那么清单里可以完全不加 WRITE_EXTERNAL_STORAGE这是很多人不知道的简化路径——私有外部目录从头到尾都不需要存储权限。还有一个隐蔽问题targetSdk 23 时即使你的 App 只在 21 上运行编译出来的行为也是按高版本逻辑走的。所以权限分支不能省少写一个分支23 真机上写公共目录就是 SecurityException一个没有任何预备知识的闪退。4. 避坑API 21 下载文件最容易翻车的 5 个现象与解决4.1 下载完成通知点了没反应现象通知栏显示「下载完成」点击后没有拉起任何界面进系统下载列表点那个文件也没反应。原因DownloadManager 的通知点击后系统按照文件 MIME 类型去找能处理它的应用。入队时你没调用setMimeType系统拿到的常常是 null 或 application/octet-stream没有应用声明能处理这个类型点击自然石沉大海。解决入队前根据文件类型明确设置 MIMEAPK 用application/vnd.android.package-archivePDF 用application/pdf。另外不要只依赖系统通知下载完成回调里自己拿 LOCAL_URI 主动发起打开或扫描比等系统动作可靠。4.2 文件下好了文件管理器里却找不到现象代码里能读出 STATUS_SUCCESSFUL文件路径存在用 adb 或文件管理工具能看到但用户打开系统的「文件管理」App 找不到这个文件。原因MediaStore 的媒体扫描是延迟的系统不会为每个新文件立刻建立索引。Android 5.0 的部分 ROM 对 DownloadManager 写出到公共目录的新文件扫描触发尤其不及时。解决下载完成后主动调用 MediaScannerConnection.scanFile 扫描一次让文件立刻进入媒体库。这段代码在第 5 章 5.1 里给出是下载完成后的标准动作之一。4.3 下载到一半 App 被杀回来后进度清零现象用 HttpURLConnection 自写的下载逻辑App 切后台、锁屏或被一键清理后再打开进度条归零甚至要从头重新下。原因下载线程跟着 App 进程走进程没了下载就断了。Android 5.0 之后后台进程回收非常积极长时间的后台网络任务很容易被系统回收这不是你代码写得不好是架构选型问题。解决换用 DownloadManager 让任务跑在系统进程App 被杀不影响下载。把downloadId持久化到 SharedPreferences应用重启后按 ID 重新 Query就能继续拿到正在跑的进度。SharedPreferences sp context.getSharedPreferences(download, MODE_PRIVATE); sp.edit().putLong(download_id, downloadId).apply(); // 应用重启后 long savedId sp.getLong(download_id, -1L); if (savedId ! -1L) { queryAndUpdate(savedId); }这里的逻辑是入队时记录任务 ID重启时先查这个 ID 是否还活着活着就接管死了再重新入队。注意一种情况如果下载文件被系统清理掉了状态会变成 FAILED原因列通常指向文件不存在。此时必须先从 DownloadManager 里 remove 掉这条记录再重新入队否则同一个 ID 反复查询失败会造成「自动更新无限重试下载」的死循环。4.4 HTTPS 接口在 5.0 真机上握手失败现象同一个下载地址Android 12 设备上下载正常Android 5.0 真机上报 SSLHandshakeException 或 Connection resetWi-Fi 和移动网络都试过稳定复现。原因API 21 的 TLS 配置偏旧HttpsURLConnection 默认启用的协议集合与当前服务器要求的 TLS 版本/密码套件不匹配。现在新部署的服务器普遍关闭了 TLSv1.0 和 TLSv1.15.0 的老协议栈就容易被拒。解决第一优先是让 DownloadManager 去联网系统下载进程的 TLS 兼容配置比 App 进程完整很多「自己写必挂、DownloadManager 能下」的案例就是这么解决的。必须自写网络层时在初始化阶段显式指定协议System.setProperty(https.protocols, TLSv1.2);这段配置要赶在第一次 HTTPS 连接之前执行放在 Application 的 onCreate 里最常见。注意它是全局属性会影响进程里所有 HTTPS 请求如果项目里只有下载是自写的影响面可控如果项目里已有第三方网络库优先用它的 TLS 配置类去指定 TLSv1.2而不是动全局属性。4.5 同一套代码在 21 上正常换高版本闪退现象下载代码在 API 21 的设备上稳定运行同一份代码装到 API 24 的设备上下载完成后打开文件直接闪退或者写公共目录时报错。原因这里有两个版本分水岭叠加。第一个是 file:// Uri 从 API 24 开始跨应用暴露会抛 FileUriExposedException第二个是公共目录写入从 API 23 开始需要运行时权限不申请直接写就是 SecurityException。你的代码用 21 的思维写自然会在新设备上翻车。解决打开文件这步必须做版本分支Uri fileUri; if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) { fileUri FileProvider.getUriForFile(context, authority, file); } else { fileUri Uri.fromFile(file); }逻辑是24 及以上用 FileProvider 生成带授权的 content:// Uri21 到 23 用 file:// 直接打开。这里的 authority 必须和 AndroidManifest 里provider的 authorities 完全一致不一致会在 getUriForFile 时直接抛 IllegalArgumentException。权限问题则用 3.4 节的运行时判断去处理两条分支缺一不可。5. 收尾技巧下载完成后的三步验证把文件稳定交到用户手里5.1 主动扫描让文件立刻可见同时校验文件长度下载完成的 STATUS_SUCCESSFUL 分支里别急着弹窗先做两件事主动扫描一次媒体库再校验文件长度。扫描让文件在文件管理器里立刻可见避免 4.2 的延迟问题。MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, new String[]{mimeType}, new MediaScannerConnection.OnScanCompletedListener() { Override public void onScanCompleted(String path, Uri uri) { // uri 可以用于后续分享或打开 } });这段代码逻辑很简单把文件路径和 MIME 告诉系统媒体库扫描完成回调里能拿到媒体库生成的 content:// Uri。校验长度时把 Query 读出来的 COLUMN_TOTAL_SIZE_BYTES 和file.length()比对不一致说明文件没下完整按失败处理。5.2 打开下载的文件API 21 用 file://24 必须 FileProvider打开文件用 ACTION_VIEW但 URI 来源必须按系统版本分流。24 以上的设备已经全面禁止跨应用暴露 file://不分支就是 4.5 的闪退。provider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider对应的res/xml/file_paths.xml里把你下载用的目录映射进去paths external-files-path namedownloads pathDownload/ / external-path namepublic_downloads pathDownload/ / /paths配置逻辑是FileProvider 只暴露你在 file_paths 里声明过的路径没声明的目录即使拿到 authority 也访问不了。API 21 上如果你不想引入依赖可以继续用 Uri.fromFile但建议一步到位统一用 FileProvider老分支留着只是兜底。5.3 入队前必做的三个动作是我被 5.0 设备教育后的固定习惯被 Android 5.0 设备反复教育之后我现在每次调用 enqueue 之前固定做三个动作把 downloadId 先写入 SharedPreferences、删除目标路径的同名旧文件、通过文件后缀设置 MIME 类型。下载完成后再补两个动作扫描媒体库、比对文件长度。任何一步不对就 remove 掉当前记录重新入队。这个习惯看上去多写了十几行代码但省掉的是用户端的「下载失败但又看不出哪里失败」的投诉。5.0 设备的用户反馈渠道少一次下载完成却打不开的体验足以让一个自动升级功能被直接判定为不可用。把 downloadId 存下来还有个附加价值用户第二天回来还能接上昨天没下完的任务这比任何花哨的进度条都实在。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站