插件化、组件化、热修复

一、Android插件化、组件化、热修复的区别

插件化
插件化是一种将应用程序按照模块或组件进行拆分,并以插件的方式动态加载和运行的技术。其主要原理包括以下几个步骤:

  • 模块划分:应用程序被分割成多个独立模块,每个模块通常作为一个单独的APK存在。
  • 动态加载:通过自定义类加载器或使用第三方框架(如DexClassLoader)来加载并实例化外部APK中的类
  • 资源隔离:由于每个APK可能有自己的资源文件,需要通过Hook机制来实现资源隔离与管理
  • 上下文运行环境:在调用外部APK中代码时,需要正确处理上下文环境以确保功能正常运行。

总之,Android插件化利用动态加载技术将应用程序切割成多个独立安装运行的组件,并借助合适框架管理各种资源及上下文环境等信息。

组件化
组件化是一种将应用程序按照功能模块拆分,并以组件的方式进行开发、管理和复用的技术。其主要原理包括以下几个步骤:

  • 功能模块划分:将应用程序按照业务逻辑或功能划分成多个独立组件。
  • 通信机制:使用合适的通信机制(如Intent、事件总线等)来实现不同组件之间的数据传递与交互。
  • 解耦:通过接口定义和依赖注入等方式,使得各个组件之间相互解耦,提高代码重用性与维护性。

Android组件化旨在实现模块化开发,让不同团队可以并行开发不同模块,并且能够灵活地替换、新增或删除某些功能。

热修复
热修复是一种在应用程序运行时对已发布版本进行动态修复bug或更新功能的技术。其主要原理包括以下几个步骤:

  • 补丁生成与发布:根据需要修复或更新的问题,生成补丁文件并推送到客户端设备上。

  • 补丁加载与应用:当检测到有新补丁时,通过自定义类加载器等机制将补丁文件加载到应用程序中

  • 动态修复/更新:在运行时,通过替换、插入或修改现有代码来解决问题,并确保新旧版本之间的兼容性与稳定性。

总之,Android热修复技术利用补丁文件实现对已发布版本进行动态修复和更新。它可以快速响应问题并部署解决方案,而无须重新发布整个应用程序。

二、Tinker热修复的原理

热修复的方案有很多种,其中原理也各不相同。目前开源的比较有名的有阿里的AndFix、美团的Robust、qq的QZone以及Tinker等。

1、Tinker的优点

  • 支持类替换、so替换,资源替换是采用类似Instant-run的方案。
  • 补丁包较小,自研diff方案,下发的是差量包,包括的是变更的内容。
  • 采用全量Dex更新,不需要额外处理 CLASS_ISPREVERIFIED 问题。

2、Tinker热修复的流程

(1)Tinker将新旧dex做了diff(差分算法),得到patch.dex
(2)然后将patch.dex下发到客户端,客户端将patch.dex与旧dex的classes.dex做合并,生成新的classes.dex
(3)在运行时通过反射将合成后的全量dex插入到dex elements前面(放在Element数组的第一个元素),完成修复。(饿了么的Amigo则是将补丁包中每个dex对应的Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element数组)。

在ClassLoader的加载过程中,其中一个环节就是调用DexPathList的findClass的方法,如下所示,libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

public Class<?> findClass(String name, List<Throwable> suppressed) {
       for (Element element : dexElements) {//1
           Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
           if (clazz != null) {
               return clazz;
           }
       }
       if (dexElementsSuppressedExceptions != null) {
           suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
       }
       return null;
   }

  • Element内部封装了DexFile,DexFile用于加载dex文件,因此每个dex文件对应一个Element。多个Element组成了有序的Element数组dexElements。
  • 当要查找类时,会在注释1处遍历Element数组dexElements(相当于遍历dex文件数组),注释2处调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类。如果在Element中(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element中进行查找。
    根据上面的查找流程,我们将有bug的类Key.class进行修改,再将Key.class打包成包含dex的补丁包Patch.jar,通过反射放在Element数组dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在bug的Key.class,排在数组后面的dex文件中的存在bug的Key.class根据ClassLoader的双亲委派模式就不会被加载,这就是类的加载方案,如下图所示:


    图片.png

类加载方案需要重启App后让ClassLoader重新加载新的类,为什么要重启呢?这是因为类是无法被卸载的,因此想要重新加载类就需要重启App,因此采用类加载方案的热修复框架是不能即时生效的。

Tinker热修复的流程图.png

3、65536限制与LinearAlloc限制

类的加载方案基于Dex分包方案,什么是Dex分包方案?这个得先从65536限制和LinearAlloc限制说起。

65536限制

随着应用功能越来越复杂,代码量不断增大,引入的库也越来越多,可能会在编译时提示如下异常:

com.android.dex.DexIndexOverflowException:method ID not in [0,0xffff]:65536

这说明应用中引用的方法数超过了最大数65536个。产生这个问题的原因就是系统的65536限制,65536限制的主要原因是DVM Bytecode的限制。DVM指令集的方法调用指令 invoke-kind索引为16bits,最多能引用65536。

LinearAlloc限制

在安装应用时可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数超出了缓存区的大小时会报错。

为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。

Dex分包方案主要有两种,分别是Google官方方案、Dex自动拆包和动态加载方案。

4、PathClassLoader和DexClassLoader

Android系统中又两个应用程序类加载器,它们分别是PathClassLoader和DexClassLoader,它们都是继承于BaseDexClassLoader的,而BaseDexClassLoader继承ClassLoader。

BaseDexClassLoader类加载:

  • 作为ClassLoader的子类,重写了父类的findClass方法:
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //在自己的成员变量DexPathList中寻找,找不到抛异常
        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;
    }
  • DexPathList的findClass方法:
public Class findClass(String name, List<Throwable> suppressed) {
        //循环遍历成员变量dexElements,调用DexFile.loadClassBinaryName加载class
        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;
    }

PathClassLoader和DexClassLoader的区别

  • 路径查找方式:
    PathClassLoader使用系统默认的路径查找机制。它会根据包名去已安装的APK文件或系统库目录下查找并加载类;而DexClassLoader可以指定自定义的dex文件路径来进行类的查找和加载。这使得DexClassLoader能够灵活地从外部存储设备(如SD卡)或其他位置动态地加载已打包成dex格式的apk或jar文件。
  • 加载资源文件
    PathClassLoader除了加载类,还可以通过getResource()方法来获取APK中的资源文件;而DexClassLoader无法直接使用getResource()方法来获取APK中的资源文件,因为它只负责加载类而不处理资源文件。但是可以通过自定义解析代码实现对资源的访问。
  • 类共享与隔离性
    所有使用相同PathClassLoader实例创建出来的对象都将共享同一个虚拟内存空间,即处于同一个”命名空间“下。这意味着不同模块之间可能存在命名冲突等问题;而每个DexClassLoader实例都会有独立的虚拟机内存空间,”命名空间“彼此隔离,避免了命名冲突问题。

总结起来:

  • PathClassLoader是系统默认的应用程序类加载器,使用系统默认的路径查找机制,在加载类和资源时比较方便。
  • DexClassLoader可以自定义dex文件路径进行灵活的类加载,但无法直接访问APK中的资源文件。

4、核心原理

PackageManagerService拿到apk,然后从后台下载修复的patch.dex包。注意,这里我们可能有多个dex文件需要更新

public static void loadFixedDex(Context context) {
        if (context == null) return;
        // Dex文件目录(私有目录中,存在之前已经复制过来的修复包)
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        File[] listFiles = fileDir.listFiles();
        // 遍历私有目录中所有的文件
        for (File file : listFiles) {
            // 找到修复包,加入到集合
            if (file.getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equals(file.getName())) {
                loadedDex.add(file);
            }
        }

        // 模拟类加载器
        createDexClassLoader(context, fileDir);
    }

这个方法的作用是找到dex文件存在的位置并遍历dex文件然后保存在一个集合中。然后调用createDexClassLoader方法。

private static void createDexClassLoader(Context context, File fileDir) {
        // 创建临时的解压目录(先解压到该目录,再加载java)
        String optimizedDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
        // 不存在就创建
        File fopt = new File(optimizedDir);
        if (!fopt.exists()) {
            // 创建多级目录
            fopt.mkdirs();
        }
        for (File dex : loadedDex) {
            // 每遍历一个要修复的dex文件,就需要插桩一次
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(),
                    optimizedDir, null, context.getClassLoader());
            hotfix(classLoader, context);
        }
    }  

private static void hotfix(DexClassLoader classLoader, Context context) {
// 获取系统PathClassLoader类加载器
PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
try {
        // 获取自有的dexElements数组对象
        Object myDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));

        // 获取系统的dexElements数组对象
        Object systemDexElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathLoader));

        // 合并成新的dexElements数组对象
        Object dexElements = ArrayUtils.combineArray(myDexElements, systemDexElements);

        // 通过反射再去获取   系统的pathList对象
        Object systemPathList = ReflectUtils.getPathList(pathLoader);

        // 重新赋值给系统的pathList属性  --- 修改了pathList中的dexElements数组对象
        ReflectUtils.setField(systemPathList, systemPathList.getClass(), dexElements);
    } catch (Exception e) {
        e.printStackTrace();
    }
}  
public class ReflectUtils {

    /**
     * 通过反射获取某对象,并设置私有可访问
     *
     * @param obj   该属性所属类的对象
     * @param clazz 该属性所属类
     * @param field 属性名
     * @return 该属性对象
     */
    private static Object getField(Object obj, Class<?> clazz, String field)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field localField = clazz.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 给某属性赋值,并设置私有可访问
     *
     * @param obj   该属性所属类的对象
     * @param clazz 该属性所属类
     * @param value 值
     */
    public static void setField(Object obj, Class<?> clazz, Object value)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        Field localField = clazz.getDeclaredField("dexElements");
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 通过反射获取BaseDexClassLoader对象中的PathList对象
     *
     * @param baseDexClassLoader BaseDexClassLoader对象
     * @return PathList对象
     */
    public static Object getPathList(Object baseDexClassLoader)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException, ClassNotFoundException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通过反射获取BaseDexClassLoader对象中的PathList对象,再获取dexElements对象
     *
     * @param paramObject PathList对象
     * @return dexElements对象
     */
    public static Object  getDexElements(Object paramObject)
            throws NoSuchFieldException, IllegalAccessException, IllegalArgumentException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }
}

参考:https://www.jianshu.com/p/e6c4eedd83abhttps://www.jianshu.com/p/6e412b0115f1

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容