Apktool ApkInfo 全解:apktool.yml 是怎么被读写并支撑重打包的
【免费下载链接】ApktoolA tool for reverse engineering Android apk files项目地址: https://gitcode.com/GitHub_Trending/ap/Apktool
用 Apktool 反编译一个 APK,输出目录里会生成apktool.yml;重新打包时,又靠这个文件决定怎么构建。可这些字段到底是谁写进去的、构建时又是谁在消费?答案是 meta 包下的 ApkInfo 类——Apktool 中 APK 元数据的唯一载体,贯穿解码与重打包两个阶段。
整体架构:一个对象、一套自研 YAML、两个消费方
ApkInfo 处在两个环节的交接点上,数据链长这样:
APK ──> ApkDecoder(解码)──> ApkInfo 对象 ──save()──> apktool.yml apktool.yml ──ApkInfo.load()──> ApkInfo 对象 ──> ApkBuilder(重打包)拆开看是三层:
- 元数据层:ApkInfo 及 meta 包下的五个子对象(UsesFramework、SdkInfo、VersionInfo、ResourcesInfo,外加 usesLibrary 与 doNotCompress 两个列表)
- 序列化层:brut.j.yaml 模块里自研的 YamlReader/YamlWriter,不依赖第三方 YAML 库,逐行解析
- 消费方:ApkDecoder 负责填充并落盘,ApkBuilder 负责读回并消费
简单说,apktool.yml是解码与重打包之间唯一的交接契约,ApkInfo 是这份契约唯一的翻译器。
核心流程:从解码到重打包走一遍
解码侧:ApkDecoder 边解边填,最后落盘
在 ApkDecoder 里能看到完整的写入链路:
mApkInfo = new ApkInfo(); mApkInfo.setVersion(mConfig.getVersion()); mApkInfo.setApkFile(mApkFile); mResDecoder = new ResDecoder(mApkInfo, mConfig); // ... 资源、Manifest 解码完成后 ... mApkInfo.save(outDir);关注最后一行:save() 就是打开输出目录下的固定文件名apktool.yml调用 write()。SDK 版本、版本号、资源包 ID 这些字段并非都直接由 ApkDecoder 填,而是解码资源与 Manifest 的阶段回写进同一个对象,featureFlags、doNotCompress 条目也是这一过程陆续追加的。
重打包侧:ApkBuilder 读回来立刻用
ApkBuilder 的第一步正好是上面链路的反向操作:
mApkInfo = ApkInfo.load(mApkDir); String minSdkVersion = mApkInfo.getSdkInfo().getMinSdkVersion(); mSmaliBuilder = new SmaliBuilder(minSdkVersion != null ? SdkInfo.parseSdkInt(minSdkVersion) : 0); mAaptInvoker = new AaptInvoker(mApkInfo, mConfig);关键在第二行起:minSdkVersion 决定 smali 汇编的输出等级,AaptInvoker 则拿 apkInfo 里的框架与资源信息决定怎么编译资源。而apktool.yml一旦缺失,load() 会抛 IOException 并被包装成 AndrolibException——这就是"没有 apktool.yml 就构建不了"的直接原因。
序列化层:逐行解析,而不是 YAML 库
YamlReader 在构造时把输入流切成一行行 YamlLine,每行只记四个属性:indent(空格数)、hasColon、isItem(-开头)、isComment。readRoot 只处理缩进为 0 且带冒号的行,再分发给 readItem();sdkInfo 这类嵌套对象由 readObject 读取,要求数据行缩进严格大于键行。这个设计带来一个直接后果:无法识别的行——未知字段、缩进不符——会被静默跳过,而不是报错。
apktool.yml 里每个字段到底存了什么
看仓库测试资源里的真实样例(testapp/apktool.yml 前段):
version: 2.3.2 apkFileName: testapp.apk usesFramework: ids: - 1 versionInfo: versionCode: 1 versionName: 1.0 resourcesInfo: packageId: 127 sparseEntries: false # ... 其余字段省略 ... doNotCompress: - arsc - png各字段与 ApkInfo 的成员变量一一对应,几个容易忽略的点:
- 空字段干脆不写:write() 里每个字段都有非空判断,所以某个文件里看不到 sdkInfo 属正常现象
- -1 与 null:versionCode 为 -1、versionName 为 null 表示原 Manifest 没声明版本,YamlWriter 会把 null 写成字符串
null,读回时再还原 - packageId 通常为 127(0x7f):APK 里的资源引用都依赖这个值,改动它会导致引用错位
- sdkInfo 存的是字符串:parseSdkInt 还认识 "O"、"TIRAMISU" 这类发布名别名;getTargetSdkVersionBounded() 会把 target 钳到 [min, max] 区间内
- apkFileName 有加载校验:值为
./..或含路径分隔符会直接抛 SecurityException,这是对 CVE-2022-0476 的防御
典型用法:查框架、批量改元数据、控压缩
- 如果你要快速确认 APK 依赖的框架:看
usesFramework.ids,1 是默认 Android 框架;tag 有值时指向自定义框架标签 - 如果你要在脚本里批量改元数据:load → 修改 SdkInfo/VersionInfo → save() 即可;仓库里的 ApkInfoSerializationTest 做的就是这种 load → save → 再 load 的往返断言,可当模板参考
- 如果你要控制重打包时的文件压缩:doNotCompress 决定哪些文件在新 APK 中以未压缩方式存放,解码器会自动往这里记录 0 字节文件和不常规扩展名的文件,别手删条目
常见坑:缩进丢数据、SecurityException、解析异常
缩进错误不报错,只丢数据。现象:改了 apktool.yml,构建行为却像没改。原因:readRoot 会静默跳过缩进不符或没有冒号的行,未知字段同样被忽略。解法:手工改完后用 ApkInfo.load() 读回并断言关键字段,ApkInfoReaderTest 的 testSkipIncorrectIndent 就是这么验证的。
apkFileName 写成路径直接 SecurityException。现象:抛 "Malicious value for apkFileName" 而非解析错误。原因:加载时校验拒绝./..及含/、\的值。解法:该字段只记 APK 原始文件名,别带路径。
sdkInfo 手写真数导致解析异常。现象:构建失败,NumberFormatException。原因:值按字符串读入,parseSdkInt 先试发布名别名、回退 Integer.parseInt,对任意写法并不宽容。解法:写数字(如 "25")或合法别名(如 "TIRAMISU")。
删掉 apktool.yml,重打包立刻失败。现象:apktool b报 FileNotFoundException。原因:ApkBuilder 第一步就是 ApkInfo.load(mApkDir),它是构建参数的唯一来源。解法:把它当作反编译目录的一部分提交进版本库。
apktool.yml是解码与重打包两个阶段之间的交接契约,ApkInfo 是这份契约唯一的翻译器。看懂这个文件,"构建为什么长这样"就不再是黑盒。
【免费下载链接】ApktoolA tool for reverse engineering Android apk files项目地址: https://gitcode.com/GitHub_Trending/ap/Apktool
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考