简介:Android SDK platform-25 面向 Android 应用开发与测试人员,提供 Android 7.1.1(API 25)的完整开发平台,可用于创建和调试对应版本项目、研究系统框架及验证新特性适配。压缩包共 2000 个文件,约 81.47MB,以 png、xml、ttf、html 为主,辅以少量 jar、properties、txt 等;其中图片与 XML 多用于界面资源和配置,字体、网页文档及库文件支撑平台展示、API 查阅和运行依赖。内容涵盖系统映像、平台库、头文件与源码、构建工具、清单文件及参考文档,便于开发者理解 App Shortcuts、圆形导航键、Night Light、Image Picker API、Daydream 支持等特性,并用于模拟器测试、源码研读和兼容性排查。已有 729 人学习,适合需要补齐 API 25 开发环境或深入理解 Android 7.1.1 平台结构的中级开发者。
1. platform-25 装的不是一个包,是一条时间线的锚点
接手一套 2016 年前后的存量 Android 工程,最先卡住的往往不是代码而是构建环境:compileSdkVersion写着 25,本机却只装了更新的 SDK,Gradle 直接甩出Failed to find target with hash string 'android-25'。Android SDK platform-25 就是补上这个缺口的那个包,它对应 API Level 25,即 Android 7.1.1,里面装着编译期用的android.jar、aapt 需要的资源表和字体、可选的兼容库,以及一份描述自身版本的source.properties。它不含运行时实现,也不能单独跑起来,但没有它,任何把targetSdkVersion定在 25 的工程都无法完成资源链接。下面这些内容面向三类人:维护老工程、要复现 API 25 行为、以及给多版本兼容矩阵补一台 7.1 回归设备的人。
2. 把 platform-25 装进构建链:目录、来源与三行自检
2.1 一个 platform 包里究竟有什么
很多人把 platform 包当成一个android.jar,解压完只检查 jar 在不在,结果编译过了、资源链接崩了、测试库找不到,回头又怀疑是 Gradle 版本不对。实际解压后是这么一堆东西,每一块都参与构建,缺哪块报哪块的错:
| 路径 | 作用 | 缺失后的典型报错 |
|---|---|---|
android.jar | 编译期桩实现,所有方法体都是throw new RuntimeException("Stub!") | Failed to find target with hash string 'android-25' |
data/res/、data/fonts/ | aapt 链接系统资源、解析主题属性时用 | 资源找不到符号,No resource identifier found for attribute |
data/layoutlib.jar | IDE 布局预览的渲染实现 | 布局编辑器黑屏或只显示灰块 |
optional/org.apache.http.legacy.jar | API 23 之后被移除的 HttpClient 兼容实现 | NoClassDefFoundError: org.apache.http.client.HttpClient |
optional/android.test.*.jar | 旧式 instrumentation 测试基类 | 老测试工程ClassNotFoundException |
build.prop | 平台属性,aapt 校验 minSdk/特性时读取 | 部分特性校验失败 |
source.properties | 版本元数据,AndroidVersion.ApiLevel=25 | SDK Manager 识别不到已安装,反复重装 |
templates/ | 新建工程时的 Activity 模板 | 只影响 IDE 新建工程,不影响编译 |
android.jar里的类是空壳这件事要提前在心里钉死。在 IDE 里 Ctrl+点击Activity.onCreate,跳进去看到的是一行throw new RuntimeException("Stub!"),这不是包坏了,也不是下载不完整,而是 SDK 平台包的设计:它只保证你能编译,不提供实现。想看真实逻辑得装sources;android-25或者去翻 AOSP 的对应 tag。血泪经验是,有人以为 jar 损坏,反复重下三遍,浪费掉一个下午。
2.2 在线与离线两条安装路径
有网环境一条命令的事,麻烦的是内网和 CI。在线走sdkmanager,注意别顺手加--update,那会把机器上其他平台一起刷新,把本来能编译的工程刷崩:
# SDK_ROOT 换成实际路径,CI 上常见是 /opt/android-sdk export ANDROID_SDK_ROOT=/opt/android-sdk export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH" # 先接受许可,否则安装会卡在 Review licenses that have not been accepted yes | sdkmanager --licenses > /dev/null # 只装 platform 和配套 build-tools,不要 --update 全量刷新 sdkmanager --install "platforms;android-25" "build-tools;25.0.3"--install后面跟的包名格式是类型;标识,platforms;android-25里的android-25就是目标目录名,写成android-25.0.1之类的变体一律找不到包。老版本工具包里sdkmanager在tools/bin/下,新版在cmdline-tools/latest/bin/下,脚本里最好先判断路径再调用,别把两种路径写死成一条。
内网环境一般是拿到platform-25_rXX.zip这类离线包,手工解压到位。这里唯一容易翻车的是内层目录名,压缩包解出来可能不叫android-25,而 SDK 只认这个名字:
mkdir -p "$ANDROID_SDK_ROOT/platforms" unzip -q platform-25_rXX.zip -d "$ANDROID_SDK_ROOT/platforms/" # 内层目录名必须是 android-25,不是就改名,否则 Gradle 认不到 [ -d "$ANDROID_SDK_ROOT/platforms/android-25" ] || \ mv "$ANDROID_SDK_ROOT/platforms/platform-25" "$ANDROID_SDK_ROOT/platforms/android-25" # 确认元数据里的 ApiLevel 是 25 grep -E "AndroidVersion.ApiLevel|Pkg.Revision" \ "$ANDROID_SDK_ROOT/platforms/android-25/source.properties"Pkg.Revision的具体数值随离线包的修订号不同而变化,它影响的是 SDK Manager 是否提示有新版本,不影响编译;真正决定 Gradle 认不认的是目录名和AndroidVersion.ApiLevel这两项。另外ANDROID_SDK_ROOT和老的ANDROID_HOME同时存在且指向不同目录时,不同工具读的变量不一样,会出现命令行能编译、IDE 报找不到 SDK 的玄学现象,统一成一个变量最省事。
2.3 装完必须验的三件事
装完直接开 IDE 是最坏的习惯,先花三十秒在命令行验完,能省掉后面半小时的误判。第一件验 jar 里类齐不齐,第二件验常量值对不对,第三件验 Gradle 能不能真的链接一次资源:
JAR="$ANDROID_SDK_ROOT/platforms/android-25/android.jar" # 1. jar 里类的数量,正常量级在几万个,几百个说明包被截断了 unzip -l "$JAR" | grep -c "\.class$" # 2. 版本常量,N 应为 24,N_MR1 才是 25,对不上就是装错了平台 javap -classpath "$JAR" 'android.os.Build$VERSION_CODES' | grep -E '\bN\b|N_MR1' # 3. 让 Gradle 真正跑一次资源链接,比任何静态检查都实在 ./gradlew :app:assembleDebug --offline第二步的输出应该是public static final int N = 24;和public static final int N_MR1 = 25;。这条命令的价值在于区分 API 24 和 25:两个版本的android.jar在文件大小上差得不多,肉眼比对文件名很容易把android-24当成android-25拷进去,编译时用到的常量值却差一整个版本号。
第三步加--offline是有意的。离线跑一次能顺带验证依赖缓存是否完整,如果工程依赖的 support 库没进本地缓存,这一步会直接暴露,而不是等到打包机上半路失败。三件事都过了再开 IDE,能规避掉绝大多数"环境问题"伪装的真实代码问题。
3. compileSdk 25 的工程配置:三个版本号各管一段
3.1 compile、min、target 的职责边界
这三个版本号被混用是老工程最常见的技术债。它们各自的边界很清楚,混了就会写出"在我机器上没事"的代码:
| 版本号 | 决定什么 | 调到 25 的后果 |
|---|---|---|
compileSdkVersion | 编译期能看到哪些 API 符号、aapt 用哪套资源表 | 只影响编译,可以调用 API 25 才有的类和方法 |
minSdkVersion | 应用能装到的最低系统版本 | 与 25 无关,一般不动,动了就是放弃老设备 |
targetSdkVersion | 系统以哪个版本的行为规范对待你的应用 | 触发 API 24/25 的一系列行为变更,是踩坑的主要来源 |
关键点在于compileSdkVersion 25不等于能随便用 API 25 的接口。工程里minSdkVersion 21时,调用App Shortcuts、android:roundIcon这些 25 才有的能力,在 21 到 24 的设备上会直接崩或者被忽略,必须用Build.VERSION.SDK_INT >= 25包起来。反过来targetSdkVersion提到 25 才会让系统切换到新的行为模式,比如文件共享的StrictMode检查、隐式广播限制,这些在targetSdkVersion 22时是关着的。
所以升级顺序建议是:先单独把compileSdkVersion提到 25 编译通过,确认没有资源符号冲突;再单独把targetSdkVersion提到 25,一次性处理行为变更。两步合在一起做,出问题时根本分不清是哪一步引入的。
3.2 build-tools 与 AGP 的配套关系
build-tools是 aapt、dx、zipalign 这些实际干活的工具,它和compileSdkVersion不是一回事,但必须能处理对应平台的资源。API 25 时代常见的搭配有这么几组,选一组锁死,别让 Gradle 自己猜:
| Android Gradle Plugin | Gradle 版本 | 建议 buildToolsVersion | 依赖配置关键字 |
|---|---|---|---|
| 2.3.x | 3.3 | 25.0.3 | compile |
| 3.0.x | 4.1 | 25.0.3 或 26.0.x | implementation |
| 3.1.x | 4.4 | 26.0.x 起 | implementation |
不写buildToolsVersion时,AGP 会挑一个它默认的版本,装没装它不管,结果就是 CI 上第一次构建直接报Failed to find Build Tools revision xx.x.x。显式写死是最省事的做法。
还有一个细节:compileSdkVersion 25配 support 库时,库的大版本要跟编译版本对齐。com.android.support:appcompat-v7:25.x.x这一系列依赖 API 25 的资源属性,混入 26 或 27 的 support 库会引入 25 平台不认识的属性,报出一堆No resource identifier found。这个错看起来像 platform 包损坏,实际是依赖版本错位。
3.3 一份能直接跑通的最小配置
下面这份app/build.gradle是在 AGP 2.3.3 下验证过的形态,换成 3.x 只需把compile改implementation、补上google()仓库:
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion "25.0.3" // 显式指定,避免 AGP 猜一个没装的版本 defaultConfig { applicationId "com.example.legacy25" minSdkVersion 21 // 保持老设备覆盖面,与 25 无关 targetSdkVersion 25 // 这一行才触发 API 24/25 的行为变更 versionCode 1 versionName "1.0" } compileOptions { // API 25 时代的工程普遍还在 Java 7 语法,别急着开 1.8 sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } lintOptions { // targetSdk 提到 25 后 lint 会集中冒出一批告警,先别让它挡住构建 abortOnError false } } dependencies { // 大版本与 compileSdkVersion 对齐,25 的工程别混 26+ 的 support 库 compile 'com.android.support:appcompat-v7:25.4.0' }compileOptions保持 Java 7 是有原因的:API 25 时代 Java 8 语法支持要走 Jack 编译器或者后来的 desugar,Jack 在 AGP 3.0 之后被移除,工程一旦切过去,回退成本很高。存量工程除非有明确需求,没必要在这个版本上动语法级别。
lintOptions里关掉abortOnError不是纵容问题,而是把 lint 告警和编译失败解耦。targetSdkVersion从 22 跳到 25 会一次性触发几十条与行为变更相关的提示,全开着会让构建直接失败,人就卡在第一步没法往下走。正确做法是先让构建通过,再把 lint 报告单独跑一遍逐条看。
配置落地后再跑一次带--info的构建,日志里会打印实际使用的 SDK 和 build-tools 路径,把这两行贴进提交说明,下次别人复现环境就有据可依:
./gradlew :app:assembleDebug --info 2>&1 | grep -E "android-25|Build Tools|Compiling"4. API 25 的行为变更:权限、通知、文件共享的改法
4.1 运行时权限在 25 上没有新花样,但有旧账
API 25 本身没在权限模型上加新东西,运行时权限是 API 23 就引入的。但大量老工程的targetSdkVersion一直卡在 22,升级到 25 时等于一次性跨了三个版本,权限这块的旧账会集中爆发。典型表现是:清单里声明了权限,代码里从来没申请过,在 22 的机器上一直好用,到 25 的机器上直接抛SecurityException。
处理顺序建议是先跑一遍全量权限盘点,把所有dangerous级别的权限列出来,再逐个确认调用点有没有做申请和拒绝分支。别指望在onCreate里一次性申请全部权限,用户拒绝一次之后就再也不会弹窗,后续逻辑全是死路。合理的做法是把申请时机挪到真正用到该能力的前一刻,拒绝时给出降级路径而不是直接退出。
4.2 文件共享:file:// 的末日与 FileProvider 落地
这是从低 targetSdk 升到 24+ 最容易崩的一处。原来一行Uri.fromFile(file)就能把文件传给相机、分享给其他应用,升级后直接抛FileUriExposedException,日志里写得很清楚:不允许把file://形式的 URI 暴露给其他应用。原因是file://让对方拿到了你私有目录的完整路径和读权限,是个实打实的越权面。
改成FileProvider分三步。第一步在清单里注册 provider:
<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 --> <paths> <cache-path name="cache" path="." /> <!-- 对应 getCacheDir() --> <files-path name="files" path="." /> <!-- 对应 getFilesDir() --> </paths>第三步把生成 URI 的地方全部换掉,并且补上授权标志:
File target = new File(getCacheDir(), "report.pdf"); Uri uri = FileProvider.getUriForFile( this, getPackageName() + ".fileprovider", // 必须与清单里的 authorities 完全一致 target); Intent share = new Intent(Intent.ACTION_SEND) .setType("application/pdf") .putExtra(Intent.EXTRA_STREAM, uri) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 少了这行,接收方读不到 startActivity(Intent.createChooser(share, "发送"));三个参数里最容易错的是authorities。清单里用${applicationId}.fileprovider占位符,代码里用getPackageName()拼接,两者必须一致;如果工程配了applicationIdSuffix(比如 debug 包加了.debug后缀),手写死字符串就会对不上,运行时报IllegalArgumentException: Failed to find configured root。FLAG_GRANT_READ_URI_PERMISSION也是高频遗漏项,加上之后接收方才有临时读权限,漏了就是对方打不开文件。
4.3 通知栏的直接回复与 RemoteInput
API 24 引入的通知内联回复,在 25 上已经稳定,但对存量工程的改造价值在于:很多应用收到消息后只能点进 App 才能回复,用户流失就发生在这一步。落地方式是给通知挂一个带RemoteInput的 Action:
RemoteInput remoteInput = new RemoteInput.Builder("key_reply") .setLabel("快速回复") .build(); NotificationCompat.Action action = new NotificationCompat.Action.Builder( R.drawable.ic_reply, "回复", replyPendingIntent) .addRemoteInput(remoteInput) .build(); Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.ic_stat) .setContentTitle("新消息") .addAction(action) .build();接收端从 Intent 里取文本时不能直接getCharSequenceExtra,必须走RemoteInput.getResultsFromIntent:
Bundle results = RemoteInput.getResultsFromIntent(intent); CharSequence reply = results != null ? results.getCharSequence("key_reply") : null; if (reply != null) { // 这里做完发送后要更新通知,否则用户会觉得没生效 }"key_reply"这个 key 是发送端和接收端唯一的约定,两边拼错一个字符就永远取不到值,而且不会报错,只是静默返回 null。修完发送逻辑记得把原通知取消或更新成"已回复",否则那条通知会一直挂在那儿,用户点进去看到的还是旧内容。
4.4 隐式广播与后台任务
targetSdkVersion到 24 之后,CONNECTIVITY_ACTION这类隐式广播的静态注册收不到了。原来在清单里注册一个NetworkReceiver监听网络变化的写法,升级后表现为"接收器代码明明在,就是不执行"。原因不是代码错,而是系统在 targetSdk 24+ 时不再把这类广播投递给静态注册者。
改法有两条路。轻量的直接用动态注册,跟着界面生命周期走:
@Override protected void onStart() { super.onStart(); registerReceiver(netReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } @Override protected void onStop() { unregisterReceiver(netReceiver); super.onStop(); }后台性质的网络恢复重试,正确姿势是换成JobScheduler把"等网络可用"这件事交给系统调度,而不是自己监听广播再判断:
JobInfo job = new JobInfo.Builder(JOB_SYNC, new ComponentName(this, SyncJobService.class)) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // 有网才执行 .setPersisted(true) // 重启后仍然保留 .build(); ((JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE)).schedule(job);setPersisted(true)需要清单里声明RECEIVE_BOOT_COMPLETED权限,否则调用会抛SecurityException。另外JobScheduler的执行时机由系统决定,最短间隔会被 Doze 模式拉长,别拿它做秒级精度的定时任务。
5. 避坑与排查:五条来自构建日志的现场记录
5.1 报找不到 android-25,但目录明明在
现象:Gradle 报Failed to find target with hash string 'android-25',去platforms/目录下看,android-25文件夹好端端摆着,里面也有android.jar。
原因:多半是local.properties里的sdk.dir指向了另一个 SDK 根目录,或者 IDE 与命令行读的环境变量不一致。还有一种情况是离线包解压出来的目录层级多套了一层,变成platforms/android-25/android-25/android.jar。
解决:先确认 Gradle 实际用的 SDK 路径,再逐层查文件:
./gradlew -q :app:properties | grep -i sdk ls -la "$(grep sdk.dir local.properties | cut -d= -f2)/platforms/android-25/android.jar"最后一行如果找不到文件,问题就在目录结构上,直接把android.jar所在的那层改名为android-25即可。
5.2 IDE 里点进系统类全是 Stub!
现象:想看看Activity或View的源码,按住 Ctrl 点进去,看到的是一堆throw new RuntimeException("Stub!"),换行也是空的。
原因:android.jar本身就是桩实现,只有方法签名没有方法体,IDE 索引到的就是这个 jar。
解决:装源码包sources;android-25,或者在 IDE 的 SDK 设置里指定源码路径。命令行下用sdkmanager --install "sources;android-25",装完后源码落在sources/android-25/下。注意源码包和平台包是两个独立的包,装了平台不等于装了源码。
5.3 升级 targetSdk 后分享文件必崩
现象:点分享按钮,App 直接退出,日志里FileUriExposedException。而且只在targetSdkVersion提到 24+ 之后才开始崩,之前一直好好的。
原因:targetSdkVersion到 24 起,StrictMode会拦截把file://URI 通过 Intent 传给其他应用的行为,这是有意的收紧,不是 bug。
解决:按 4.2 换成FileProvider,authorities与getPackageName()对齐,分享 Intent 补FLAG_GRANT_READ_URI_PERMISSION。改完记得把所有Uri.fromFile的调用点搜索一遍,这类代码往往散落在多个模块里,改一处不够。
5.4 支持库版本串了,报一堆资源属性找不到
现象:No resource identifier found for attribute 'xxx' in package 'com.example',报错的属性名看着不像自己写的,全是 support 库里的东西。反复确认 platform 包没坏,还是报。
原因:compileSdkVersion是 25,但依赖树里混进了 26 或 27 版本的 support 库,那些库引用了 API 25 资源表里不存在的属性。多模块工程里子模块各自声明依赖,版本很容易串。
解决:先看依赖树,把所有 support 库统一到 25.x,必要时用resolutionStrategy强制锁定:
configurations.all { resolutionStrategy.eachDependency { details -> if (details.requested.group == 'com.android.support') { details.useVersion '25.4.0' // 与 compileSdkVersion 保持同一大版本 } } }改完执行./gradlew :app:dependencies复查,确认没有更高版本被传递依赖拉回来。
5.5 CI 上构建卡在许可协议
现象:本地构建正常,上到构建机就卡在Review licenses that have not been accepted,或者直接失败退出。
原因:sdkmanager安装任何包之前都要确认许可协议,全新容器里没有任何已接受记录。
解决:在安装步骤之前显式接受,并且把整个安装流程串成一条命令,避免中途交互卡死:
yes | sdkmanager --licenses > /dev/null 2>&1 sdkmanager --install "platforms;android-25" "build-tools;25.0.3" < /dev/null重定向< /dev/null是为了防止某些交互式提示在无 TTY 环境下挂起,比只加yes更稳。另外构建机最好把 SDK 目录做成缓存层,别每次构建都重新下载,一次几十兆的东西积少成多会拖长流水线。
6. 进阶:用 API 25 做多版本回归矩阵
存量工程真正的麻烦不在编译,而在"改完之后怎么确认没把老版本弄坏"。我的做法是把 API 25 当成兼容矩阵里的一个锚点,配合最低支持版本一起跑回归。
先看 APK 里的实际配置,确认打出来的包确实是按预期标的:
# 逐个 APK 打印版本约束,确认 targetSdk 与 minSdk 没有被构建变体改掉 for f in app/build/outputs/apk/**/*.apk; do echo "== $f" aapt dump badging "$f" | grep -E "^package|sdkVersion|targetSdkVersion" done输出里targetSdkVersion:'25'和sdkVersion:'21'两行必须和build.gradle对得上。多渠道构建时最容易出现某个 flavor 覆盖了targetSdkVersion而没人发现,等到线上行为不一致才回头查,成本高得多。
回归设备不用多,两台就够:一台 API 21 的模拟器验兼容分支,一台 API 25 的模拟器验行为变更后的主路径。镜像按需装:
sdkmanager --install "system-images;android-25;google_apis;x86" avdmanager create avd -n api25 -k "system-images;android-25;google_apis;x86" -d pixel代码里的版本分支要写窄,别写成>= 24一把梭。API 24 和 25 之间在快捷方式、圆形图标这些点上有明确差异,用Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1单独判断 25,比笼统判断 N 更准确:
| 能力 | 起始 API | 判断写法 |
|---|---|---|
| App Shortcuts | 25 | SDK_INT >= N_MR1 |
android:roundIcon | 25 | 属性本身在低版本被忽略,但需要 compileSdk ≥ 25 |
| 通知内联回复 | 24 | SDK_INT >= N |
FileProvider强制 | 24(targetSdk 维度) | 与设备版本无关,看 targetSdk |
最后一条最容易被误解:FileUriExposedException的触发条件是你的targetSdkVersion,不是设备版本。所以哪怕只在 API 21 的机器上跑,只要 targetSdk 是 25,file://一样会崩。
从那以后我每次升targetSdkVersion,都强制先跑一遍全量aapt dump badging确认打包结果,再把file://、隐式广播、权限申请点这三类代码全局搜一遍,最后才动业务逻辑。这套流程枯燥,但它把"升级后突然崩了但不知道崩在哪"变成了"升级前就知道哪几处必须改"。希望帮到你。
本文还有配套的精品资源,点击获取