热修复框架Nuwa

来源:互联网 发布:宝宝计划软件 编辑:程序博客网 时间:2024/05/21 08:38

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/70284239
本文出自:【顾林海的博客】

前言

当热修复框架还没出现时,我们的整个开发流程是这样的:先是开发,接着测试,如果有bug修复,当测试实在测不出问题,就打包上线,如果在线上出现问题,就需要修复Bug,并再次打包上线,由于各大平台的审核机制不同,上线的时间也是不固定,在这个阶段用户在多次打开APP并出现相同问题后就有可能卸载软件,这样的话公司就会流失部分用户,在热修复出现后,可以避免这种情况的发生,因为线上出现bug后,我们可以通过热修复来修复Bug,不用每次出现Bug都要重新上架。市面上的热修复有很多,像Nuwa、微信的Tinker以及阿里百川HotFix,这篇文章讲述Nuwa的使用以及相关的原理。

集成热修复框架Nuwa

步骤一:

工程根目录中添加:

classpath 'cn.jiajixin.nuwa:gradle:1.2.2'

最后工程根目录下的build.gradle是这样的:

buildscript {    repositories {        jcenter()    }    dependencies {        classpath 'com.android.tools.build:gradle:1.2.0'        classpath 'cn.jiajixin.nuwa:gradle:1.2.2'        // NOTE: Do not place your application dependencies here; they belong        // in the individual module build.gradle files    }}allprojects {    repositories {        jcenter()    }}task clean(type: Delete) {    delete rootProject.buildDir}



步骤二:

在app下的build.gradle添加依赖:

apply plugin: "cn.jiajixin.nuwa"dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    testCompile 'junit:junit:4.12'    compile 'com.android.support:appcompat-v7:24.2.1'    compile 'cn.jiajixin.nuwa:nuwa:1.0.0'//添加nuwa sdk}



步骤三:

在app下的build.gradle中dubug和release开启混淆

在AndroidManifest.xml中添加权限:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />



步骤四:

创建项目的Application并添加到AndroidManifest.xml中,在Application中添加:

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

使用热修复框架Nuwa

ok,整体流程已经结束,现在我们编写一个有bug的app,我先在MainActivity添加以下代码:

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);//        findViewById(R.id.tv_show).setOnClickListener(new View.OnClickListener() {//            @Override//            public void onClick(View v) {//                Toast.makeText(MainActivity.this, "nuwa", Toast.LENGTH_SHORT).show();//            }//        });    }}

这段代码中tv_show是不可点击的,我们点击run这个项目,并在安装在手机上,这时我们查看app/build/outputs文件下会出现一个nuwa的文件夹,我们看看这个文件夹下有些什么:

这里写图片描述

我们看的有两个文件,这两个文件在后面会用到,我们将整个nuwa文件夹复制到某个路径下。

接着我们修改上面MainActivity,修改内容如下:

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViewById(R.id.tv_show).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Toast.makeText(MainActivity.this, "nuwa", Toast.LENGTH_SHORT).show();            }        });    }}

上面我将注释去掉,也就是点击这个TextView会出现弹窗,我们打开android studio 的Terminal窗口,输入以下内容:

gradlew clean nuwaDebugPatch -P NuwaDir=F:/glhproject/nuwa/nuwa

上面的F:/glhproject/nuwa/nuwa就是之前我们复制的nuwa文件夹。

这时我们再查看app/build/outputs会发现多了一个叫patch.jar的东西:

这里写图片描述

在日常开发中,这个patch.jar是需要放在服务器上,通过推送或接口调用来下载这个patch.jar文件,我们这里直接复制到手机的sdcard上:

adb push app/build/outputs/nuwa/debug/patch.jar /sdcard/

最后的最后我们重启app,这样我们第二次修改的内容就生效了。

热修复框架Nuwa原理

在一头埋入nuwa源码前,我们先来扫扫盲,聊聊PathClassLoader,这货有什么用,PathClassLoader的作用就是从文件系统中加载类文件:

public class PathClassLoader extends BaseDexClassLoader {    public PathClassLoader(String dexPath, ClassLoader parent) {        super((String)null, (File)null, (String)null, (ClassLoader)null);        throw new RuntimeException("Stub!");    }    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {        super((String)null, (File)null, (String)null, (ClassLoader)null);        throw new RuntimeException("Stub!");    }}

PathClassLoader继承BaseDexClassLoader,在BaseDexClassLoader中有个findClass方法:

protected Class<?> findClass(String name) throws ClassNotFoundException {    throw new RuntimeException("Stub!");}

查看findClass方法的具体实现:

@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();    Class c = pathList.findClass(name, suppressedExceptions);    if (c == null) {        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);        for (Throwable t : suppressedExceptions) {            cnfe.addSuppressed(t);        }        throw cnfe;    }    return c;}

在findClass方法中调用了pathList对象的findClass方法,pathLit的类型是DexPathList,查看DexPathList中的findClass方法:

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

在findClass方法中,通过循环遍历dexElements,在循环遍历中,调用loadClassBinaryName方法来加载,加载成功就返回这个Class对象。讲到这里,我们就知道,dexElements的先后顺序是非常重要,决定着哪个dex被加载,因此要想实现热修复,就需要我们将修复后的dex文件放在dexElements前面,使得这个dex文件能先被加载,从而达到热修复。

Nuwa的原理就是利用了上面的dexElements来加载修复完的dex文件,我查看到Nuwa的源码其实比较少的:

这里写图片描述

调用 Nuwa.init(this):

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方法中,创建nuwa文件,并从assets目录中拷贝一个叫hack.apk空实现的文件到nuwa文件中,在调用下面方法来进行热修复,方法内的第二个参数就是我们在上面生成的patch.jar的路径。

Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch.jar"));

接着调用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();    }}

在这个方法中,先是进行两次判空,分别是context和我们存放修复后的dex文件否存在,接着创建一个nuwaopt文件夹,接着调用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);}

获取DexClassLoader实例,DexClassLoader的作用是动态的装载class文件,并且DexClassLoader继承与BaseDexClassLoader,接着通过反射获取到DexPathList属性对象pathList,getDexElements方法中通过反射获取dexElements,上面两个baseDexElements和newDexElements分别是当前的dexElements和补丁dex的dexElements,随后将两个dexElements进行合并:

private static Object combineArray(Object firstArray, Object secondArray) {    Class<?> localClass = firstArray.getClass().getComponentType();    int firstArrayLength = Array.getLength(firstArray);    int allLength = firstArrayLength + Array.getLength(secondArray);    Object result = Array.newInstance(localClass, allLength);    for (int k = 0; k < allLength; ++k) {        if (k < firstArrayLength) {            Array.set(result, k, Array.get(firstArray, k));        } else {            Array.set(result, k, Array.get(secondArray, k - firstArrayLength));        }    }    return result;}

将patch.dex放在最前面,最后加载Element数组,来完成修复。

虽然Nuwa框架有很多优点,但由于它不是即时生效,并且修复的类过大,会导致加载时间延长,以及在ART模式下,类修改了结构,会导致内存错乱,如果想解决这个问题,就需要将相关的调用类、父类、子类等都加载到patch.dex中,会导致补丁过大。

3 0