Android热更新

Android热更新

组件化

组件化和模块化其实一回事,都是拆分多个 module 进行开发,组件化的叫法更偏向封装系统功能,比如统一对话框封装,网络封装等,而模块化叫法更偏向业务方面,比如登录模块等。


插件化

App 的部分功能模块在打包的时候不以传统的方式打包进 apk 中,而是另一种方式封装到apk 内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。

这些单独二次封装的功能模块 apk,称为插件,初始安装的 apk 称为宿主。

插件化基础是反射。


反射

Java 提供了关键字 public、private 用来限制代码之间的可见性,但又提供了反射来访问不可见方法。

可见特性的支持并不是为了保护代码不被别人使用,而是为了程序开发的简洁性。可见性的的支持提供的是 Safety 的安全,而不是 Security 的安全。可见性的支持是让程序的开发者不容易写出 bug ,而不是更容易外部被入侵。

反射的支持可以让开发者在可见性的不对外的时候,突破可见性的限制来调用自己需要的 API.


反射的使用:

package com.reflect.utils;
class Utils {
  
  	private Utils(){}
  
    private int method(int a) {
        System.out.println("Utils.method打印了... a:" + a);
        return 10;
    }
}



// 通过反射调用 Utils 中的方法
try {
    Class utilsClass = Class.forName("com.reflect.utils.Utils");// Utils.class
    Constructor constructor = utilsClass.getDeclaredConstructor();
    constructor.setAccessible(true);// 构造方法设置为可以访问
    Object utilsObj = constructor.newInstance();
    Method method = utilsClass.getDeclaredMethod("method", int.class);
    method.setAccessible(true);// 方法设置为可访问
    Object result = method.invoke(utilsObj, 1);
    System.out.println(result);
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

DEX

class:java 编译后的文件,每一个类对应一个 class 文件。

dex:Dalvik Executable 把 class 打包到一起,一个 dex 可以包含多个 class 文件。

odex:Optimized DEX 针对系统的优化,例如某个方法的调用指令,会把虚拟机的调用转换为使用具体的 index,这样在执行的就不用再查找了。

oat:Optimized Android file Type,使用 AOT(Ahead-Of-Time compilation预先编译) 策略对 dex 预先编译成本地指令,这样在运行阶段就不需要再经历一次解释过程。


插件化原理

动态加载,通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用。

private void pluginLoad() {
    File pluginApk = new File(getFilesDir() + "/plugin.apk");
    AssetManager assets = getAssets();
    try(Source source = Okio.source(assets.open("plugin.apk"));
        BufferedSink sink = Okio.buffer(Okio.sink(pluginApk))) {
        sink.writeAll(source);
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 调用 plugin.apk 中的方法
    DexClassLoader classLoader = new DexClassLoader(pluginApk.getAbsolutePath(),
            getCodeCacheDir().getPath(),
            null,
            null);
    try {
        Class<?> utilClass = classLoader.loadClass("com.chen.hotfix.utils.Utils");
        Constructor<?> constructor = utilClass.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        Object utilObj = constructor.newInstance();
        Method getNameMethod = utilClass.getDeclaredMethod("getName");
        getNameMethod.setAccessible(true);
        Object name = getNameMethod.invoke(utilObj);
        Log.e("TAG", "name:" + name);

    } catch (ClassNotFoundException
            | InvocationTargetException
            | IllegalAccessException
            | InstantiationException
            | NoSuchMethodException e) {
        e.printStackTrace();
    }
}

插件化和热更新
  1. 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
  2. 插件化在代码中有固定的入口,而热更新则可能改变了任何有一个位置的代码

热更新的原理

ClassLoader 的 dex 文件的替换,直接修改字节码


类加载过程

双亲委托机制,是一个带缓存的,从上到下(从父到子)的加载过程。

//ClassLoader.java
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 首先判断该类型是否已经被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
        try {
            if (parent != null) {
                //如果存在父类加载器,就委派给父类加载器加载
                c = parent.loadClass(name, false);
            } else {
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
            // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

对于一个具体的 ClassLoader,先从自己的缓存中取,自己缓存中没有,就找父 ClassLoader 要(parent.loadClass()),父 View 也没有,就自己加载。(父View的加载流程和自己是一样的)

BaseDexClassLoader 的 findClass() 是通过它的 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass(),所以热更新的关键在于,把补丁 dex 文件加载放进一个 Element,并且插入到 dexElements 这个数组的前面。


手写热更新

因为无法在更新之前就指定要更新谁,所以不能定义新的 ClassLoader,而只能选择对 ClassLoader 进行修改,让它能够加载补丁里面的类。

因为补丁中的类在之前的应用中已经存在,所以应该把补丁中的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。


具体的做法:

  1. 用补丁创建一个 PathClassLoader
  2. 把补丁 PathClassLoader 里面的 elements 插入到旧的 dexElements 前面去

尽早加载热更新,通常是把加载过程放在 Application.attachBaseContext()
热更新下载完之后需要先杀死程序才能让补丁生效
用 d8 把指定的 class 打包进 dex


sample

HotfixApplication

public class HotfixApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // 通过反射将dex的dexElements插入到应用的dexElements前面,使热更新(hotfix.dex)生效
        File hotfixDex = new File(getFilesDir() + "/hotfix.dex");
        if (hotfixDex.exists()) {
            try {
                // originalLoader.pathList.dexElements = classLoader.pathList.dexElements;
                // originalLoader.pathList.dexElements += classLoader.pathList.dexElements;
                ClassLoader originalClassLoader = getClassLoader();// App的ClassLoader
                DexClassLoader newClassLoader = new DexClassLoader(hotfixDex.getPath(),
                        getCodeCacheDir().getPath(), null, null);// dex的ClassLoader

                Class<BaseDexClassLoader> loaderClass = BaseDexClassLoader.class;
                Field pathListField = loaderClass.getDeclaredField("pathList");
                pathListField.setAccessible(true);
                Object pathListObj = pathListField.get(newClassLoader);

                Class<?> pathListClass = pathListObj.getClass();
                Field dexElementsField = pathListClass.getDeclaredField("dexElements");
                dexElementsField.setAccessible(true);
                Object dexElementsObj = dexElementsField.get(pathListObj);

                Object originalPathListObj = pathListField.get(originalClassLoader);
                Object originalDexElementsObj = dexElementsField.get(originalPathListObj);

                int originalLength = Array.getLength(originalDexElementsObj);
                int newLength = Array.getLength(dexElementsObj);
                Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), originalLength + newLength);
                for (int i = 0; i < newLength; i++) {
                    Array.set(concatDexElementsObject, i, Array.get(dexElementsObj, i));
                }
                for (int i = 0; i < originalLength; i++) {
                    Array.set(concatDexElementsObject, newLength + i, Array.get(originalDexElementsObj, i));
                }
                dexElementsField.set(originalPathListObj, concatDexElementsObject);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

}

下载热更新包

private void hotfixLoad() {
    File hotfixDex = new File(getFilesDir() + "/hotfix.dex");
    AssetManager assets = getAssets();
    try (Source source = Okio.source(assets.open("hotfix.dex"));//TODO 需要从网络下载hotfix.dex
         BufferedSink sink = Okio.buffer(Okio.sink(hotfixDex))) {
         sink.writeAll(source);
    } catch (IOException e) {
        e.printStackTrace();
    }
    android.os.Process.killProcess(android.os.Process.myPid());// 重启生效
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值