Android插件化浅析(中)

阿凡达2018-08-10 10:21

紧接Android插件化浅析(上),我们继续结合实际插件框架进行分析!

4 插件化框架实例分析

前面介绍了插件化的基础概念、发展历史以及实现的基本原理之后,下面我们将可以一些典型插件框架进行分析讲解。限于篇幅,这里将只对一些有代表性的框架进行分析,依次是DynamicLoadApkDroidPluginAtlasReplugin

在讲解之前,先上一个各个插件框架的基本功能对比图。


以上内容主要来自网上收集的资料和自己的理解(内容如有理解不对的地方,请指正!),各种框架各有自己的优缺点,这里只是简单列出一些基础功能对比,更多详细的可以去GitHub上去查看文档。

4.2 DynamicLoadApk

DynamicLoadApk是一个插件化框架,以静态代理的方式实现。

4.2.1 整体架构


该框架主要由三部分构成:

  • DLPluginManager:插件管理模块,负责插件的加载、管理以及启动
  • Proxy:代理组件模块,在宿主 Manifest 中提前注册,是插件组件启动时首先被启动的组件,内部会完成插件的初始化和启动
  • Base Plugin:插件组件的基类模块,目前支持Activity、FragmentActivity、Service三种

4.2.2 组件启动流程

启动流程分析以Activity的启动为例,后续的框架分析都是如此


  • 首先通过DLPluginManager的loadApk函数加载插件,每个插件只会调用一次
  • 然后通过DLPluginManager的startPluginActivity函数启动代理Activity,并将要启动插件的信息放在Intent中
  • 最后在代理Activity的启动过程中构建、绑定并启动插件Activity

该框架采用了接口机制,将Activity的大部分生命周期方法提取出来作为一个接口(DLPlugin),然后通过代理Activity(如DLProxyActivity)去调用插件Activity实现的生命周期方法,这样就完成了插件Activity的生命周期管理,整个过程没有用到反射。

4.2.3 插件类和资源的加载


为了更好地对多插件进行支持,提供了一个DLClassoader类,专门去管理各个插件的DexClassoader,这样,同一个插件就可以采用同一个ClassLoader去加载类从而避免多个classloader加载同一个类时所引发的类型转换错误。


资源的加载时通过通过反射调用AssetManager的addAssetPath函数,从而将apk中的资源加载到Resources中。

4.2.4 框架的优缺点

特点

  • 支持插件对主程序无调用、接口调用、完全调用3种集成方式。
  • 支持Activity、FragmentActivity、Service组件,并且组件不需要在主程序的AndroidManifest中声明。
  • DexClassLoader加载插件代码,AssetManager管理插件资源。插件之间资源和代码可以做到相互隔离。
  • 自定义Intent和API实现对插件Activity的启动。

不足

  • 强侵入性,组件内很多API需要使用that调用,并需要继承特定的类,启动组件需要调用私有API。
  • 插件的集成方式一旦确定将不能更改。不同的集成方式,编译配置也不相同。
  • 插件之间资源和代码无法共享。启动插件中的组件必须使用包名+类名的方式。

4.3 DroidPlugin

DroidPlugin是360手机助手实现的一种插件化框架,以动态替换的方式实现,可以加载任意Apk。

4.3.1 整体架构


插件的启动,首先依赖于Manifest里面注册的占坑组件和权限,然后通过Hook模块(包括Binder、代理等)获取系统的相关服务,通过解析插件Apk,构建一个插件LoadedApk对象,并将其以反射的方式插入到系统对象缓存中,从而欺骗系统最终完成插件的启动。

在详细介绍该插件框架的组件启动过程之前,我们先回顾一下App进程和系统服务进程中的AMS的通信过程:


图中,有一个ActivityThread对象和ApplicationThreadActivityMangerService两个Binder对象。ActivityThread对象所在线程是Android应用的主线程或者UI线程,主要用来快速处理各种UI事件或者广播消息等等;ApplicationThread对象是App所在的进程与AMS所在进程system_server之间通信的桥梁;ActivityMangerService主要用来完成进程管理、组件生命周期管理等系统服务。

在Activity启动的过程中,App进程会频繁地与AMS进程进行通信:

  • App进程需要Binder机制委托AMS完成Activity生命周期的管理以及任务栈的调度。这个通信过程中AMS是作为Server端,而App进程通过持有AMS的Client端Binder代理来完成通信过程;
  • 在AMS进程完成生命周期管理以及任务栈管理后,会通过Binder机制把控制权转移到App进程中。这个通信过程App进程的ApplicationThread是服务端,而AMS充当客户端,AMS通过持有ApplicationThread的Binder代理来完成通信过程。
  • 最终,在控制权交回到App进程后,ApplicationThread所在Binder线程与App主线程通过Handler机制进行通信,从而完成组件的创建以及生命周期回调。
4.3.2 组件启动流程

接下来我们详细分析一下DroidPlugin框架的组件启动过程。


其主要思路是:启动插件Activity A时进行拦截,先在Manifest中查找一个合适的、已声明的占位Activity B,将A替换成B并将A的信息封装在B的Intent中发往AMS进程。由于Activity B已在Manifest中声明,AMS会校验通过,最终控制权又交回到App进程。在App进程完成组件创建之前进行拦截,将封装在B的Intent中的A的信息取出,从而完成插件A组件的创建和声明周期回调。

欺骗系统,瞒天过海!

其中,图中的LoadedApk对象是Apk文件在系统内存中的表示,包括Apk的代码和资源,甚至代码里面的Activity、Service等组件的信息我们都可以通过此对象获取,后续分析也会用到。

4.3.3 插件类和资源的加载


前面讲到在App进程和AMS通信流程中提到,最后一步AMS会将控制权通过Binder机制转移到ApplicationThread所在的Binder线程,然后通过Handler机制和UI线程通信。接下来的过程我们继续进行更加深入的分析。

如图:

  • 上述过程之后,ActivityThread对象的H类字段mH,调用handleMessage时,会首先调用getPackageInfoNoCheck方法来获取待启动组件的详细信息(即LoadedApk对象)。分析这个方法的源码发现,它会优先查找mPackages(包名到LoadedApk的一个映射Map)字段中的缓存信息,如果没有就会进行创建LoadedApk对象。
  • 然后,我们先构建插件的ApplicationInfo对象和CompatibilityInfo对象(就是getPackageInfoNoCheck方法的参数),再反射调用该方法从而可以获得插件的LoadedApk对象。
  • 之后,就会命中插件的loadedApk对象并返回,然后H类调用handleLaunchActivity方法,最终转发到performLaunchActivity方法,在这个方法中利用getPackageInfoNoCheck获得的LoadedApk对象中的mClassLoader来加载插件Activity类,进而使用反射创建Activity实例;接着创建Application,Context等完成Activity组件的启动。

总之,这里关键之处就是欺骗PMS服务,让它觉得插件Apk已经安装在系统中。骗子!

4.3.4 框架的优缺点

特点

  • 支持四大组件,通过Stub插桩的方式预先静态注册多个不同属性的组件。
  • 插件资源和代码相互隔离,一个插件一个ClassLoader,一个插件一个AssetManager。
  • Hook了几乎所有的通过Context获取到的系统服务(AMS、PMS等)以及和应用进程密切相关的Instrucmentation、ApplicationThread、ActivityThread等相关类。
  • 不同的插件APK运行于不同的进程,互不干扰。插件之间支持aidl进程间通信,并提供进程管理机制。

不足

  • 由于进程隔离的原因,插件和宿主、插件和插件之间资源和代码不能共享。
  • 插件apk中所有的Intent Filter无效。要启动插件中的四大组件,必须要指定包名和类名。
  • RemoteView支持不是太好,不支持自定义资源的Notification。
  • 带有native库的插件支持不是太好,可能存在异常崩溃。
  • Hook了很多系统类,兼容性方面可能会隐患较多。

综上,DroidPlugin比较适合主程序只提供入口,而插件apk对宿主不存在依赖,插件之间无太多通信要求的场景

4.4 Atlas

Atlas是伴随着手机淘宝的不断发展而衍生出来的一个运行于Android系统上的一个容器化框架,我们也叫动态组件化(Dynamic Bundle)框架。它主要提供了解耦化、组件化、动态性的支持。它以动态替换的方式实现,可以动态加载class、so和资源文件。

首先我们看下手机淘宝APK包的目录结构:


这是一个手机淘宝的APK包,第一层目录上与标准的APK是完全一样的,在APP会有很多的so文件,如果解开来看的话,它的结构类似于完整的APK,但本身并不能独立运行(放入lib目录只是为了安装时借用系统的能力从apk中解压出来,方便后续安装),它跟很多插件化的差别是在运行期,它是运行在整个容器里的,每一个组件都是独立的Bundle。

从模块来划分,可以分为两层,上层是经过拆分的业务Bundle,扫码、评价、详情,各个业务之间可以进行功能的调用,可以通过路由调度到其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,会在容器里进行统一地把控,这样做的好处是包做到尽可能小,第二是性能佳。

4.4.1 整体架构


总体架构,从下向上:

  • Hack工具层:包括了容器所需的所有系统层面的注入和hack的工具类初始化和校验,容器启动时先校验设备是否支持容器运行,不支持则采取降级并记录原因。
  • Bundle Framework层:负责bundle的安装 更新 操作以及管理整个bundle的生命周期。
  • Runtime层:主要包括清单管理、版本管理、以及系统代理三大块,基于不同的触发点按需执行bundle的安装和加载;runtime层同时提供了开发期快速debug支持和监控两个功能模块。从Delegate层可以看到,最核心的两个代理点:一个是DelegateClassLoader:负责路由class加载到各个bundle内部,第二个是DelegateResource:负责资源查找时能够找到bundle内的资源;这是bundle能够真正运行起来的根本;其余的代理点均是为了保证在必要的时机按需加载起来目标bundle,让其可以被DelegateClassloader和DelegateResource使用。
  • 对外接入层:AtlasBridgeApplication是atlas框架下Apk的真正Application,在基于Atlas框架构建的过程中会替换原有manifest中的application,通过构建脚本的方式完成了接入过程。

4.4.2 组件启动流程


  • Installed:bundle被安装到storage目录
  • Resolved:classloader被创建,assetpatch注入DelegateResoucces
  • Active:bundle的安全校验通过;bundle的dex检测已经成功dexopt(or dex2oat),resource已经成功注入
  • Started:bundle开始运行,bundle application的onCreate方法被调用

4.4.3 插件类和资源的加载
类的加载


Atlas里面通常会创建了两种classLoader:DelegateClassLoader和BundleClassLoader。

  • DelegateClassLoader:在启动时被atlas注入LoadedApk中,替换原有都PathClassLoader,作为一个路由器而存在,本身并不负责类的加载。在路由过程中能够找到所有bundle的ClassLoader。
  • BundleClassLoader:每个bundle解析时会被分配一个BundleClassLoader,负责该bundle的类加载。BundleClassLoader的类加载逻辑是在Bundle进行查找的时候首先会去查找自己的Bundle里面有没有这个类,如果有的话就不会再去寻找,如果没有就会去找Host,也就是整个宿主类的Classloader里面有没有这个类,如果还没有就会去系统或者其所依赖的关系中去找寻这个类。

从图上可以看出,Atlas只是在Android和Java的基础之上设计了两层Classloader,通过自己定义的Classloader的加载规则来实现对于整个宿主的隔离。

资源的加载


在Android某些系统上面将资源进行隔离时会存在一定的兼容性问题,所以Atlas采用了如下图中右图所示的另外一种做法,把所有的Bundle资源在编译期进行了隔离,类似每个Bundle的资源特征如图:

  • Bundle构建过程中,每个Bundle会被独立进行分区,packageId保证全局唯一,packageID在host的构建工程内会有个packageIdFile.properties进行统一分配。
  • 虽然每个Bundle的manifest都声明了自己的packagename,但是在aapt过程中,arsc文件里面所有Bundle的packagename均被统一为hostApk的package,这样改的目的是为了解决在资源查找中一些兼容性问题。

这里再多说一点,关于资源ID定义:

资源ID(32)=PackageID(8)+TypeID(8)+EntryID(16)

  • PackageID:应用程序默认0x7f,系统默认0x01,开发者可以指定0x02~0x7e
  • TypeID:资源的类型ID,表示一个资源类型在当前包下的index值,值是从1开始的。资源的类型有color、drawable、layout和string等,每一种都会被赋予一个ID
  • EntryID:每一个资源在其所属的资源类型中所出现的次序,默认从1开始,依次累加
4.4.4 框架的优缺点

特点

  • 单进程运行,一个组件化框架,包含host、local bundle和remote bundle。
  • 所有bundle的代码和资源都是动态加载,并且支持按需加载。
  • 自定义两个ClassLoader,一个实现类加载时的路由逻辑,另一个则主要负责加载bundle中的类和其依赖的bundle中的类。
  • 自定义ResourceManager,每个bundle里的资源都加入了同一个AssetManager。
  • 支持差分升级和远程bundle调用。
  • 提供了打包插件,编译期自动合并Manifest文件,自动分配bundle的资源package id,自动生成差分包,并提供单模块调试功能。

不足

  • 项目相关文档较少,主要功能不稳定。
  • 差分升级和远程bundle调用,需要强有力的后台和测试支撑。
  • 差分升级还不支持动态添加四大组件的功能。


相关阅读: Android插件化浅析(下)


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者付光鑫授权发布。