1.热修复原理
参考文档:
什么是热修复
定义:在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。
怎么进行热修复
服务端:补丁包管理
用户端:执行热修复
开发端:生成补丁包
热修复需要解决的问题
开发端
- 补丁包是什么?
- 如何生成补丁包?
- 开启混淆后呢?
- 对比改动自动生成补丁包(gradle)?
用户端
- 什么时候执行热修复?
- 怎么执行热修复(使用补丁包)?
- Android版本兼容问题?
热修复解决方案
热补丁方案有很多,其中比较出名的有腾讯Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案。
AndFix
在native动态替换java层的方法,通过native层hook java层的代码。
Robust
对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中。
Tinker
Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。
运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。
Qzone
2.ClassLoader
双亲委托机制
某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。
1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
2、安全性考虑,防止核心API库被随意篡改。
在线源码阅读:www.androidos.net.cn/或http://and…
类查找流程
类加载实现热修复
类是怎么被加载的? 怎么使用补丁包中的类? 已经被加载过的类还能够替换修复吗? 怎样保证补丁包中正确class 的dex先加载?
加载修复后的类
EnjoyFix.installPatch(this, new File("/sdcard/patch.jar"));
EnjoyFix.class
public class EnjoyFix {
private static final String TAG = "EnjoyFix";
private static File initHack(Context context) {
File hackFile = new File(context.getExternalFilesDir(""), "hack.dex");
FileOutputStream fos = null;
InputStream is = null;
try {
fos = new FileOutputStream(hackFile);
is = context.getAssets().open("hack.dex");
int len;
byte[] buffer = new byte[2048];
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return hackFile;
}
/**
* 1、获取程序的PathClassLoader对象
* 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
* 3、反射获取pathList的dexElements对象 (oldElement)
* 4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
* 5、合并patchElement+oldElement = newElement (Array.newInstance)
* 6、反射把oldElement赋值成newElement
*
* @param application
* @param patch
*/
public static void installPatch(Application application, File patch) {
File hackDex = initHack(application);
List<File> patchs = new ArrayList<>();
patchs.add(hackDex);
if (patch.exists()) {
patchs.add(patch);
}
//1、获取程序的PathClassLoader对象
ClassLoader classLoader = application.getClassLoader();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
ClassLoaderInjector.inject(application, classLoader, patchs);
} catch (Throwable throwable) {
}
return;
}
//2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
try {
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
Object pathList = pathListField.get(classLoader);
//3、反射获取pathList的dexElements对象 (oldElement)
Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
Object[] oldElements = (Object[]) dexElementsField.get(pathList);
//4、把补丁包变成Element数组:patchElement(反射执行makePathElements)
Object[] patchElements = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Method makePathElements = ShareReflectUtil.findMethod(pathList, "makePathElements",
List.class, File.class,
List.class);
ArrayList<IOException> ioExceptions = new ArrayList<>();
patchElements = (Object[])
makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
ArrayList.class, File.class, ArrayList.class);
ArrayList<IOException> ioExceptions = new ArrayList<>();
patchElements = (Object[])
makePathElements.invoke(pathList, patchs, application.getCacheDir(), ioExceptions);
}
//5、合并patchElement+oldElement = newElement (Array.newInstance)
//创建一个新数组,大小 oldElements+patchElements
// int[].class.getComponentType() ==int.class
Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
oldElements.length + patchElements.length);
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
//6、反射把oldElement赋值成newElement
dexElementsField.set(pathList, newElements);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.插桩式热修复运行期修复落地
热修复流程
1.获取到当前应用的PathClassloader; 2.反射获取到DexPathList属性对象pathList; 3.反射修改pathList的dexElements
- 把补丁包patch.dex转化为Element[] (patch)
- 获得pathList的dexElements属性(old)
- patch+dexElements合并,并反射赋值给pathList的dexElements
Element数组
补丁包其实就是一个dex或者包含dex的jar包。怎么把dex变成Element?
Android N混合编译
ART 是在 Android KitKat(Android 4.0)引入并在 Lollipop(Android 5.0)中设为默认运行环境,可以看作Dalvik2.0。ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一 些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全 时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当 于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复。
解决方案
运行时替换PathClassLoader方案 App image中的class是插入到PathClassloader中的ClassTable中。假设我们完全废弃掉PathClassloader,而 采用一个新建Classloader来加载后续的所有类,即可达到将cache无用化的效果。
CLASS_ISPREVERIFIED异常
如果MainActivity类中只引用了:Utils类。当打包dex时, MainActivity与Utils都在classes.dex中,则加载 时MainActivity类被标记为 CLASS_ISPREVERIFIED。
如果使用补丁包中的Utils类取代出现bug的Utils,则会导致MainActivity与其引用的Utils不在同一个Dex, 但MainActivity已经被打上标记,此时出现冲突。导致校验失败!
解决方案
阻止标记:防止类被打上CLASS_ISPREVERIFIED标志。
AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的 AntilazyLoad类,防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
字节码插桩:在Java字节码Class中某些位置插入或修改一些代码。
4.字节码插桩
在Java字节码中某些位置插入或修改一些代码。
Android打包流程
- AAPT :打包资源文件,并生成R.java和resources.arsc文件。
- AIDI : 读取.aidl文件生成对应Java文件
- JAVAC : 将java文件编译成.class文件;
- DX : 多个.class文件转换打包为.dex文件
ASM
操作Java 字节码的框架,按照Class文件的格式,解析、修改、生成Class,可以动态生成类或者增强现有类的 功能。 正如GSON操作json的框架
5.Gradle插件开发
插件开发
apply plugiin: 'com.android.application'
apply plugiin: 'com.android.library'
方式 | 说明 |
---|---|
Build script脚 本 | 把插件写在 build.gradle 文件中,一般用于简单的逻辑,只在该 build.gradle 文件可见。 |
buildSrc目录 | 将插件源代码放在 buildSrc/src/main/groovy/ 中,只对该项目中可见。 |
独立项目 | 独立项目 一个独立的 Java 项目/模块,可以将文件包发布到仓库(Jcenter), 使其他项目方便引入。 |
插件实现
正如我们开发Android项目,创建Activity需要继承android.app.Activity或其子类,插件需 要实现org.gradle.api.Plugin<Project>
接口。
右值为实现了插件结构的类全限定名
插件扩展
引入某些插件可能会需要进行插件配置,如:
这些信息由使用者配置,目的是为了能够在插件执行时获得用户配置信息,进行不同的逻辑处理。
project.getExtensions().create("xx", JavaBean.class);
依赖插件的插件
一般情况下,我们自定义插件主要为了Android项目使用,在插件执行过程中,希望动态获得 android插件中的信息,如:APK输出目录、包名、变体(debug/release)等信息。 我们的插件需要用过Android插件编写的API获取这些参数,因此我们的插件需要依赖Android 插件。 以独立模块开发插件为例,独立模块的build.gradle中加入:
Task
Task,即任务。Android工程引入的Android插件中,创建了多个自定义任务,如:编译Java文件、生成 dex文件、打包等等,每个任务执行一项工作。
6.自动生成补丁包
- 编译时生成一份缓存文件,里面记录了所有class文件的md5,以及mapping混淆文件。
- 在后续的版本中使用-applymapping选项,应用mapping文件实现每次混淆类名相同,然后计算编译 完成后的class文件的md5和缓存进行比较,把不相同的class文件打包成补丁包。
代码待补充...