本文最后更新于 2026-07-25,文章内容可能已经过时。

App:Android APK 本地化与逆向修改实践

在一次 Android 应用本地化学习过程中,我选择了两款第三方应用作为测试案例。它们的功能本身不差,但全英文界面用起来总有点膈应。于是某天下午,闲来无事,我决定自己动手研究如何做 APK 本地化。

本以为就是改几个字符串重新打包的事,结果一路踩坑,从签名方案到 DEX 反编译再到外科手术式改二进制,硬是把 Android APK 修改流程从资源层摸到了二进制层。趁着记忆还新鲜,把这套流程记录下来,给后来的朋友们一个参考。

注意:本文仅用于学习 Android 应用本地化、无障碍优化以及个人软件定制。请确保你拥有 APK 的修改权限,不要用于绕过授权、破解付费功能或传播未经授权的软件。


一、准备工作

1.1 工具链

工具

用途

apktool 2.10.0

传统 APK 反编译/回编译工具(适合资源分析和 smali 修改)

APKEditor 1.4.9

较新的 APK 编辑工具,在部分现代 APK(尤其高 targetSdk、高混淆程度的应用)上的资源处理兼容性较好,适合快速修改资源和 smali

zipalign

对 APK 内文件做 4 字节对齐

apksigner.jar

Android APK 签名工具(v1/v2/v3/v4)

JDK 17+

Java 运行环境

其中 zipalign 和 apksigner 如果你安装了 Android SDK,应该在 build-tools/ 下面能找到。如果没有,可以单独下载 Android Build Tools,或者从其他 Android 开发环境附带的工具目录中获取。

1.2 签名密钥

在开始之前,先准备好签名密钥。如果没有的话先生成一个:

 keytool -genkey -v -keystore myapp.keystore \
   -alias myalias -keyalg RSA -keysize 2048 -validity 36500

记好密钥别名和密码,之后每次签名都要用。注意:密钥一旦生成就不要再变,否则安装时会因为签名不一致无法覆盖更新。重新签名后的 APK 与原版已经不是同一个签名身份,因此无法直接覆盖安装官方版本,需要先卸载原版。


二、APK 反编译

拿到一个 APK 后,首先需要了解它的内部结构。APK 本质上是个 ZIP 包,解压后可以看到:

 ├── AndroidManifest.xml      (二进制格式)
 ├── classes.dex              (Java 代码编译产物,可能有多个)
 ├── resources.arsc           (编译后的资源索引表)
 ├── res/                     (资源文件:布局、字符串、图片等)
 ├── lib/                     (native .so 库)
 ├── META-INF/                (签名信息)
 └── assets/                  (原始资源文件)

差不多就这样:

文件.png

2.1 反编译资源

对于近年来高 targetSdk、高混淆程度以及采用新构建链的 APK,推荐使用 APKEditor:

 java -jar APKEditor.jar d -i input.apk -o output_dir

解码后的 output_dir 目录中:

  • resources/package_1/res/ → 资源文件(可直接编辑)

  • smali/ → 反编译后的 DEX 代码(smali 格式)

  • AndroidManifest.xml → 明文清单文件

2.2 逆向前先摸清底细

在动手之前,建议先做三件事:

 # 1. 备份原版 APK
 cp input.apk input.apk.bak
 ​
 # 2. 查看签名方案(后面签名时要用)
 java -jar apksigner.jar verify -v input.apk
 ​
 # 3. 判断是否有加壳/混淆/完整性校验
 # 如果发现 lib/ 下有 libjiagu.so、libprotect.so 等,说明应用加了壳
 # 这种情况通常需要额外分析处理,不在本文展开讨论

三、翻译资源文件

3.1 strings.xml

这是最主要的工作量所在。每个字符串对应一个 <string name="xxx">value</string> 条目:

 <!-- 翻译前 -->
 <string name="app_name">MyApp</string>
 <string name="login_welcome">Welcome to MyApp</string>
 ​
 <!-- 翻译后 -->
 <string name="app_name">MyApp汉化版</string>
 <string name="login_welcome">欢迎使用 MyApp</string>

直接用文本编辑器搜索替换即可。但有几个坑要注意:

  1. 不要翻译格式占位符%1$s%d%2$d 这类 Java 格式化占位符保持原样。

  2. 不要翻译 HTML 标签<b><font><br> 等保持原样。

  3. 不要翻译特殊语法<annotation link="xxx"> 这类自定义标签保持原样。

3.2 快速定位要翻译的内容

新手最头疼的是不知道哪些文件需要改。几个实用的搜索技巧:

Linux / macOS / WSL:

 # 搜索资源中的英文界面文本
 grep -r "Settings" res/
 grep -r "Enable" res/
 grep -r "Disable" res/
 ​
 # 搜索 smali 中的硬编码字符串
 grep -r 'const-string.*"Login"' smali/
 grep -r 'const-string.*"Welcome"' smali/

Windows PowerShell:

 Select-String -Path res\values\strings.xml -Pattern "Settings"
 Select-String -Path smali\**\*.smali -Pattern "Login"

关键词建议:从常见 UI 词汇入手——LoginSettingsEnableDisableConfirmCancelSaveDeleteOK

3.3 plurals.xml

复数形式字符串也需要翻译:

 <!-- 翻译前 -->
 <plurals name="new_notifications">
     <item quantity="one">%d new notification</item>
     <item quantity="other">%d new notification(s)</item>
 </plurals>
 ​
 <!-- 翻译后 -->
 <plurals name="new_notifications">
     <item quantity="one">%d 条新通知</item>
     <item quantity="other">%d 条新通知</item>
 </plurals>

3.4 两种思路:标准本地化 vs APK 修改

如果是自己开发 App,推荐标准做法:新增 values-zh 等语言资源目录,Android 会根据系统语言自动切换。但对于已经发布的第三方 APK,通常没有预留完整的多语言结构,直接替换默认资源或修改语言逻辑是更实际的方案。本文以 APK 修改路线为主。

3.5 新增字符串资源

如果在 strings.xml 中新增了自创字符串(比如汉化署名),使用 apktool 等传统方式时可能需要同步处理资源 id 映射。APKEditor 等较新的工具通常可以自动管理,但遇到资源引用错误时,可以通过资源 dump 工具或 APKEditor 的资源引用保持功能进行排查。


四、翻译 Smali 硬编码字符串

有些字符串不在 strings.xml 中,而是直接写死在 Java/Kotlin 代码里。反编译后以 const-string 形式出现在 .smali 文件中:

 # 翻译前
 const-string v0, "Basic incubator"
 ​
 # 翻译后
 const-string v0, "基础孵化器"

关键判断依据:只改「显示标签」类的字符串,不改「存储键值」和「比较用常量」。怎么判断?

  • 如果这个字符串被 setText()TextView 使用 → 可以翻译(是 UI 显示)

  • 如果这个字符串被 equals() 比较或存入 SharedPreferences 的 key → 不要改(是逻辑代码)

举例,在实际案例的某个 smali 文件中:

 # 这三行是显示用的下拉选项,可以翻译
 const-string v4, "Random"              →  "随机"
 const-string v4, "Long Distance First" →  "长距离优先"
 const-string v4, "Short Distance First"→  "短距离优先"

但类似 "xxx_auto_xxx_" 这种功能配置键值绝对不能动——改了键值会导致所有已保存的配置失效。


五、处理服务端本地化

不少现代 App 会把动态内容(比如实体名称、属性描述等)从服务器拉取,本地只缓存一份。例如某些 App 会在代码中固定请求语言:

 setLocalizationLocale("en_US");

这种情况下,即使 strings.xml 已经翻译完成,动态数据仍可能保持英文。

以某第三方应用为例,其初始化请求中固定使用了 "en_US",同时缓存读取模块也锁定了 "en_US" 作为查找前缀。导致动态字符串(名称、描述等)始终是英文。

解决方案:把本地化请求语言改成 zh_CN,同时修改缓存键值的读取逻辑:

 // 翻译前
 setLocalizationLocale("en_US");
 ​
 // 翻译后
 setLocalizationLocale("zh_CN");

需要注意的是,这个方法的前提是服务端确实支持目标语言。实际中服务器可能只支持部分语言(比如仅 en_USja_JP),如果目标语言不在支持列表中,修改 locale 后拿到的可能仍是英文甚至空数据。这种情况下需要额外做本地映射或数据替换,不在本文讨论范围内。


六、回编译与签名

6.1 回编译

APKEditor 回编译:

 java -jar APKEditor.jar b -i output_dir -o app_unsigned.apk

6.2 对齐

 zipalign -f 4 app_unsigned.apk app_aligned.apk

注意:zipalign 必须在签名前执行。v2/v3/v4 签名会校验 APK 内容,签名后再做 zipalign 会导致签名失效。

6.3 签名

这是最大的一个坑。Android 7.0+ 引入了 APK Signature Scheme v2,Android 9.0+ 又加了 v3,Android 11+ 加 v4。签名方案需要和原版保持一致,否则安装时会报「安装包解析失败」。

查看原版 APK 的签名方案:

 java -jar apksigner.jar verify -v original.apk

输出示例:

 Verified using v1 scheme (JAR signing): false
 Verified using v2 scheme (APK Signature Scheme v2): true
 Verified using v3 scheme (APK Signature Scheme v3): false

根据原版的方案来指定你的签名参数。比如原版只用 v2,那就:

 java -jar apksigner.jar sign \
   --ks myapp.keystore --ks-key-alias myalias \
   --ks-pass pass:yourpassword --key-pass pass:yourpassword \
   --v1-signing-enabled false \
   --v2-signing-enabled true \
   --v3-signing-enabled false \
   --in app_aligned.apk --out app_signed.apk

最后验证:

 java -jar apksigner.jar verify -v app_signed.apk

重要:重新签名后的 APK 与官方 APK 已经不是同一个签名身份。因此:

  1. 无法直接覆盖安装官方版本,需要先卸载原版;

  2. 如果需要与原版共存,可以修改 applicationId(包名)生成独立应用,即可与官方版本同时安装。


七、踩坑实录

7.1 第一次签名:信心满满,换来五个字

第一次尝试的时候,想法非常简单:改几行 XML,重新打包,签名,完事。

于是照着网上的教程:

  1. apktool 解包

  2. 修改 strings.xml

  3. apktool 回编译

  4. jarsigner 签名

  5. 传到手机安装

结果,Android 只回了我五个字:

 安装包解析失败
安装失败.png

这五个字大概是我学逆向以来最讨厌的提示。不说哪里错,不说为什么,不说怎么修。

反复检查了 XML 有没有语法错误,重新打包了三遍,依然失败。最后偶然发现——不是翻译内容的问题,是签名方案不对。原版 APK 用的是 APK Signature Scheme v2,而我用的 jarsigner 只能打 v1。系统一看签名方案对不上,直接拒了。

解决之后我以为终于可以了,结果传到手机上——

 安装包解析失败

……又来。


7.2 第二次签名:方案对了,工具错了

真的哭死了

哭.png

这次签名是 v2 了,为什么还失败?

排查了一圈,发现是 apktool 回编译的产物本身有问题。某些设备的 ROM(尤其国产厂商深度定制的系统)对 apktool 重新生成的 resources.arsc 兼容性很差。哪怕一个字都没改,只是解包→回编译→签名,照样装不上。

但同样的 APK,直接对原版重签名,不经过 apktool,就能装。

这说明什么?问题不在签名,在 回编译工具链产出的文件结构和系统期望不一致。

解决办法:换工具。改用 APKEditor 之后,回编译产物的结构更接近原版,总算通过了那台设备的安装校验。


7.3 高级方案:绕开编译,直接修改二进制资源

APKEditor 解决问题后,我又遇到了新情况。

有些功能字符串不在 XML 里,写在 smali 代码中。用 APKEditor 改 smali 虽然可以,但每次改完都要重新回编译、对齐、签名,改一行就要跑一次完整流程,非常耗时。

于是我想了一个更"暴力"的办法——不对 APK 做反编译和回编译,直接修改原版 APK 二进制中的 resources.arsc 字符串池。

流程是:

  1. 用 Python 的 zipfile 读出 resources.arsc

  2. 用 struct 解析字符串池的二进制结构

  3. 找到所有英文文本,替换为中文

  4. 新字符串池补齐到和原来一样长(不能改变文件大小,否则 ZIP 偏移全乱)

  5. 覆盖回去,更新 CRC-32 校验

  6. 重签名

这个方法绕开了 aapt2 的重新编译,完全不碰 smali 和布局文件,理论上兼容性最好。

但是这个"外科手术"有两个巨坑。


7.4 styles 终止符:一个字节都不许错

resources.arsc 的字符串池 chunk 有一个规定:styles 块必须位于 chunk 末尾,并且最后 4 个字节必须是 0xFFFFFFFF 作为终止标记。

我最早写脚本的时候,替换完字符串后池子变短了,就在最后补了一堆 \x00,然后自信地跑 aapt2 验证。

结果:

 corrupt string pool

看了半天才发现,padding 补在了 styles 块后面,把 0xFFFFFFFF 终止符顶到中间去了。正确做法是把 padding 放在 styles 前面,让 styles 始终紧贴末尾。

这个教训让我明白:二进制资源格式没有容错机制。差一个字节,整个 APK 就废了。除了 styles 终止符,字符串池相关的计数和偏移信息也需要保持一致,否则解析同样会失败。


7.5 emoji 长度:Python 也会骗你

UTF-8 字符串池中,每个字符串除了存储原始 UTF-8 字节,还要记录它在 UTF-16-LE 编码下的单元数(不是字节数)。

大部分中英文字符在 UTF-16 中都是 1 个单元。所以我一开始直接用了 Python 的 len(s)

 utf16_len = len(s)  # 天真

翻译进展顺利,直到我遇到一个带 emoji 的字符串(比如 🎉)。Python 的 len() 返回的是 Unicode 码点数,但 emoji 这类非 BMP 字符在 UTF-16 中占 2 个单元len() 返回 1,实际需要 2。

于是 aapt2 又报:

 Bad string block

正确写法:

 utf16_len = len(s.encode('utf-16-le')) // 2

一行代码改了三个小时。

后来我发现,很多逆向问题不是难在原理,而是难在这些看似不起眼的小细节上。


7.6 翻完不等于翻对:那些不能碰的字符串

字符串翻译完成后,我把改好的 APK 装上了。打开一看,界面中文了,高兴了大概两分钟——然后发现有些按钮点了没反应。

排查发现,我翻译了不该翻译的东西。

有些 smali 中的 const-string 不是给用户看的,是代码逻辑用的——比如 SharedPreferences 的存取键值、条件判断的比较字符串、广播 action 名。我把它们也"汉化"了,结果是代码找不到对应的 key,默默失败,没有报错,没有崩溃,只是功能静悄悄地废掉了。

血的教训:看到英文不要急着翻译,先判断它是给用户看的,还是给程序看的。

如果是给机器看的,留着它。


7.7 动画错乱:资源 id 漂移

还有一个诡异的问题:回编译后某些页面切换动画变得卡顿,或者图形位置莫名其妙偏移。

一开始以为是性能问题,后来发现是资源 id 在重新编译时发生了偏移。原来引用 @id/someView 的地方,因为 public.xml 重新生成了 id,指向了错误的目标。画面不崩,只是引用了不对的动画资源,看起来像慢了半拍。

APKEditor 的 --ref-res 参数可以固定引用关系,避免这种漂移。


八、流程总结

 原始 APK
     │
     ├── 保存备份
     ├── 查看签名方案 (apksigner verify -v)
     └── 判断是否有加壳/完整性校验
     │
     ▼
 APKEditor 反编译 ────────→ 搜索定位英文字符串
     │                              │
     │                         ├── strings.xml / plurals.xml
     │                         ├── smali const-string
     │                         │   (判断是显示标签还是逻辑键值)
     │                         └── 服务端本地化(修改 locale)
     │                              │
     │                        新增字符串 → 检查 public.xml
     │                              │
     ▼                              ▼
 APKEditor 回编译 ←────────── 所有修改完成
     │
     ▼
 zipalign 对齐
     │
     ▼
 apksigner 签名(与原版方案一致)
     │
     ▼
 验证:apksigner verify -v
     │
     ▼
 安装测试

写在最后

整件事花了我不少时间,从一无所知到能独立完成汉化,最关键的不是技术本身——Android 逆向工程的工具已经很成熟了——而是耐心排查问题的心态。每一个「安装包解析失败」背后都藏着一个具体的原因,找到了就是一层窗户纸。

本文仅讨论 Android 应用本地化和逆向分析技术,不提供任何第三方应用修改版本、补丁文件或下载地址。请遵守软件作者协议以及相关平台规则。