Android 热补丁动态修复框架小结

本文详细解释了Android的类加载机制,特别是PathClassLoader和DexClassLoader的作用,以及它们在热修复过程中的运用,包括如何动态改变BaseDexClassLoader的dexElements和处理CLASS_ISPREVERIFIED问题。重点介绍了插件化中DexClassLoader的应用和防止CLASS_ISPREVERIFIED标志的方法。
摘要由CSDN通过智能技术生成

does not attempt to load classes from the network. Android uses

this class for its system class loader and for its application

class loader(s).

可以看出,Android是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到Android系统中的apk文件。

  • 对于DexClassLoader,依然看下注释:

A class loader that loads classes from {@code .jar} and

{@code .apk} files containing a {@code classes.dex} entry.

This can be used to execute code not installed as part of an application.

可以看出,该类呢,可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。

ok,如果大家对于插件化有所了解,肯定对这个类不陌生,插件化一般就是提供一个apk(插件)文件,然后在程序中load该apk,那么如何加载apk中的类呢?其实就是通过这个DexClassLoader,具体的代码我们后面有描述。

ok,到这里,大家只需要明白,Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件就好了。

上面我们已经说了,Android使用PathClassLoader作为其类加载器,那么热修复的原理具体是?

ok,对于加载类,无非是给个classname,然后去findClass,我们看下源码就明白了。

PathClassLoaderDexClassLoader都继承自BaseDexClassLoader。在BaseDexClassLoader中有如下源码:

#BaseDexClassLoader

@Override

protected Class<?> findClass(String name) throws ClassNotFoundException {

Class clazz = pathList.findClass(name);

if (clazz == null) {

throw new ClassNotFoundException(name);

}

return clazz;

}

#DexPathList

public Class findClass(String name) {

for (Element element : dexElements) {

DexFile dex = element.dexFile;

if (dex != null) {

Class clazz = dex.loadClassBinaryName(name, definingContext);

if (clazz != null) {

return clazz;

}

}

}

return null;

}

#DexFile

public Class loadClassBinaryName(String name, ClassLoader loader) {

return defineClass(name, loader, mCookie);

}

private native static Class defineClass(String name, ClassLoader loader, int cookie);

可以看出呢,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。

ok,通俗点说:

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(来自:安卓App热补丁动态修复技术介绍)

那么这样的话,我们可以在这个dexElements中去做一些事情,比如,在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类,这样的话,当遍历findClass的时候,我们修复的类就会被查找到,从而替代有bug的类。

说到这,你可能已经露出笑容了,原来热修复原理这么简单。不过,还存在一个CLASS_ISPREVERIFIED的问题,对于这个问题呢,详见:安卓App热补丁动态修复技术介绍该文有图文详解。

ok,对于CLASS_ISPREVERIFIED,还是带大家理一下:

根据上面的文章,在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志。

那么,我们要做的就是,阻止该类打上CLASS_ISPREVERIFIED的标志。

注意下,是阻止引用者的类,也就是说,假设你的app里面有个类叫做LoadBugClass,再其内部引用了BugClass。发布过程中发现BugClass有编写错误,那么想要发布一个新的BugClass类,那么你就要阻止LoadBugClass这个类打上CLASS_ISPREVERIFIED的标志。

也就是说,你在生成apk之前,就需要阻止相关类打上CLASS_ISPREVERIFIED的标志了。对于如何阻止,上面的文章说的很清楚,让LoadBugClass在构造方法中,去引用别的dex文件,比如:hack.dex中的某个类即可。

ok,总结下:

其实就是两件事:1、动态改变BaseDexClassLoader对象间接引用的dexElements;2、在app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志。

如果你没有看明白,没事,多看几遍,下面也会通过代码来说明。

三、阻止相关类打上CLASS_ISPREVERIFIED标志


ok,接下来的代码基本上会通过https://github.com/dodola/HotFix所提供的代码来讲解。

那么,这里拿具体的类来说:

大致的流程是:在dx工具执行之前,将LoadBugClass.class文件呢,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。注意:AntilazyLoad.class这个类是独立在hack.dex中。

ok,这里大家可能会有2个疑问:

  1. 如何去修改一个类的class文件

  2. 如何在dx之前去进行疑问1的操作

(1)如何去修改一个类的class文件

这里我们使用javassist来操作,很简单:

ok,首先我们新建几个类:

package dodola.hackdex;

public class AntilazyLoad

{

}

package dodola.hotfix;

public class BugClass

{

public String bug()

{

return “bug class”;

}

}

package dodola.hotfix;

public class LoadBugClass

{

public String getBugString()

{

BugClass bugClass = new BugClass();

return bugClass.bug();

}

}

注意下,这里的package,我们要做的是,上述类正常编译以后产生class文件。比如:LoadBugClass.class,我们在LoadBugClass.class的构造中去添加一行:

System.out.println(dodola.hackdex.AntilazyLoad.class)

下面看下操作类:

package test;

import javassist.ClassPool;

import javassist.CtClass;

import javassist.CtConstructor;

public class InjectHack

{

public static void main(String[] args)

{

try

{

String path = “/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/”;

ClassPool classes = ClassPool.getDefault();

classes.appendClassPath(path + “bin”);//项目的bin目录即可

CtClass c = classes.get(“dodola.hotfix.LoadBugClass”);

CtConstructor ctConstructor = c.getConstructors()[0];

ctConstructor

.insertAfter(“System.out.println(dodola.hackdex.AntilazyLoad.class);”);

c.writeFile(path + “/output”);

} catch (Exception e)

{

e.printStackTrace();

}

}

}

ok,点击run即可了,注意项目中导入javassist-*.jar的包。

首先拿到ClassPool对象,然后添加classpath,如果你有多个classpath可以多次调用。然后从classpath中找到LoadBugClass,拿到其构造方法,在其最后插入一行代码。ok,代码很好懂。

ok,我们反编译看下我们生成的class文件:

ok,关于javassist,如果有兴趣的话,大家可以参考几篇文章学习下:

(2)如何在dx之前去进行(1)的操作

ok,这个就结合https://github.com/dodola/HotFix的源码来说了。

将其源码导入之后,打开app/build.gradle

apply plugin: ‘com.android.application’

task(‘processWithJavassist’) << {

String classPath = file(‘build/intermediates/classes/debug’)//项目编译class所在目录

dodola.patch.PatchClass.process(classPath, project(‘:hackdex’).buildDir

.absolutePath + ‘/intermediates/classes/debug’)//第二个参数是hackdex的class所在目录

}

android {

applicationVariants.all { variant ->

variant.dex.dependsOn << processWithJavassist //在执行dx命令之前将代码打入到class中

}

}

你会发现,在执行dx之前,会先执行processWithJavassist这个任务。这个任务的作用呢,就和我们上面的代码一致了。而且源码也给出了,大家自己看下。

ok,到这呢,你就可以点击run了。ok,有兴趣的话,你可以反编译去看看dodola.hotfix.LoadBugClass这个类的构造方法中是否已经添加了改行代码。

关于反编译的用法,工具等,参考:http://blog.csdn.net/lmj623565791/article/details/23564065

ok,到此我们已经能够正常的安装apk并且运行了。但是目前还未涉及到打补丁的相关代码。

四、动态改变BaseDexClassLoader对象间接引用的dexElements


ok,这里就比较简单了,动态改变一个对象的某个引用我们反射就可以完成了。

不过这里需要注意的是,还记得我们之前说的,寻找class是遍历dexElements;然后我们的AntilazyLoad.class实际上并不包含在apk的classes.dex中,并且根据上面描述的需要,我们需要将AntilazyLoad.class这个类打成独立的hack_dex.jar,注意不是普通的jar,必须经过dx工具进行转化。

具体做法:

jar cvf hack.jar dodola/hackdex/*

dx --dex --output hack_dex.jar hack.jar

如果,你没有办法把那一个class文件搞成jar,去百度一下…

ok,现在有了hack_dex.jar,这个是干嘛的呢?

应该还记得,我们的app中部门类引用了AntilazyLoad.class,那么我们必须在应用启动的时候,降这个hack_dex.jar插入到dexElements,否则肯定会出事故的。

那么,Application的onCreate方法里面就很适合做这件事情,我们把hack_dex.jar放到assets目录。

下面看hotfix的源码:

/*

  • Copyright © 2015 Baidu, Inc. All Rights Reserved.

*/

package dodola.hotfix;

import android.app.Application;

import android.content.Context;

import java.io.File;

import dodola.hotfixlib.HotFix;

/**

  • Created by sunpengfei on 15/11/4.

*/

public class HotfixApplication extends Application

{

@Override

public void onCreate()

{

super.onCreate();

File dexPath = new File(getDir(“dex”, Context.MODE_PRIVATE), “hackdex_dex.jar”);

Utils.prepareDex(this.getApplicationContext(), dexPath, “hackdex_dex.jar”);

HotFix.patch(this, dexPath.getAbsolutePath(), “dodola.hackdex.AntilazyLoad”);

try

{

this.getClassLoader().loadClass(“dodola.hackdex.AntilazyLoad”);

} catch (ClassNotFoundException e)

{

e.printStackTrace();

}

}

}

ok,在app的私有目录创建一个文件,然后调用Utils.prepareDex将assets中的hackdex_dex.jar写入该文件。

接下来HotFix.patch就是去反射去修改dexElements了。我们深入看下源码:

/*

  • Copyright © 2015 Baidu, Inc. All Rights Reserved.

*/

package dodola.hotfix;

/**

  • Created by sunpengfei on 15/11/4.

*/

public class Utils {

private static final int BUF_SIZE = 2048;

public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) {

BufferedInputStream bis = null;

OutputStream dexWriter = null;

bis = new BufferedInputStream(context.getAssets().open(dex_file));

dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath));

byte[] buf = new byte[BUF_SIZE];

int len;

while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {

dexWriter.write(buf, 0, len);

}

dexWriter.close();

bis.close();

return true;

}

ok,其实就是文件的一个读写,将assets目录的文件,写到app的私有目录中的文件。

下面主要看patch方法

/*

  • Copyright © 2015 Baidu, Inc. All Rights Reserved.

*/

package dodola.hotfixlib;

import android.annotation.TargetApi;

import android.content.Context;

import java.io.File;

import java.lang.reflect.Array;

import java.lang.reflect.Field;

import java.lang.reflect.InvocationTargetException;

import dalvik.system.DexClassLoader;

import dalvik.system.PathClassLoader;

/* compiled from: ProGuard */

public final class HotFix

{

public static void patch(Context context, String patchDexFile, String patchClassName)

{

if (patchDexFile != null && new File(patchDexFile).exists())

{

try

{

if (hasLexClassLoader())

{

injectInAliyunOs(context, patchDexFile, patchClassName);

} else if (hasDexClassLoader())

{

injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);

} else

{

injectBelowApiLevel14(context, patchDexFile, patchClassName);

}

} catch (Throwable th)

{

}

}

}

}

这里很据系统中ClassLoader的类型做了下判断,原理都是反射,我们看其中一个分支hasDexClassLoader();

private static boolean hasDexClassLoader()

{

try

{

Class.forName(“dalvik.system.BaseDexClassLoader”);

return true;

} catch (ClassNotFoundException e)

{

return false;

}

}

private static void injectAboveEqualApiLevel14(Context context, String str, String str2)

throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException

{

PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

Object a = combineArray(getDexElements(getPathList(pathClassLoader)),

getDexElements(getPathList(

new DexClassLoader(str, context.getDir(“dex”, 0).getAbsolutePath(), str, context.getClassLoader()))));

Object a2 = getPathList(pathClassLoader);

setField(a2, a2.getClass(), “dexElements”, a);

pathClassLoader.loadClass(str2);

}

首先查找类dalvik.system.BaseDexClassLoader,如果找到则进入if体。

在injectAboveEqualApiLevel14中,根据context拿到PathClassLoader,然后通过getPathList(pathClassLoader),拿到PathClassLoader中的pathList对象,在调用getDexElements通过pathList取到dexElements对象。

ok,那么我们的hack_dex.jar如何转化为dexElements对象呢?

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
thList对象,在调用getDexElements通过pathList取到dexElements对象。

ok,那么我们的hack_dex.jar如何转化为dexElements对象呢?

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

[外链图片转存中…(img-DgLPwkoB-1714450520301)]

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-RPKTNKMo-1714450520303)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值