APK修改全链路:apktool反编译、重签名与对齐避坑指南
简介:面向Android应用开发者与逆向学习者的APK修改工具资料包,围绕APK包结构、资源编辑、反编译重打包、签名校验与权限调整等核心环节展开,帮助读者建立从解包到重签的完整认知,适合具备一定Android基础、希望进行个性化定制或安全研究的人群。内容结合AndroidManifest.xml、res资源目录、classes.dex、lib库与META-INF签名信息等组成,梳理Apktool、dex2jar与JD-GUI、Jeb、Apk Editor、SignApk等工具的适用场景,并提示版权、安全、兼容性与隐私风险。压缩包约28.58MB,文件总数与类型明细暂未提供,实际内容以压缩包为准。已有1320人学习,可作为了解APK修改流程、工具选型与风险边界的入门参考,也便于按学习路径逐步练习资源替换、代码查看和签名验证。
1. 拿到一个 APK 想动手改,先别急着找一键工具
改图标、换文案、调渠道号、改包名做白牌分发,这些需求在渠道运营和 OEM 定制里几乎每周都会遇到。很多人第一反应是搜「apk修改工具」,下一个带界面的绿色软件,拖进去点两下,结果改完装不上,或者装上一点开就闪退。真正能干活的不是某一个工具,而是一条链路:apktool 负责拆和装,jadx 负责读逻辑,apksigner 和 zipalign 负责让改完的包在新系统上还能过安装校验。这篇笔记按「拆开看结构 → 决定改资源还是改 smali → 回编译 → 重签名对齐 → 排错 → 验证」的顺序把这条链走一遍。
适合做渠道包分发、白牌定制、多语言适配、自家应用线上排障的从业者。前提只有一条:只对自己开发、或已经拿到明确授权的 APK 动手,别人的商业应用不要碰,签名校验和加固那一层本来也不是常规改包能解决的。
2. 拆开 APK 看结构:zip 里那几类文件决定了你能改什么
2.1 一个 APK 本质是改了后缀的 zip,但不是所有文件都能文本编辑
把任意 APK 后缀改成 zip 就能解开,里面通常是这几类东西,职责分得很清楚:
| 文件 / 目录 | 形态 | 能不能直接改 | 说明 |
|---|---|---|---|
| AndroidManifest.xml | 二进制 AXML | 不能 | 包名、版本号、权限、入口 Activity 都在这 |
| classes.dex / classes2.dex… | Dalvik 字节码 | 不能 | 方法数超 65536 会拆成多个 dex |
| resources.arsc | 二进制资源索引表 | 不能 | 字符串、颜色、尺寸、资源 ID 映射 |
| res/ | 图片与 XML | 部分能 | 图片可直接换,XML 若是编译过的也是二进制 |
| lib/ | .so 动态库 | 看情况 | 按 ABI 分目录,arm64-v8a、armeabi-v7a 等 |
| assets/ | 任意文件 | 能 | 游戏资源、配置、离线包常放这 |
| META-INF/ | 签名文件 | 必须重做 | v1 签名的摘要和证书都在这里 |
新手最容易翻车的一步,是用文本编辑器直接打开 AndroidManifest.xml,看到一堆乱码就以为文件坏了。它没坏,只是被 aapt 编译成了二进制 AXML,必须经过 apktool 反编译才能还原成可读 XML,改完再由 apktool 重新编译回二进制。resources.arsc 同理,里面的字符串池和资源 ID 表是强关联的,手改一个字节就可能让整张表的偏移全错。
顺带说一句,从 Android 9 开始不少应用会把 resources.arsc 做成「资源混淆」形态,res 目录里的文件名变成r/a.png这种,arsc 里的条目名也被压短。这种包用 apktool 解出来照样能改,但你在 res 里按名字找图会找得很痛苦,得先用aapt2 dump resources把资源 ID 和实际路径对出来。
2.2 工具链选型:apktool、jadx、dex2jar 各管哪一段,别拿错锤子
这四个工具经常被混着叫「apk修改工具」,其实边界很清楚:
| 工具 | 输入 | 输出 | 能否回编译 | 典型用途 |
|---|---|---|---|---|
| apktool | APK | smali + 可读 XML + 资源 | 能 | 改资源、改 smali、改清单 |
| jadx / jadx-gui | APK / dex | Java 伪代码 | 不能 | 快速读懂逻辑、搜关键词 |
| dex2jar + JD-GUI | dex | jar → Java 源码 | 不能 | 老组合,jadx 出来之后用得少了 |
| aapt2 | APK | 资源清单、badging | 不能 | 查包名版本、查资源 ID |
| apksigner + zipalign | APK | 已签名 / 已对齐的 APK | — | 让改完的包能装 |
选型逻辑很简单:只要你要产出可安装的包,就必须走 apktool 这条线,因为它能回编译。jadx 是只读的,它的价值在于让你在动手之前先搞清楚「我要改的那个判断在哪个类里」,不然你在几万个 smali 文件里瞎 grep,纯属浪费时间。
我一般的顺序是:先用 aapt2 看包名和版本,再用 jadx 打开搜关键词定位到类名和方法名,然后 apktool 解包,直接按类名去找对应的 smali。这样比全程在 smali 里翻要快一个数量级。
提示:apktool 依赖 Java 运行环境,JDK 8 以上都能跑,但版本别太旧。新系统的 APK 会用到新的资源属性,老版本 apktool 回编译时会报「No resource found」,这不是你改错了,是工具不认识新属性。
2.3 第一次解包:命令、产物和三个必看参数
先用 aapt2 摸一遍底,确认包名、版本号、最低支持版本和入口 Activity:
# 查看 APK 基本信息,重点看 package、versionCode、sdkVersion、launchable-activity aapt2 dump badging demo.apk然后解包:
# -f 覆盖已存在的输出目录;-o 指定输出目录;-s 不反编译 dex(只改资源时用) apktool d demo.apk -o demo_out -f # 只解 dex,不动资源,适合只改 smali 的场景,速度更快 apktool d demo.apk -o demo_out_src -f --no-res # 只解资源,不动 dex,适合只换图和文案 apktool d demo.apk -o demo_out_res -f --no-src解完之后目录长这样,每一项对应后面要改的地方:
demo_out/ ├── AndroidManifest.xml # 已还原成可读 XML ├── apktool.yml # 工具版本、sdkInfo、doNotCompress 等元信息 ├── res/ # 资源,图片和 XML 都在这里 ├── resources.arsc # 注意:这个通常不会出现在解包目录,回编译时重新生成 ├── smali/ # 主 dex 反编译出来的 smali ├── smali_classes2/ # 第二个 dex,以此类推 ├── lib/ # so 库 ├── assets/ # 原始资源 └── original/ # 原始 META-INF 和 AndroidManifest,回编译时参考用几个参数值得单独说。-s和--no-src是一个意思,跳过 dex 反编译,只改资源时能省掉大半时间;-r和--no-res跳过资源,只留 smali;-f不加的话,输出目录已存在会直接中断。original/目录别删,回编译时 apktool 会拿它做参考,尤其是签名相关文件。
apktool.yml建议每次解包后都扫一眼,里面有几个关键字段:minSdkVersion决定你签名时要不要保留 v1;doNotCompress列出不能被压缩的文件后缀,动过之后可能导致某些资源读不出来;versionInfo是版本号,改渠道包时经常要动它。
3. 改资源还是改 smali:两条路的边界和各自的回编译姿势
3.1 资源替换:res 目录改了,资源 ID 不能跟着乱
改资源是最常见也最容易上手的一类需求:换图标、换启动图、改应用名、改默认语言文案。操作本身不复杂,但有一个隐形的坑——资源 ID。
APK 里的每个资源都有一个 32 位 ID,格式是0xPPTTEEEE,PP 是包 ID,TT 是类型(string、drawable、layout),EEEE 是条目序号。这个序号在回编译时由 apktool 按照res/values/public.xml重新分配。如果你删掉了一个资源文件,后面的序号就会整体前移,其他还在引用旧 ID 的地方就会指错,表现出来就是「改了个图标,结果设置页的文案变了」。
所以第一条铁律:改资源只做替换,不做删除。要「删掉」某个资源,最稳的做法是把它替换成一个透明图或者空字符串,而不是把文件删了。
具体操作:
# 1. 改应用名:找到 res/values/strings.xml 里的 app_name grep -n "app_name" demo_out/res/values/strings.xml # 2. 换图标:确认图标实际路径,mipmap 目录下通常有多个密度版本 ls demo_out/res/mipmap-xxhdpi/ # 3. 替换时保持文件名、格式、尺寸一致,png 换 png,webp 换 webp cp new_icon.png demo_out/res/mipmap-xxhdpi/ic_launcher.png # 4. 如果是 .9.png,边界那 1 像素黑线必须保留,否则回编译直接报错.9.png是九宫格拉伸图,文件四周各有 1 像素宽的黑线用来标记可拉伸区域和内容区。用普通图片编辑器打开再保存,很容易把这圈黑线抹掉或者改色,回编译时 aapt2 会直接拒绝。正确做法是用 Android SDK 里的 draw9patch 工具,或者干脆不要在像素层面动它,只做整体替换。
资源替换还有一个版本兼容问题:同一个图标在不同密度目录下都要换,只换mipmap-xxhdpi会导致低密度设备上还是旧图。要么全换,要么在res/values/下加一条引用指向新图。
3.2 smali 定位:从入口 Activity 顺着找到那个方法
改 smali 是「改逻辑」这条路,典型场景是自家应用线上出问题要临时打补丁、或者给不同渠道包注入不同的初始化参数。它比改资源门槛高,但套路是固定的。
第一步永远是从 AndroidManifest 找入口:
# 找 launchable-activity,这是应用启动的第一个页面 grep -n "android.intent.action.MAIN" -A 3 demo_out/AndroidManifest.xml拿到 Activity 的全限定名,比如com.demo.app.MainActivity,对应的 smali 路径就是smali/com/demo/app/MainActivity.smali。打开它,找onCreate方法,顺着invoke-*指令往下看调用了谁。
如果不知道目标在哪,用关键词反查更快:
# 在 smali 里搜字符串常量,比搜方法名更有效,因为常量不会被打散 grep -rn "channel_id" demo_out/smali*/ | head -20 # 搜到之后看它所在的 .method,再往上找这个方法的调用方 grep -rn "Lcom/demo/app/Config;->getChannel" demo_out/smali*/smali 的语法有几个必须知道的点。.locals N声明这个方法用几个寄存器,v0 到 v(N-1);带参数的方法还会用 p0 到 pN,p0 通常是 this。改返回值最常用的两条指令:
# 返回布尔 true const/4 v0, 0x1 # 返回布尔 false const/4 v0, 0x0改条件跳转的话,if-eq(相等跳转)和if-ne(不等跳转)互换,或者在跳转前插入一条const/4改掉比较用的值。这里有个血泪经验:改完一定要对一遍寄存器数量。如果你新增了一条用到 v3 的指令,但方法头写的是.locals 3(只有 v0、v1、v2),回编译能过,装上去运行到这就抛VerifyError直接崩,而且 logcat 里报的位置和真实原因隔得很远,很难查。
改 smali 只对自有应用或已授权应用动手。商业应用里的签名校验、完整性校验本来就会拦这一类改动,硬绕属于另一条路,不在常规改包范围内。
3.3 回编译:apktool b 的三类报错和对应改法
改完之后回编译:
# 基本回编译,产物是未签名的 APK apktool b demo_out -o demo_new.apk # 指定 aapt2 作为资源编译器,新版 APK 用得上 apktool b demo_out -o demo_new.apk --use-aapt2报错大概分三类,对照处理就行:
| 报错关键词 | 常见原因 | 处理方式 |
|---|---|---|
No resource found that matches the given name | 引用了不存在的资源,或工具版本不认识新属性 | 先确认是不是自己删了资源;不是的话升级 apktool 或加--use-aapt2 |
Public symbol ... defined multiple times | public.xml里同一个资源 ID 被分配了两次 | 检查是否手动往res/values/里加过同名资源 |
Invalid 9-patch或Crunching ... failed | 九宫格图边界黑线被破坏,或图片格式与后缀不符 | 用 draw9patch 重做边界;确认 png 没被改存成 jpg |
回编译成功后先别急着签名,用 aapt2 快速验一下产物结构对不对:
# 确认包名、版本号、入口 Activity 是否还是你要的 aapt2 dump badging demo_new.apk | head -5如果这里显示的package: name=和你预期的不一致,说明 AndroidManifest.xml 在解包或回编译过程中被动过,先回去查清单再往下走,别急着签名。
4. 重签名与对齐:改完的包装不上,九成卡在这两步的顺序
4.1 签名的三段结构:v1 在文件里,v2 和 v3 在整个文件的摘要里
APK 签名经过三代演进,现在一个包可能同时带三种签名:
- v1(JAR 签名):在
META-INF/下放MANIFEST.MF、CERT.SF、CERT.RSA,对每个文件单独算摘要。改任何一个文件,对应的摘要就对不上。 - v2(APK Signing Block):在 zip 中央目录之前插入一个签名块,对整个 APK 文件算摘要。它的特点是校验极快,而且任何字节的改动都会让签名失效,包括你以为无关紧要的对齐填充。
- v3:在 v2 基础上增加了密钥轮换支持,结构类似,Android 9 以上优先用它。
这就解释了一个高频疑问:为什么我用 apktool 改完,装的时候报INSTALL_PARSE_FAILED_NO_CERTIFICATES或者Package ... has no signatures?因为 apktool 回编译出来的是未签名包,original/META-INF里的老签名在你改动文件的那一刻就已经失效了。改完必须重签,没有例外。
还有一类现象是「装上了但应用行为异常」,比如登录失败、支付回调不回来。这通常是应用内部做了签名校验,拿到的证书指纹和预期不一致。这种情况不是签名工具的问题,是应用侧的设计,需要应用方提供调试包或带白名单的构建,硬改不属于常规流程。
4.2 apksigner 与 jarsigner:别再用后者签新包
jarsigner是 JDK 自带的,很多人顺手就用它,但它只能做 v1 签名。在 Android 7.0 以下的设备上还能用,7.0 以上虽然系统会回落到 v1 校验,但不少应用市场和分发渠道已经不接受纯 v1 包了。
apksigner在 Android SDK 的 build-tools 目录下,支持 v1/v2/v3/v4,而且能明确控制开哪几个。对比一下:
| 维度 | jarsigner | apksigner |
|---|---|---|
| 支持签名版本 | 仅 v1 | v1 / v2 / v3 / v4 |
| 是否校验对齐 | 否 | 是(会提示未对齐) |
| 密钥格式 | JKS 为主 | JKS / PKCS12 都行 |
| 建议用途 | 维护老包 | 新包一律用它 |
如果目标设备最低支持版本低于 Android 7.0(minSdkVersion < 24),签名时要把 v1 一起打开,不然老设备装不上;只面向新系统的话,v2 + v3 就够。这个判断依据在apktool.yml的minSdkVersion里就能看到。
4.3 正确顺序:zipalign → apksigner → verify,顺序错了前面的活全白干
这是整条链路里最容易翻车的地方。zipalign的作用是把 APK 里未压缩的数据按 4 字节边界对齐,让系统用 mmap 直接读,省内存也快。它会移动文件内容,所以必须在签名之前做。如果你先签名再对齐,v2 签名块里的整文件摘要立刻失效,包就废了。
完整流程:
# 1. 生成签名密钥,只做一次,之后复用。dname 里的信息按自己情况填 keytool -genkeypair -v \ -keystore demo-release.jks \ -alias demo \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass 123456 -keypass 123456 \ -dname "CN=demo, OU=demo, O=demo, L=demo, ST=demo, C=CN" # 2. 对齐。-p 对 .so 做页对齐;-f 覆盖输出;-v 打印详情;4 表示 4 字节边界 zipalign -p -f -v 4 demo_new.apk demo_aligned.apk # 3. 签名。minSdk < 24 时把 v1 打开,纯新系统可以只留 v2/v3 apksigner sign \ --ks demo-release.jks --ks-key-alias demo \ --ks-pass pass:123456 --key-pass pass:123456 \ --v1-signing-enabled true \ --v2-signing-enabled true \ --v3-signing-enabled true \ --out demo_signed.apk demo_aligned.apk # 4. 校验。这一步不能省,要看输出里三个版本是不是都 verification succeeded apksigner verify --verbose --print-certs demo_signed.apk参数说明几个关键的。--ks-pass pass:123456是明文传密码,本地脚本里方便,但别把带密码的命令提交到共享仓库;--print-certs会打印证书指纹,用来和应用侧记录的值对一下;-p是页对齐,Android 15 之后对 16KB 页大小的设备有要求,可以用-P 16显式指定,老设备不受影响。
verify的输出如果只有v1 scheme (JAR signing): true,说明 v2 没签上,多半是--v2-signing-enabled没开或者对齐在签名之后又做了一次。看到DOES NOT VERIFY就直接回头查顺序,别在别的地方找原因。
5. 避坑:五类最高频的翻车现场
5.1 装的时候报「解析包时出现问题」
现象:adb install直接失败,返回INSTALL_PARSE_FAILED_NO_CERTIFICATES或INSTALL_FAILED_INVALID_APK。原因基本是两个:一是回编译后忘了签名,二是签完名又跑了一次zipalign,把 v2 摘要破坏了。解决方式是严格按「对齐 → 签名 → 校验」的顺序走一遍,签名之后任何移动文件内容的操作都不能再做。判断方法很简单,跑一次apksigner verify --verbose,看 v2 那行是不是 true。
5.2 装上了,一点开就闪退,logcat 报 Resources$NotFoundException
现象:安装成功,启动瞬间退出,logcat 里能看到某个资源 ID 找不到。原因是改资源时删了文件或改了名字,导致资源 ID 序号整体偏移,而别处的引用还指着旧 ID。解决办法是回到「只替换不删除」的原则,用同名同格式的文件覆盖;如果必须下线某个资源,把它替换成透明占位图而不是删掉。已经改乱的,重新从原始 APK 解一次包,把改动重新做一遍,比在错位的表里修要快。
5.3 回编译报「No resource found」,但你明明没删东西
现象:刚解包什么都没改,直接apktool b就报资源找不到。原因是目标 APK 用了较新的资源属性或者 aapt2 的特性,而本地 apktool 版本偏旧,它的内置资源表里没有这些定义。处理办法是先升级 apktool 到最新版,再不行就加--use-aapt2让资源走 aapt2 编译。还有一个容易被忽略的点:某些包依赖 framework 资源,涉及系统私有属性的,需要先用apktool if framework-res.apk把框架资源装进去。
5.4 解出来的 dex 只有几百 KB,方法数少得离谱
现象:apktool 解包很快完成,但smali/目录里只有寥寥几个类,jadx 打开也是一堆空壳。原因是这个 APK 加了壳,真正的业务 dex 在运行时才被解密加载。这类包不在常规改包的处理范围内,硬解出来的只是壳的入口。遇到这种情况,先看lib/下有没有可疑的 so,再看assets/里有没有加密的 dex 文件,确认是加固之后就换思路——找应用方要未加固的构建,或者放弃这条路。绕过加固属于另一个领域,涉及授权问题,不建议在这条链路上继续投入。
5.5 只解了 base.apk,装上去提示缺少资源或直接安装失败
现象:从应用市场下载下来是一个目录,里面有base.apk、split_config.arm64_v8a.apk、split_config.xxhdpi.apk好几个文件,你只改了base.apk就签名安装。原因是现代分发用 split APK,资源、so、语言包都被拆到不同的 split 里,单独装 base 是不完整的。解决办法是用adb install-multiple把整组包一起装,或者用专门工具把它们合并成一个通用 APK 再改。改之前先确认目录里到底有几个文件,别拿到一个 base 就当完整包处理。
6. 验证与固化:把「能装」升级成「装对了」
改完签名通过只是第一关,真正要交付之前,我一般会跑一遍固定的四步验证,这套动作帮我把返工率压下来不少。
第一步,结构比对。用 aapt2 把原包和新包的关键字段拉出来对比:
# 分别导出关键字段,diff 一下,确认只有预期内的字段发生变化 aapt2 dump badging demo.apk | grep -E "^package|launchable-activity|sdkVersion" > before.txt aapt2 dump badging demo_signed.apk | grep -E "^package|launchable-activity|sdkVersion" > after.txt diff before.txt after.txt第二步,签名校验。apksigner verify --verbose --print-certs的输出里,三个 scheme 的状态和证书指纹都要看,指纹和应用侧登记的值对不上就说明用错了密钥。
第三步,安装并观察首屏。
# -r 覆盖安装,-d 允许降级,调试阶段用 adb install -r -d demo_signed.apk # 清空日志后启动,只看崩溃相关的 tag adb logcat -c adb shell monkey -p com.demo.app -c android.intent.category.LAUNCHER 1 adb logcat -s AndroidRuntime:E ActivityManager:I第四步,跑一遍核心路径。改过资源就重点看设置页、关于页、启动图;改过 smali 就重点看被改方法涉及的业务入口。安装成功不代表功能正常,很多资源错位的问题要点进二级页面才暴露。
最后分享一个我踩过之后一直保留的习惯:每次改动都留一份最小 diff 清单。解包之后立刻把原始目录复制一份做基线,改完用diff -r对比,确认改动只落在预期的文件上。有一次我改一个渠道号,顺手把一个看起来没用的values-zh-rTW目录删了,结果繁体用户那边整个界面变成英文,问题反馈回来查了两个小时才定位到。从那以后我每次动手前都强制走一遍「备份基线 → 改 → diff 复核 → 对齐 → 签名 → verify → 装机验核心路径」,一步不跳。这套工具链本身不复杂,难的是把顺序和边界记牢,希望帮到你。
本文还有配套的精品资源,点击获取