简介:Android SDK android-33 平台包面向需要补充或离线安装 Android 13(API 33)开发环境的 Android 应用开发者与学习者,主要用于在 Android Studio 等 IDE 中补齐目标平台,解决新版本编译、接口调用与设备测试时的平台缺失问题。压缩包共约 2000 个文件,大小 60.84MB,其中以 1974 个 xml 平台配置与资源描述文件为主,另有 22 个 html 文档和 4 个 txt 说明,整体属于 SDK platforms 目录下的平台内容,解压后可按 SDK 目录结构放入 platforms 文件夹供 IDE 识别。已有 857 人浏览学习,适合需要离线部署或对照目录结构排查 SDK 配置的读者。借助该平台包,可完成 API 33 对应系统库、平台元数据与文档的本地补齐,便于验证 Android 13 特性、处理依赖与编译兼容问题,也能作为多版本 SDK 并存管理的参考材料。
1. android sdk android-33 为什么成了绕不过去的一道坎
android sdk android-33 这个组合,我最近在一个模拟项目的升级里又碰了一次:工程原本躺在 compileSdk 32 上,应用市场的新提交规则一落地,整条流水线当天就红了,报错五花八门,从 “Failed to find target” 到资源编译失败,再到通知一条都不弹。多数人以为它不过是“多下 100 多兆的一个包”,真正卡住人的是平台包、构建工具、AGP、JDK 四者的版本咬合,以及 targetSdk 提到 33 之后那几条不显眼却改行为的变化。下面按我自己复现的顺序,把 android-33 这份 SDK 的目录构成、命令行安装、行为变更改造、翻车排查,一路拆到能用为止。手里有存量工程、打算把 targetSdkVersion 提到 33 的 Android 开发者适合看,刚接手别人留下的老项目、还在纠结该装哪几个 SDK 包的同学同样适合。
2. android-33 平台包里到底装了什么:目录结构与四个关键构件
2.1 platforms/android-33 目录里每个文件的职责
把 SDK 根目录摊开看,platforms/android-33 是真正的“契约层”。最核心的是 android.jar,它里面的每个类、每个方法签名都对应 API 33 的公开接口,但方法体是空的,调用会抛 RuntimeException(“Stub!”)。这不是安装坏了,而是设计如此——编译只需要签名,运行时要靠设备上的 framework.jar 兜底。很多人第一次在本地 JVM 单元测试里调到 Landroid/os/SystemClock 就报这个错,然后去翻 SDK 是不是损坏了,白白浪费一晚上。
data 目录放的是 res 相关的编译期资源,比如系统自带的 attrs、themes;optional 里是可选依赖,像 org.apache.http.legacy.jar,老项目用到 HttpClient 又没迁移到 OkHttp 的时候,就是靠它续命;templates 是新建工程时的模板文件;uiautomator.jar 供 UI 自动化使用;framework.aidl 是 binder 接口的声明,AIDL 编译时要用。source.properties 里有一行 Pkg.Revision,这是判断你手上这份包到底是哪个补丁级别的唯一凭据,比看目录名靠谱。
还有一个容易被忽略的点:android.jar 并不包含隐藏 API。如果你的代码历史遗留里有用反射调 @hide 方法的,编译能过(反射绕过了签名检查),但运行到 33 上可能被非 SDK 接口限制拦住,日志里是灰色的 NoSuchMethodException 或者一段提示字符串。这类问题在升级时集中爆发,排查成本极高,所以真遇到“只在高版本崩、低版本正常”的情况,先把反射点全部列出来,比对着改代码更快。
2.2 platform、build-tools、platform-tools 三个包的分工
新手最容易把三个包混成一件事,其实它们的生命周期完全独立。platform 是编译期的类型契约,build-tools 是把资源、字节码加工成 APK 的“机床”,platform-tools 是 adb、fastboot 这类和设备打交道的命令行。
| 构件 | 典型路径 | 作用 | 缺失时的报错 |
|---|---|---|---|
| platform | platforms/android-33/android.jar | 编译期 API 契约 | Failed to find target with hash string 'android-33' |
| build-tools | build-tools/33.0.2/aapt2 | 资源编译、dex、zipalign、签名 | aapt2 error / Failed to crunch file |
| platform-tools | platform-tools/adb | 设备连接与调试 | adb: command not found |
| 系统镜像 | system-images/android-33/… | 模拟器运行环境 | No system image available |
这里有个血泪经验:只装 build-tools 不装 platform,Gradle 会在配置阶段直接失败,报错文本却指向 “SDK location not found”,让人以为是 local.properties 写错了。反过来只装 platform 不装 build-tools,报错会落在资源编译那一步,aapt2 找不到可执行文件。判断方法很简单,去 SDK 根目录下 ls 一下,看 platforms 和 build-tools 里是不是都有以 33 开头的目录。
2.3 compileSdk 33 对 AGP、Gradle、JDK 的版本要求
版本咬合是升级路上最硬的墙。compileSdk 33 本身只是数字,但它背后的 AGP 必须认得这个数字,AGP 又绑死 Gradle 的最低版本,Gradle 又绑死 JDK。常见做法是按下表对齐,不要试图用“最新版一切都好”的思路去碰。
| 组件 | 建议下限 | 关键约束 |
|---|---|---|
| AGP | 7.4 及以上 | 8.x 系列强制 JDK 17 |
| Gradle | 7.5 及以上 | 与 AGP 主版本严格对应 |
| JDK | 11(AGP 8 需 17) | 与 compileOptions 里的版本一致 |
| build-tools | 33.0.x | 与 compileSdk 大版本对齐即可 |
为什么强调“对齐”而不是“越高越好”?因为 build-tools 的 aapt2 在小版本之间改过资源压缩策略和 PNG crunch 的默认行为,某些老工程的九宫格图片在 33.0.2 上能过、在更高版本上会报错。项目里如果有一堆历史图片资源,建议先锁死一个确定能过的版本,写进 build.gradle 的 buildToolsVersion,等资源清理完再往上走。
2.4 为什么我不建议在团队里靠 IDE 自动下载
IDE 的 SDK 管理器确实方便,点两下就装完了,但它在三个地方会坑人:一是默认装在用户目录,路径里往往带中文用户名或空格,aapt2 在某些版本对空格路径处理不干净;二是它会顺带装一堆你根本不需要的包,CI 上复现时对不上;三是下载中断后留下的半成品目录不会被自动清理,后续一直报 “corrupted”。
我一般的做法是:本地用命令行装到统一路径(如 /opt/android-sdk 或 D:/android-sdk),IDE 里只做“指向这个路径”这一个动作。这样本地、CI、同事机器三处的 SDK 内容一致,出问题才好比对。团队协作里,环境不一致造成的“我这能跑”是最耗时间的玄学问题,能提前消灭就别留。
3. 命令行装 android-33:从 licences 到 local.properties 的完整链路
3.1 先看清单再装,别凭记忆写包名
sdkmanager 的包名格式是“类型;版本;子变体”,写错一个字就是找不到包。装之前先查一遍远端清单,确认这个源里确实有 android-33 的条目,也能顺便看到可选的 build-tools 小版本。这一步在受限网络或者内网镜像环境下尤其必要,因为镜像同步不全时,你写的包名是对的但就是拉不下来,会误判成命令写错。
# SDK 根目录统一放无空格、无中文的路径 export ANDROID_SDK_ROOT=/opt/android-sdk # 查远端清单里所有和 33 相关的条目 sdkmanager --list | grep -E "android-33|build-tools;33" # 接受许可,CI 环境必须非交互,否则会卡在 y/N 提示 yes | sdkmanager --licenses这里的 --list 会同时打印已安装和可安装两部分,grep 时注意区分。--licenses 是全局动作,一次接受之后同一 SDK 根目录下不再询问。如果 CI 上每次构建都重新拉镜像,这一步要写进流水线脚本,否则 Gradle 会在 sync 阶段抛 “You have not accepted the license agreements”。
3.2 装平台包、构建工具与系统镜像
确认清单无误后一次性装齐三件套,顺序无所谓,但不要在装了一半的时候中断——中断留下的不完整目录就是后面 “corrupted” 报错的源头。
# 平台包 + 构建工具 + 平台工具,三件套缺一不可 sdkmanager "platforms;android-33" "build-tools;33.0.2" "platform-tools" # 需要跑模拟器时再补镜像与 emulator,按 CPU 架构二选一 sdkmanager "system-images;android-33;default;x86_64" "emulator" # Apple Silicon 或 ARM 服务器上换成: # sdkmanager "system-images;android-33;default;arm64-v8a" "emulator"参数含义逐个说清:“platforms;android-33” 是编译契约,缺它编译阶段就崩;“build-tools;33.0.2” 里的 33.0.2 是可以替换的小版本,选一个团队内验证过的;“platform-tools” 不带版本号,永远装最新,因为它和 adb 协议绑定,用旧版连新设备反而容易出问题。系统镜像的 default 表示不带额外服务的纯净镜像,体积小、启动快,做兼容性验证够用;只有需要验证依赖系统服务的功能时才换其他变体。
3.3 装完必须校验,别直接开 IDE
装完不看一眼就打开工程,是翻车的常见起点。三步校验足够确认这份包是完整可用的:看版本号、看 android.jar 里有没有真东西、看设备侧 API 级别对不对。
# 1. 版本号:Pkg.Revision 是你手上这份包的真实补丁级别 cat $ANDROID_SDK_ROOT/platforms/android-33/source.properties # 2. 完整性:数一下 android.jar 里的类数量,几千个才算正常 unzip -l $ANDROID_SDK_ROOT/platforms/android-33/android.jar | grep -c "android/app/" # 3. 设备侧:确认连上的设备或模拟器确实是 API 33 adb devices adb shell getprop ro.build.version.sdk # 期望输出 33第一条输出里如果 Pkg.Revision 缺失或者文件不存在,说明平台包没装完整,直接删目录重装。第二条的数字如果只有个位数,多半是下载到了残缺包。第三条的 getprop 输出如果不是 33,那你在 33 上验证出来的结论对 32 的设备并不成立——这一点在做兼容性回归时特别重要,别拿一台机器上的结果推所有机型。
3.4 local.properties 与模块级 build.gradle 的配置
SDK 装好了,工程还得知道它在哪、用哪个版本编。这两件事分别由 local.properties 和 build.gradle 负责,前者是本机路径,后者是工程契约。
# local.properties:本机 SDK 路径,绝对不要提交到版本库 sdk.dir=/opt/android-sdk// 模块级 build.gradle android { namespace 'com.example.demo' compileSdk 33 // 编译期契约,决定能引用哪些 API buildToolsVersion '33.0.2' // 显式锁定,避免多机环境漂移 defaultConfig { applicationId 'com.example.demo' minSdk 21 // 兼容下限,决定要写多少分支代码 targetSdk 33 // 行为变更开关,不是"能跑就行"的数字 versionCode 1 versionName '1.0.0' } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = '11' } }三个 sdk 相关参数的区别值得单独讲:compileSdk 决定“你能写什么代码”,写超范围的 API 编译期就报错;targetSdk 决定“系统拿哪套规则对待你的应用”,它才是触发行为变更的开关;minSdk 决定“你要为多少老设备写兼容分支”。很多人升级时只改前两个,把 minSdk 一起往上抬,结果丢掉一批存量用户,这个决定得产品和技术一起拍,不能顺手改。local.properties 不进版本库这条是硬规矩,CI 上通过环境变量 ANDROID_SDK_ROOT 提供路径即可。
4. targetSdkVersion 提到 33 之后:通知、媒体与附近设备三处权限改造
4.1 POST_NOTIFICATIONS:从“默认能发”到“必须申请”
这是 33 上最容易漏、后果又最不明显的一条。以前通知能不能发只取决于有没有通知渠道、渠道有没有被用户关掉;从 targetSdk 33 起,发通知多了一道运行时权限,没申请到的话通知会被静默丢弃,logcat 里连个异常都不给你,只有一行不起眼的提示。我在一个下载器模块上就吃过这个亏,测试同学说“下载完成了但没提示”,查了半天以为是自己 channel 建错了。
<!-- AndroidManifest.xml:先声明,别指望运行时自动有 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />// 通知权限的申请与降级处理 private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { showDownloadDoneNotification() // 授权成功,走正常通知 } else { showInAppBanner() // 被拒,降级为应用内提示,别反复弹系统框 } } fun ensureNotificationPermission() { // 33 以下没有这个运行时权限,直接发就行,别多此一举去申请 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { showDownloadDoneNotification() return } val state = ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) if (state == PackageManager.PERMISSION_GRANTED) { showDownloadDoneNotification() } else { notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } }代码里三个点值得说:一是 Build.VERSION_CODES.TIRAMISU 的值就是 33,用常量而不是硬编码数字,可读性差很多;二是 RequestPermission 契约会把结果回调成布尔值,但它不告诉你“用户是不是勾了不再询问”,真要区分得配合 shouldShowRequestPermissionRationale,而这个方法在首次请求前返回 false、拒绝后返回 true、勾了不再询问后重新返回 false,三种情况共用两个返回值,是个经典的坑;三是被拒绝后不要反复弹系统对话框,33 上连续拒绝两次之后系统就不再展示了,你的调用会静默返回 false,用户视角就是“点了没反应”。降级成应用内横幅是更稳妥的做法。
4.2 READ_MEDIA_* 替换 READ_EXTERNAL_STORAGE 的迁移写法
33 把外部存储的读权限按媒体类型拆成了三个:图片、视频、音频。以前申请一个 READ_EXTERNAL_STORAGE 就能读相册,现在在 33 设备上即使拿到了这个权限,查询图片也会返回空游标。这个变化最阴的地方在于它不报错,只是数据没了。
<!-- 33 起按类型申请,粒度更细,也更符合最小权限原则 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <!-- 旧权限保留给 32 及以下,用 maxSdkVersion 卡住上限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />maxSdkVersion=“32” 这一行是老工程平滑过渡的关键:低于 33 的设备走旧权限,33 及以上走新权限,两套声明共存不会冲突。运行时请求时要按 Build.VERSION.SDK_INT 分支,33 以上请求 READ_MEDIA_IMAGES,以下请求 READ_EXTERNAL_STORAGE,千万别图省事两个都请求,用户会看到一个莫名其妙的双重弹窗,通过率直线下降。
还有一个边界:如果你的应用只是让用户“选一张图”,根本不需要这些权限。系统照片选择器从 33 开始提供,走它完全不涉及权限,用户体验还更好。只有在需要批量扫描本地媒体、自己做列表展示的场景下,才需要申请读权限。这个判断做错了,等于白写一堆权限代码。
4.3 NEARBY_WIFI_DEVICES 与 neverForLocation
33 把 Wi-Fi 扫描相关的能力从位置权限里拆了出来,新增 NEARBY_WIFI_DEVICES。以前做设备配网,为了拿到周边 Wi-Fi 列表,不得不申请精确定位,用户看到“一个灯泡应用要定位权限”直接卸载。现在可以声明新权限并标注用途与位置无关。
<!-- xmlns:tools 要加到 manifest 根节点上才有 tools: 前缀 --> <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation" tools:targetApi="33" /> <!-- 32 及以下仍然需要定位权限才能扫描 Wi-Fi --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32" />usesPermissionFlags=“neverForLocation” 是个强承诺,声明之后系统会限制你用这批 Wi-Fi 数据去推导物理位置。承诺了就不能反悔,否则相关 API 会返回空结果。有些团队为了“兼容老逻辑”两个权限都留着,结果在 33 上依然被要求定位授权,等于白拆。这里的判断标准很清晰:功能只做配网、不做地理围栏,就大胆用 neverForLocation。
4.4 前台服务类型与 intent filter 匹配
这两条在 33 上不是硬性崩溃,但等你升到更高版本时一定会撞上,早点理解能省一次重构。前台服务从 33 起要求在清单里声明类型,缺了在 33 上可能还能跑,但系统对类型的检查只会越来越严,后面直接把异常抛出来。
<service android:name=".DownloadService" android:exported="false" android:foregroundServiceType="dataSync" />dataSync 适合下载、同步这类场景,别的类型按功能实际选。选错类型不一定会当场报错,但审核和后台限制策略会找你麻烦,所以要按真实用途选,而不是挑一个最常见的抄。
intent filter 匹配这条影响面更大:targetSdk 33 起,系统不再把不匹配的隐式 intent 投递给声明了 intent-filter 的组件。以前你的 Activity 只要声明了某个 action,外部随便发个带点余量参数的 intent 也能进来;现在参数、category、data 对不上就直接被拦,调用方收到 ActivityNotFoundException。这个变化在插件化、跨应用跳转多的工程里会集中爆发,判断方法是把所有对外暴露组件的 intent-filter 逐条核对,确认 action、category、scheme、mimeType 四要素都能被真实调用方满足。
5. 升级到 33 的五个翻车现场:现象、原因与止损动作
5.1 报 Failed to find target with hash string 'android-33'
现象是 Gradle 同步阶段直接失败,提示找不到 android-33 这个 target,但打开 SDK 目录一看 build-tools 里明明有 33 开头的文件夹。原因很单纯:平台包和构建工具是两个独立包,装了后者不代表有前者,而报错文本又往往指向 SDK 路径本身,误导性极强。解决动作是先 ls 一下 platforms 目录,没有 android-33 就补装,装完同步一次即可。止损习惯是每次在 CI 里加一条“启动前校验 platforms/android-33/source.properties 是否存在”的检查,把问题挡在编译之前。
5.2 aapt2 报错,提示构建工具损坏
现象是资源编译阶段崩,日志里有 “Installed Build Tools revision 33.0.x is corrupted” 或者一堆 aapt2 执行失败。原因通常是下载中断留下的半成品目录,也可能是 SDK 路径里带了空格或中文,aapt2 对这类路径的处理在不同小版本上并不一致。解决动作是把 build-tools 下对应版本整个删掉重装,并把 SDK 挪到无空格路径。这个坑最值得反省的地方在于它看起来像代码问题,实际是环境问题,很容易让人去翻资源文件白折腾两小时。
5.3 通知一条都发不出来,也没有异常
现象是业务代码执行完了,通知栏什么都没有,logcat 里干干净净。原因分三层:清单没声明 POST_NOTIFICATIONS、运行时没申请、或者通知渠道被建成了 IMPORTANCE_NONE。33 上最常见的是第二层,代码里压根没写请求逻辑,因为从低版本升上来的工程默认“通知一直是能发的”。解决动作是先补清单声明,再加运行时请求,最后检查渠道重要性。止损方法是把“发通知”这个动作封装成一个统一入口,权限判断收在里面,业务侧只调一次,避免各处重复遗漏。
5.4 相册读取返回空列表,权限弹窗却不出现
现象是查询媒体库返回空游标,但应用也没崩,权限对话框更是从没弹过。原因是 targetSdk 33 之后系统只认 READ_MEDIA_* 系列,旧的 READ_EXTERNAL_STORAGE 在 33 设备上不再授予对应能力,你的请求代码可能压根没被执行到。解决动作是按清单里的新权限改写申请逻辑,并用 maxSdkVersion 保留旧声明给低版本设备。这里要提醒一句:不要用“两个权限一起申请”的偷懒写法,弹窗顺序在部分机型上不可控,用户看到两次授权会直接拒绝。
5.5 本地 JVM 单测一调系统方法就抛 Stub!
现象是纯逻辑单测里只要碰到 android.jar 中的方法就抛 RuntimeException(“Stub!”),明明代码在真机上跑得好好的。原因是 android.jar 是空实现,所有方法体都是 stub,JVM 环境里没有真实 framework。解决动作是把依赖 Android 框架的测试移到仪器化测试里跑,或者引入影子环境在 JVM 上模拟。判断标准是:只测纯 Java/Kotlin 逻辑就留在 JVM,一旦涉及 Context、SharedPreferences、PackageManager,就别硬扛。这个坑本身与 33 无关,但升级时重跑测试会一次性全暴露,很多人误以为是 SDK 装坏了。
6. 用合并报告和 lint 基线守住 33 的兼容边界
依赖一多,权限声明就不再只由你一个人决定。三方库的清单在合并阶段会把 POST_NOTIFICATIONS、READ_MEDIA_* 这类权限悄悄并进来,用户在安装页看到一长串权限,转化率就是这么掉的。我的习惯是每次升到新的 targetSdk 后,先翻一遍合并报告,看看到底是谁加的。
# 合并报告在构建产物目录里,按模块找 find app/build/outputs/logs -name "manifest-merger-*-report.txt" \ -exec grep -n "POST_NOTIFICATIONS\|READ_MEDIA" {} + # 生成 lint 基线:存量问题冻结,新增问题才让流水线失败 ./gradlew :app:lintDebug -Dlint.baselines.continue=true第一条命令把所有和 33 新权限相关的合并记录打出来,能直接看到来源模块;第二条生成基线文件,之后在模块配置里引用它,CI 上只有新引入的问题才会红。基线不是免死金牌,它只是把“历史欠账”和“新欠账”分开,让升级这件事能分批次推进,而不是被几百条存量告警堵死在一个分支上。
android { lint { baseline file('lint-baseline.xml') // 存量问题不阻塞构建 abortOnError true // 新增问题仍然让构建失败 checkReleaseBuilds true } }abortOnError 保持 true 很关键。有人为了“先让流水线绿了”把它设成 false,结果几周后技术债滚成一团,再想收拾就得整体重构。基线文件要进版本库,每次清理掉一批存量问题就重新生成一次,体积会慢慢变小,这个过程本身就是升级进度的可视化。
提醒一句,lint 报告里和 targetSdk 强相关的那几条(比如未声明通知权限、使用已废弃的存储权限)优先级最高,先清它们,剩下的样式类问题可以后面慢慢来。
从那以后我每次动 targetSdk 这个数字,都会强制走一遍同样的顺序:先核对 platforms 与 build-tools 是否齐全,再跑一次合并报告看权限来源,最后用 lint 基线卡住新增问题,三步做完才允许提合并请求。这套动作看着笨,但它把“玄学崩溃”压缩成了可复现的检查项。希望帮到你。
本文还有配套的精品资源,点击获取