只要你在 Android 逆向这个坑里待过几天,就一定绕不开 Apktool。它是把 APK 解包成可读资源、smali 代码,再重新编译回 APK 的那个“万能手术刀”。很多人觉得反编译就是把 APK 拖进工具看源码,其实真正动手改资源、改逻辑、重新打包、重新签名这一整条链路,Apktool 才是核心中的核心。这篇文章我会从环境准备、命令参数、资源处理、smali 改写到签名打包,把我实际踩过的坑和常用套路一次性讲透,适合刚接触 Android 逆向的初学者,也适合做过几次但总在重打包阶段翻车的老手。
1. 先搞明白 Apktool 到底在哪个环节干活
1.1 反编译、重新编译、签名这三件事千万别混为一谈
很多新手会把“反编译 APK”理解成“看 Java 源码”,实际上 Apktool 做的并不是这件事,或者说不止这一件事。它主要负责的是:把 APK 里的资源文件(res、AndroidManifest.xml、resources.arsc)解包成可以阅读、修改的原始格式,把 dex 文件反汇编成 smali 汇编代码。注意,这里拿到的是 smali,不是 Java 源码。想看 Java 源码,一般要用 jadx 或 dex2jar 配合 jd-gui,那是另一条路线,改起来也不如 smali 灵活。
重新编译就是反过来:把改过的资源目录和 smali 目录打包回 APK。这个环节最容易出问题,因为 Android 的资源编译规则非常严格,资源 ID、public.xml、文件名、字符串转义任何一处出错,apktool 都会在 build 阶段给你颜色看。签名是在重新编译之后做的,Apktool 本身默认不负责签名,或者说它只是帮你把产物生成出来,最终能不能装到手机里,取决于你是否正确签名。
所以完整的链路是:apktool decode(解包)-> 修改 smali / 资源 / Manifest -> apktool build(重打包)-> 签名工具签名 -> zipalign(非必需但建议)-> 安装测试。这条链路里 Apktool 负责头尾两段,签名和优化是它之外的工具,很多教程把签名也塞进 Apktool 里讲,其实容易误导人。
1.2 它和 jadx、Android Studio 的定位差异
Android Studio 是开发工具,它把源代码编译成 APK;Apktool 是逆向工具,把 APK 还原成接近工程的结构。jadx 更像是一个“阅读器”,它能直接反编译 dex 为 Java 代码,浏览非常舒服,但你很难用 jadx 改完了再重新打包。dex2jar 就更单纯了,只负责把 dex 转成 jar,适合分析不适合修改。
我自己的习惯是:先 apktool d 把 APK 解包,用 jadx 看整体逻辑定位关键代码,然后根据 jadx 里的类名、方法名回到 smali 里精准改动。jadx 是导航,apktool 是操作台,两者配合而不是二选一。遇到加壳或资源混淆的 APP,jadx 经常啥也看不到,这时候 Apktool 反而能先解出资源文件,帮你看清楚入口点、权限、组件声明,再决定下一步怎么搞。
2. 环境准备和最少必要命令
2.1 JDK 版本和 apktool 包装脚本
Apktool 是 Java 写的工具(目前主要是 Kotlin),所以必须先装 JDK。我建议装 JDK 11 或 JDK 17,太老的 JDK 8 跑最新版 Apktool 有时会报 unsupported class version 之类的错。装完 JDK 后,去官网下载 apktool.jar,再根据官方文档写一个包装脚本。
Windows 上我会新建一个apktool.bat,内容大致是:
@echo off java -jar C:\tools\apktool.jar %*macOS / Linux 就写个 shell 脚本,或直接 alias:
alias apktool='java -jar ~/tools/apktool.jar'官方文档里还有apktool.bat的详细写法,但核心就是让命令行能直接调用。注意apktool.jar路径里不要有空格,一旦路径带空格,脚本很容易在后续传参时出问题。我遇到过脚本能启动但无法传参的坑,最后发现是路径引号没加全。
2.2 解包、重打包、安装这三条命令压箱底
这可能是你接下来三个月用得最频繁的三条命令:
apktool d xxx.apk -o output_dir apktool b output_dir -o new.apk apktool b output_dir --use-aapt2第一条命令把 APK 解包到 output_dir,默认会包含AndroidManifest.xml、res、smali(可能有 smali_classes2、smali_classes3 等,对应 multidex)、unknown、original等目录。第二条命令把修改后的目录重新打包。第三条指定用 aapt2 编译资源,新版 Apktool 默认可能已经用 aapt2,但老版本需要手动指定。
如果你只关心资源,不想处理 dex,可以加-s参数(表示 no source)。反过来只要代码不要资源,用-r。这两个参数一定要记牢:
apktool d xxx.apk -s # 只解资源,跳过 dex 反汇编 apktool d xxx.apk -r # 只解 dex,跳过资源解码还有-f是强制清除输出目录,重解包时非常好用。如果上次解包一半失败,输出目录残留,下次不解-f会直接告诉你输出目录不为空。
2.3 第一次解包后你会看到什么
解包成功后,目录结构大致长这样:
output_dir/ ├── AndroidManifest.xml ├── apktool.yml ├── original/ ├── res/ ├── smali/ └── unknown/AndroidManifest.xml已经从二进制 XML 转成明文 XML,可以直接改权限、组件、应用名称。apktool.yml是 Apktool 自己生成的元信息,里面记录了原 APK 的版本信息、minSdk、targetSdk、是否使用 aapt2 等,重打包时会参考,不建议随意删字段。original目录保留的是原始的签名文件和 AndroidManifest(二进制版),一般很少动。unknown是 Apktool 不认识的文件残留,比如某些 APK 里的特殊目录。
我第一次解包的时候最惊讶的是 smali 文件实在太多了,全是.smali后缀的文本文件,打开看一眼就能感受到“汇编风格”的阅读压力。但没关系,后面我会说怎么定位想改的方法,不用真的逐行读完所有 smali。
3. 资源解码、修改与重编译的关键细节
3.1 resources.arsc 是资源索引的中枢,不要乱动
APK 里的resources.arsc是一个二进制资源表,记录着资源名称、类型、ID 和具体配置的映射关系。Apktool 解包时会把这张表解成可读的res/values/public.xml,里面每一个资源都有固定的编号,像0x7f0a001e这样的格式。你在 res 里改文件内容没问题,但不要自己随意往 public.xml 里加资源或改 ID,除非你完全知道后果。
为什么不能乱动?因为 Android 在运行时是通过资源 ID 来查找资源的,代码里引用的是 int 值,不是字符串名称。如果你手工改乱了 public.xml,轻则资源错乱,重则生成 APK 后安装没问题,一运行就闪退。我自己吃过这个亏:为了给一个 App 加一张启动图,擅自复制了一条<public>记录并改了 ID,结果整个 App 的资源索引错位,启动后大量资源找不到,黑屏。
正确的改资源方式是:在 res 目录里增删文件,然后通过重编译让 Apktool 自动重新生成 public.xml。不要手工维护 ID 表,也不要依赖旧版本的 public.xml 结构。
3.2 XML 里的路径与资源引用,改的时候要小心转义
解包后的res目录里可能有各种类型的资源:layout、drawable、values、mipmap等。XML 文件打开后是明文,但里面很多属性值看起来像@string/app_name、@drawable/ic_launcher,这些是资源引用。如果你修改了一个字符串资源,最好检查它有没有被其他地方引用,因为改错会影响整个 App 的展示逻辑。
我实际改资源时遇到过一个大坑:在res/values/strings.xml里把一条带英文引号的字符串改成了中文内容,忘了处理引号转义,结果重编译直接报 XML 格式错误。XML 的规则很严格,&、<、>、"这些字符必须转义,文本里的%系列格式化占位符也要注意,尤其在多语言资源里,Android 会把它当作格式化字符串处理。
这里引入一个最常用的操作:修改 App 显示名称。你在AndroidManifest.xml里搜索android:label,如果是@string/app_name,就去res/values/strings.xml里改<string name="app_name">。这算最简单的改造,适合练手。
3.3 重编译时 aapt 与 aapt2 的选择不能乱来
Apktool 早期版本默认使用 aapt 编译资源,现在新版更推 aapt2,因为 aapt2 对版本兼容和资源压缩的处理更好。重编译时加--use-aapt2能解决很多莫名其妙的问题,比如某些 AndResGuard 混淆过的资源、超大 arsc 文件、非标准资源目录等。
但 aapt2 也有它的问题:对 Java 环境更敏感,JDK 版本太老或某些字符集设置不对,会报java.lang.UnsupportedOperationException或编码错误。我的经验是:默认先用 apktool 自带的资源编译方式,报错就加--use-aapt2重试,再不行就切换 JDK 版本。
如果你重打包后报错aapt2相关的w(warning),大多数情况不影响产物,但如果报e开头就是 error,说明资源编译确实失败了。注意区分日志级别。
3.4 重打包后的签名:v1、v2、v3 缺哪个都可能安装失败
Apktool build 生成的 APK 是未签名的,直接装到手机必失败。签名工具有两个选择:旧的jarsigner来自 JDK,新的apksigner来自 Android SDK Build Tools。我现在一律用 apksigner,因为它原生支持签名方案 v2、v3,而 jarsigner 只支持 v1。
签名命令是这个样子的:
apksigner sign --ks mykey.jks --ks-key-alias mykey --ks-pass pass:123456 --key-pass pass:123456 --out signed.apk unsigned.apk如果目标 App 的 targetSdk 比较高(比如 30 以上),系统要求必须包含 v2 签名,否则 Android 11+ 会提示“安装包解析失败”或“应用未安装”。反过来,如果老设备或某些国内 ROM 只支持 v1,你签 v2 可能又能装,但某些系统校验更严。稳妥做法是 v1、v2 都保留,apksigner 默认会启用当前 minSdk/targetSdk 下所有支持的签名方案。
签名完建议顺手跑一遍 zipalign,虽然 apksigner 对未对齐安装包能够修正,但我习惯先 zipalign 再签名:
zipalign -f 4 input.apk aligned.apk apksigner sign ... aligned.apk顺序别搞反,对齐之后再签名基本不会出问题,签名之后再对齐会导致签名失效(因为对齐会改动文件内容)。
4. smali 修改:改资源简单,改逻辑才有技术含量
4.1 一个能直接上手的 smali 修改案例
先看一个场景:App 每次启动都会弹更新弹窗,你不想让它弹,想绕过。用 jadx 反编译后定位到弹窗的类和方法,比如com/example/MainActivity;->showUpdateDialog()。接下来在 smali 目录里找到对应文件,打开后把方法体改掉,让它直接return-void。
原始 smali 可能长这个样子:
.method public showUpdateDialog()V .locals 2 # 弹窗逻辑 invoke-direct {p0}, Landroid/app/AlertDialog$Builder;-><init>()V ... return-void .end method改成最简单的空实现:
.method public showUpdateDialog()V .locals 0 return-void .end method这就是最基础的 smali 修改。修改完了重打包、签名、安装,弹窗就消失了。当然真实应用不一定这么简单,很多弹窗函数带有参数和返回值,定位方法可能藏在 Fragment、Service 甚至回调里,但思路完全相同。
4.2 smali 语法快速上手,记住这四条规则
smali 本质上就是 dex 反汇编出来的寄存器语言,你不必学得很深,常用规则就几条:
- 寄存器用
v0、v1表示局部变量,p0表示 this(非静态方法),p1、p2等表示方法参数。 - 指令是
opcode {regs}, args的格式,比如const/16 v0, 0x1就是给 v0 赋常量 1。 - 方法调用用
invoke-*系列指令,比如invoke-virtual、invoke-static、invoke-direct,后面跟{寄存器}, 类;->方法签名。 - 返回值用
return、return-void、return-object,根据方法返回类型而定。
想快速定位方法,不建议用文件搜索硬找,太慢。直接在 smoke(这里修正为 smali)目录里配合 grep 搜索字符串或注释,比如弹窗标题文本在 strings.xml 里可以看到@string/update_tip,再去 smali 里搜索update_tip就能快速定位。这个方法比搜类名可靠得多,因为很多 App 会混淆类名,但字符串资源往往还能保留。
4.3 smali 目录有多个时,别改错 dex
现在的 App 基本都开启了 multidex,解包后会出现smali_classes2、smali_classes3等目录。这意味着同一个工程被拆成多个 dex 文件,smali 文件的物理位置并不一定对应逻辑上的类路径。如果你在 jadx 里看到一个类,搜索 smali 时一定要在所有 smali 目录里搜索,而不能只搜smali根目录。
有一种情况最容易翻车:同一个类在多个 dex 里存在重复定义(业务上少见,但部分壳和热修复机制会这么做)。修改的时候要确定哪个是实际生效的,一般通过查看调用方或运行时行为来判断,比较费时。快速做法是先改你认为生效的那个,重打包测试,不行再看另一个。
4.4 资源混淆、加固壳对 smali 的影响
如果 App 用了加固壳,Apktool 解出来的 dex 很可能只是壳的加载器,真正的业务代码在加密的 so 或 dex 里,smali 目录里基本找不到有意义的内容。这时 Apktool 只能用于看资源和 Manifest,分析代码要靠脱壳工具,不在本文范围。
如果 App 做了资源混淆(比如微信的 AndResGuard),resources.arsc 里的资源名会被人为改成短字符,Apktool 解码后你看到的文件可能是res/xxxxx这种无意义名称,public.xml 大量 ID 混乱。遇到这种 App,重编译时最容易报错,--use-aapt2也不是万能药,需要先想办法恢复资源映射,或者干脆放弃资源修改,只操作 smali。
5. 全局配置、框架文件和高级用法
5.1 框架文件 framework-res.apk 和 -t 参数什么时候用
某些 App 深度定制了系统主题,或引用了系统私有资源,比如@android:style/Theme.DeviceDefault、@android:color/holo_blue_dark之类。Apktool 解码时通常自带一份框架文件,但如果你解包的系统 App 或定制 ROM 资源包,会提示 missing framework files,这时就需要导入系统框架。
命令是:
apktool if framework-res.apk-t参数则是在解码时指定使用某个框架 tag。我解包 MIUI、EMUI 这类 ROM 里的内置应用时,几乎必用这个命令,否则资源 id 解析范围不够,解出来的资源会缺 id。注意:framework tag 存在~/.local/share/apktool/framework/目录下,不同系统路径不一样,重装系统后别忘了重新导入。
5.2 apktool.yml 里可以调整的配置项
解包后的apktool.yml记录了原 APK 的关键信息,我整理几个常用的:
minSdkVersion、targetSdkVersion:影响重编译时 aapt 的版本处理,改动过大可能装不上。versionName、versionCode:App 版本信息,去更新弹窗的另一种粗暴做法就是改这两个值。usesFramework:引用框架版本信息,系统应用修改时需要加版本号。renameManifestPackage:有些工具用它做 Manifest 包名替换,但 Apktool 本身不直接改包名,改包名要动 smali 里的常量字符串,这是个容易踩坑的操作。
如果你修改了 manifest 里的package属性,重编译大概率报错。现在很多 App 的包名不在 manifest 中,而是在 applicationId 和 build 配置里,改包名属于一个复杂工程,不要想着改个 manifest 就完事。
5.3 命令行常用参数,这篇文的总结性干货
最后把我常用的 Apktool 命令和参数状态表放这里,方便你直接复制:
| 场景 | 命令 |
|---|---|
| 标准解包 | apktool d app.apk |
| 解包到指定目录 | apktool d app.apk -o out |
| 只解资源 | apktool d app.apk -s |
| 只解 dex | apktool d app.apk -r |
| 强制覆盖输出目录 | apktool d app.apk -f |
| 使用 aapt2 重打包 | apktool b out --use-aapt2 |
| 输出指定文件名 | apktool b out -o new.apk |
| 无资源模式重打包 | apktool b out --no-res |
| 无 dex 模式重打包 | apktool b out --no-src |
| 导入框架文件 | apktool if framework-res.apk |
这些参数是高频操作,剩下的遇到再查也行,别指望一次背完。
5.4 集成到脚本的批量处理思路
如果手里的 APK 不止一个,比如要批量给多语言包改名、批量替换启动图,手动一条条敲命令太累了。可以写个简单的 shell 脚本循环处理。
下面是一个批量解包并重打包的小例子(macOS/Linux):
for apk in *.apk; do apktool d "$apk" -o "out_${apk%.apk}" apktool b "out_${apk%.apk}" -o "new_${apk%.apk}.apk" done要注意的是,如果原包结构复杂,脚本很容易因为某个包的特殊性问题中断,建议在脚本里加入单文件失败后继续处理的逻辑,比如|| echo "error on $apk"。批量操作的精髓不是一次跑完,而是让失败项足够清晰。
6. 常见报错和排查技巧实录
6.1 重编译报错:Invalid file name
这是资源文件名不合法导致的,通常出现在解包后被修改过的资源文件里。例如把res/drawable/xxx.jpg改成了带中文或空格的文件名,Android 资源系统不接受这种命名。排查方法是看 build 日志里哪一行报的FileNotFoundException或Invalid file name,直接把文件名改回合法格式。
还有一个类似问题:资源文件路径包含大写字母,Android 的资源名标准要求全小写,如果你手误改成大写,同样会报错。这种事很蠢但确实会发生,尤其当你从 macOS 复制文件时,文件名大小写很容易在 Windows 上出问题。
6.2 重编译报错:Could not decode arsc file
解包阶段报这个,说明 resources.arsc 文件格式特殊,常见于资源混淆、劣质打包工具或加固壳。你可以先试apktool d app.apk -r,绕过资源解码,只看 dex。如果必须改资源,需要借助其他工具先还原资源表,或者找专业的脱修工具,Apktool 本身没有魔法。
我在解某个老版本 App 时遇到过这个报错,原因是它用了自定义的资源压缩算法,Apktool 官方修复之前无法解码。最后我只能放弃资源修改,只用-r模式处理 dex。所以不必死磕一个工具搞不定的事,换个思路也许更快。
6.3 重编译报错:duplicate entry
重打包阶段报duplicate entry,一般是res/下存在重复资源或.git残留、临时文件被一起打进去了。比如你解包后用了 Finder/Windows Explorer 浏览目录,系统悄悄生成了Thumbs.db或.DS_Store文件,这些文件可能被当作资源文件打进去,造成重复或类型错误。
解决办法是重打包前清理目录里的隐藏文件,或者用命令行find . -name ".DS_Store" -delete之类清理掉。养成习惯:解包后的目录只保留 Apktool 生成的目录,不要在里面乱塞文件。
6.4 重打包后安装失败:INSTALL_PARSE_FAILED_NO_CERTIFICATES 或 INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION
这类问题绝大多数是签名没有完成。记住:apktool build 出来的 APK 是裸包,不签名不可能安装成功。先检查是否执行了 apksigner,再看 v1/v2 签名是否对齐。
如果提示签名方案不受支持,可能是 apksigner 版本太老,无法识别较新的 v3 签名;也可能是你用了自签的 v1 证书,但 targetSdk 太高,系统要求 v2。最省心的做法是生成一个新的 keystore,用 apksigner 默认参数签名,不要动额外的 flag。
6.5 安装成功但启动闪退
闪退原因比编译错误更难排查,常见的有:
- 改动 smali 导致逻辑异常,比如 register 数量不够。
- 修改 Manifest 后丢了必要的组件声明或权限。
- 签名不一致导致原有 App 的签名校验失败,应用自校验。
- 修改了资源 ID 或 public.xml,运行时资源索引错位。
- so 库架构缺失,比如原包只有 arm64,你改过的设备模拟器是 x86。
建议从小改动开始测试,每次改完只验证一个点,别一次改完再找问题。我之前图省事一次性改了名称、图标、逻辑三处,出问题后根本分不清是哪一步搞坏的。后来学聪明了,每改一步就重打包测试一次,虽然多花时间,但排错效率反而高很多。
6.6 常见问题速查表
| 现象 | 原因 | 快速处理 |
|---|---|---|
| 解包就报错 | 加固壳 / 资源混淆 | 换工具或只解 dex |
| 编译报 aapt2 错误 | 资源格式或路径问题 | 加--use-aapt2或换 JDK |
| 编译报 UTF-8 编码错误 | strings.xml 有非法字符 | 检查特殊符号转义 |
| 重打包后安装失败 | 未签名 / 签名方案缺失 | apksigner 重新签名 |
| 重打包后闪退 | 资源 ID 错乱 / smali 改坏 | 回退到上一个可用版本排查 |
| 找不到某个类 | 多 dex 或加固 | 所有 smali 目录都搜一遍 |
7. 写在最后的几句经验
Apktool 是一个“上限很高、下限很低”的工具,刚接触时你只需要三条命令就能跑通解包到重打包的流程,但真正把它用得顺手,需要你熟悉 Android 资源编译机制、dex 文件格式,以及一套调试思路。我个人的建议是:先从改应用名称和启动图标这种无风险操作入手,再尝试去更新弹窗、改版本号这类逻辑修改,最后才考虑复杂场景。每一类操作都先备份原始的 APK 和解包目录,尤其是AndroidManifest.xml和apktool.yml,这两个文件改动错误造成的损失往往最大。
另外,Apktool 本身不提供回滚机制,也没有断点调试能力,它的定位就是“文本化 APK 内部结构”。所以修改前做好备份、修改时一次只动一点、重打包后立刻安装测试,这三条习惯比任何高级技巧都重要。希望这篇文章能帮你少走点弯路。