简介:Android SDK platform-25 是面向 Android 应用开发者、特别是需兼容 Android 7.1.1(API 25)老项目与教学环境的平台开发包,用于补齐目标平台、系统映像与构建依赖。压缩包共 2000 个文件,约 81.47MB,以 png、xml、ttf、html 资源为主,辅以 jar、properties、template 及少量配置/数据文件,覆盖界面资源、布局定义、字体、文档与构建相关素材。已有 729 人学习下载。解压到 platforms 目录后,可配合 SDK Manager 与 Android Studio 创建 API 25 工程,借助平台库、头文件与源码、Build Tools、系统映像完成编译调试,并实践 App Shortcuts、Night Light、Image Picker、Daydream 与多窗口等特性,便于老版本适配和系统行为研究。
1. 从一台同步失败的旧工程说起:platform-25 到底是什么
上周有人抱着一台旧本子来找我,工程打开后 Gradle 同步始终卡在Failed to find target with hash string 'android-25'。代码没动过,依赖也没改,纯粹是环境里少了 Android SDK platform-25 这一块拼图。它是 API Level 25 对应的平台包,也就是 Android 7.1 那一代,目录里装着编译用的 android.jar、可运行的 system image 和一批平台数据文件。还停在 targetSdkVersion 25 的存量工程、要给 7.1 设备做兼容性回归的团队、以及想复现某个旧版本行为差异的排查场景,都绕不开它。新立项的项目基本不会再用,但「存量维护」和「兼容性验证」这两件事把它变成了必经之路。
2. 拆开 platform-25 的安装目录:android.jar、系统镜像与 data 各管什么
装完之后很多人不知道钱花在哪了。打开$ANDROID_SDK_ROOT/platforms/android-25/,能看到的东西其实就那么几样,但每一样的职责边界非常清楚,搞混了就会写出「编译过、运行崩」的代码。
2.1 android.jar 是签名桩,不是可运行的实现
这是最容易踩的第一个认知坑。android.jar 里所有方法体都被替换成了抛异常的桩代码,反编译任意一个类就能看到:
# 找一个平台包里的类,看方法体到底长什么样 javap -c -p -classpath "$ANDROID_SDK_ROOT/platforms/android-25/android.jar" \ android.app.Activity | head -40输出里大量出现throw new RuntimeException("Stub!"),这就是它的真面目——只提供编译期签名,不提供运行期实现。真正的实现躺在设备或模拟器的系统分区里,由 ART 在运行时加载。理解了这一点,下面几个连锁结论就顺理成章了:
- 不能把 android.jar 直接丢进
libs/当普通依赖用,否则运行到第一行业务代码就炸; - 不能拿它跑 JVM 单元测试,除非配合 mock 框架或 Robolectric 这类替身方案;
- 它只保证「你调用的方法在这个 API 级别存在」,不保证「设备上真的有实现」,后者要靠
Build.VERSION.SDK_INT判断。
目录里另一个关键文件是source.properties,内容大致是Pkg.Revision=25.x.x和Pkg.Desc之类的元信息。Gradle 和 sdkmanager 就是靠它识别「这个目录到底是哪个平台、补丁版本是多少」。这个文件丢了或者被手工改坏了,同步阶段就会报 target not found——很多人从别人机器上直接拷目录,拷漏了隐藏文件,翻车就翻在这。
data/下面是平台相关的资源,比如res/里的系统 drawable、fonts/、layoutlib.jar这类供 IDE 预览渲染用的东西。它不影响 APK 打包产物,但布局编辑器的实时预览要靠它,缺了之后预览面板会退化成一片空白,别以为是 IDE 坏了。
2.2 系统镜像怎么挑:tag 与 ABI 两把尺子
platform 包只解决「编译」,要「跑起来」还得有 system image。路径在$ANDROID_SDK_ROOT/system-images/android-25/下,按<tag>/<abi>两级目录组织。挑错组合是新手最常浪费时间的环节。
| tag | 含义 | 适用场景 |
|---|---|---|
default | 纯净 AOSP 镜像,无附加服务 | 只想验证基础 API 行为、跑最快的启动 |
google_apis | 带基础服务框架,无应用商店 | 需要账号、地图、推送等能力做联调 |
google_apis_playstore | 带完整商店环境 | 验证商店分发相关逻辑,镜像通常无 root |
ABI 这边,x86和x86_64在带硬件虚拟化的机器上能跑出接近真机的流畅度,前提是宿主机的加速驱动装好了;arm和arm64-v8a走的是指令翻译,启动能磨掉你半杯咖啡的时间,但它对某些只发了 arm 原生库的旧 APK 是唯一选择。我的习惯是:日常回归固定用google_apis;x86_64,遇到 native 库加载崩溃再临时切 arm 镜像复现。
提示:判断宿主机加速是否可用,跑一次模拟器启动,看日志里有没有回落到软件渲染的警告。回落了就别抱怨模拟器慢,先去修加速环境。
2.3 用 sdkmanager 装齐一套最小可用组合
命令行安装比图形界面可靠,尤其是需要在 CI 或干净容器里复现环境的时候。先看有哪些包可选:
# --sdk_root 显式指定 SDK 根目录,避免写死到默认用户目录 export ANDROID_SDK_ROOT="$HOME/Android/Sdk" SDKMANAGER="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" # 列出所有可用包,用 grep 把 android-25 相关的捞出来 "$SDKMANAGER" --sdk_root="$ANDROID_SDK_ROOT" --list \ | grep -E "android-25|build-tools;25"确认包名之后一次性装齐,别一个个装,--licenses也要先过一遍:
# 先批量接受许可,否则后续安装会卡在交互式确认 yes | "$SDKMANAGER" --sdk_root="$ANDROID_SDK_ROOT" --licenses # 平台 + 构建工具 + 一个可运行的模拟器镜像 "$SDKMANAGER" --sdk_root="$ANDROID_SDK_ROOT" \ "platforms;android-25" \ "build-tools;25.0.3" \ "platform-tools" \ "system-images;android-25;google_apis;x86_64" \ "emulator"三条命令的分工要说清楚:platforms;android-25提供编译期的 android.jar 和 data,build-tools;25.0.3提供 aapt、dx、zipalign 这些打包工具链,system-images;...提供运行时环境,platform-tools里的 adb 是连接这一切的管道。少装任何一个,后面都会以某种莫名其妙的形式报错。
再看环境变量。ANDROID_SDK_ROOT是老名字,新版本工具链更认ANDROID_HOME,两个都设上最省事:
# 写进 shell 配置文件,长久生效 export ANDROID_HOME="$HOME/Android/Sdk" export ANDROID_SDK_ROOT="$ANDROID_HOME" export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin"有一种完全离线的场景:机器连不上外网仓库,只能拿别人给的压缩包。做法是把包解压后手工放到platforms/android-25/,然后必须确认source.properties存在且内容完整,再跑一次sdkmanager --list_installed看能不能识别出来。识别不出来就说明目录层级放错了——多套了一层android-25/是高频错误。
3. 让工程真正编译在 25 上:Gradle、AGP 与 build-tools 的版本咬合
平台装好了,同步还是会失败,因为版本号是一套互相咬合的齿轮,动一个就得动一串。这一代工具链的年代感很强,用今天的默认配置去套它,基本等于拿新钥匙开旧锁。
3.1 三个 sdkVersion 的分工与设错后果
compileSdkVersion、targetSdkVersion、minSdkVersion经常被当成同一件事,实际上各管一摊:
compileSdkVersion 25决定编译时能看到哪些 API 签名,它对应你装的 platform 包。设成 25 却只装了 23,同步直接失败。targetSdkVersion 25决定系统用哪一套行为规则对待你的应用。这是兼容性问题的总开关,很多「在低版本好好的、升上去就崩」的现象都源于它。minSdkVersion决定安装门槛,跟 platform 包无关,但会限制你能用的 API 范围。
举个具体的:targetSdkVersion 从 23 改到 25,file://URI 传递会直接抛异常,后台任务被系统大幅收紧,静态注册的网络状态广播收不到了。这些都是行为变更,不是编译错误,所以它在开发机上可能完全复现不出来——因为开发机的系统版本往往比测试机新,规则早就生效了,反而是老测试机上「看起来正常」。
3.2 一套能跑通的老组合与 build.gradle 写法
下面这套配置在大量 API 25 时代的存量工程里能直接跑起来,关键在于 AGP 和 Gradle 的大版本必须对齐:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Android Gradle Plugin | 2.3.3 | 2.x 系列最后一个稳定线,配 25 最稳 |
| Gradle | 3.3 | AGP 2.3.x 的官方配套区间 |
| build-tools | 25.0.3 | 与 platform 25 同代,避免 aapt 行为差异 |
| JDK | 8 | 高版本 JDK 编译老 AGP 会直接报错 |
对应的模块级配置:
// app/build.gradle android { compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { applicationId "com.example.legacy" minSdkVersion 16 targetSdkVersion 25 // 行为变更的总开关,别乱调 versionCode 1 versionName "1.0" // 老工程常见:为了兼容旧机型保留 multidex multiDexEnabled true } // AGP 2.x 用 buildTypes + productFlavors,不要套新版的 signingConfigs DSL 写法 buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { // 25 时代还没有 AndroidX,全靠 support 库 compile 'com.android.support:appcompat-v7:25.4.0' compile 'com.android.support:design:25.4.0' compile 'com.android.support:multidex:1.0.1' }几个参数值得单独说。buildToolsVersion显式写死比留空好,留空时 AGP 会挑它认为最新的本地版本,机器换一台就可能变成 26+,而 26 开始 aapt2 成为默认打包器,资源处理的容错度比 aapt 低,一批历史遗留的畸形资源文件会集中爆出来。multiDexEnabled在老工程里几乎是标配,因为 support 库加业务代码很容易越过 65536 方法数。还有compile这个关键字现在已经被implementation取代,但在 AGP 2.x 里写implementation是不认的,直接报找不到方法。
根工程的 Gradle 版本在gradle/wrapper/gradle-wrapper.properties里锁定:
# 改这一行就能切 Gradle 版本,比手工装快得多 distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip改完之后一定要跑./gradlew --version确认真的切过去了。缓存导致没生效的情况不少见,尤其是从高版本降下来的时候。
3.3 support 25.4.0 与 AndroidX 的取舍
25.4.0是 support 库 25 系列的收尾版本,也是最后一个支持 API 25 完整特性的版本线。存量工程里往往混着appcompat-v7、recyclerview-v7、support-v4一堆坐标,版本号还各不相同。
统一版本号有个简单办法:在根工程里加一段强制解析,把所有 support 组件的版本拉到同一个值,避免「A 库依赖 25.2.0、B 库依赖 25.4.0」导致的方法签名不一致崩溃。
// 根 build.gradle,统一 support 库版本,消除传递依赖的版本冲突 allprojects { configurations.all { resolutionStrategy { force 'com.android.support:appcompat-v7:25.4.0' force 'com.android.support:support-v4:25.4.0' force 'com.android.support:recyclerview-v7:25.4.0' } } }至于要不要升 AndroidX:如果这个工程的唯一使命是维护和打补丁,答案是不升。AndroidX 迁移会连带要求 AGP 3.2+ 和 compileSdkVersion 28+,等于把整条工具链推倒重来,而收益在这个场景下接近于零。真要迁移,也应该先在一份独立分支上把 Gradle、AGP、compileSdk 全部升到位,再执行 Jetifier 转换,别指望在老工具链上开个开关就完事。
4. API 25 的行为变更:从 file:// 被封到多窗口状态丢失
编译通过只是入场券。真正让人熬夜的是那些「代码逻辑没动、行为却变了」的地方。这一代平台的行为变更密度很高,而且大部分是单向的——一旦 targetSdkVersion 提上去,就没有回头路。
4.1 FileProvider:file:// URI 从这一代开始彻底封死
这是 API 24 引入、在 25 上完全生效的变更,也是最经典的翻车现场。以前把file:///data/data/.../cache/x.jpg塞进 Intent 就能分享图片,现在直接抛FileUriExposedException。
正确做法是走内容提供者,配置加代码一共三步。先在清单里声明:
<!-- AndroidManifest.xml,authorities 用包名 + .fileprovider,避免多应用冲突 --> <provider android:name="android.support.v4.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,只开放 cache 下的 shared 子目录 --> <?xml version="1.0" encoding="utf-8"?> <paths> <cache-path name="shared_cache" path="shared/" /> <external-files-path name="ext_files" path="." /> </paths>最后在代码里生成带临时授权的 URI:
// 把 File 转成 content:// URI,并授予对方临时读写权限 File image = new File(getCacheDir(), "shared/photo.jpg"); Uri uri = FileProvider.getUriForFile( this, getPackageName() + ".fileprovider", // 必须和清单里的 authorities 一致 image); Intent share = new Intent(Intent.ACTION_SEND); share.setType("image/*"); share.putExtra(Intent.EXTRA_STREAM, uri); // FLAG_GRANT_READ_URI_PERMISSION 少了这一句,接收方打开时会报权限拒绝 share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(share, "分享"));参数上最容易错的是authorities:代码里的字符串必须和清单里的完全一致,用${applicationId}占位符的话,多 flavor 打包时前缀会变,代码里就不能写死包名,得用getPackageName()拼。另外path属性是相对路径,写/shared/带前导斜杠会被当成绝对路径匹配失败,这个细节坑过不少人。
4.2 应用快捷方式与圆形启动图标
API 25 引入的两个「看得见」的变化。快捷方式通过静态 XML 声明,长按桌面图标就能出现:
<!-- res/xml/shortcuts.xml,最多静态声明 4 个,超出部分会被系统截断 --> <shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> <shortcut android:shortcutId="compose" android:enabled="true" android:shortcutShortLabel="@string/shortcut_compose_short" android:shortcutLongLabel="@string/shortcut_compose_long" android:icon="@drawable/ic_shortcut_compose"> <intent android:action="android.intent.action.VIEW" android:targetPackage="com.example.legacy" android:targetClass="com.example.legacy.ComposeActivity" /> </shortcut> </shortcuts>清单里给主 Activity 挂上android:shortcutId之外,还要在 launcher activity 上加meta-data指向这个文件。这里有个隐性约束:shortcutShortLabel建议控制在 10 个字符以内,shortcutLongLabel不超过 25 个字符,超了会被系统按规则截断,界面上看起来就像文案被吞了。图标必须是单色透明背景的矢量图,彩色图标放上去会被渲染成一团黑块。
圆形图标对应的是android:roundIcon属性,只在支持圆形遮罩的启动器上生效。注意它和自适应图标不是一回事:自适应图标(adaptive icon)要到 API 26 才支持,在 25 上强写mipmap-anydpi-v26目录不会报错,但完全不起作用,白忙一场。想要老版本也好看,老老实实准备一套圆形切图放进mipmap-*dpi。
4.3 多窗口与配置变更下的状态保存
多窗口在 7.0 引入,25 上继续演进。它对代码的直接影响是:应用可能在「非全屏」状态下被用户操作,onConfigurationChanged的触发频率显著上升,Activity 重建次数也跟着涨。
<!-- 声明支持分屏;不写这一行在部分设备上会被强制全屏 --> <activity android:name=".MainActivity" android:resizeableActivity="true" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />configChanges里加这几个值能减少重建,但这是把双刃剑:加了之后onConfigurationChanged会被调用而onCreate不会,如果你的布局适配逻辑写在onCreate里,界面就会停留在旧尺寸上,出现「分屏后布局错位」的诡异现象。
我的做法是:只在纯展示型页面上加configChanges,任何带输入框、滚动位置、播放进度的页面一律不加,让系统重建,靠onSaveInstanceState恢复状态。
// 保存滚动位置,重建后恢复;别存大对象,Bundle 有大小限制 @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt("scroll_y", recyclerView.computeVerticalScrollOffset()); outState.putString("draft", editText.getText().toString()); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); int y = savedInstanceState.getInt("scroll_y", 0); recyclerView.scrollBy(0, y); }这里有个血泪经验:onSaveInstanceState里存的东西会跟着系统进程一起被序列化,存 Bitmap 或者几 MB 的 JSON 会让重建过程卡到 ANR 边缘。超过 100KB 的数据就该考虑落盘或者放 ViewModel。
4.4 后台优化对定时任务的收紧
从 7.0 开始的后台优化在 25 上继续生效,最直接的表现是两件事:静态注册的CONNECTIVITY_ACTION广播收不到了,Doze 状态下网络和唤醒锁被大幅限制。
前者用registerReceiver动态注册解决,在onResume注册、onPause反注册。后者要看任务性质:精确到秒级的用AlarmManager.setExactAndAllowWhileIdle,但系统对它的调用频率有配额;可延迟的用 JobScheduler,把约束条件交给系统调度。
// JobScheduler 声明一个带网络约束的周期任务,交给系统决定何时跑 JobInfo job = new JobInfo.Builder(JOB_ID, new ComponentName(this, SyncJobService.class)) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) .setPeriodic(15 * 60 * 1000) // 最小周期 15 分钟,写更小会被系统抬到 15 分钟 .setPersisted(true) // 重启后仍然保留,需要 RECEIVE_BOOT_COMPLETED 权限 .build(); ((JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE)).schedule(job);setPeriodic的参数有个硬下限,15 分钟是系统规定的最小值,写 60000 不会报错但会被静默抬到 15 分钟——这个「不报错但行为不符预期」的设计,debug 时非常消耗耐心。setPersisted(true)需要额外权限,漏了会在schedule时抛异常,不是静默失败,还算友好。
5. 避坑记录:装完 platform-25 之后最容易翻车的五个地方
下面这五条都是实打实踩过的,按出现频率从高到低排。每条都写清「现象 → 原因 → 解决」,照着对照能省掉大半天。
5.1 现象:sdkmanager 提示包不存在或列表里根本没有 android-25
原因:三种可能。一是 sdkmanager 版本太老,仓库索引里已经不列老平台了;二是包名写错,正确的是platforms;android-25,分号不能换成冒号或斜杠;三是本地缓存索引过期,还停留在很久以前的快照上。
解决:先用sdkmanager --version看版本,低于 26 的建议换新版 cmdline-tools。然后强制刷新索引:sdkmanager --list --verbose,观察输出里有没有拉取仓库元信息的步骤。包名这块记住规则——platforms;android-25是平台,build-tools;25.0.3是工具链,system-images;android-25;google_apis;x86_64是镜像,三级结构用分号分隔,中间少一级都会报「package not found」。
如果机器在内网、只能走内部仓库,那要在~/.android/repositories.cfg或者命令行参数里指定仓库地址。这种情况下的报错通常长这样:Warning: Failed to fetch URL ...,看到了就直接查网络访问配置,别在包名上浪费时间。
5.2 现象:同步报 Failed to find target with hash string 'android-25'
原因:包确实装了,但 Gradle 找不到。要么是ANDROID_HOME/ANDROID_SDK_ROOT指向了另一个 SDK 目录(机器上装了两份 SDK 的情况非常常见),要么是platforms/android-25/里缺source.properties。
解决:先在工程根目录建一个local.properties把路径钉死,比依赖环境变量可靠:
# local.properties,路径按实际机器改;这个文件不要提交到版本库 sdk.dir=/Users/yourname/Library/Android/sdk然后在终端跑一条命令确认 SDK 目录里到底有什么:
# 列出已安装平台,输出里必须能看到 android-25 ls -1 "$ANDROID_HOME/platforms/" # 单独确认元信息文件在不在 cat "$ANDROID_HOME/platforms/android-25/source.properties"如果platforms/下确实是空的或者只有 26 以上,说明装到别的目录去了,用find全盘找一下android-25目录。如果目录在但source.properties缺失,最省事的办法是重装一遍,别手工编造内容——里面的哈希和修订号对不上,同步照样失败。
5.3 现象:aapt 执行报错,或者一跑 Gradle 就抛与 Java 版本相关的异常
原因:用 JDK 9 及以上版本去编译 AGP 2.x 时代的工程。老版本 AGP 依赖了若干在后续 JDK 中被移除或改签名的内部 API,跑起来会看到java.lang.NoClassDefFoundError或者Unsupported class file major version之类。
解决:给这个工程单独指定 JDK 8,不要动全局的JAVA_HOME。在gradle.properties里加:
# 让这个工程只用 JDK 8 编译,不影响其他项目的全局配置 org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/Home路径按实际安装位置改。配完之后跑./gradlew -version,输出里会打印实际使用的 JVM,确认是 1.8 才算生效。还有一种偶发情况:aapt报「error while loading shared libraries」,那是 32 位二进制在纯 64 位系统上跑不起来,只能换 build-tools 版本或补 32 位运行库,这个在服务器环境里遇到的概率更高。
5.4 现象:模拟器能启动但黑屏、卡死,或者慢到无法操作
原因:九成是镜像 ABI 选错了,或者硬件加速没生效。选了 arm 镜像在没有对应加速的 x86 宿主机上,走的是全指令翻译,启动五分钟、点击响应两秒是常态。另一种情况是选了 x86 镜像但加速驱动没装好,模拟器静默回落到软件模式。
解决:先确认镜像 ABI,ls $ANDROID_HOME/system-images/android-25/*/看目录名。日常用x86_64,只有在复现 native 库相关问题时才切 arm。然后检查加速状态,启动模拟器时加-verbose看日志里有没有HAXM is not installed或KVM is not accessible这类提示。Linux 上还要确认当前用户在kvm组里,没有的话加进去再重新登录。
内存也别给太少,API 25 的镜像给 1536MB 起步,低于这个值系统本身就跑得磕磕绊绊。启动参数里-no-snapshot-load可以跳过快照加载,排查「快照损坏导致黑屏」时很有效——这个原因相当玄学,现象是冷启动正常、热启动黑屏。
5.5 现象:升级到 AndroidX 或调高依赖版本后,整个工程编译不过
原因:AndroidX 与 support 库互斥,两者的包名完全不同,混用必然冲突。而升级 AndroidX 又强制要求更高的 compileSdkVersion 和 AGP 版本,等于把整条工具链往前推。很多人在老工程上直接改依赖坐标,结果报Duplicate class android.support.v4.app.NotificationCompat之类的错误。
解决:如果要留在 API 25 这条线上,就不要动 AndroidX,把所有 support 依赖的版本统一到 25.4.0,用resolutionStrategy.force消除传递依赖的版本漂移。如果一定要迁,步骤是:先把 AGP 升到 3.2+、Gradle 升到 4.6+、compileSdkVersion 升到 28,再开android.useAndroidX=true和android.enableJetifier=true,最后批量替换 import。顺序错一步,就会卡在某个无解的编译错误上。
注意:
enableJetifier是转换第三方依赖的兼容开关,它不会转换你自己的源码。自己代码里的android.support.*import 必须手工或脚本替换。
6. 把兼容性回归跑成流水线:一条脚本校验 targetSdk 与真机行为
前面几章讲的都是「让它跑起来」,但存量工程真正的风险在于:你本地跑通了,发出去的包在 7.1 设备上照样出问题。所以我把这套检查固化成了一个脚本,每次出包前跑一遍,三十秒出结果。
#!/usr/bin/env bash # verify-api25.sh —— 出包前的 API 25 兼容性门禁 set -euo pipefail SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-$HOME/Android/Sdk}}" APK="${1:?用法: ./verify-api25.sh path/to/app.apk}" # 1) 平台包是否存在,这是所有检查的前提 PLATFORM_JAR="$SDK/platforms/android-25/android.jar" [ -f "$PLATFORM_JAR" ] || { echo "[FAIL] platform-25 未安装"; exit 1; } # 2) 取本地最新的 build-tools,调 aapt 读 APK 元信息 BT_DIR="$(ls -1 "$SDK/build-tools" | sort -V | tail -1)" AAPT="$SDK/build-tools/$BT_DIR/aapt" [ -x "$AAPT" ] || { echo "[FAIL] 未找到可执行的 aapt"; exit 1; } BADGING="$("$AAPT" dump badging "$APK")" echo "$BADGING" | grep -E "^(package|sdkVersion|targetSdkVersion)" || true TARGET="$(echo "$BADGING" | sed -n "s/.*targetSdkVersion:'\([0-9]*\)'.*/\1/p")" if [ "$TARGET" != "25" ]; then echo "[WARN] targetSdkVersion=$TARGET,非 25,行为变更项需重新评估" fi # 3) 确认 FileProvider 已声明,避免运行时才炸 "$AAPT" dump xmltree "$APK" AndroidManifest.xml \ | grep -q "FileProvider" && echo "[ OK ] FileProvider 已声明" \ || echo "[WARN] 未发现 FileProvider,检查分享链路" # 4) 装机跑一次冷启动,捕获崩溃 adb install -r -t "$APK" >/dev/null adb shell am force-stop "$(echo "$BADGING" | sed -n "s/package: name='\([^']*\)'.*/\1/p")" adb logcat -c adb shell monkey -p "$(echo "$BADGING" | sed -n "s/package: name='\([^']*\)'.*/\1/p")" \ -c android.intent.category.LAUNCHER 1 >/dev/null adb logcat -d -b crash | grep -q "FATAL EXCEPTION" \ && { echo "[FAIL] 冷启动崩溃,见上方日志"; exit 1; } || echo "[ OK ] 冷启动无崩溃"脚本里几个参数值得解释。sed -n "s/.*targetSdkVersion:'\([0-9]*\)'.*/\1/p"是从 aapt 输出里抠数字,aapt dump badging的格式这些年来基本稳定,可以放心用正则。adb install -r -t的-t允许安装测试包,漏了这个参数在带testOnly标记的包上会安装失败。monkey -c android.intent.category.LAUNCHER 1只发一次启动事件,比am start更省事,不需要知道入口 Activity 名。
再补一个对照表,把「检查项 / 失败信号 / 对应处理」摆在一起,出问题时不用翻脚本:
| 检查项 | 失败信号 | 处理方向 |
|---|---|---|
| 平台包存在性 | [FAIL] platform-25 未安装 | 回第 2 章重装 |
| targetSdkVersion | 输出非 25 并告警 | 确认是否有意调整 |
| FileProvider 声明 | [WARN] 未发现 FileProvider | 检查清单合并结果 |
| 冷启动崩溃 | logcat 出现 FATAL EXCEPTION | 看堆栈定位,多为权限或 URI |
这套东西最有用的一次,是拦住了一个只在 API 25 上复现的空指针:开发机上系统版本更高,走的是新分支逻辑,测试机是 7.1,走了另一条路径,崩在分享图片那一步。脚本跑出来 logcat 里明晃晃的FileUriExposedException,一分钟定位。
从那以后我每次接到存量工程的维护任务,第一件事都是先确认platforms/android-25和system-images/android-25是否成对装好,第二件事是把这条脚本挂到出包流程里强制走一遍——环境不齐就别谈代码问题,能省掉大量「以为是 bug、其实是没装包」的无效排查。这份 platform-25 资源包适合放在手边常备,尤其是还在维护 7.x 兼容线的团队。希望帮到你。
本文还有配套的精品资源,点击获取