从热更新到Nuwa源码分析

参考文章:
安卓App热补丁动态修复技术介绍——by QQ空间终端开发团队
Android dex分包方案
Android热更新方案Robust

开源库:
https://github.com/jasonross/Nuwa
https://github.com/dodola/HotFix

最近很多优秀的热更新开源库,特别是前几天Tinker的出现我觉得很有必要去学习一下热更新知识。然而为什么不挑最新的Tinker来分析而是选择Nuwa和HotFix呢,因为相对早出来的库比起最新我们更能挖掘其内部的纯粹的原理而新的库创新性可能太高了,本文主要记录分析Nuwa这个热更新库并且从中悟出的一些知识点。

分析之前我们需要掌握几个知识点

一、ClassLoader的dex分包原理
好像是由于版权和性能原因(忘记了),Android并没有采用java的JVM而是自己开发了一个新的叫做DVM,所以Android中ClassLoader体系跟Java是不一样的,主要包含有:
PathClassLoader:用于加载系统的类和已安装的应用类并且不推荐我们去用它。
DexClassLoader:可以从一个未安装的apk或者jar包中加载dex。
BaseDexClassLoader: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);

这样我们就大概了知道ClassLoader的整体的加载流程,而我对这些的类名的理解是这样的:BaseDexClassLoader存放整个app的dex于DexPathList中,而DexPathList采取的是Element来存放具体的DexFile,每一个DexFile中就是一个个具体的dex文件,最终通过调用底层的defineClass来获取一个个Class。
有了一点认识之后,我们回头看下DexPathList来的findClass方法里面的这么一段代码:

Class clazz = dex.loadClassBinaryName(name, definingContext); 
if (clazz != null) { 
    return clazz; 
}

这段的意思是如果某一个DexFile可以读取到Class就直接return回去,整个遍历就结束了。
所以是时候抛出热修复原理是什么了?
答:我们把出现bug的类修复成功后重新打成一个dex文件,然后直接插入到dexElements的前面,这样在遍历的时候直接取到我们修复后的dex文件后就直接跳过了存在bug的dex文件就直接返回了Class,这样就达到修复bug的效果了。

借用Qzone文章的图片参考一下:
这里写图片描述
patch.dex是我们修复后的dex,插入到前面,那么classes.dex中的Qzone.class就并不会被加载到而是加载patch.dex中的Qzone.class,所以就达到热修复效果了。

二、Nuwa源码分析
有了上面的ClassLoader的加载机制认识后,我们拿Nuwa的源码来分析一下,先看下Nuwa的所有源码,确实少得可怜,但是左边的代码采用Groovy写的gradle插件等下并不做分析,后面再说。
这里写图片描述

public class NuwaApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Nuwa.init(this);
        Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch.jar"));
    }
}

我们从Application这边出发开始研究一下,这里需要先init然后再调用loadpatch方法。

    public static void init(Context context) {
        File dexDir = new File(context.getFilesDir(), DEX_DIR);
        dexDir.mkdir();

        String dexPath = null;
        try {
            dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
        } catch (IOException e) {
            Log.e(TAG, "copy " + HACK_DEX + " failed");
            e.printStackTrace();
        }

        loadPatch(context, dexPath);
    }

init里面主要做的就是讲assets中的hack.apk读取出来并返回了地址,可是init里面也调用了loadPatch这个方法,是干嘛的呢?这我们就需要从loadPatch去了解:

    public static void loadPatch(Context context, String dexPath) {

        if (context == null) {
            Log.e(TAG, "context is null");
            return;
        }
        if (!new File(dexPath).exists()) {
            Log.e(TAG, dexPath + " is null");
            return;
        }
        File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
        dexOptDir.mkdir();
        try {
            DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
        } catch (Exception e) {
            Log.e(TAG, "inject " + dexPath + " failed");
            e.printStackTrace();
        }
    }

发现这个方法主要是DexUtils.injectDexAtFirst,那么我们进入看看。

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

很明显这个方法就是把心得dex插入到dexElements的前面去。
但是为什么在init也做了这么一个操作呢?

/**
 * Created by jixin.jia on 10/25/15.
 */
public class Hack {
}

通过DexClassLoader去加载hack.apk就是为了读取到Hack.class,作用是为了解决CLASS_ISPREVERIFIED这个错误,因为在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记,被打上标记的就无法调用其他dex中的类从而会报出错误。
解决方法:在所有类的构造函数中插入这行代码 System.out.println(AntilazyLoad.class);
这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
所以说需要再Application中加载hack.dex,来确保这个错误不会出现。
ps:以上的结论来自Qzone的技术文章。

看上去Nuwa的源码就这样分析完了,但是Gradle插件那个部分的代码主要的作用是注入代码来防止CLASS_ISPREVERIFIED错误、混淆代码的时候修复dex的自动生成等一些功能。

先记着,后面学习了Groovy后再来补写这部分分析。

猜你喜欢

转载自blog.csdn.net/Neacy_Zz/article/details/52710930