Skip to content

qqiabc521/FragPlugin

Repository files navigation

android插件化方案

一、前言

插件化的好处:

1.动态功能更新,可以进行动态添加新功能而无需用户下载安装应用

2.热修复,发现bug后,直接更新存在bug的插件,用户无需重新安装应用,甚至不需要关闭应用,bug即可修复。

3.业务模块拆分,强制约束业务代码间的耦合度。

二、主体方案

按照业务把不同业务拆分成各个不同的子apk作为插件。主apk作为宿主,负责插件的加载、管理、升级等职责。

按照安卓系统的特性,要完成上面的方案,首先要解决插件apk能够越过系统安装的过程,在后台自动安装。解决这个问题的目前的方案是不进行插 件apk的安装,而是提取插件apk中的dex文件,动态加载dex。这样逻辑代码部分就可以完成动态的加载。但这就引入了新的问题,资源和Activity 如何处理。

1.资源的处理:

因为目前我们只是动态加载了插件的dex文件,插件的资源并没有引入,这样会造成插件的资源无法使用。为了解决插件资源加载的问 题,我们知道,安卓中资源的获取是通过getResouce()这个方法进行的,而这个方法返回的Resource对象的构造函数中需要传入AssetManager对象 ,所以安卓的资源管理实际是通过Resource对象包裹的AssetManager对象来管理的,核心的资源管理是AssetManager负责。所以我们可以构造自己 的AssetManager来接管插件的资源管理。AssetManager中有一个addAssetPath的方法,此方法可以增加一个路径到AssetManager中,这样在AssetM anager资源时就可以去新增的路径中查找。所以为了插件的资源能够被AssetManager找到,我们通过调用该方法增加插件的资源路径到AssetManage r中。因为AssetManager是系统类,所以我们需要通过反射的方式调用该类的构造方法和addAssetPath方法。至此,我们构造了自己的AssetManager 并且AssetManager具有去插件路径加载资源的能力。但目前我们还没有把AssetManager与getResouce()联系起来。getResouce()方法是存在于Cont ext中的,所以我们只需为我们的每一个插件构造一个Context来实现重写getResouce()方法,在getResouce()实现中把我们构建的AssetManager对 象包裹进去即可。而我们的插件是与宿主在同一进程,同时也需要有访问宿主资源的能力,所以我们把宿主的Context作为插件的父Context,并重 写getResouce()方法,这样插件的资源就可以通过我们构造的插件Context来载入。资源的问题解决。

2.Activity的处理:

安卓系统接管了整个Activity的生命周期管理。要求所有的Activity必须在manifest中进行声明。否则无法启动。而插件中Activ ity是无法动态在宿主的manifest中声明的,所以为了解决这个问题,我们采用占坑+Fragment方式。占坑:即我们把插件的入口Activity在宿主的m anifest中声明,Fragment:即我们把插件的逻辑代码都通过Fragment实现,这样宿主的插件入口Activity只要动态替换Fragment就能达到页面跳转 等相关操作,基本能够替代原来Activity的页面管理方式。

其中Activity为宿主的manifest中声明的插件入口Activity。Fragment为插件开发需要继承的负责插件管理的Fragment。其中包含一个插件管理类PluginManager,负责插件的加载,插件资源的管理等。PluginManager主要包含两部分功能,一个是Load dex,另一个是构造插件的context,完成插件的资源管理。其中load dex负责插件的dex加载。构造插件的context是为了插件资源的加载。其中主要重写getClassLoader、getAssets()、getResource()三个方法。其中在后两个方法中,通过反射调用AssetManager的构造函数和addAssetPath方法,来构建插件自己的AssetManager,使插件具有资源加载的能力。

至此,插件的总体方案基本完整。核心就是解决两个问题:

a) 编译期:资源和代码的编译

b) 运行时:资源和代码的加载

三、 具体细节

1.dex加载

dex加载我们采用为每个插件建立一个独立的DexClassLoader,同时插件的DexClassLoader的父类均为宿主的DexClassLoader。这样插件之间的Dex ClassLoader为兄弟关系。保证了插件之间的隔离性。因为根据java ClassLoader双亲委托模型的结构可知,DexClassLoader的尝试加载顺序是自顶而下的,而不会去查找兄弟关系的DexClassLoader。所以这样即使插 件之间存在相同的class,也不会相互影响。同时由于这种结构,也能保证插件能够使用宿主中的所有class。 Java双亲委托模型

2.resource处理

通过反射调用AssetManager的构造函数构造自己的AssetManager对象,每一个插件构造一个AssetManager对象。然后通过反射调用AssetManager的 addAssetPath方法把插件的资源目录添加到AssetManager对象中。这样使AssetManager对象能够加载插件资源。然后用AssetManager对象构造一个 Resource对象用来管理插件资源。

3.assets处理

因为构造了插件自己的AssetManager,所以只要在插件Context中的getAssets()直接返回插件自己的AssetManager对象。

4.service

由于插件的动态加载特性,所以插件中无法注册service,如果需要使用service,可以在宿主的manifest中进行注册。插件中实现具体的业务逻辑 。Service启动后,把具体的业务处理跳转到具体插件中处理。

5.broadcast

同样由于插件的动态加载特性,插件中无法使用静态广播。如果需要静态广播,可以在宿主中进行注册,把具体的业务逻辑放入插件处理。如果使 用动态广播,插件是可以支持的。

6.database

我们的插件与宿主是在同进程,所以插件是支持database的,但是涉及不同插件之间数据共享时,因为上面我们知道插件之间的隔离性。是不能直 接访问的。这时我们采用的策略是contentProvicer。插件中数据库管理模块,提供contentProvider为其他插件调用。通过contentProvider得到 的都是基础类型。但是有些时候我们需要传递自定义类型,如实体类。因为classLoader采用的是双亲委托模型。自顶向下的尝试加载,与自底向上 检查类是否加载的方式。决定了java对在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的 。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载 ,JVM也会认为它们是两个不同class。比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件 NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.l ang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的 对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。 所以如果在插件A中定义一个实体类传到插件B中,插件B中虽然已经定义了同样的class,但是却无法转换成功。所以这时我们需要把需要使用的实 体类定义在一个公共组件中,同时这个组件应该由宿主去加载。这样在不同插件之间就能相互传递了。但是这引入了一个新问题。就是这个公共组 件的维护问题,因为插件升级造成接口改变,导致公共组件中的实体类对应不上等问题。这些问题我们在插件的版本约束中说明。

7.shareprefence

因为插件的context实现,我们采用包裹宿主centext的方式,所以插件的shareprefence是与宿主共用的。也就是插件可以访问宿主的shareprefenc e,同时插件之间的shareprefence也是共享的。

四、 插件的版本控制

因为宿主和插件之间会相互调用,插件和插件之间也可能相互调用。但插件和插件之间的相互调用应该通过宿主来中转。这样就存在插件的升级及宿主的升级造成相互调用的部分出现问题。因为如果插件或者宿主修改调用部分的逻辑,则就会出现调用失败问题。为了解决这个问题,我们引入插件的版本控制。通过版本约束来达到相互调用可以顺利进行,插件和宿主升级时而不影响相互调用。主体思路,采用相互约束,双重验证思路解决这个问题。

第一重验证:

在宿主中增加一个映射文件。映射的关系是当前宿主可以安装的各个插件的最小版本。这样保证宿主更新一些插件用的公共组件时造 成很早的插件不能使用。这个映射关系应该在宿主进行了某些公共组件的修改。而这些公共组件被插件使用时的情况。这是就应该叠加这张映射关 系中的插件版本。这个映射关系文件可以放在宿主的assets目录中。 映射文件结构参考: { "plugin": [ { }, { }, { } ] "name": "com.plugin.A", "PluginMinVer": "2.1.0" "name": "com.plugin.B", " PluginMinVer ": "1.2.0" "name": "com.plugin.C", " PluginMinVer ": "2.3.0" }

第二重验证:

在每个插件中都增加一个标识该插件应该安装的宿主的最小版本。这样在插件更新一些功能,造成低版本宿主无法使用时,则通过这 个约束来保证低版本宿主不能安装该版本插件。同时,插件与插件之间的约束也间接通过该标识来解决。当插件A修改了一处代码,是插件B需要调 用的时候,这时应该同时升级插件A和插件B中对宿主的依赖版本。同时提升宿主版本。这样保证插件A和插件B之间的相互调调用正常进行。这个标 识的叠加应该发生在插件修改某个为外部调用的接口时发生。 这个标识建议放在插件apk的manifest中,增加一个节点作为标识。这样在取这个标识的时候比较容易获取。 插件的manifest中的映射节点结构参考: 对于版本控制这部分的双重验证非常重要,可以通过覆盖安装情况和安装残留这些特殊情况来验证方案的缜密性。

五、升级策略

升级策略包括插件升级与宿主升级两部分,下面分别就两部分分别说明。 

插件升级

为保证插件升级正常进行,需要对不同插件做唯一标识,保证插件的唯一性。同时插件升级又数据库升级、资源升级、dex升级三个主要方面。

1.插件的唯一标识

可以通过开发规范约束插件的命名规则,可以采用在插件apk的manifest中的包名作为唯一标识。只要约束插件apk的manifest中包名来区别不同插 件,同时获取apk中包名操作上比较容易。

2.数据库升级

如果不升级外部contentProvider接口,则插件的数据库升级直接随插件版本升级即可。如果升级涉及了contentProvicer接口的修改,则插件的版 本叠加的同时还需要叠加插件中对宿主最小版本的描述。

3.资源升级

插件的资源升级如果只是升级插件内部使用的资源,则直接随插件版本升级即可。若升级的资源涉及宿主的公共组件资源,则插件的升级需要提成 宿主中插件版本映射表中,涉及到公共组件资源使用的所有插件。同时要通知其他插件修改公共组件资源的新修改,保证其他插件升级最新的公共 组件资源的调用方式。

4.Dex升级

插件dex升级即插件的逻辑代码部分的升级,如果这部分的修改涉及到了宿主的调用部分,则需要提升插件中对宿主最小版本描述的标识。同时需要 宿主发布新版本。

宿主升级

宿主升级如果不涉及公共组件则直接升级宿主即可。如果宿主升级了公共组件,同时公共组件的修改涉及到了插件的接口,则需要提升宿主中对各 个插件描述的映射文件中影响的插件版本。

6.服务端处理

插件上传服务端,需要对插件根据唯一标识分别存储,同时要存储各个插件对应的版本号以及能够安装的宿主最小版本号。同时需要提供两个接口。

a)升级接口 根据客户端传入的客户端版本号,返回客户端可用的最高版本插件。返回值应该根据服务端存储的插件能够安装宿主的最小版本计算出来。

b)插件上传接口 插件上传接口,提供插件上传web页、权限验证等。同时能够根据上传的插件,解析出插件中的宿主最小版本和插件的唯一标识。

7.客户端处理

客户端应该通过上报自己目前的版本号给服务端,服务端返回可用插件的最新版本,客户端用获取的最新版本与当前本地已经安装的插件版本进行 比较。如果服务端返回版本比较高,则进行下载并安装。下载需要支持断点续传功能。

六、 插件职责划分

1.开发者职责

a) 插件业务开发,调试。

b) 插件中对宿主最小版本描述的维护。

c) 宿主中对各个插件可用的最小版本的维护。

d) 插件打包。

2.上传者职责

负责把插件上传到插件管理后台。

3.测试职责

因为引入插件化,所以测试需要引入更多的测试任务。需要对插件的升级、插件的下载、插件的覆盖安装等进行额外的测试。

插件开发

1.开发约束: 必须使用Fragment方式进行页面控制。插件Apk中仅存在一个用于调试的入口Activity,不应该存在其他Activity。插件的Fragment必须继承自含有 插件管理能力的公共组件包中定义的Fragment。插件之间涉及到公共资源时,需要抽取到公共组件包中并在ResManager中定义对应的静态变量作为 中转。插件中引用公共组件包中的资源必须通过ResManager中的静态变量来引用。 插件中不能注册service、broadcast、Activity。如果需要使用,应该在宿主的manifest中进行注册。

2.升级约束 插件和宿主的改动必须遵循插件的版本管理机制。

3.调试: 在debug模式,增加一个debug目录,用于默认安装的debug插件,方便调试。插件开发中可独立进行开发,当涉及与宿主调试时可把插件放入指定的 debug目录,进行默认安装调试。

About

基于Fragment作为页面加载的插件化框架

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages