简介面向需要在 macOS 上进行 APK 逆向与资源提取的开发者、安全学习者及移动应用分析人员这份工具集合围绕 apktool 的获取、配置与使用场景整理可帮助降低在 Mac 环境中解包 APK、查看资源文件和理解工具参数的门槛。压缩包共 3 个文件包含可执行的 jar 工具、便捷启动的 sh 脚本以及一份 md 使用说明整体约 10.25MB体量轻便适合随取随用。说明文档对工具用途与操作方式作了交代配合脚本可减少手动配置环节jar 主体则承担 APK 解包与资源还原等核心工作便于后续查看布局、图片、清单等文件。已有 2832 人学习或下载适合希望快速搭建 apktool 使用环境、补齐逆向分析基础流程的读者参考。1. Apktool 装不上的真相JDK 版本比命令更先决定成败apk逆向 获取apk资源 这类需求落到手上往往就是一条apktool d的事。上周我只想从一份自研的安装包里把图标、strings.xml和清单里的渠道号抠出来做对照结果在一台新机器上折腾了两个多小时先撞UnsupportedClassVersionError换完 JDK 又卡在回编译阶段找不到 aapt。apktool 本体只是一个几百 KB 的 jar真正决定它能不能跑起来的是背后那套 JDK、wrapper 脚本和 aapt2 二进制。这套环境的坑有个特点——它不在安装那一步爆而在你 decode 到一半、build 到结尾的时候才爆报错信息还经常是 Java 的类版本号、二进制路径这种看不出因果的东西。手上只有一份 apk、想拿到里面资源的人照着下面这条链路走一遍能跑通已经装过但总在某个版本上翻车的也能找到对号入座的那一段。2. JDK 环境搭建apktool 报 UnsupportedClassVersionError 的根因在这2.1 JDK 8 还是 JDK 17class 文件版本对照表apktool 的 jar 是用 Java 编译出来的编译目标和运行环境之间有一条硬约束运行它的 JVM 版本不能低于编译目标。apktool 2.6 到 2.9 这一档jar 的字节码目标大致在 Java 8机器上有 JDK 8 就能起来2.10 之后往上抬了一档只有 JDK 8 的机器会直接报错报错长这样Exception in thread main java.lang.UnsupportedClassVersionError: brut/androlib/Androlib has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0这个报错里唯一有用的信息是括号里那两个版本号。它说的是「这个 class 文件需要 55.0 的运行时而你只有 52.0」。把这张对照表记住以后看到任何 Java 的版本报错都能一眼判断差了几档class 文件版本对应 JDK 大版本52.0JDK 855.0JDK 1161.0JDK 1765.0JDK 21我的做法是机器上只留一份 JDK 17apktool 用 2.9.x 之后的版本省得两套环境互相干扰。但有些老包只有 apktool 2.4、2.5 那个年代能解新版本反而报资源解析失败这时候才需要单独装一份 JDK 8 放在/opt下用的时候临时切JAVA_HOME不动全局的默认版本。这一点很关键全局默认 JDK 一旦被改低机器上其他构建工具会跟着出问题。还有一种更隐蔽的情况java -version显示的是 17但javac -version显示的是 8或者反过来。这说明 PATH 里排在前面的 java 和 javac 不是同一份 JDK。apktool 是纯运行期程序只用 java所以它本身不会因为这个出事但你在同一台机器上做回编译、跑 gradle 的时候就会撞上玄学问题。装完 JDK 的第一件事就是把这两条命令的输出对齐。2.2 Linux 与 macOS把 JAVA_HOME 写到不带版本号的目录Linux 上我习惯把 JDK 解压到一个固定路径目录名不带版本号这样所有脚本里的路径永远不用改将来升级只换目录内容# 解压到 /opt/jdk--strip-components1 剥掉压缩包自带的那层版本目录 sudo mkdir -p /opt/jdk sudo tar -zxvf ./jdk-17_linux-x64_bin.tar.gz -C /opt/jdk --strip-components1--strip-components1是这里的关键参数。下载回来的压缩包解开后通常自带一层jdk-17.x.x的目录不加这个参数最后会变成/opt/jdk/jdk-17.x.x/bin/java路径里带版本号脚本又得写死。剥掉这一层/opt/jdk/bin/java就是稳定入口。接着写环境变量。我不用~/.bashrc因为那只对交互式 shell 生效cron 和某些 CI 脚本跑起来是另一套环境。写进/etc/profile.d/更稳# /etc/profile.d/java.sh # JAVA_HOME 指向 JDK 根目录末尾不要带 /bin export JAVA_HOME/opt/jdk # 放在 PATH 最前面压过系统自带的 java export PATH$JAVA_HOME/bin:$PATHJAVA_HOME末尾带/bin是最常见的写法错误很多工具会自己拼$JAVA_HOME/bin/java带上去就变成/opt/jdk/bin/bin/java。PATH 追加位置也有讲究写在后面会被/usr/bin/java抢先命中那就等于白配。改完执行source /etc/profile.d/java.sh让当前会话生效然后开一个新终端再验一次。macOS 上不要自己拼路径系统自带一个定位工具用它返回的路径最准# 用系统工具按大版本匹配避免多个 JDK 并存时选错 export JAVA_HOME$(/usr/libexec/java_home -v 17) export PATH$JAVA_HOME/bin:$PATH-v 17匹配的是大版本号装了几个 17.x 的补丁版本时它会挑一个可用的返回。这条命令如果报「Unable to find any JVMs matching version」说明 JDK 根本没装上或者装的位置不被系统索引先解决安装再谈配置。2.3 Windows别用 setx 写 PATHWindows 上最容易翻车的是setx。它确实能把变量写进注册表但有两个坑一是对当前命令行窗口不生效必须新开一个二是 PATH 超过 1024 个字符会被截断截断之后是静默的你只会发现某些命令莫名其妙找不到了。# 用 .NET 接口写用户级变量避开 setx 的 1024 字符截断 [Environment]::SetEnvironmentVariable(JAVA_HOME, C:\Java\jdk-17, User) $old [Environment]::GetEnvironmentVariable(Path, User) [Environment]::SetEnvironmentVariable(Path, C:\Java\jdk-17\bin;$old, User)第三个参数User表示只写当前用户不动系统级变量出问题影响面小。改完关掉所有命令行窗口重开Windows 的进程环境是启动时快照的老窗口读不到新值。还有一处 Windows 特有的陷阱系统里可能已经存在一个C:\Program Files\Common Files\Java\javapath目录里面放的是 java、javac 的软链而且它默认排在 PATH 靠前的位置。你新加的 JDK 路径如果排在它后面java -version依然会显示旧版本。遇到「明明配了却还是老版本」先where java看命中的是哪个。2.4 装完先做一致性自检三条命令的输出必须互相对得上任何一条对不上都别往下走#!/usr/bin/env bash # JDK 一致性自检JAVA_HOME、java、javac 三处必须指向同一份 set -u echo JAVA_HOME ${JAVA_HOME:-未设置} echo which java $(command -v java) echo which javac $(command -v javac) java -version 21 | sed -n 1p javac -version 21java -version的输出走的是 stderr所以后面要21才能被管道接住这一点很多人第一次写校验脚本时会漏。sed -n 1p只取第一行版本信息后面那些运行时参数跟排查无关。如果which java指向/usr/bin/java而JAVA_HOME是/opt/jdk说明 PATH 顺序反了如果 java 是 17 而 javac 是 8说明机器上装了两份PATH 里混着走。Windows 上对应的写法是where java、where javac、echo %JAVA_HOME%判断逻辑一样。这里有个容易误解的地方apktool 的 wrapper 脚本通常直接调 PATH 里的java并不读JAVA_HOME。所以真正决定 apktool 用哪个 JDK 的是 PATH不是JAVA_HOME。但JAVA_HOME配错会影响同机的其他构建链两者都要对。3. Apktool 本体安装jar、wrapper 脚本和 PATH 三件套3.1 发布包里到底有哪两个东西apktool 的发布包通常包含两部分apktool.jar是核心所有逻辑都在里面另一个是几行的 wrapper 脚本Linux 上叫apktoolWindows 上叫apktool.bat。很多教程只让你下 jar然后每次敲java -jar apktool_x.y.z.jar d xxx.apk。这样能跑但版本号要手敲换台机器就忘参数一多打字还容易错。我习惯的目录规划是把 jar 和脚本分开放jar 集中在/opt/apktool/文件名带上版本号wrapper 放/usr/local/bin/apktool走 PATH 直接调用。Windows 上就都塞进C:\tools\apktool\再加进用户 PATH。下载完之后先验一下包完整性别等到 decode 的时候才发现 jar 只下了一半# 检查 jar 里有没有入口类能排除下载中断的半个包 unzip -l apktool-2.9.3.jar | grep -m1 brut/androlib/Androlib.classgrep -m1命中第一条就退出不用把整个 jar 的几千个文件名刷满屏幕。如果这条命令没有任何输出说明 jar 结构不对重新下别在坏包上浪费时间——这种情况下java -jar只会给你一句Invalid or corrupt jarfile看不出是下载问题还是版本问题。3.2 Linux 与 macOS 的 wrapper$的引号不能省wrapper 脚本我只让它做两件事定位 jar、把参数原样透传。#!/usr/bin/env bash # /usr/local/bin/apktool set -euo pipefail APKTOOL_JAR/opt/apktool/apktool.jar # jar 不在就直接报错退出别让 java 抛一堆栈 if [ ! -f $APKTOOL_JAR ]; then echo apktool.jar 不存在: $APKTOOL_JAR 2 exit 127 fi # exec 替换当前进程CtrlC 时信号能直接传给 JVM exec java ${APKTOOL_OPTS:-} -jar $APKTOOL_JAR $逐行说下取舍。set -euo pipefail里-u让未定义变量直接报错pipefail让管道中任一环节失败都算失败这行脚本短加不加区别不大但养成习惯之后排查会快很多。exec是把当前 shell 进程替换成 java 进程而不是 fork 一个子进程等它结束好处是 CtrlC 的信号能直接送到 JVM不会留下僵尸进程。$的引号绝对不能省省掉之后路径里带空格的参数会被 shell 重新拆成多个——这就是「文档目录带空格导致命令诡异失败」的根源。${APKTOOL_OPTS:-}是留给临时加大堆内存的口子默认展开成空串。碰上几十 MB 的大包decode 阶段 OOM 的时候可以这样临时救一次# 只对这一次调用生效不改脚本 APKTOOL_OPTS-Xmx2g apktool d big-app.apk -o out写完给执行权限这一步经常被忘sudo chmod x /usr/local/bin/apktool。3.3 Windows 的 bat%~dp0和%*Windows 上没有 exec 这套语义脚本要简单得多但两个符号必须搞明白echo off rem C:\tools\apktool\apktool.bat rem %~dp0 是脚本自身所在目录末尾自带反斜杠 set APKTOOL_JAR%~dp0apktool.jar if not exist %APKTOOL_JAR% ( echo apktool.jar 不存在: %APKTOOL_JAR% exit /b 127 ) java -jar %APKTOOL_JAR% %*%~dp0展开成 bat 文件所在的目录好处是这个目录整个搬到别的盘也能用不用改脚本里的绝对路径。%*是把用户传进来的所有参数原样展开这里不加引号反而更对——如果用户自己写了引号加了外层引号会变成双重引号java 收到的参数就带上了字面引号字符。set VARvalue这种写法把整个赋值包在引号里能容忍值里带空格和特殊字符比set VARvalue稳。exit /b 127只退出当前批处理不关整个命令行窗口。3.4 版本切换只动软链不覆盖文件装好之后第一件事是冒烟测试确认能调起来apktool --version # 期望输出类似 2.9.3版本号能打出来说明 wrapper、jar、PATH 这条链路通了。无参数执行apktool应该打印 usage如果这时候报的是 Java 异常回到第 2 章查 JDK。版本管理上我不覆盖文件而是保留多个 jar用软链切换。这是装这类工具唯一值得留的「后悔药」# 新版本另存一份切版本只动软链 sudo cp apktool-2.10.0.jar /opt/apktool/ sudo ln -sfn /opt/apktool/apktool-2.10.0.jar /opt/apktool/apktool.jar apktool --versionln -sfn的三个参数各有作用-s建软链-f目标存在时强制覆盖-n防止目标本身是个目录时把链接建到目录里面去。少了-n某些情况下会在/opt/apktool/apktool.jar/下面再建一个同名链接排查起来很费时间。切换完立刻打一次--version确认别等用到的时候才发现链到了旧文件。4. 反编译参数配置decode 与 build 的关键取舍4.1 默认 decode 出来的目录长什么样先跑一次不带任何取舍参数的完整反编译看清楚产物结构再决定砍哪部分# 最基础的 decode-o 指定输出目录-f 允许覆盖已存在的目录 apktool d app-release.apk -o out -f-f不加的话输出目录已存在时它会停下来问你脚本里跑就会卡住。产物大致是这么几块目录或文件内容什么时候会看AndroidManifest.xml已解码成可读 XML 的清单查权限、组件、渠道号apktool.yml版本号、SDK 区间、压缩规则回编译报错时第一个看它res/图片、布局、values/strings.xml提取文案和图标smali/、smali_classes2/多 dex 反汇编出的 smali只需要看代码逻辑时lib/按 ABI 分目录的 so核对架构支持original/原始清单和签名信息对比解码前后差异unknown/没识别出来的文件一般不用管apktool.yml值得单独说。它是回编译的输入清单里面记着这个包是用哪个 apktool 版本解的、minSdk 和 targetSdk 是多少、哪些后缀的文件不能压缩。回编译失败的时候第一件事就是打开它看sdkInfo那一段很多资源冲突是因为 SDK 区间和资源类型对不上。4.2-s和-r按需求砍掉一半工作量只要资源、不看代码用-s# -s 等价于 --no-src跳过 dex 反编译classes.dex 原样拷进输出目录 apktool d app-release.apk -o out-res -s -f这个参数省掉的是最耗时的那一步。一个中等体积的包完整 decode 可能要几十秒到一两分钟加了-s通常几秒就完事因为 smali 反汇编是整个流程里最重的一环。产物里依然有res/、assets/、清单只是没有smali/目录classes.dex以原始二进制形式躺在输出目录里。反过来只要代码、不要资源用-r# -r 等价于 --no-res不解码资源只出 smali 和清单 apktool d app-release.apk -o out-smali -r -f这里有个容易误解的点-r之后res/目录依然可能存在但里面是原始的二进制资源values/strings.xml这种解码产物不会有。如果你只是想看某个方法怎么写的-r能让输出目录小一个数量级。两个参数都能同时用但那样基本等于只把 apk 解压了一遍没什么场景需要。4.3-p框架目录隔离多项目并行的必需品apktool 解码时需要一份系统资源框架来解析android:style/...这类引用默认存在用户目录下Linux 一般是~/.local/share/apktoolWindows 在%LOCALAPPDATA%\apktool。单项目无所谓同时处理几个不同系统版本的包时就会串——上次解一个老包报了一堆 style 找不到查了半天才发现是前一个项目留下的框架覆盖了默认目录。解决办法是给每个项目单独指定框架目录# 先把系统框架装进独立目录 apktool if framework-res.apk -p /opt/apktool/framework # 之后所有操作都显式指定同一个目录 apktool d app.apk -p /opt/apktool/framework -o out-p也就是--frame-pathdecode 和 build 两个子命令都支持两边必须指向同一个目录否则回编译时解析出的资源 ID 会对不上表现是装上去之后界面错乱或者直接崩。这是血泪经验框架目录前后不一致导致的资源 ID 错位从报错信息上完全看不出来。4.4 回编译、对齐、签名顺序错一步全废改完资源之后回编译# 先出未签名包--use-aapt2 在资源引用报错时优先尝试 apktool b out -o dist/app-unsigned.apk --use-aapt2--use-aapt2决定用哪一代资源编译器。aapt2 的检查更严格也更接近现在的构建链aapt1 对老包的容忍度更高。碰上style attribute not found或者资源 ID 相关的报错把这两个选项来回切一次能快速定位是不是编译器差异引起的。回编译产物还不能直接装必须对齐再签名而且顺序不能反# 1) 4 字节对齐-p 让 so 按页对齐必须在签名之前做 zipalign -p -f 4 dist/app-unsigned.apk dist/app-aligned.apk # 2) 同时出 v1 和 v2 签名 apksigner sign \ --ks ~/.android/debug.keystore \ --ks-pass pass:android \ --key-pass pass:android \ --ks-key-alias androiddebugkey \ --out dist/app-signed.apk \ dist/app-aligned.apk # 3) 验签别等装到设备上才发现 apksigner verify --print-certs dist/app-signed.apkzipalign必须排在apksigner前面。原因是 v2 签名会覆盖整个 zip 的文件布局先签名再对齐等于把签名破坏掉产物的表现是「装不上」或者「装上闪退」而apksigner verify那一步如果不跑你根本不知道是哪一环出的问题。-p参数是让 so 文件按页边界对齐对性能有实际影响不是可选项。调试用的 keystore 没有的话现生成一个参数照抄即可keytool -genkeypair -v \ -keystore ~/.android/debug.keystore \ -storepass android -keypass android \ -alias androiddebugkey \ -keyalg RSA -keysize 2048 -validity 10000 \ -dname CNDebug,ODemo,CCN-validity 10000是天数差不多二十七年调试证书不用考虑过期。-dname里随便填反正是本地验证用。5. 常见问题排查apktool 五个高频翻车现场5.1 现象一执行就抛 UnsupportedClassVersionError原因jar 的字节码版本高于本机 JVM。apktool 2.10 之后的包在只有 JDK 8 的机器上必然触发报错里的class file version数字就是线索。解决先java -version确认本机版本对照第 2 章那张表算出差几档。要么升级 JDK要么把 apktool 降到和现有 JDK 匹配的版本。不要试图改 jar 的字节码那套工具链比装个 JDK 麻烦得多。升降级之后记得重新跑一次apktool --version避免 PATH 里还挂着旧 jar。5.2 现象decode 中途报 Could not decode arsc或者资源解析到一半中断原因三个可能。apktool 版本比目标包老认不出新的资源格式aapt2 二进制缺失或者没有执行权限框架目录里的系统资源和目标包不匹配。解决按顺序排查。先换用 2.9.x 之后的版本重试再确认--use-aapt2与不用它两种模式各跑一次看报错是否变化最后检查框架目录用-p指定一个干净的目录重新apktool if。Linux 上如果是 aapt 相关报错顺手ls -l看一眼二进制有没有执行位从压缩包里解出来的文件经常是 644。5.3 现象回编译成功但装到设备上报解析包错误或直接闪退原因九成是签名链路的问题——没签名、签完又对齐、或者只出了 v1 签名但目标系统要求 v2。少数情况是apktool.yml里的 SDK 区间被改坏了回编译时资源引用越界。解决严格执行「对齐 → 签名 → 验签」三步中间不插别的操作。apksigner verify --print-certs有输出且证书信息正常说明签名这一环没问题。如果验签通过还是装不上把apktool.yml里的minSdkVersion和targetSdkVersion跟原包对比一下看看是不是解码过程中被改写。5.4 现象路径带空格或中文时命令行为完全不符合预期原因wrapper 脚本里参数展开没加引号shell 把带空格的路径拆成了多个参数。Windows 上还有一种情况是 bat 里用了%*但外层多包了一层引号导致双重引号。解决Linux/macOS 的脚本里$和$APKTOOL_JAR的引号一个都不能少这是硬性要求。Windows 上把%*改成不加外层引号的写法。另外输出目录尽量别用中文名虽然现代工具链大多支持但aapt系列在部分版本上处理非 ASCII 路径时会出问题把工作目录换到纯英文路径能排除一整类干扰。5.5 现象解出来的 res 目录里全是a/b/c这种短名找不到原文件名原因这不是装坏了是原包做了资源混淆资源名被重写成短路径映射表在打包时被剥离或者加密了。apktool 只能按现有的资源表还原还原不出来就是还原不出来。解决接受这个现实通过resources.arsc里的资源 ID 反查用途或者从布局文件里的引用关系倒推。这种情况对「提取文案」影响不大values/strings.xml通常还是完整的对「替换图片」确实麻烦需要结合布局文件逐个比对尺寸和引用位置。6. 装完怎么验证三条命令确认 apktool 真的能跑通装完不验证等于没装。我验收的顺序是固定的先证明工具能起来再证明它能把一个包解开最后证明它能把包再打回去并且签名有效。第三步最容易被跳过但恰恰是它能把「decode 正常、build 崩」这类半残状态暴露出来。第一步只看工具本身apktool --version java -version两条输出对得上说明 wrapper 找到的 jar 和 PATH 里的 java 是配套的。如果apktool --version打出来的版本和/opt/apktool/下软链指向的不一致说明 PATH 里还有另一个 apktoolcommand -v apktool查一下位置。第二步和第三步我写成一个脚本用一个自己的小包跑一遍三十秒出结论#!/usr/bin/env bash # apktool 装完验收解码 - 回编译 - 产物结构检查 set -euo pipefail APK${1:?用法: ./verify_apktool.sh your.apk} WORK$(mktemp -d) # 1) 只解资源跑得快够验证主链路 apktool d $APK -o $WORK/out -f -s # 2) 关键产物必须在缺一个就说明解码不完整 test -f $WORK/out/AndroidManifest.xml || { echo 清单没解出来; exit 1; } test -f $WORK/out/apktool.yml || { echo apktool.yml 缺失; exit 1; } test -d $WORK/out/res || { echo res 目录缺失; exit 1; } # 3) 原样回编译不做任何修改 apktool b $WORK/out -o $WORK/unsigned.apk apksigner verify --print-certs $WORK/unsigned.apk /dev/null 21 \ echo 注意: 产物自带原签名 \ || echo OK: 回编译通过未签名符合预期 echo 验收完成工作目录: $WORK脚本里几个细节值得说。${1:?...}是参数校验的简写没传参时直接把用法打出来并退出比if [ $# -lt 1 ]短。-s只是为了让验收跑得快它不覆盖 smali 那条链路如果日常主要工作是看代码把-s去掉再跑一次。最后那段验签判断用的是「回编译产物默认不带签名」这个事实——如果它反而验签通过说明apktool.yml或者original/里有东西被带进了产物值得看一眼。这套验收我建议固定成一个基线包每次换 apktool 版本、换 JDK、换机器都拿它跑一遍。有了基线出问题的时候能立刻判断是环境变了还是目标包变了不用在两个变量之间来回猜。从那以后我每次装完 apktool都会先拿这个基线包完整走一遍 decode 加 build再动真实项目——省下来的不是装工具那十分钟是后面排查「到底是包的问题还是环境的问题」的两个小时。希望帮到你。本文还有配套的精品资源点击获取