手把手带你打造一个 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资源的热修复,就 ...
随机推荐
- Provisional headers are shown
问题: Chrome请求出现"Provisional headers are shown": 原因: 这种一般是由于浏览器端的插件或客户端的软件对请求进行了拦截:我们出现的情况,是 ...
- APUE 12.7 取消选项
- Aria2 无限制下载神器
Aria2 是一款免费开源跨平台且不限速的多线程下载软件,Aria2的优点是速度快.体积小.资源占用少:支持 HTTP / FTP / BT / Magnet 磁力链接等类型的文件下载:支持 Win. ...
- php解析xml文件的方法
最近一段时间在做模板包导入.模板包中包含有xml文件,,需要解析成给定的php数组格式. 我接触到了两种方法,分别是DOMDocument 方法和 simple_load_file. 个人偏好后一种, ...
- 【转】Json判断是否存在某个属性和遍历各个属性和值
var field='uid'; var jsonObj={uid:'001'}; 一. jsonObj[field] != undefined //注意:如果field值正好是undefined那就 ...
- 静默安装oracle后,启动oem报错,解决方法!
一.手工重建EM Oracle 的gridcontrol 由两部分组成:dbcontrol 和repository. 我们可以对某一部分进行操作,也可以同时进行操作. 这里先看一个因为修改hostna ...
- BZOJ2281:[SDOI2011]黑白棋(博弈论,组合数学,DP)
Description 小A和小B又想到了一个新的游戏. 这个游戏是在一个1*n的棋盘上进行的,棋盘上有k个棋子,一半是黑色,一半是白色. 最左边是白色棋子,最右边是黑色棋子,相邻的棋子颜色不同. 小 ...
- ajax调用webservice 跨域问题
用js或者jquery跨域调用接口时 对方的接口需要做jsonp处理,你的ajax jsonp调用才可以 egg 接口中已经做了jsonp处理,所以可以跨域调用 //$.ajax({ // url: ...
- 多线程之synchronized
Java并发编程:synchronized 虽然多线程编程极大地提高了效率,但是也会带来一定的隐患.比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据.今天我们就 ...
- c++—— 函数重载(Overroad)
5 函数重载(Overroad) 函数重载概念 1 函数重载概念 函数重载(Function Overload) 用同一个函数名定义不同的函数 当函数名和不同的参数搭配时函数的含义不同 2 函数重载的 ...