把一个服务已死的 2015 年 Android 应用(doit.im v4.4.5)通过”apktool 反编译树 + 最小侵入 smali 补丁”复活为纯本地版,并新增 AI、GitHub 同步、统计等功能,迭代 58 个版本的全部工程经验。源头是 49 条”坑→原因→解法”格式的踩坑记录。

总纲:三种视图各司其职

读代码用 jadx,改代码用 smali,验证用回解。

  • jadx 反编译结果只读不改(控制流还原不完美,会有 Code decompiled incorrectly 和 access$N 合成方法干扰);
  • 一切修改打在 apktool 的 smali 树上,两边对照看;
  • 构建后回解成品 APK 逐项 grep 验证补丁真的进了包。

工具链

  • apktool 2.9.1(资源 + smali 树)、jadx 1.5.3(读代码)、javac → d8 → baksmali(新功能链)。
  • baksmali 没有可用 CLI:apktool fat jar 只带引擎类不带运行器。写 5 行驱动类直接调引擎,永远可用:
// Bsm.java —— javac -cp apktool.jar 编译,java -cp .:apktool.jar 运行
DexFile dex = new DexBackedDexFile(Opcodes.forApi(19), Files.readAllBytes(new File(in).toPath()));
BaksmaliOptions opt = new BaksmaliOptions();
opt.apiLevel = 19;
Baksmali.disassembleDexFile(dex, new File(outDir), 1, opt);
// 注意:DexBackedDexFile 构造参数是 byte[],不是流
  • 构建三步:apktool b → zipalign -f 4 → apksigner sign + verify。

往 smali 工程加新功能(核心方法论)

  1. 永远不手写大段 smali:新功能用 Java 写 → javac → d8 → baksmali → 拷 smali 合入树。把 smali 当编译产物,不是手写语言。
  2. R.id/R.layout 引用不了(apktool 树没有 R.java):运行时 getResources().getIdentifier(name, type, pkg) 解析;新 id 往 res/values/ids.xml 加 <item type="id" />。
  3. 引用原版内部类用 stub 编译法:写同名同包空壳 stub 只参与编译不进 APK(d8 需 --classpath stubs-classes);baksmali 输出里 stub 自己的 smali 绝不拷进基线树(同名会顶掉原版)。
  4. stub 方法签名必须逐字节核对真身:曾因 stub 里 getActivity() 返回 Activity 而真身(support-v4 Fragment)返回 FragmentActivity,运行时 NoSuchMethodError 秒崩。凡涉及继承体系的方法,先 grep 真实类及其父链的 .method 签名。
  5. 跨包静态调用必须 public:smali 调用绕过 Java 编译期访问检查,包私有方法运行时 IllegalAccessError。
  6. 重写生命周期方法必须保留 invoke-super:删掉 super.onCreate 当时正常,返回键触发后续生命周期就 NPE。
  7. 复用原版 API 前看三样:签名、@Deprecated 注解、方法体是否只有 return 常量(空壳方法恒返回 null 也能让审计通过)。
  8. 字段语义看写入方,别按建表类型猜:如 completed 列建表 NUMERIC,实际 DAO 存的是 Calendar 完成时间戳(未完成=0),按 0/1 查询永远为空。
  9. 统计类需求直查 SQLite 绕开 DAO:openDatabase(OPEN_READONLY) 只读连接与主连接共存,免 stub、免空壳陷阱。

隐形地雷(排查口诀)

  • android:onClick 无声失效的头号原因:原版自定义控件在构造器里 setOnClickListener 覆盖了 XML 监听器。解法:代码接线(原版组件调它的 setOnLayoutClickListener,stub 里声明;或遍历视图树按文本找)。
  • 静态排查被”字符串只在 R 类引用”骗:业务代码引用的是数字资源 ID(如 0x7f0c0092)。查”谁触发了这文案”:先查 res/values/public.xml 把资源名映射成十六进制 ID,再用 ID 反查 smali。
  • 清除数据 = 首次真实验收:此前一切正常可能依赖旧版安装残留的登录态/偏好;假用户对象字段全空,清数据后才走到。假 User 必须给完整、稳定、合理的身份(uuid/timezone 等),并在模型 getter 层兜底救已入库的脏数据。
  • 无 adb 环境取崩溃日志:Application.onCreate 里装 Thread.setDefaultUncaughtExceptionHandler(装在原处理器之后保留链),堆栈落盘到外部文件目录 + 公共 Download 目录,用户用文件管理器就能取。
  • 自定义 versionName 前全局搜 parseInt:原版版本检查按点切分 Integer.parseInt,4.4.5-local-r10 会 NumberFormatException 潜伏多日才爆。
  • 同屏同名按钮穷举:用户说”XX 按钮”,布局文件和 menu 目录都要搜,再用资源 ID 反查点击处理类。
  • Fragment 里接线不能用 getView():onCreateView 返回前 getView() 为 null,NPE 被 try/catch 吞掉表现为”点了没反应”;把 inflate 出来的 layoutView 直接传给接线方法。
  • 借原版”协议化”接缝:原版附件模型通用(uuid/type/title/url)+ setResult 回传管线自动去重落库刷新——新页面按同样协议 setResult,劫持点小到一行。先找协议化接缝,比硬改 UI 层优雅一个数量级。
  • 换控件类型 = 闪退:原版代码对布局控件有 check-cast 硬转换。文案可以随便换,控件类不能随便换;改前 grep 资源 ID 找出全部消费方。

老 App 上新 Android 的兼容补丁清单

问题修法
Android 14+ 装不上apktool.yml targetSdkVersion: 23(minSdk 不动)
HttpClient NoClassDefFoundErrormanifest 加 <uses-library android:name="org.apache.http.legacy"/>
长屏信箱黑边android:maxAspectRatio="2.4" + resizeableActivity="true"
闹钟 Doze 下不响AlarmManager.set() 同签名替换为 setExactAndAllowWhileIdle(smali 一行 sed)+ 补 POST_NOTIFICATIONS 权限;国产 ROM 还需用户侧加自启动白名单
升级锁死versionCode 必须在原版序列上递增(14144xx);Android 不允许降级安装,“回退” = git checkout 旧代码 + 打更高 versionCode 重发

versionName 用 4.4.5-local-rN 让用户能在系统设置里肉眼确认装的哪版——用户报”没变化”时先问版本号,再怀疑代码。

构建后验证(固定动作)

  1. aapt2 dump badging 查版本号;
  2. unzip -p apk classes.dex | strings | grep 关键类 确认 smali 补丁;
  3. aapt2 dump xmltree/resources 确认 res 补丁与新 id 分配。
  4. 分发后重新下载线上包校验字节数与哈希(CDN ~10 分钟缓存,下载可能截断——先比字节数再比哈希)。

原始素材

  • playbook
  • REPORT
  • AGENTS

相关页面