Android之热修复框架Nuwa

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://glhcode.blog.csdn.net/article/details/70284239

转载请标明出处:
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中添加:

@Override
protected 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方法的具体实现:

@Override
protected 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中,会导致补丁过大。

展开阅读全文

没有更多推荐了,返回首页