Android插件化进阶——插件化原理和插件管理器(一)

之前已经对我们学习插件化原理需要的预备知识进行了比较详细的讲解了,从这篇文章开始,我们将具体介绍插件化原理,同时会根据原理写一个比较简单的插件化管理器。

插件化主要用到的技术知识有:

  • Android ClassLoader 加载 class 文件原理,这也是插件化最重要的技术点,我们在上篇文章中讲解的也比较详细了,插件化框架都会通过自定义 ClassLoader 来加载插件中的 class 文件。
  • Java 反射原理,这是制作插件化框架中最基础和最核心的知识点了。
  • Android 资源加载原理,即 Android 如何加载资源文件,主要通过 Resource 类和 AssetManager 类等来完成资源的加载。
  • 四大组件加载原理。了解四大组件的加载流程,以及它们是如何通过 ActivityManagerService 完成与系统的通信。

这四点是最基本要了解的点,你还需要了解像 Gradle 打包原理,Android Framework 层以及一些包的管理——PackageManager 的原理,所以如果想制作一个插件化框架,实际上是非常复杂的,要对 Android 系统非常熟悉,这里我们只讲解基本原理。

Manifest 处理

清单文件在 App 中非常重要,它记录了你的应用中有哪些组件。

Manifest处理

这是市面上大部分的框架对清单文件的处理方式。首先,无论是宿主 app 还是 aar 还是 Bundle,都是有自己的清单文件的,那么我们平时使用的时候,当我们依赖 library 或者 aar 的时候,就会有多个清单文件,这个时候,Gradle 在构建 app 产物的时候,会将 aar 的 manifest 文件 merge 到 app module 中的清单文件中。

基于这个原理,插件化框架会修改整个打包流程,在输出 apk 的 manifest 文件的时候,会将所有插件的清单文件都合并到宿主清单文件中,这样的话,宿主 manifest 就记录了所有插件和 aar 文件中所有清单的内容,这样就可以保证我们调用各个插件组件的时候不会报错。

光是清单文件的处理,就比较复杂了。你不仅要了解 Gradle 的打包原理,甚至还需要去修改流程。这样才能打到合并清单文件的要求。

我们来总结下插件化框架对于 manifest 的处理,主要工作主要有两个:

  • 文件的合并,实际上就是一个 IO 操作,将所有的清单文件合并到一个总的 manifest 文件中。
  • 修改构建流程,在构建时将所有插件的清单文件合并到宿主的 manifest 文件中。大家有兴趣的话可以深入了解下如何修改编译流程从而完成清单文件的合并。

插件类加载

插件类加载

每个插件实际上都是一个 APK,每个 APK 有自己的 Dex 文件,所有的 class 字节码就都存储在了这些 dex 文件中的。市面上绝大多数的插件化框架都是根据上图的这样的方式来加载插件中的类的。

在加载之前,他首先会区分宿主 apk 和 插件 apk,这样区分的好处是,因为宿主 apk 已经安装到了系统中了,所以系统会给宿主 apk 创建 ClassLoader ,而无需手动去创建了。所以宿主 apk 的 ClassLoader 使用 PathClassLoader 就完全够用了,PathClassLoader 可以加载已安装 apk 中的类,这个我们在之前的文章中已经分析过了

对于插件 apk 来说,因为没有安装到我们系统中,所以插件 apk 本身是没有 ClassLoader 的,系统也不会帮我们创建,需要我们自己手动创建 apk,插件化框架会给每个插件创建对应的 ClassLoader,在加载插件中 apk 文件的时候,就使用我们创建的 ClassLoader 来加载插件 apk 中的类。

根据这个思路,我们就会引出两个问题:

  • 如何自定义 ClassLoader 加载类文件
  • 如何调用插件 apk 文件中的类

下面我们就简单实现下上面两个问题的代码,通过简单的模拟来理解原理。

首先我们新建一个项目,并且在项目下再新建一个 Project 作为插件模块。


项目结构

这里的app代表宿主模块,app.bundle代表某个插件模块,名字大家不要过多纠结,这里只是为了遵循 Small 框架而起的插件名。app.bundle中有一个简单的类,静态方法输出一段 Log:

public class BundleUtil {
    public static void printLog(){
        Log.e("Bundle","I am a class in the Bundle");
    }
}

现在我们的宿主模块并没有在build.gradle中 compile 这个模块,我们要手动在宿主 apk 中加载并调用这个类。现在我们在宿主模块的 MainActivity 中的 onCreate() 方法中实现加载逻辑。

protect void onCreate(Bundle savedInstanceState){
    //省略一些代码
    ...
    String apkPath = getExternalCacheDir().getAbsolutePath()+"/bundle-debug.apk";
    loadApk(apkPath);
}

private void loadApk(String apkPath) {
    //应用内部目录,MODE_PRIVATE 代表只有自己应用可以访问这个路径。
    File optDir = getDir("opt", MODE_PRIVATE);
    //初始化 classLoader,通过 DexClassLoader 来加载指定目录下的插件中的类
    DexClassLoader classLoader = new DexClassLoader(apkPath,
            optDir.getAbsolutePath(), null, this.getClassLoader());

    try {
        //获取指定路径插件的 class 字节码文件
        Class cls = classLoader.loadClass("org.sojex.stockquotes.bundle.BundleUtil");
        if (cls != null) {
            Object instance = cls.newInstance();
            Method method = cls.getMethod("printLog");
            method.invoke(instance);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
 }

这里apkPath指的是插件 apk 存放的路径,我们把app.bundle通过./gradlew assembleDebug指令打出 apk 包,并通过adb push命令推送到手机上的 apkPath 对应的目录中。

loadApk()方法是我们的核心方法,我们传入一个apkPath参数,指定插件 apk 存放的路径,再通过Context.getDir获取一个应用内部路径,使用这两个参数,可以新建一个DexClassLoader对象,我们之前讲过,DexClassLoader可以加载没有安装的 apk 文件中的类,通过它的loadClass方法,获取到BundleUtil的字节码文件。最后通过反射,即可调用到插件 apk 类中的 printLog()方法。我们运行宿主 apk ,发现结果成功的打印了出来。

我们下面自定义一个 ClassLoader。

public class CustomClassLoader extends DexClassLoader {
    public CustomClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
    }

    /**
     * 定义 ClassLoder 要以何种策略加载 class 文件
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData != null) {
            return defineClass(name, classData, 0, classData.length);
        } else {
            throw new ClassNotFoundException();
        }
    }

    private byte[] getClassData(String name) {
        try {
            InputStream inputStream = new FileInputStream(name);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = -1;
            while ((bytesNumRead = inputStream.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

自定义一个 ClassLoader 最核心的就是重写findClass方法,方法里要定义我们要以何种策略加载 class 文件。现在我们没有什么特殊的策略,

这里就是通过指定类的路径加载字节码,最后通过获取到的字节码转化为 class 对象,getClassData方法就是一个简单的文件读取。这里只是一个模拟如何定义 ClassLoader。通过自定义 ClassLoader,表明插件化框架可以为每一个插件维护一个 ClassLoader,在加载普通类的时候就会绕过 Android 系统的加载机制,即使没有安装这些插件 apk,我们依然能加载其中的类。

因为 Android 系统在加载 apk 的时候会创建一个 PathClassLoader,而插件 apk 的加载绕过了 Android 系统,所以我们就要手动的为每一个插件 apk 都要创建一个 ClassLoader。不仅如此,如果宿主和各插件 apk 中有同名类,如果不为每个插件创建 ClassLoader,那么如果该同名类已经被 ClassLoader 加载过,其他的同名类就无法再被加载了,而不同的 ClassLoader 的同名类不会被判定为同一个类,插件中的同名类在调用的时候依然会被加载。

当然,真正的商业插件化框架不会这么简单,类加载模块不仅要完成类的查找和加载,还要对插件的 ClassLoader 进行管理,确保所有类都能加载。

源码地址

那么下一篇文章我们将写一个简单的插件管理器来模拟插件化框架的管理步骤。


本文部分内容参考于慕课网实战课程「Android 应用发展趋势必备武器 热修复与插件化」,有兴趣的朋友可以付费学习。
插件化实战课程

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

推荐阅读更多精彩内容