如果你经常跟 APK 打交道,无论是做移动安全分析、SDK 问题排查,还是单纯想搞清楚一个安装包里的资源结构,就必须掌握一套基础工具链。这套工具链通常就是标题里的四件套:apktool、dex2jar(有人会误写成 jar2dex)、apksigner、zipalign。别小看这几个命令,它们几乎覆盖了从解包、查看、修改、对齐到重新签名的完整流程。
总有人以为“反编译”就是一次点个按钮全部搞定,实际上这套流程是有明确分工的:apktool 负责拆资源文件和 smali 汇编代码,dex2jar 负责把 dex 字节码还原成可读的 Java 层级,zipalign 负责做内存对齐优化,apksigner 负责最终签名。四个工具配合起来,才能组成一条可复现的操作链路。
这篇文章不会扯太多理论,我直接把这几个命令在实际项目中怎么用、有哪些坑、参数怎么选讲明白。不管是刚接触的小白,还是已经踩过坑的开发者,都可以把这里的内容当作备忘录用。需要说明的是,我们讲的所有操作都应该围绕合法场景展开,比如分析自己的应用、排查崩溃问题、做兼容性验证,或者学习开源项目;不要拿这些工具去对未经授权的应用做分发、破解或商业利用。
1. 工具链全景与适用场景
1.1 先理清四件套的分工
我见过不少新手一上来就追问“哪个工具最牛逼”,其实这四个工具根本不是同一个层面的东西,它们负责的阶段完全不同。apktool 的核心能力是解包和回包,但它处理的是资源层面和 smali 层面,也就是说它能把 AndroidManifest.xml、布局文件、图片、字符串资源全部解出来,同时也能把 classes.dex 转成可以改的 smali 代码。
dex2jar 则是把 dex 字节码转换成 JAR 文件,转换后你再用 JD-GUI 或者 Bytecode Viewer 打开,就能看到近似 Java 的源码。注意是“近似”,因为反编译不可能百分百还原注释和局部变量,但逻辑结构基本能看到。
zipalign 做的是对齐优化,它会把 APK 里未压缩的数据按 4 字节对齐,这样 Android 系统在读取资源时可以直接通过内存映射方式访问,不用拷贝一遍数据。apksigner 是官方提供的签名工具,负责给 APK 打上完整的签名信息,用在最终交付前的最后一步。
为了方便记忆,我一般把这个链条理解成四句话:apktool 拆解资源,dex2jar 还原逻辑,zipalign 调优结构,apksigner 确认身份。
1.2 什么场景会用到这套组合
我最初接触这套工具是因为要排查一个崩溃问题。线上反馈说某机型在启动阶段频繁闪退,但是客户端本地日志没有捕获到异常。当时我只有一个旧的 APK 和一份崩溃堆栈,堆栈指向一个 SDK 初始化方法。为了定位问题,我把 APK 拆开,用 apktool 查看清单文件里的 provider 配置,用 dex2jar 查看相关 class 的调用逻辑,最后发现是某个第三方 SDK 在特定系统版本上初始化顺序不对,跟 APK 本身的签名方式也有耦合关系。
这个场景是典型的“分析别人代码”——但不是为了抄袭,而是为了确认责任方和规避问题。类似的工作还有很多,比如检查自己发布的 APK 是否被二次打包、验证加固方案是否生效、看看资源文件有没有泄漏,都需要用到这套工具。
还有一类场景是重打包自己的应用。比如你的应用需要让测试团队安装一个带特殊配置的版本,又不想重新走一遍完整打包流程,那就可以用 apktool 解包、修改某个配置文件或者 smali 逻辑,再经过 zipalign 和 apksigner 处理,快速生成一个可安装的测试包。这个流程在自动化测试里也经常出现。
1.3 环境准备:先把依赖装好
这几个工具都需要 Java 环境。apktool 和 dex2jar 都依赖 JRE,apksigner 和 zipalign 在 Android SDK 的 build-tools 目录里。安装 JDK 时建议选择 8 或 11,太老或太新的版本反而容易碰到兼容性问题。
我在某台机器上装过 JDK 17,跑 apktool 时偶尔会出奇怪的反射警告,换成 JDK 8 就稳了。虽然新版本 apktool 也支持高版本 JDK,但如果你不是刻意追求新特性,稳定优先就好。
另外,如果你以后还要验证 APK 是否能用apksigner verify通过,最好把 build-tools 的版本固定在 30.0.3 以上。apksigner 的版本和 Android 系统签名方案紧密相关,太老的工具不支持 v2、v3 签名,新项目用老工具会直接失败。按我现在的习惯,直接把 build-tools 升级到最新稳定版,省得后面还要折腾。
2. apktool:资源与 Smali 的拆装主力
2.1 解码命令的常用参数
apktool 最基础的命令是d,也就是 decode。最简单的用法是:
apktool d app.apk执行完会在当前目录生成一个和 APK 同名的文件夹,里面就是解码后的资源文件。这个文件夹里的关键内容包括:
AndroidManifest.xml:已经转成明文 XML,可以直接查看和修改。res/:资源目录,布局、图片、字符串都在里面。smali/:由 dex 反汇编出来的 smali 代码。original/:原始元信息和签名相关文件,一般不用动。
实际项目里我常用的是这些参数:
apktool d app.apk -o output_dir-o指定输出目录,避免默认目录名夹杂版本号导致脚本不好处理。
apktool d app.apk -r-r表示不反编译资源文件。如果你只需要 smali,不想看资源,加这个参数会快很多,而且能减少出错概率。资源解码过程偶尔会因为某些框架资源问题失败,跳过后就能绕过去。
apktool d app.apk -s-s则和-r相反,它表示不反编译 dex 到 smali,只解资源。
还有一个容易被忽略的参数-f,强制覆盖已有输出目录。写脚本跑批量任务时,如果前一次残留了旧目录,不加-f会直接中断。
2.2 回编译命令与后续配合
改完资源或者 smali 之后,要用b命令回编译:
apktool b output_dir -o new.apk这一步会重新生成一个 APK,但这个 APK 是没有签名的,不能直接安装。这里有一个很多人踩过的坑:回编译后的 APK 如果要继续做签名,必须先把 zipalign 和 apksigner 的流程走完,不能拿回编译包直接装。
apktool 在回编译时偶尔会报错,尤其是用了比较高版本的 Android SDK 资源类型时。遇到这类问题,可以加--use-aapt2来切到 AAPT2 处理资源编译,AAPT2 是当前 Android Gradle 插件默认的编译器,兼容性更好。
2.3 拆包时的三个实操笔记
第一,apktool 解码过程有时会提示Framework not found。这通常是因为设备或系统框架的 framework-res.apk 没有缓存。解决方法是在解包命令里加上-t指定框架目录,或者先把对应的 framework-res.apk 安装到 apktool 的框架目录里。不过大多数普通 APK 不会遇到,遇到的多半是定制 ROM 的系统应用。
第二,解包后如果发现res/里有大量.arsc相关的资源配置,不要直接删掉某些看起来没用的目录。很多应用把资源 ID 混淆后,删除一个目录可能导致运行时找不到资源 ID,回编译虽然能成功,安装后一打开就崩。
第三,修改 smali 需要一定语法基础,但如果你只是修改字符串、布局或者开关量,直接在资源文件里改就行。改完 smali 后,类函数的签名和寄存器数量必须匹配,否则回编译后运行时会报VerifyError。新手我建议先从资源层入手,别一上来就改逻辑。
3. dex2jar 与 Java 层代码还原
3.1 关于 jar2dex 这个叫法的纠正
标题里写的是 jar2dex,我估计是笔误或者记忆偏差。实际工具链里,真正常用的是 dex2jar,方向是从 dex 到 jar。为什么方向这么重要?因为 APK 里的可执行代码叫 classes.dex,我们要把它转成 JAR 文件,才能用 Java 反编译工具阅读。所以命令里出现的是d2j-dex2jar,不是jar2dex。
如果哪天你看到一个工具真叫 jar2dex,它做的是把 jar 打包成 dex,这属于编译期工具(类似 dx 或 d8),在反编译场景里用不到。我在这篇文章里聊的,都是 dex2jar。
3.2 基本命令与参数
把 APK 转成 jar 最粗暴的方式是直接指定 APK 文件:
d2j-dex2jar.sh app.apk或者先解压出 classes.dex,再单独转:
unzip app.apk classes.dex d2j-dex2jar.sh classes.dex前者内部会先解出 dex,所以大多数时候直接用前者就行。执行结束后,目录下会生成一个类似app-dex2jar.jar的文件。
如果没生成,或者生成到一半内存不足,通常是某些 APK 的 dex 太大。我给 d2j-dex2jar 设置过 JVM 参数,效果明显:
export _JAVA_OPTIONS="-Xmx2048m" d2j-dex2jar.sh app.apk注意,dex2jar 在转换过程中会有大量警告,比如某个方法无法解析、某个指令出现非法分支,这些大部分不影响最终结果。你要做的是用 JD-GUI 打开生成的 jar,看关键类的方法逻辑是否完整,而不是去看控制台警告。
3.3 多 dex 与混淆代码的常见问题
现在很多应用都不止一个 dex,而是通过 multidex 拆成了 classes.dex、classes2.dex、classes3.dex。处理这种包,我是先把 APK 全部解压,然后对每个 dex 执行一次转换:
d2j-dex2jar.sh classes.dex d2j-dex2jar.sh classes2.dex d2j-dex2jar.sh classes3.dex得到多个 jar 文件后,你可以在 JD-GUI 里同时打开它们,或者用 zip 工具合并到一个 jar 里。但合并时要注意重复类名覆盖的问题,这并不是推荐做法,建议分开看。
遇到混淆过的代码,类名和方法名会变成 a、b、c 之类的短名,阅读起来非常痛苦。这种时候我一般会结合堆栈里的行号,先定位具体是哪个类,再看方法内部的调用。混淆只是提高阅读成本,不代表不能逆向,但不代表你可以拿混淆代码去干别的事情,这点自己要有底线。
还可以用jd-gui打开 jar 后,直接搜索某个类名或者方法名。如果你想看更精确的字节码,可以配合Bytecode Viewer使用,它能同时展示反编译源码和字节码,排查某些混淆逻辑时比单纯 JD-GUI 好用。
4. zipalign 与 apksigner:打包与签名闭环
4.1 为什么需要 4 字节对齐
很多人在刚接触时会问,APK 不签名不就能装吗?为什么一定要 zipalign?其实 zipalign 和签名是两个维度的事。
zipalign 做的事情是把 APK 中未压缩的数据起始位置对齐到 4 字节边界。Android 系统在读取 APK 时,如果文件里的资源数据是 4 字节对齐的,就可以通过mmap直接把文件映射到内存,从映射区读取资源,无需再执行一次数据拷贝。这个小优化对安装包体积大、资源多的应用来说,启动速度和内存占用都有实实在在的改善。
不 zipalign 不会导致安装失败,但会让应用在运行时多耗费一点 CPU 和内存。在正式发布和测试中,我基本都会做这一步骤,成本极低,收益稳定,没理由跳过。
4.2 zipalign 命令与验证
zipalign 在 Android SDK 的 build-tools 目录里,先找到你的 SDK 路径:
find "$ANDROID_HOME" -name "zipalign"使用方式很直接:
zipalign -v 4 input.apk output.apk这里-v表示输出详细信息,4是对齐字节数,input 和 output 不能是同一个文件。对齐后,可以用-c验证:
zipalign -c -v 4 output.apk看到Verification successful就说明对齐没问题。
我习惯把 output.apk 写成aligned.apk,这样后续签名的时候一眼能看出这一步已经做完。
4.3 apksigner 的完整参数
apksigner 是官方签名工具,也是构建系统里真正给 APK 签名的工具。要签一个 APK,首先需要有 keystore,生成一个测试 key 可以这么做:
keytool -genkey -v -keystore test.jks \ -alias testalias \ -keyalg RSA \ -keysize 2048 \ -validity 10000然后使用 apksigner 执行签名:
apksigner sign \ --ks test.jks \ --ks-key-alias testalias \ --ks-pass pass:你的密码 \ --out signed.apk \ aligned.apk这里用--ks-signed==交互输入密码也可以,但我更推荐命令行里传参,方便写自动化脚本。注意,如果密码包含特殊字符,最好用pass:密码这种形式,或者把密码写到文件,用--ks-pass file:/path/to/passfile来引用,避免命令行历史留下明文。
签名完成后验证:
apksigner verify --print-certs signed.apk输出会显示证书的 SHA-256、算法、签名方案等信息,看到 v2 或 v3 签名方案存在,说明现代 Android 版本能正确识别。
4.4 先对齐还是先签名?这一坑很多人踩
重要的事情说三遍:先 zipalign,再 apksigner。
道理其实很简单,签名就是对 APK 文件的整个内容计算摘要,并把摘要写入签名块。如果你先签名,再执行 zipalign,那么文件内容发生了变化,签名就失效了。所以顺序必须是:
- 生成未签名的 APK(比如 apktool 回编译的结果)
zipalign -v 4 raw.apk aligned.apkapksigner sign --out signed.apk aligned.apk
如果先签名再对齐,安装时会报签名不一致,或者干脆提示“解析包时出现问题”。这个坑我见过太多次了,每次排查最后都是顺序搞错。
另外,apksigner 默认会自动根据 APK 的 minSdkVersion 选择启用 v1 和 v2 签名。如果你要兼容 Android 7.0 以下的设备,v1 签名必须开启。大多数情况下不必手动干预,但如果你发现某些老设备安装时报签名错误,可以在签名命令中显式加上:
apksigner sign --v1-signing-enabled true --v2-signing-enabled true ...5. 一条龙实操:从 APK 到重新签名
5.1 准备一个测试 APK
为了演示完整链路,我用一个简单的演示应用来做实验。这个 APK 是通过正常构建流程生成的,来自某个内部练习项目,不含任何敏感逻辑。实际测试中,你也可以拿自己开发的应用来跑,避免法律风险。
先把环境变量准备好,让后续命令都能找到工具:
export APKTOOL=apktool export D2J=d2j-dex2jar.sh export ZIPALIGN="$ANDROID_HOME/build-tools/30.0.3/zipalign" export APKSIGNER="$ANDROID_HOME/build-tools/30.0.3/apksigner"5.2 完整命令序列
第一步,用 apktool 解包:
apktool d demo.apk -o demo_src第二步,这里假设我要把应用名称字符串改掉。打开demo_src/res/values/strings.xml,找到<string name="app_name">,修改内容后保存。
第三步,回编译:
apktool b demo_src -o rebuilt.apk第四步,对齐:
zipalign -v 4 rebuilt.apk aligned.apk第五步,生成签名密钥(如果已有测试 key,这步可跳过):
keytool -genkey -v -keystore test.jks \ -alias testalias -keyalg RSA -keysize 2048 -validity 10000第六步,签名:
apksigner sign \ --ks test.jks \ --ks-key-alias testalias \ --ks-pass pass:test123 \ --out final.apk \ aligned.apk第七步,签名验证和对齐验证:
apksigner verify --print-certs final.apk zipalign -c -v 4 final.apk到这里,final.apk就是一个可以安装到设备上的测试包。
上面这套命令序列我封装在一个脚本里跑过很多次,关键点是每一步的输入输出要衔接好。如果你是在 CI 流水线里用,建议把-o目录用时间戳命名,避免多个任务并发时互相覆盖。
5.3 验证结果与常见错误
在实际跑这条链路时,最容易出问题的环节是 apktool 回编译。如果apktool b报资源编译失败,先检查demo_src/build目录下的错误日志,定位到具体的资源文件。常见原因是某个 XML 文件被误删了标签,或者字符串里有未转义的字符。
如果 apksigner 签完用verify显示失败,先确认是不是没把aligned.apk作为--out的输入,而是把签名输出又套了一层对齐。前面说过,顺序错了就是白忙活。
还有一次我遇到 zipalign 输出文件和输入文件同名,直接报错退出了。zipalign 不支持原地对齐,你必须提供两个不同的文件名,这个细节写脚本时要特别注意。
6. 常见问题速查表
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| apktool 解码时报 Framework not found | 缺少框架资源缓存 | 用-t指定框架目录,或安装对应 framework-res.apk |
| apktool 回编译时报资源编译失败 | XML 标签损坏或字符串非法 | 查看build目录日志,修复资源文件 |
| dex2jar 转换时内存不足 | dex 文件太大 | 设置_JAVA_OPTIONS="-Xmx2048m"后重试 |
| 生成的 jar 里类名全是 a、b、c | 应用开启了混淆 | 使用 Bytecode Viewer 查看字节码,或结合堆栈信息定位 |
| 安装时提示解析包出现异常 | 签名顺序错误或签名不匹配 | 先 zipalign 再 apksigner,用verify检查签名 |
| apksigner 提示 no schemes set | 手动禁用了 v1/v2 签名 | 去掉--v1-signing-enabled false或显式开启 |
| zipalign 验证失败 | 文件被二次修改 | 重新执行对齐,并确认所有修改都发生在签名前 |
6.1 我在使用中的一些额外习惯
除了基础命令,我通常会记录每个 APK 的 SHA-256 值,防止二次打包和对比异常。用sha256sum或者apksigner verify --print-certs里的证书信息都行。遇到要长期维护的 APK,我会把 keystore、alias、签名参数写到一个加密配置里,避免每次签名都翻历史文档。
还有一个容易漏掉的细节:apktool 解包后可能会在原目录下生成一个original文件夹,里面包含META-INF的原始签名信息。如果回编译后没有清理这个目录,它可能会被一起打回新 APK 里,导致新老签名信息同时存在。我一般在回编译前先删掉original目录和旧的META-INF,保证新 APK 干干净净。
如果你是写自动化脚本,可以创建一个简单的目录结构,把解包后的输出、对齐中间件、签名产物分开:
src/存放解码后的源码目录prebuild/存放回编译未签名的 APKaligned/存放 zipalign 后的中间 APKrelease/存放最终签名 APK
这个结构能让流水线清晰很多,排查问题时也能快速定位是哪一步产物出了问题。
我个人在实际操作中的体会是,这四个工具的命令本身并不复杂,真正的门槛在于理解每一步为什么存在、它们之间的先后顺序如何影响结果。尤其是 zipalign 和 apksigner 的关系,搞反一次,后面再遇到类似问题就会本能地去怀疑签名工具,而忽略了对齐步骤。希望这篇梳理能让你在处理 APK 时少走弯路。
最后再分享一个小技巧:做完整套流程之后,可以把验证命令固定成一个函数,每次签名完都自动跑apksigner verify和zipalign -c,凡是有一步不过就发告警。时间长了你会发现,这比人工检查靠谱得多。