插件化技术出现背景
APP体积越来越庞大,功能模块越来越多
模块耦合度高,协同开发沟通成本极大
方法数可能查过65535,占用内存过大
以上问题如何解决
将一个大的apk按照业务分割成多个小的apk
每个小的apk即可以独立运行又可以作为插件运行
插件化优势
基本概念
结构对比
插件化和组件化的对比
插件化与动态更新(热修复)对比
插件化原理
相关知识
- android ClassLoader加载class文件原理
- Java 反射原理
- android 资源加载原理
- 四大组件加载原理
Manifeast处理
- 构建期进行全量Merge操作
- Bundle的依赖单独Merge,生成Bundle的Merge manifest
- 解析各个Bundle的Merge Manifest,得到整包的BundleInfoListundleInfoList
插件类加载
- DelegateClassLoader以PatchClassLoader为父classLoader,找不到的情况下根据BundleList找到对应的BundleClassLoader
- DelegateClassLoader的父对象为BootClassLoader,包含PathClassLoader对象,先查找当前的classLoader,再查找当前PatchClassLoader
如何定义ClassLoader加载类文件?如何调用插件apk文件中的类?
我们通过一个简单的例子来看一下来简单模拟一下如何调用插件apk文件中的类。
- 新建一个工程名称为ClassLoader,在工程里面新建一个名为Bundle的Module,Module App(代表宿主应用)和Module Bundle(代表插件应用) 两者没有任何依赖关系,我们看一下如何通过宿主应用调用我们插件应用中的类。
- 在插件应用中创建一个类BundleUtil,方便我们宿主应用调用。
1 | package com.lbz.bundle; |
- 实现App module中的代码
1 | package com.lbz.classloader; |
- 在module Bundle 中打一个包apk文件,然后把这个包通过adb push命令上传到我们指定的文件目录中
1 | $ adb push E:/zhiLearn/test/ClassLoader/bundle/build/outputs/apk/debug/bundle.apk \storage/emulated/0/Android/data/com.lbz.classloader/cache/bundle.apk |
- 运行宿主app,发现Log打印,成功表明我们的宿主调用我们的插件apk中的类,是在我们没有添加app和bundle联系的情况下通过DexClassLoader加载指定目录下的apk文件。
1
208-16 14:59:11.675 21886-21886/com.lbz.classloader I/bundle: I am a class in the bundle
自定义ClassLoader
1 | package com.lbz.bundle; |
我们这个类仅仅模拟如何定义一个自己的classLoader,并没有属于自己的策略,只是简单完成一个通过指定路径完成classes字节码的加载。以及通过获取的字节码转化成的对应class对象。
是不是要为每一个插件apk都要创建一个classLoader?
答案是肯定的。原因:
- android系统会为每一个安装过的应用程序至少分配一个classLoader就是PathClassLoader,插件化框架是替代android系统的,所以他要为每一个插件创建一个对应classLoader。
- 宿主或者插件类名可能同名,如果不为每一个插件单独创建一个他自己classLoader,那么路径一样的类只要在加载过一次,他就不会被加载,就会出现插件中对应的类永远不会被加载
实际插件化框架开发中,类加载模块不仅仅类的查找加载,维护每一个插件的classLoader,保证每一个插件的类都能被加载到
模拟插件化框架管理步骤
资源加载
- 所有的Bundle Res资源都是加入到一个DelegateResources
- Bundle Install 的时候将Res,so 等目录通过反射加入(AssetManager.addAssetPath)
- 通过不同的packageId来区分Bundle的资源ID
- 覆盖getIdentifier方法,兼容5.0以上系统
核心技术
- 处理所有插件apk文件中的Manifest文件
- 管理宿主apk中所有的插件apk信息
- 为每个插件apk创建对应的类加载器,资源管理器
下面通过一个简单的类来模拟为插件apk创建对应的类加载器,资源加载器,为最核心最基本的实现。
1 | package com.lbz.bundle.plugin; |
插件化框架
框架对比
参考Small官方比较:COMPARISION.md
为方便列表,简写如下:
1 | DyLA : Dynamic-load-apk @singwhatiwanna, 百度 |
- 功能
\ | DyLA | DiLA | ACDD | DyAPK | DPG | APF | Small |
---|---|---|---|---|---|---|---|
加载非独立插件[1] | × | x | √ | √ | × | √ | √ |
加载.so后缀插件 | × | × | ! [2] | × | × | × | √ |
Activity生命周期 | √ | √ | √ | √ | √ | √ | √ |
Service动态注册 | × | × | √ | × | √ | √ | x [3] |
资源分包共享[4] | × | × | ! [5] | ! [5] | × | ! [6] | √ |
公共插件打包共享[7] | × | × | × | × | × | × | √ |
支持AppCompat[8] | × | × | × | × | × | × | √ |
支持本地网页组件 | × | × | × | × | × | × | √ |
支持联调插件[9] | × | x | × | × | × | × | √ |
[1] 独立插件:一个完整的apk包,可以独立运行。比如从你的程序跑起淘宝、QQ,但这加载起来是要闹哪样?
非独立插件:依赖于宿主,宿主是个壳,插件可使用其资源代码并分离之以最小化,这才是业务需要嘛。
– _“所有不能加载非独立插件的插件化框架都是耍流氓”_。
[2] ACDD加载.so用了Native方法(libdexopt.so),不是Java层,源码见dexopt.cpp。
[3] Service更新频度低,可预先注册在宿主的manifest中,如果没有很好的理由说服我,现不支持。
[4] 要实现宿主、各个插件资源可互相访问,需要对他们的资源进行分段处理以避免冲突。
[5] 这些框架修改aapt源码、重编、覆盖SDK Manager下载的aapt,我只想说_“杀(wan)鸡(de)焉(kai)用(xin)牛(jiu)刀(hao)”。
Small使用gradle-small-plugin,在后期修改二进制文件,实现了PP_段分区。
[6] 使用public-padding对资源id的_TT_段进行分区,分开了宿主和插件。但是插件之间无法分段。
[7] 除了宿主提供一些公共资源与代码外,我们仍需封装一些业务层面的公共库,这些库被其他插件所依赖。
公共插件打包的目的就是可以单独更新公共库插件,并且相关插件不需要动到。
[8] AppCompat: Android Studio默认添加的主题包,Google主推的Metrial Design包也依赖于此。大势所趋。
[9] 联调插件:使用Android Studio调试![🐞][as-debug]宿主时,可直接在插件代码中添加断点调试。
- 透明度
ACDD | DyAPK | APF | Small | |
---|---|---|---|---|
插件Activity代码无需修改 | √ | √ | √ | √ |
插件引用外部资源无需修改name | × | × | × | √ |
插件模块无需修改build.gradle | × | × | × | √ |
如何选择?
只有Small和Atlas更新比较频繁,其余的基本超过一年两年没有更新了,不建议再使用,small对比于atlas较为简单,容易集成,建议使用small