【Android 修炼手册】常用技术篇 -- Android 插件化解析

本文详细介绍了Android插件化开发的历史、原理与实现方法,包括代理分发和hook系统机制。通过理解ClassLoader、Activity生命周期、资源处理等方面,探讨了插件化框架如VirtualApp、Atlas、DroidPlugin等的实现思路。同时,对比了手动调用生命周期与hook Instrumentation的优缺点,并给出了实际的代码示例。此外,还简述了Service、BroadcastReceiver和ContentProvider的插件化实现策略。
摘要由CSDN通过智能技术生成

预备知识

  1. 了解 android 基本开发
  2. 了解 android 四大组件基本原理
  3. 了解 ClassLoader 相关知识

看完本文可以达到什么程度

  1. 了解插件化常见的实现原理

阅读前准备工作

  1. clone CommonTec 项目

文章概览

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gB0axkS9-1629895859810)(https://user-gold-cdn.xitu.io/2019/7/8/16bd22de0ea2a81c?imageView2/0/w/1280/h/960/ignore-error/1)]

一、插件化框架历史

整个插件化框架历史部分参考了包建强在 2016GMTC 全球开发大会上的演讲
2012 年 AndroidDynamicLoader 给予 Fragment 实现了插件化框架,可以动态加载插件中的 Fragment 实现页面的切换。
2013 年 23Code 提供了一个壳,可以在壳里动态化下载插件然后运行。
2013 年 阿里技术沙龙上,伯奎做了 Atlas 插件化框架的分享,说明那时候阿里已经在做插件化的运用和开发了。
2014 年 任玉刚开源了 dynamic-load-apk,通过代理分发的方式实现了动态化,如果看过 Android 开发艺术探索这本书,应该会对这个方式有了解。
2015 年 张勇 发布了 DroidPlugin,使用 hook 系统方式实现插件化。
2015 年 携程发布 DynamicApk
2015 - 2016 之间(这块时间不太确定),Lody 发布了 VirtualApp,可以直接运行未安装的 apk,基本上还是使用 hook 系统的方式实现的,不过里面的实现要精致很多,实现了自己的一套 AMS 来管理插件 Activity 等等。
2017 年阿里推出 Atlas
2017 年 360 推出 RePlugin
2017 年滴滴推出 VirtualApk
2019 年腾讯推出了 Shadow,号称是零反射,并且框架自身也可实现动态化,看了代码以后发现,其实本质上还是使用了代理分发生命周期实现四大组件动态化,然后抽象接口来实现框架的动态化。后面有机会可以对其做一下分析。

这基本上就是插件化框架的历史,从 2012 至今,可以说插件化技术基本成型了,主要就是代理和 hook 系统两种方式(这里没有统计热修复的发展,热修复其实和插件化还是有些想通的地方,后面的文章会对热修复进行介绍)。如果看未来的话,斗胆预测,插件化技术的原理,应该不会有太大的变动了。

二、名词解释

在插件化中有一些专有名词,如果是第一次接触可能不太了解,这里解释一下。
宿主
负责加载插件的 apk,一般来说就是已经安装的应用本身。
StubActivity
宿主中的占位 Activity,注册在宿主 Manifest 文件中,负责加载插件 Activity。
PluginActivity
插件 Activity,在插件 apk 中,没有注册在 Manifest 文件中,需要 StubActivity 来加载。

三、使用 gradle 简化插件开发流程

在学习和开发插件化的时候,我们需要动态去加载插件 apk,所以开发过程中一般需要有两个 apk,一个是宿主 apk,一个是插件 apk,对应的就需要有宿主项目和插件项目。
CommonTec 这里创建了 app 作为宿主项目,plugin 为插件项目。为了方便,我们直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 启动时直接放到内部存储空间中方便加载。
这样的项目结构,我们调试问题时的流程就是下面这样:
修改插件项目 -> 编译生成插件 apk -> 拷贝插件 apk 到宿主 assets -> 修改宿主项目 -> 编译生成宿主 apk -> 安装宿主 apk -> 验证问题
如果每次我们修改一个很小的问题,都经历这么长的流程,那么耐心很快就耗尽了。最好是可以直接编译宿主 apk 的时候自动打包插件 apk 并拷贝到宿主 assets 目录下,这样我们不管修改什么,都直接编译宿主项目就好了。如何实现呢?还记得我们之前讲解过的 gradle 系列么?现在就是学以致用的时候了。
首先在 plugin 项目的 build.gradle 添加下面的代码:

project.afterEvaluate {
    project.tasks.each {
        if (it.name == "assembleDebug") {
            it.doLast {
                copy {
                    from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePath
                    into new File(project.getRootProject().getProjectDir(), 'app/src/main/assets')
                    rename 'plugin-debug.apk', 'plugin.apk'
                }
            }
        }
    }
} 

这段代码是在 afterEvaluate 的时候,遍历项目的 task,找到打包 task 也就是 assembleDebug,然后在打包之后,把生成的 apk 拷贝到宿主项目的 assets 目录下,并且重命名为 plugin.apk。 然后在 app 项目的 build.gradle 添加下面的代码:

project.afterEvaluate {
    project.tasks.each {
        if (it.name == 'mergeDebugAssets') {
            it.dependsOn ':plugin:assembleDebug'
        }
    }
} 

找到宿主打包的 mergeDebugAssets 任务,依赖插件项目的打包,这样每次编译宿主项目的时候,会先编译插件项目,然后拷贝插件 apk 到宿主 apk 的 assets 目录下,以后每次修改,只要编译宿主项目就可以了。

四、ClassLoader

ClassLoader 是插件化中必须要掌握的,因为插件是未安装的 apk,系统不会处理其中的类,所以需要我们自己来处理。

4.1 java 中的 ClassLoader

BootstrapClassLoader
负责加载 JVM 运行时的核心类,比如 JAVA_HOME/lib/rt.jar 等等

ExtensionClassLoader
负责加载 JVM 的扩展类,比如 JAVA_HOME/lib/ext 下面的 jar 包

AppClassLoader
负责加载 classpath 里的 jar 包和目录

4.2 android 中的 ClassLoader

在这里,我们统称 dex 文件,包含 dex 的 apk 文件以及 jar 文件为 dex 文件 PathClassLoader 用来加载系统类和应用程序类,可以加载已经安装的 apk 目录下的 dex 文件

DexClassLoader 用来加载 dex 文件,可以从存储空间加载 dex 文件。

我们在插件化中一般使用的是 DexClassLoader。

4.3 双亲委派机制

每一个 ClassLoader 中都有一个 parent 对象,代表的是父类加载器,在加载一个类的时候,会先使用父类加载器去加载,如果在父类加载器中没有找到,自己再进行加载,如果 parent 为空,那么就用系统类加载器来加载。通过这样的机制可以保证系统类都是由系统类加载器加载的。 下面是 ClassLoader 的 loadClass 方法的具体实现。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 先从父类加载器中进行加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 没有找到,再自己加载
                    c = findClass(name);
                }
            }
            return c;
    } 
4.4 如何加载插件中的类

要加载插件中的类,我们首先要创建一个 DexClassLoader,先看下 DexClassLoader 的构造函数需要那些参数。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        // ...
    }
} 

构造函数需要四个参数:
dexPath 是需要加载的 dex / apk / jar 文件路径
optimizedDirectory 是 dex 优化后存放的位置,在 ART 上,会执行 oat 对 dex 进行优化,生成机器码,这里就是存放优化后的 odex 文件的位置
librarySearchPath 是 native 依赖的位置
parent 就是父类加载器,默认会先从 parent 加载对应的类

创建出 DexClassLaoder 实例以后,只要调用其 loadClass(className) 方法就可以加载插件中的类了。具体的实现在下面:

 // 从 assets 中拿出插件 apk 放到内部存储空间
    private fun extractPlugin() {
        var inputStream = assets.open("plugin.apk
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值