手把手带你打造一个 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资源的热修复,就 ...
随机推荐
- ZT 类模板Stack的实现 by vector
*//*第3章 类模板 与函数相似,类也可以被一种或多种类型参数化.容器类就是一个具有这种特性的典型例子,它通常被用于管理某种特定类型的元素.只要使用类模板,你就可以实现容器类,而不需要确定容器中元素 ...
- 个人作业2:APP案例分析--腾讯动漫
第一部分 调研,评测 个人第一次上手体验 以往看漫画就是在浏览器直接搜索在网页上看,直到用了腾讯动漫APP,我才摒弃这个很low的方法.腾讯动漫直接用qq就可以登陆,有更齐全的漫画分类,更清晰的画质, ...
- TCP socket和web socket的区别
小编先习惯性的看了下某中文百科网站对Web Socket的介绍,觉得很囧.如果大家按照这个答案去参加BAT等互联网公司的前端开发面试,估计会被鄙视. 还是让我们阅读一些英文材料吧. 让我们直接看sta ...
- 华为18.9.5校招笔试题AK
26进制加法(一) 'a'-'z'代表十进制的0-25,求26进制加法.例如 'z'+'bc'= 'cb' 博主思路: 首先将长度不同的字符串高位补'a' 从低位开始将字符转换为10进制相加 计算进位 ...
- 将Python打包成可执行文件exe的心路历程
导言: 我们有时候需要将做好的Python程序打包成为一个exe , 方便我们使用,查找了资料发现 pyinstaller .py2exe,最后还是选择的pyinstaller,用的时候踩过了挺多的坑 ...
- JdkDynamicAopProxy-笔记
这个接口的继承体系图: 一.AopProxy InvocationHandler就不说了,看看AopProxy的源码. /** * Delegate interface for a configure ...
- swift的enum模式—对Alamofire入口的解析--数据结构与操作结合的模式
swift的枚举模式是数据结构与操作结合的模式 1.enum本质是一个类型,可以定义变量: 它定义的变量可以用到其它变量用的的任何地方:函数的输入.输出.成员变量.临时变量等: 这个变量还可以带有附加 ...
- [BJWC2011]最小三角形
嘟嘟嘟 这一看就是平面分治的题,所以就想办法往这上面去靠. 关键就是到\(mid\)点的限制距离是什么.就是对于当前区间,所有小于这个距离的点都选出来,参与更新最优解. 假设从左右区间中得到的最优解是 ...
- Day3JavaScript(一)JavaScript初识以及bom操作
JavaScript简介 什么是JavaScript 弱类型,动态类型,基于原型的直译性的编程语言.1995年netscape(网景)在导航者浏览器中设计完成. JavaScript的特点 1.与HT ...
- transfer function
线性变化后,往往希望进行非线性变化,常用的非线性变化函数有Sigmoid,Tanh,ReLU.会发现,经过这三个函数变化后,Tensor的维度并不发生变化. tanh(双曲正切函数):