手把手带你打造一个 Android 热修复框架(上篇)
本文来自网易云社区
作者:王晨彦
前言
热修复和插件化是目前 Android 领域很火热的两门技术,也是 Android 开发工程师必备的技能。
目前比较流行的热修复方案有微信的 Tinker,手淘的 Sophix,美团的 Robust,以及 QQ 空间热修复方案。
QQ 空间热修复方案使用Java实现,比较容易上手。
如果还不了解 QQ 空间方案的原理,请先学习安卓App热补丁动态修复技术介绍
今天,我们就基于 QQ 空间方案来深入学习热修复原理,并且手把手完成一个热修复框架。
本文参考了 Nuwa,在此表示感谢。
本文基于 Gradle 2.3.3 版本,支持 Gradle 1.5.0-3.0.1。
实战
了解了热修复原理后,我们就开始打造一个热修复框架
关闭dex校验
根据文章中提到的第一个问题,在 Android 5.0 以上,APK安装时,为了提高 dex 加载速度,未引用其他 dex 的 class 将会被打上 CLASS_ISPREVERIFIED 标志。
打上 CLASS_ISPREVERIFIED 标志的 class,类加载器就不会去其他 dex 中寻找 class,我们就无法使用插桩的方式替换 class。
文章给出了解决办法,即让所有类都依赖其他 dex。如何实现呢?
新建一个 Hack 类,让所有类都依赖该类,将该类打包成 dex,在应用启动时优先将该 dex 插入到数组的最前面,即可实现。
OK,确定思路后,我们就开始动手。
找出编译后的 class
听起来好像很简单,那么如何让所有类依赖 Hack 类呢,总不能一个一个类改吧,怎么才能在打包时自动添加依赖呢?
接下来就要用到 Gradle Hook 和 ASM。
还不了解 Gradle 构建流程的赶快去学习啦
要想修改编译后的 class 文件,首先要 Hook 打包过程,在 Gradle 编译出 class 文件到打包成 APK 之间植入我们的代码,对 class 文件进行修改。
找到编译后的class文件要依赖 Gradle Hook ,而修改 class 文件要依赖 ASM。
首先,我们要找到编译后的 class 文件
新建一个 Project CFixExample,然后执行 assembleDebug
观察 Gradle Console 输出
- :app:preBuild UP-TO-DATE
- :app:preDebugBuild UP-TO-DATE
- :app:checkDebugManifest
- :app:preReleaseBuild UP-TO-DATE
- :app:prepareComAndroidSupportAnimatedVectorDrawable2540Library// 省略部分Task:app:prepareComAndroidSupportSupportVectorDrawable2540Library
- :app:prepareDebugDependencies
- :app:compileDebugAidl UP-TO-DATE
- :app:compileDebugRenderscript UP-TO-DATE
- :app:generateDebugBuildConfig UP-TO-DATE
- :app:generateDebugResValues UP-TO-DATE
- :app:generateDebugResources UP-TO-DATE
- :app:mergeDebugResources UP-TO-DATE
- :app:processDebugManifest UP-TO-DATE
- :app:processDebugResources UP-TO-DATE
- :app:generateDebugSources UP-TO-DATE
- :app:incrementalDebugJavaCompilationSafeguard
- :app:javaPreCompileDebug
- :app:compileDebugJavaWithJavac
- :app:compileDebugNdk NO-SOURCE
- :app:compileDebugSources
- :app:mergeDebugShaders
- :app:compileDebugShaders
- :app:generateDebugAssets
- :app:mergeDebugAssets
- :app:transformClassesWithDexForDebug
- :app:mergeDebugJniLibFolders
- :app:transformNativeLibsWithMergeJniLibsForDebug
- :app:processDebugJavaRes NO-SOURCE
- :app:transformResourcesWithMergeJavaResForDebug
- :app:validateSigningDebug
- :app:packageDebug
- :app:assembleDebug
- BUILD SUCCESSFUL in 10s
这些就是 Gradle 打包时执行的所有任务,不同版本的 Gradle 会有所不同,这里我们基于 Gradle 2.3.3。
请注意 processDebugManifest 和 transformClassesWithDexForDebug 这两个Task,根据名字我们可以先猜测一下
第一个 Task 的作用应该是处理Manifest,这个我们等会儿会用到
第二个 Task 的作用应该是将 class 转换为 dex,这不正是我们要找的 Hook 点吗?
没错,为了验证我们的猜测,我们打印一下 transformClassesWithDexForDebug 的输入文件
在 app 的 build.gradle 中添加如下代码
- project.afterEvaluate {
- project.android.applicationVariants.each { variant ->
- Task transformClassesWithDexTask = project.tasks.findByName("transformClassesWithDexFor${variant.name.capitalize()}")
- println("transformClassesWithDexTask inputs")
- transformClassesWithDexTask.inputs.files.each { file ->
- println(file.absolutePath)
- }
- }
- }
再次打包,观察输出
- transformClassesWithDexTask inputs
- C:\Users\hzwangchenyan\.android\build-cache\97c23f4056f5ee778ec4eb674107b6b52d506af5\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\6afe39630b2c3d3c77f8edc9b1e09a2c7198cd6d\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\c30268348acf4c4c07940f031070b72c4efa6bba\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\5b09d9d421b0a6929ae76b50c69f95b4a4a44566\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\e302262273df85f0776e06e63fde3eb1bdc3e57f\output\jars\classes.jar
- C:\Users\hzwangchenyan\.gradle\caches\modules-2\files-2.1\com.android.support\support-annotations\25.4.0\f6a2fc748ae3769633dea050563e1613e93c135e\support-annotations-25.4.0.jar
- C:\Users\hzwangchenyan\.android\build-cache\36b7224f035cc886381f4287c806a33369f1cb1a\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\5d757d92536f0399625abbab92c2127191e0d073\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\011eb26fd0abe9f08833171835fae10cfda5e045\output\jars\classes.jar
- D:\Android\sdk\extras\m2repository\com\android\support\constraint\constraint-layout-solver\1.0.2\constraint-layout-solver-1.0.2.jar
- C:\Users\hzwangchenyan\.android\build-cache\36b443908e839f37d7bd7eff1ea793f138f8d0dd\output\jars\classes.jar
- C:\Users\hzwangchenyan\.android\build-cache\40634d621fa35fcca70280efe0ae897a9d82ef8f\output\jars\classes.jar
- D:\Android\AndroidStudioProjects\CFixExample\app\build\intermediates\classes\debug
build-cache 就是 support 包
看起来这些都是 app 依赖的 library,但是我们自己的代码呢
看看最后一行 app\build\intermediates\classes\debug 目录
没错,正是我们自己的代码,看来我们的猜测是正确的。
将 class 插入对 Hack 的引用[重点]
找到了编译后的 class 文件,接下来使用 ASM 对 class 文件进行修改
- ClassReader cr = new ClassReader(inputStream)
- ClassWriter cw = new ClassWriter(cr, 0)
- ClassVisitor cv = new ClassVisitor(Opcodes.ASM4, cw) { @Override
- MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
- MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions)
- mv = new MethodVisitor(Opcodes.ASM4, mv) { @Override
- void visitInsn(int opcode) { if ("<init>".equals(name) && opcode == Opcodes.RETURN) { super.visitLdcInsn(Type.getType("Lme/wcy/cfix/Hack;"))
- } super.visitInsn(opcode)
- }
- } return mv
- }
- }
- cr.accept(cv, 0)
我们通过复写 ClassVisitor 的 visitMethod 方法,得到 class 的所有方法,在构造函数中插入 Hack 类的引用。
可以看到,即将打包为dex的源文件既有 jar 又有 class,class 文件我们直接修改就好,而对于 jar 文件,我们需要先将其解压,对解压后的 class 文件进行修改,然后再压缩。
- File optDirFile = new File(jarFile.absolutePath.substring(0, jarFile.absolutePath.length() - 4))
- File metaInfoDir = new File(optDirFile, "META-INF")
- File optJar = new File(jarFile.parent, jarFile.name + ".opt")
- CFixFileUtils.unZipJar(jarFile, optDirFile)if (metaInfoDir.exists()) {
- metaInfoDir.deleteDir()
- }
- optDirFile.eachFileRecurse { file ->
- if (file.isFile()) {
- processClass(file, hashFile, hashMap, patchDir, extension)
- }
- }
- CFixFileUtils.zipJar(optDirFile, optJar)
- jarFile.delete()
- optJar.renameTo(jarFile)
- optDirFile.deleteDir()
保存文件 Hash 值
我们今天的目的是打造一个热修复框架,因从我们需要对于引入了 Hack 的 class 做一个记录,让我们在修改代码后打补丁包时可以知道哪些类发生了改变,只需要打包修改了的类作为补丁即可。
如何记录呢,我们知道,Java 在编译时同样的 Java 文件编译为 class 后字节码是一致的,因此直接计算文件 Hash 值并保存即可。
制作补丁时对比 class 文件的 Hash 值,如果不同,则打包进补丁。
插入 Hack dex
新建 Hack.java
- public class Hack {
- }
上面我们提到,将包含 Hack 类的 dex 插入到 dex 数组的最前面,不然的话将会出现 Hack ClassNotFoundException,打包 dex 可以使用 build tool 的 dx 命令,位于 /sdk/build-tools/version/dx
- dx --dex --output=patch.jar classDir
打包为 dex 并压缩为 jar
打包完成,如何插入到数组最前面呢,其实就和普通的补丁文件一样,只不过在普通补丁之前插入
- public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
- DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader()); Object baseDexElements = getDexElements(getPathList(getPathClassLoader())); Object newDexElements = getDexElements(getPathList(dexClassLoader)); Object allDexElements = combineArray(newDexElements, baseDexElements); Object pathList = getPathList(getPathClassLoader());
- ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
- }
这里采用反射的方法,对 BaseDexClassLoader 的 dexElements 进行修改。
这个插入操作是在应用启动时完成的,那 dex 文件从哪里来呢,我们可以将 dex 放在 assets 中,插入前先将其复制到应用目录。
这个操作我们放在 Application 的 attachBaseContext 中执行。
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区。
相关文章:
【推荐】 市场调研中数据分析之道
【推荐】 移动端爬虫工具与方法介绍
手把手带你打造一个 Android 热修复框架(上篇)的更多相关文章
- 手把手带你打造一个 Android 热修复框架
本文来自网易云社区 作者:王晨彦 Application 处理 上面我们已经对所有 class 文件插入了 Hack 的引用,而插入 dex 是在 Application 中,Application ...
- Android热修复框架汇总整理(Hotfix)
Android平台出现了一些优秀的热更新方案,主要可以分为两类:一类是基于multidex的热更新框架,包括Nuwa.Tinker等:另一类就是native hook方案,如阿里开源的Andfix ...
- [Android]热修复框架AndFix测试说明
AndFix,全称是Android hot-fix.是阿里开源的一个热补丁框架,允许APP在不重新发布版本的情况下修复线上的bug.支持Android 2.3 到 6.0,并且支持arm 与 X86系 ...
- Android热修复之微信Tinker使用初探
文章地址:Android热修复之微信Tinker使用初探 前几天,万众期待的微信团队的Android热修复框架tinker终于在GitHub上开源了. 地址:https://github.com/ ...
- Android热修复之AndFix使用教程
AndFix的github地址 AndFix 全称Android hot-fix,是alibaba的Android热修复框架,支持Android 2.3到6.0的版本,支持arm与X86系统架构,支持 ...
- 手把手带你做一个超炫酷loading成功动画view Android自定义view
写在前面: 本篇可能是手把手自定义view系列最后一篇了,实际上我也是一周前才开始真正接触自定义view,通过这一周的练习,基本上已经熟练自定义view,能够应对一般的view需要,那么就以本篇来结尾 ...
- Android 热修复方案Tinker(一) Application改造
基于Tinker V1.7.5 Android 热修复方案Tinker(一) Application改造 Android 热修复方案Tinker(二) 补丁加载流程 Android 热修复 ...
- Android 热修复使用Gradle Plugin1.5改造Nuwa插件
随着谷歌的Gradle插件版本号的不断升级,Gradle插件如今最新的已经到了2.1.0-beta1,相应的依赖为com.android.tools.build:gradle:2.0.0-beta6, ...
- 深入探索Android热修复技术原理读书笔记 —— 资源热修复技术
该系列文章: 深入探索Android热修复技术原理读书笔记 -- 热修复技术介绍 深入探索Android热修复技术原理读书笔记 -- 代码热修复技术 1 普遍的实现方式 Android资源的热修复,就 ...
随机推荐
- 裸机恢复 (BMR) 和系统状态恢复
DPM 将系统保护数据源视为两个组成部分 – 裸机恢复 (BMR) 和系统状态保护. BMR 涉及保护操作系统文件和重要卷上的所有数据,用户数据除外. 系统状态保护涉及保护操作系统文件. DPM 使用 ...
- HDD + HDD(SSD) 多硬盘系统启动问题
一.问题描述 最近购一了款有预留msata接口的hp电脑,买回来开始折腾SSD和内存的问题,内存一次性就解决了:但SSD在安装过程中遇到三个问题,分别是: 无螺丝,无法固定固态硬盘(后在一个旧鼠标上弄 ...
- ord 字符转code chr : code转字符
print(ord('刀')) # ord 字符转Unicode # 20992 print(chr(20992)) # Unicode 转成chr(字符)
- APP案例分析-摩拜单车app
第二次作业-App案例分析 本次案例分析选用的是 摩拜单车IOS5.7.5版本 测试环境为 IPhone 6s (IOS11.0.1,含有3DTOUCH功能).本次案例分析仅针对APP 而言,并不涉及 ...
- Oracle 空间查询, 数据类型为 sdo_geometry
因网上搜索到的相关资料大部分都是关于sdo_geometry的介绍和以及通过sql语句添加要素,查询要素等等.没有找到存储过程相关的例子,所以只好自己动手啦. 准备 环境:windowsxp系统,安装 ...
- 洛谷 P1251 餐巾计划问题(线性规划网络优化)【费用流】
(题外话:心塞...大部分时间都在debug,拆点忘记加N,总边数算错,数据类型标错,字母写错......) 题目链接:https://www.luogu.org/problemnew/show/P1 ...
- 1927. [SDOI2010]星际竞速【费用流】
Description 10年一度的银河系赛车大赛又要开始了.作为全银河最盛大的活动之一,夺得这个项目的冠军无疑是很多人的 梦想,来自杰森座α星的悠悠也是其中之一.赛车大赛的赛场由N颗行星和M条双向星 ...
- servlet使用
一.使用IDEAL创建项目 1) 2) 3) 4) 5) 6) 7) 8) 9) 二.路径介绍: 配置文件: servlet配置文件: package ser_Test; import javax.s ...
- java 控制流
一: 块作用域 块(即复合语句):是指由一对花括号括起来的若干条简单的java语句.块决定了变量的作用域,一个块可以嵌套在另一个块中,如下: public class print_In { p ...
- DRCNN超分辨重建2016年
论文疑点: Embedding层是怎么操作的? https://gshtime.github.io/2018/06/01/tensorflow-embedding-lookup-sparse/ 这篇文 ...