简介: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_PENDING | 1 | 排队中,还没开始 |
| STATUS_RUNNING | 2 | 下载中 |
| STATUS_PAUSED | 4 | 暂停,常见原因是网络不可用或流量限制 |
| STATUS_SUCCESSFUL | 8 | 下载完成,可以取文件了 |
| STATUS_FAILED | 16 | 下载失败,要继续查 COLUMN_REASON |
STATUS_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:name="android.permission.INTERNET" /> <uses-permission android:name="android.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,没有应用声明能处理这个类型,点击自然石沉大海。
解决:入队前根据文件类型明确设置 MIME,APK 用application/vnd.android.package-archive,PDF 用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 reset,Wi-Fi 和移动网络都试过,稳定复现。
原因:API 21 的 TLS 配置偏旧,HttpsURLConnection 默认启用的协议集合与当前服务器要求的 TLS 版本/密码套件不匹配。现在新部署的服务器普遍关闭了 TLSv1.0 和 TLSv1.1,5.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:// Uri,21 到 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:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>对应的res/xml/file_paths.xml里,把你下载用的目录映射进去:
<paths> <external-files-path name="downloads" path="Download/" /> <external-path name="public_downloads" path="Download/" /> </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 存下来还有个附加价值:用户第二天回来,还能接上昨天没下完的任务,这比任何花哨的进度条都实在。希望帮到你。
本文还有配套的精品资源,点击获取