Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

Android热修复学习(HotFix)

$
0
0

参考1
参考2
参考3
参考4

一:热修复相关

热修复概念: 以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载。
PathClassloader和DexClassLoader:
(1)PathClassloader作为其系统类和应用类的加载器,只能去加载已经安装到Android系统中的apk文件。
(2)DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。
(3)Android使用PathClassLoader作为其类加载器,DexClassLoader可以从.jar和.apk类型的文件内部加载classes.dex文件。
热修复原理:
PathClassLoader和DexClassLoader都继承自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);1  n j                                                                                                                         
n 

1 BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的集合dexElements,而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。
2 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
3 理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

图1
4 把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图:

图2

二:阻止相关类打上CLASS_ISPREVERIFIED标志

dex校验: 如果两个相关联的类在不同的dex中就会报错,例如ClassA 引用了ClassB,但是发现这这两个类所在的dex不在一起,其中:
1. ClassA 在classes.dex中
2. ClassB 在patch.dex中
结果发生了错误。

dex校验的前提: 如果引用者这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dex的校验。

相关类打上CLASS_ISPREVERIFIED标志的发生场景:
在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么这个类就会被打上CLASS_ISPREVERIFIED
下图是class A 打上CLASS_ISPREVERIFIED标志
图三
下图是class A 没有打上CLASS_ISPREVERIFIED标志
图四
其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

在class文件中插入代码来阻止相关类打上CLASS_ISPREVERIFIED标志:在dx工具执行之前,将LoadBugClass.class文件呢,进行修改,再其构造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),然后继续打包的流程。
原始代码

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();
    }
}

三:插入jar

插入代码

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

在构造函数中插入操作代码(javassist)

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();
        }

    }
}

把AntilazyLoad.class打包成jar包,然后写入App的私有目录,最后把该jar对应的dexElements文件插入到数组的最前面。

public class HotfixApplication extends Application
{

    @Override
    public void onCreate()
    {
        super.onCreate();
       //创建jar对应的文件
        File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
       //将asset文件中的jar写到App的私有目录下面。
        Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
       // 把jar对应的dexElements插入到dex数组最前面。
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

    }
}

创建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;
        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;

}

找相应的ClassLoader进行操作

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)
            {
            }
        }
    }
 }

Combine(合并)App的DexElements和*AntilazyLoad.class的DexElements*

 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);
}

将Patch.jar补丁插入到APP中,过程和插入AntilazyLoad.class一样

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, "hack_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
        try
        {
            this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
        } catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }

        dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar");
        Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar");
        HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass");

    }
}

四:总结

(1)因为我们的Patch是以独立的jar包,插入到APP的DexElements中,
所以如果APP中中的类引用了Patch中的类,就会在校验时报错。因为当进行dex校验时,如果两个相关联的类在不同的dex中就会报错。(LoadBugClass引用BugClass)
(2) 为了防止上述错误就要阻止Dex校验,阻止Dex校验的方法是阻止相关类打上CLASS_ISPREVERIFIED标志。
(3) 阻止相关类打上CLASS_ISPREVERIFIED标志的做法是:在相关引用的类(LoadBugClass.class)的构造方法中,引用另外一个jar中类AntilazyLoad.class
(4)因为AntilazyLoad.class在另一个jar中,所以需要把该jar对应的dex插入到App中。并且在Application中的onCreate()方法中将该类加载进来。
(5)把Patch.jar对应的dexElement加载进App中。因为Patch.jar放在dex数组的第一个位置,所以首先被加载。即:如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类。

作者:shuixingge1992 发表于2016/9/24 10:30:49 原文链接
阅读:220 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>