简介:面向安卓开发者和逆向入门者的 APK 修改工具资料包,围绕安装包结构认识、界面资源替换、反编译重打包、签名校验与权限调整等常见需求展开,适合希望系统了解安装包组成并动手定制应用的中初级读者。内容从清单配置文件、资源目录、字节码文件、架构库目录、附加资源与签名信息等模块讲起,并介绍反编译、代码查看、图形化修改和命令行签名等工具的使用场景,兼顾图标、启动画面、文字与布局替换,以及代码查看、广告去除、资源复用等操作思路。压缩包为 zip 格式,约 28.58MB,文件总数显示为 0,类型明细暂未提供,因此不展开具体文件构成。已有 1320 人学习下载,可帮助读者建立从解包、修改到重新签名的基础流程认知,同时提示版权、安全、兼容性与隐私风险,便于在合法前提下开展个性化定制与研究。
1. 从一次渠道包事故说起:apk修改工具到底在改什么
去年帮某公司的安卓渠道包做灰度,测试机上只回了一行INSTALL_PARSE_FAILED_INVALID_APK,日志里什么都没多说一句。翻到打包脚本最后才发现,zipalign和apksigner的顺序被上一位同事调反了,v2 签名把对齐结果又覆盖了一遍。apk修改工具这个说法听着笼统,落到手上其实就一组命令:解包、定位、改、回编译、对齐、签名、验证,七步里任何一步偷懒,后面全是玄学问题。这篇按我实际排障的顺序写,从 APK 的文件结构讲到 apktool 与 jadx 两条路线,再落到签名顺序和五个高频翻车点,适合给自有应用做渠道定制、兼容性适配、体积裁剪的 Android 开发,也适合想搞明白重打包到底动了哪些字节的逆向入门者。
2. 拆开一个 APK:五层结构决定你该用哪把刀
很多人拿到 APK 第一反应是解压,然后对着一堆二进制 XML 发愣。先把容器结构看清楚,后面选工具就不会乱。一个标准 APK 从上到下大致是 ZIP 容器、编译后的二进制资源、编译后的字节码、原生库和签名块这几层,每层的可编辑性差别很大,能不能改、改完要不要重新编译,取决于你动的是哪一层。
我一般会先用unzip -l把目录树打出来,比任何工具都快,也最不容易被工具的“智能处理”误导。
2.1 ZIP 容器与三个不能碰的目录
# 只看文件清单,不解压 unzip -l app-release.apk | head -40 # 看某个条目是否被压缩存储(关键看 Compressed size 是否等于 Uncompressed size) unzip -v app-release.apk | grep -E "resources.arsc|AndroidManifest|\.so$"上面两条命令解决的是两个问题:文件清单告诉你这个包有多大、有没有多余 dex、so 库铺了几套 ABI;unzip -v的压缩率则直接决定重打包后能否安装。APK 本质是 ZIP,但不是所有条目都能随便压缩,AndroidManifest.xml、resources.arsc以及lib/下的 so 在多数 targetSdk 下要求存储或特定对齐方式,尤其是 Android 11 之后resources.arsc必须保持未压缩且四字节对齐,否则安装器直接拒绝。
而META-INF/目录是签名块的家,任何改动只要动了这个目录里的文件,v1 签名立刻失效,所以它属于“改了就得整包重签”的区域。实务上我会把 APK 内部划成三块:可自由改的(assets、raw 资源)、编译后要重打包的(res、dex、manifest)、绝对不能手改只能靠工具重建的(META-INF)。分清楚这三块,你才知道自己该走全量重打包还是只替换 dex。
2.2 dex、resources.arsc、AndroidManifest 的分工
| 文件/目录 | 实际格式 | 常用编辑工具 | 改错后的典型症状 |
|---|---|---|---|
classes*.dex | Dalvik 字节码 | jadx 看、baksmali/smali 改、apktool 全量 | 类找不到、NoClassDefFoundError |
resources.arsc | 二进制资源索引表 | apktool 反编译为res/values | Resources$NotFoundException、安装失败 |
AndroidManifest.xml | 二进制 XML | apktool 解为明文 | 组件不注册、exported报错 |
res/下 XML | 二进制 XML | apktool 解码 | 布局错乱、资源 ID 对不上 |
lib/<abi>/*.so | ELF | 一般不动,只做裁剪 | UnsatisfiedLinkError |
META-INF/ | 签名文件 | apksigner 重建 | 签名校验失败 |
表格里最容易被低估的是resources.arsc。它不是简单的字符串表,而是资源 ID 到值的映射索引,res/目录下的每个文件都和它有一一对应关系。你手改res/layout/xxx.xml却不重新编译 arsc,运行时拿到的还是旧 ID,轻则布局没变化,重则直接崩在inflate。所以改资源必须走 apktool 回编译,没有捷径。
AndroidManifest.xml同理,它是二进制 XML 格式,用文本编辑器打开只会看到乱码。apktool 会把它还原成可读 XML,但还原过程中会插入platformBuildVersionCode之类的属性,某些校验严格的场景下需要手动清掉。
2.3 工具选型:四条路线对照
| 路线 | 工具组合 | 适用场景 | 代价 |
|---|---|---|---|
| 全量重打包 | apktool d + b | 改资源、改 manifest、改 smali | 慢,容易踩资源编译坑 |
| 仅资源 | apktool d -s + b | 只改res/、strings、图标 | 不能碰代码 |
| 仅代码 | baksmali + smali + zip 替换 | 只改一两个方法 | 需自己处理 dex 合并 |
| 二进制补丁 | 十六进制对比 + 重签 | 极小的字符串替换 | 长度必须严格一致 |
选型上有条血泪经验:改动范围越小,走的工具链就该越短。改一个开关常量,用不着 apktool 全量重打包再等三分钟,baksmali 打完补丁塞回 ZIP 就行;反过来,只要碰了res/values/strings.xml,就必须老老实实走 apktool,因为资源 ID 会重新分配。
另外两个辅助工具值得常备:jadx负责把 dex 还原成可读 Java 用于定位,apkanalyzer(Android SDK 自带)能直接对比两个 APK 的体积构成和方法数,裁 so、找冗余资源时比肉眼看目录快得多。
3. apktool 全量路线:解码、改资源、回编译
apktool 是这条路线上绕不开的工具,但它也是最容易用错参数的一个。很多人把它当成“解压 + 反编译”一步到位的黑匣子,结果改完回编译出来装不上,又回头怀疑工具本身有 bug。实际上 apktool 的参数决定了它到底帮你做了多少事,选错参数,改动就是白做。
3.1 解码参数:-s、-r、-f 各自的代价
# 全量解码:资源 + dex 都反编译成 smali apktool d app-release.apk -o decoded -f # 只解资源,dex 原样保留(不生成 smali 目录) apktool d app-release.apk -o decoded -s -f # 只解 dex 和 manifest,资源保持二进制不反编译 apktool d app-release.apk -o decoded -r -f # 指定 aapt2 处理,兼容较新的资源格式 apktool d app-release.apk -o decoded -f --use-aapt2-s的含义是 skip sources,即不反编译 dex,decoded/下会保留原始的classes.dex而不生成smali/目录。这个参数适合“只改资源”的场景,能省掉一大半解码时间,但代价是你没法在里面改任何代码,回编译时 apktool 会原样把 dex 塞回去。
-r是 skip resources,反过来保留二进制资源,只把 dex 和 manifest 解出来。这里有个坑:-r模式下res/目录里躺的是原始二进制文件,你即便改了也不会被重新编译,很多人以为改了图标就生效,实际打出来还是旧图。除非你只动 smali,否则别用-r。
-f是强制覆盖已有输出目录。这个参数本身无害,但它会让“上次解码残留的文件”被清空,如果你在decoded/里手写了补丁文件却忘了备份,一条-f就全没了。我一般会先cp -r decoded decoded.bak再动手。
--use-aapt2建议默认带上。老版本 apktool 用的 aapt 对某些新资源格式支持不好,回编译时报Invalid resource或者资源 ID 冲突,换 aapt2 基本能解决。
3.2 改 resources 与 AndroidManifest 的边界
资源侧能改的东西比想象中多:res/values/strings.xml改文案、res/values/colors.xml改主题色、res/mipmap-*换图标、res/values/bools.xml关掉某个特性开关。改完这些不需要动 smali,apktool 回编译时会重新生成resources.arsc,资源 ID 映射自动对齐。
<!-- decoded/res/values/strings.xml:改应用名 --> <string name="app_name">渠道演示包</string> <!-- decoded/AndroidManifest.xml:targetSdk 31+ 必须显式声明 exported --> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>上面这段exported是这两年最常见的坑。targetSdk 升到 31 之后,带 intent-filter 的组件不写android:exported会在安装阶段被拒,报错信息还很不直观。做渠道定制时如果把启动 Activity 换掉,记得同步补这个属性。
版本号不在 manifest 里改,而是在decoded/apktool.yml:
versionCode: 10203 versionName: 1.2.3-channel minSdkVersion: 23 targetSdkVersion: 33这个文件是 apktool 回编译时的权威来源,直接改 manifest 里的versionCode反而会被覆盖。改完apktool.yml再回编译,adb shell dumpsys package <包名>里能看到新版本号,这是验证改动是否真的写进去了的最快方式。
3.3 回编译与增量替换 smali
# 回编译,输出的是未签名、未对齐的 APK apktool b decoded -o dist/app-unsigned.apk --use-aapt2 # 如果只想替换 dex,不重打包资源,可以走增量路线 # 1) 用 baksmali 把原始 dex 拆开 baksmali d dist/original/classes.dex -o smali_out # 2) 改完 smali 后重新汇编回 dex smali a smali_out -o classes.dex # 3) 直接替换 ZIP 里的条目,资源部分完全不动 zip -j dist/app-unsigned.apk classes.dex增量替换这条路的价值在于:资源部分一个字节都没动,resources.arsc不会被重新生成,也就避开了所有资源编译相关的坑。代价是你得自己保证 dex 的格式正确,多 dex 场景下还要注意classes2.dex的对应关系,改错 dex 就是白忙。
回编译失败时,先看 apktool 的 stderr 第一行,通常是资源编译错误,会直接指出哪个 XML 的哪一行。如果报的是brut.androlib.AndrolibException: Could not smali,那基本是 smali 语法问题,把改过的文件单独拿出来对照原文件看寄存器数量是否匹配。
4. 轻量路线:jadx 定位 + smali 单点补丁
只改一两个方法的时候,全量 apktool 显得太重。我的常规做法是 jadx 先把目标方法找出来,再用 baksmali/smali 做单点替换,整个过程不碰资源,回编译成本几乎为零。这条路线适合加调试开关、改常量、临时屏蔽某个 SDK 的初始化。
4.1 用 jadx 找到要改的那一行
# 反编译成 Java 源码便于阅读,输出到 out/ jadx -d out app-release.apk # 只搜字符串,快速定位常量或日志 tag jadx -d out --no-res app-release.apk grep -rn "isDebugEnabled" out/ | head-d指定输出目录,--no-res跳过资源反编译能省不少时间。jadx 的输出是“尽量还原”的 Java,遇到混淆代码会有synthetic方法或者$后缀的类名,但方法体逻辑基本可信。找到目标方法后,记下它的完整签名(类名 + 方法名 + 参数类型 + 返回类型),smali 里就是靠这个签名定位的。
一个实用技巧:jadx 反编译出来的方法旁边会标注它在 dex 里的位置信息,但更直接的办法是在 smali 目录里搜方法签名。混淆过的类名可能变成a.b.c,方法名也可能变成a,这时候只能靠字符串常量反查——先 grep 到常量所在的 smali 文件,再往上看它属于哪个方法。
4.2 baksmali / smali 只动一个方法
# 原始方法:返回一个 boolean 开关 .method public static isDebugEnabled()Z .registers 1 const/4 v0, 0x0 return v0 .end method # 补丁后:常量改成 1,本地调试时强制打开 .method public static isDebugEnabled()Z .registers 1 const/4 v0, 0x1 return v0 .end method这个例子里.registers 1表示方法体分配了 1 个寄存器,v0就是那唯一的寄存器,const/4表示四字节常量赋值。改动只涉及一个字节的立即数,方法签名和寄存器数量都没变,所以不会影响调用方。
要动更复杂的逻辑时,寄存器数量是最容易翻车的地方。比如给方法体加一行日志,就需要额外的寄存器,.registers的值必须同步加一,否则回编译出来的 dex 校验不过,运行时报VerifyError。我一般的做法是宁愿不改寄存器总数,用方法里已有的空闲寄存器,或者干脆把整个方法体替换成一段固定逻辑。
# 拆 dex baksmali d classes.dex -o smali_src # 改完之后汇编回去,注意 -a 指定 API 级别要和原包一致 smali a smali_src -o classes.dex -a 33 # 替换进 APK zip -j app-unsigned.apk classes.dex-a参数指定 API 级别,影响 smali 汇编时使用的指令集版本。这个值和原 APK 的minSdkVersion对不上时,可能出现低版本设备无法验证方法的问题。稳妥做法是从apktool.yml里读出原值照抄。
4.3 怎么验证补丁真的生效
改完不验证等于没改。我的验证顺序是三步:
第一步,apktool d把刚打出来的包重新解一遍,grep 你改的那个常量或者字符串,确认写进去了。这一步排除“改错文件”和“ZIP 替换没生效”两种可能。
第二步,装到设备上,adb shell dumpsys package <包名>看版本号、adb logcat | grep <你的 tag>看运行时行为。如果改的是开关方法,找一个会调用它的入口触发一次,看日志分支有没有变化。
第三步,如果改动涉及 dex 结构,用dexdump -d classes.dex | head -50检查 dex 头部是否合法,或者干脆用 Android SDK 里的apkanalyzer dex packages看类列表能不能正常解析。dex 损坏的症状很有迷惑性,安装能过,一启动就ClassNotFoundException,很容易误判成混淆问题。
5. 签名、对齐与常见排查:安装失败的九成在这
到这一步,包已经改完了,剩下的是让它能装上去。签名和对齐是整条链路上最没有技术含量、却又最容易出错的两步,我见过的安装失败里至少九成能归到这两件事上。搞懂 v1/v2/v3 的差异和工具执行顺序,能省掉大量无效排查。
5.1 v1/v2/v3 的差异与选择
| 签名方案 | 引入版本 | 覆盖范围 | 特点 |
|---|---|---|---|
| v1 (JAR) | 很早 | META-INF/下三个文件 | 逐文件摘要,改任何文件都会失效 |
| v2 | Android 7.0 | 整个 APK 的二进制块 | 校验快,但对齐必须在签名前 |
| v3 | Android 9.0 | 增加密钥轮换支持 | 与 v2 并存,向下兼容 |
| v4 | Android 11 | 独立.idsig文件 | 增量安装用,一般打包不需要 |
实际打包时我的建议是三个都开:--v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true。v1 保证低版本设备能装,v2/v3 保证新设备校验通过。只开 v2 在 Android 6 及以下会直接装不上,这个兼容性问题在渠道包里特别常见。
# 生成签名密钥,一次性操作 keytool -genkeypair -v \ -keystore demo-release.jks \ -alias demo \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass 你的密码 -keypass 你的密码 \ -dname "CN=demo, OU=dev, O=demo, L=city, ST=state, C=CN"-validity 10000是天数,约 27 年,够用了。-dname里不要填真实信息,测试密钥随便写就行。这个 jks 文件要保管好,丢了就没法给已安装的包做覆盖升级,只能让用户卸载重装。
5.2 zipalign 与 apksigner 的顺序
# 顺序不能反:先对齐,再签名 zipalign -p -f -v 4 dist/app-unsigned.apk dist/app-aligned.apk # 签名,输出最终包 apksigner sign \ --ks demo-release.jks \ --ks-key-alias demo \ --ks-pass pass:你的密码 \ --key-pass pass:你的密码 \ --v1-signing-enabled true \ --v2-signing-enabled true \ --v3-signing-enabled true \ --out dist/app-signed.apk \ dist/app-aligned.apk # 验证签名和对齐 apksigner verify -v --print-certs dist/app-signed.apk zipalign -c -v 4 dist/app-signed.apkzipalign的-p参数是针对 so 库的页对齐,API 23 以上必须带,否则部分设备加载 so 会失败。4是四字节对齐,这个值不要改。
顺序的原理很简单:v2/v3 签名是对整个 APK 文件做二进制摘要,签名之后再跑zipalign会把文件内容挪位置,摘要立刻失效,但apksigner verify有时候还能通过(因为它读的是签名块里的摘要),装到设备上才报错。这种“验证通过但装不上”的情况最消耗时间,我踩过一次之后就把顺序写死在脚本里了。
apksigner verify -v的输出会列出 v1/v2/v3 各自的校验结果,三个都是true才算干净。--print-certs打印证书指纹,和已安装版本对比能快速判断是不是签名不一致导致的覆盖安装失败。
5.3 五条高频翻车记录
第一条:resources.arsc被压缩,安装报INSTALL_PARSE_FAILED_INVALID_APK。现象是签名验证全过,装到 Android 11 以上设备直接被拒。原因是某些打包脚本用zip命令重新打包时默认压缩所有条目,把 arsc 也压了。解决办法是用zipalign -p -f -v 4处理,它会顺带把 arsc 调整为存储且对齐;或者用zip -X -0 resources.arsc显式指定不压缩。
第二条:解码时用了-r,改完res/里的图标没生效。现象是包能装、能跑,就是图标和文案还是旧的。原因是-r模式下资源没有被反编译,res/下是原始二进制,改了也不会进编译流程。解决办法是去掉-r重新全量解码,改完再回编译。这个坑我见过不止一个人踩,因为-r解码速度快,很容易图省事。
第三条:解码时用了-s,改完 smali 改动全丢。现象是明明改了smali/下的文件,回编译出来行为没变。原因是-s模式下 apktool 保留了原始classes.dex,回编译时优先使用它,smali/目录压根没参与编译。解决办法是改代码就不要带-s,或者删掉decoded/下的原始 dex 文件强制走 smali 重编。
第四条:重签名后覆盖安装报INSTALL_FAILED_UPDATE_INCOMPATIBLE。现象是adb install -r失败,提示签名不一致。原因是设备上已装的版本用的是另一套密钥。解决办法是adb uninstall <包名>后重装,或者从原包里把签名证书导出来复用(但那需要私钥,一般拿不到)。做渠道包时一定要用同一套 jks,别每次临时生成。
第五条:先签名后对齐,安装成功但一启动就崩。现象很隐蔽,apksigner verify说没问题,装上去也能打开,跑到某个功能点直接Resources$NotFoundException或者 so 加载失败。原因是zipalign在签名之后执行,破坏了 v2 签名的完整性校验。解决办法是把顺序改成zipalign→apksigner,并且把这条写进 CI 脚本,靠人记迟早会出事。
6. 自动化流水线与差分校验:把改动关进笼子
手工敲一遍上面所有命令,十分钟能搞定;一周做二十个渠道包,手工敲就是灾难。我的做法是把整条链路固化成一个脚本,参数全部走变量,任何一步失败就退出,不给“中间状态”留机会。
6.1 一条命令跑到签名完成
#!/usr/bin/env bash set -euo pipefail SRC_APK="$1" WORK="build/$(basename "${SRC_APK%.apk}")" KS="demo-release.jks" ALIAS="demo" rm -rf "$WORK" && mkdir -p "$WORK/dist" # 1) 解码:改代码就带 --use-aapt2,不要带 -s apktool d "$SRC_APK" -o "$WORK/decoded" -f --use-aapt2 # 2) 这里插入你的改动(脚本化替换、资源覆盖等) # cp -r patches/res/. "$WORK/decoded/res/" # 3) 回编译 apktool b "$WORK/decoded" -o "$WORK/dist/unsigned.apk" --use-aapt2 # 4) 对齐必须在签名之前 zipalign -p -f 4 "$WORK/dist/unsigned.apk" "$WORK/dist/aligned.apk" # 5) 签名 apksigner sign \ --ks "$KS" --ks-key-alias "$ALIAS" \ --ks-pass "pass:$KS_PASS" --key-pass "pass:$KEY_PASS" \ --v1-signing-enabled true --v2-signing-enabled true --v3-signing-enabled true \ --out "$WORK/dist/final.apk" "$WORK/dist/aligned.apk" # 6) 双重验证,任何一项不过直接退出 apksigner verify -v "$WORK/dist/final.apk" > /dev/null zipalign -c 4 "$WORK/dist/final.apk" echo "OK: $WORK/dist/final.apk"set -euo pipefail是这段脚本最值钱的一行,任何一条命令返回非零就立刻中断,不会带着半成品继续往下跑。密码走环境变量KS_PASS/KEY_PASS,不写死在脚本里,避免提交到仓库。第六步的zipalign -c是对齐检查模式,只校验不改文件,放在最后当守门员。
脚本化之后还有个额外好处:改动可以做成 patch 目录,res/下放渠道专属的资源文件,脚本里一句cp -r覆盖进去,比手工一个个改稳得多,也方便版本管理。
6.2 差分校验:确认只改了该改的
# 解压两个包对比文件清单和大小,快速看改动范围 mkdir -p diff/orig diff/new unzip -q original.apk -d diff/orig unzip -q final.apk -d diff/new diff -rq diff/orig diff/new | grep -v "META-INF" | head -20 # 对比 dex 里的类清单,确认没有意外引入新类 apkanalyzer dex packages diff/orig/classes.dex > diff/orig-classes.txt apkanalyzer dex packages diff/new/classes.dex > diff/new-classes.txt diff diff/orig-classes.txt diff/new-classes.txt | headdiff -rq会列出所有内容不同的文件,grep -v META-INF是为了排除签名文件本身的差异,否则每次都会刷一屏。如果改动只在预期范围内,输出应该只有你动过的那几个文件。apkanalyzer那段是防“误引入”的——有时候 smali 汇编参数不对,会把整个 dex 的类索引重排,表面上看不出来,实际方法数暴涨,装到低端机上直接65535溢出。
从那以后我每次出渠道包,都会强制走一遍这个差分,哪怕只改了一个字符串。改动范围失控是重打包里最难查的一类问题,肉眼看不出来,工具一对比就现原形。希望帮到你。
本文还有配套的精品资源,点击获取