Android类加载机制html
咱们先来看一下BaseDexClassLoader源码中比较重要的code
java
能够看出最终在此处找到了某一个类android
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);复制代码
按照这个原理,咱们能够把有问题的类打包到一个dex(patch.dex)中去,而后把这个dex插入到Elements的最前面,当遍历findClass的时候,咱们修复的类就会被查找到,从而替代有bug的类便可,那么下面来进行这一个过程的操做吧。编程
新建一个Hotfix的工程,而后新建一个BugClass类数组
package ydc.hotfix;
public class BugClass {
public String bug() {
return "fix bug class";
}
}复制代码
在新建一个LoadBugClass类安全
public class LoadBugClass {
public String getBugString() {
BugClass bugClass = new BugClass();
return bugClass.bug();
}
}复制代码
注意 LoadBugClass应用了BugClass类。bash
而后在界面层是这样调用的:微信
ok,假设咱们把该apk发布出去了,那么用户看到效果应该是“ 测试调用方法:fix bug class”。这个时候公司领导认为这样的提示对于用户是致命的。那么咱们要把BugClass 类中的bug()方法中字符串替换一下,仅仅是修复一句话而已,实在没有必要走打包发布下放市场等复杂的流程。app
public String bug() {
return "fix bug class";
}复制代码
ok,把这个有问题的地方修正为:eclipse
public String bug() {
return "杨德成正在修复提示语fix bug class";
}复制代码
三、先把BugClass.class文件作成成jar,注意路径,必定要定位到该位置执行如下命令:
jar cvf path.jar ydc/hotfix/BugClass.class复制代码
依然在该路径下执行如下命令:
dx --dex --output=path_dex.jar path.jar
五、拷贝path_dex
咱们把path_dex文件拷贝到assets目录下
首先咱们看一下hotfix的源码:
根据截图所示,作了两个动做复制代码
a、建立一个私有目录,并把补丁包文件写入到该目录下
File dexPath = new File(getDir(“dex”, Context.MODE_PRIVATE), “path_dex.jar”);复制代码
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;
try {
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;
} catch (IOException e) {
if (dexWriter != null) {
try {
dexWriter.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
if (bis != null) {
try {
bis.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
return false;
}
}
}复制代码
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;
}
}复制代码
系统中确定会存在”dalvik.system.BaseDexClassLoader”类,那么接下来应该进入injectAboveEqualApiLevel14(context, patchDexFile, patchClassName)方法,代码以下:
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);
}复制代码
根据Android系统源码解读源以上代码
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();复制代码
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}复制代码
根据以上代码片断,能够看出这里根据引用类名称”BaseDexClassLoader”查找有个叫”pathList”属性名的被引用类型。
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}复制代码
上面这个片断经过反射找到对应的被引用类”DexPathList”,上个”BaseDexClassLoader”系统源码:
b、getDexElements(getPathList(pathClassLoader))方法解读:
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}复制代码
上面的这个代码片断根据a步骤获得的DexPathList对象获取到了没有打补丁以前的dexElements有序数组对象
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}复制代码
根据代码可知依然使用反射原理获取DexPathList对象中的有序数组dexElements。
DexPathList类系统源码以下:
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))
根据该类的系统源码看出其实该类的构造函数并无作具体的事情
真正作之情的是它的直接父类BaseDexClassLoader的构造函数,如图所示
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);复制代码
看到没,根据传入参数初始化了咱们补丁包对应的 DexPathList对象,注意这一步仅仅是初始化哦
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}复制代码
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}复制代码
上面这两段代码根据引用名”dalvik.system.BaseDexClassLoader”和被引用类属性名”pathList”获得DexPathList对象
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}复制代码
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}复制代码
上面的这两端片断根据 DexPathList类及属性名dexElements获取到咱们补丁包对应的有序数组dexElements上面已经获得了两个有序数组dexElements,一个存放的的是没有打补丁以前的dex有序数组dexElements,另一个是咱们的补丁包对应的dex有序数组dexElements,那么是否是到了该合并两个数组的时候了呢,没错
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));复制代码
到这里终于知道这整句代码到底干了什么事情了,Object a 就是咱们合并后的有序dex数组dexElements合并过程以下:
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}复制代码
其实就是把补丁包对应的dex插入到原来有序数组dexElements的最前面了。
Object a2 = getPathList(pathClassLoader);复制代码
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
declaredField.set(obj, obj2);
}复制代码
依然是使用反射机制设置新值。
pathClassLoader.loadClass(str2);复制代码
str参数是经过如下代码传入,即(ydc.hotfix.BugClass)HotFix.patch(this, dexPath.getAbsolutePath(), “ydc.hotfix.BugClass”);复制代码
这时候loadClass到的就是咱们补丁包中的BugClass类了,这是由于咱们把补丁包对应的dex文件插入到dexElements最前面。因此找到就BugClass直接返回了,代码以下:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}复制代码
按照咱们以前的推论,到这里应该就完成了补丁动态修复了,那么真的是这样的吗,咱们不防运行下项目看看。
很不幸,运行时报错:
-这是因为LoadBugClass引用了BugClass,可是发现这这两个类所在的dex不在一块儿,其中:
BugClass在path_dex.jar中结果发生了错误。
究其缘由是 pathClassLoader.loadClass(str2)的时候,会去校验LoadBugClass所在的dex和BugClass所在的dex是不是同一个,不是则会报错。那么校验的前提是有一个叫CLASS_ISPREVERIFIED的类标志,若是引用者被打上这个标识,就会去校验,就会致使报错,那么咱们能够想象若是引用者LoadBugClass 没被打上这个标识,是否就会运行经过了呢,没错,就是这个原理。
而后把这个被引用类打包成一个单独的dex文件。这样就能够防止了LoadBugClass类被打上CLASS_ISPREVERIFIED的标志了,那咱们如今来开始作这件事情。
一、动态被注入类的制做
b、在该Module之下,新建一个AntilazyLoad空类。
```
package dodola.hackdex;
/**
d、依然要把这个dex文件插入到dexElements有序数组的中,插入原理和补丁包插入原理彻底一致,并且这个dex文件须要在程序的入口进行插入,保证它是在有序数组的最前面,由于咱们要把该dex文件中的AntilazyLoad要动态注入到其它包里面的某一个类的构造方法中。切记,dexElements里面能够塞入无数个dex文件。
/**
* 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,下面就是如何注入的问题了,这个时候应该到了咱们的AOP三剑客之一”javassist”闪亮登场了。
javassist这货是个好东西啊,它能够以无侵入的方式重构你的原代码。我以前编写过另一个三剑客之一的文章,原理基本同样。参考地址:blog.csdn.net/xinanheisha…
a、建立buildSrc模块,这个项目是使用Groovy开发的,听说这货具有Java, JavaScript, Phython, Ruby等等语言的优势,并且Groovy依赖于Java的,和Java无缝挂接的
b、导入javassist
```
apply plugin: 'groovy'
repositories {
mavenCentral()
}
dependencies {
compile gradleApi()
compile 'org.codehaus.groovy:groovy-all:2.3.6'
compile 'org.javassist:javassist:3.20.0-GA'
}
- c、PatchClass 代码截图以下

其实很简单的,这几句的意思就是经过反射相关类,而后在相关类的构造方法中插入一句输出语句。复制代码
CtClass c = classes.getCtClass("ydc.hotfix.BugClass")
if (c.isFrozen()) {
c.defrost()
}
println("====添加构造方法====")
def constructor = c.getConstructors()[0];
constructor.insertBefore("System.out.println(dodola.hackdex.AntilazyLoad.class);")
//constructor.insertBefore("System.out.println(888);")
c.writeFile(buildDir)
执行完这段代码以后,也无形中应用了AntilazyLoad这个类。
- d、这个工程不须要引用到主app(Module)中,只须要在 app->build.gradle中配置一个任务:

在配置一下,侵入时期

ok,总算把整个过程写完了,准备开始运行了,无论你激不激动,反正本人是挺激动的了。在运行以前,先看一下咱们的引用者类

没错,能够确认这是咱们的源代码,化成灰我也能够认出它来。在看一下运行以后的引用者类

没错,就是这个效果,咱们的源码被javassist 赤裸裸的侵犯了,是否是瞬间觉的本身的“东西”很不安全,这就是AOP编程的强大之处啊。
项目讲解到这里,我想估计没有几我的能有耐心的看到这里来了,由于以为文章实在太长,须要有多大耐心才能扛到这里,连我本身也怀疑本身如何写出来的,不过我认为,这么强大并且实用的技术点,不是可以三五两语就能说清的,咱们要有足够的耐心来探索咱们所不知的,有耐心,咱们就有但愿,有但愿就不会失望!
ok,咱们见证一下奇迹。

看到这效果,我手已累,键盘已坏。。。。
>Demo下载地址:
>
> http://download.csdn.net/download/xinanheishao/9902530
演示环境:demo导入不能正常运行,建议先调整环境,跑起来,再进阶。复制代码
classpath 'com.android.tools.build:gradle:1.3.0'
#Thu Jul 13 16:40:06 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip
```
若是默认的jdk环境找不到,手动指向一下
若是你已经准备好足够信心的话,能够按照文章,本身尝试一方
最后感谢腾讯空间给出的解决方法思路和HotFix开源做者。
原文地址
项目相关:
相关demo下载地址:
若是你以为此文对您有所帮助,欢迎入群 QQ交流群 :232203809
微信公众号:终端研发部