安卓动态化实践概览---插件化

前言:热修复框架应该也属于业务插件化的一种类型

一、插件化的实现原理

1、加载class,通过DexClassLoader,分为单DexClassLoader与多DexClassLoader,

对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题,RePlugin采用的就是此方案。

将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。

插件调用主工程

在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。

主工程调用插件

若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。

若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。

2、资源路径加载

  合并式:addAssetPath时加入所有插件和主工程的路径;

  独立式:各个插件只添加自己apk路径

合并式由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。

独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。

3、Context的处理

替换了主工程context中LoadedApk的mResource对象。将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。

4、资源冲突

合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。如下表所示:所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种:

修改aapt源码,编译期修改PP段。

修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。

5、四大组件的支持

具体实现有以下两种:

ProxyActivity代理

预埋StubActivity,hook系统启动Activity的过程

ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。

具体的过程如下:

 

        首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。

        ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。

        PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。

        ProxyActivity中的其他生命周期回调函数中调用相应PluginActivity的生命周期。

理解ProxyActivity代理方式主要注意两点:

        ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。

        PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。

缺点

        插件中的Activity必须继承PluginActivity,开发侵入性强。

        如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。

        插件中需要小心处理Context,容易出错。

        如果想把之前的模块改造成插件需要很多额外的工作。

该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,dynamic-load-apk之后的插件化方案很少继续使用该方式,而是通过hook系统启动Activity的过程,让启动插件中的Activity像启动主工程的Activity一样简单。

hook方式

在介绍hook方式之前,先用一张图简要的介绍下系统是如何启动一个Activity的

 

上图列出的是启动一个Activity的主要过程,具体步骤如下:

        Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法,Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。

        通过跨进程的binder调用,进入到ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。

        ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。

        主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。

下面介绍如何通过hook的方式启动插件中的Activity,需要解决以下两个问题:

        插件中的Activity没有在AndroidManifest中注册,如何绕过检测。

        如何构造Activity实例,同步生命周期

解决方法有很多种,以VirtualAPK为例,核心思路如下:

        先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。

        hook第10步,通过插件的ClassLoader反射创建插件Activity

        之后Activity的所有生命周期回调都会通知给插件Activity

经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。

其他组件

四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:

Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。

BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。

ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。

二、热更新的实现原理

1、基于multidex的热更新框架,包括Nuwa、Tinker等

需要反射更改DexElements,改变Dex的加载顺序,这使得patch需要在下次启动时才能生效,实时性就受到了影响

2、另一类就是native hook方案,如阿里开源的Andfix和Dexposed

native替换,通过PLT Hook等技术,改变方法的地址,

不支持新增方法,新增类,新增field等

3、插入代码美团robust,编译打包阶段自动为每个class都增加了一个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。

  • 代码是侵入式的,会在原有的类中加入相关代码
  • so和资源的替换暂时不支持
  • 会增大apk的体积,平均一个函数会比原来增加17.47个字节,10万个函数会增加1.67M。
  • 会增加少量方法数,使用了Robust插件后,原来能被ProGuard内联的函数不能被内联了

三、优缺点:

优点:支持动态下发,缩小安卓包的大小

缺点:笨重的插件化框架不仅影响应用的启动速度,而且多团队协作的时候并没有想象得那么和谐,接口混乱、仓库不好管理、编译速度慢,androidP私有 API 限制的出现对热修复和插件化打击很大,但是Android Q增加了替换 Classloader 的接口 instantiateClassloader。在 Android Q 以后,我们可以实现在运行时替换已经存在 ClassLoader 和四大组件。Android Q 之后,动态加载的 Dex 都只使用解释模式执行,会加剧对启动性能的影响。因为性能的问题,目前大公司基本暂停了全量用户的热修复,只使用热修复用于灰度和测试。

三、热修复和插件化的未来

1)热修复

1、Instant Run:采用的通过classLoader,

1.取更改后资源resource.ap_的路径
2.设置ClassLoader。setupClassLoader:
3.使用IncrementalClassLoader加载apk的代码,将原有的BootClassLoader → PathClassLoader改为BootClassLoader → IncrementalClassLoader → PathClassLoader继承关系。
createRealApplication:
创建apk真实的application
4.monkeyPatchApplication
反射替换ActivityThread中的各种Application成员变量
5.monkeyPatchExistingResource
反射替换所有存在的AssetManager对象
6.调用realApplication的onCreate方法
7.启动Server,Socket接收patch列表

2、Apply Changes:采用的JVM TI

对这个「Apply Changes」来说,比较重要的应该是 ClassTransform 和 ClassRedefine;它允许虚拟机在运行时动态修改类(Redefine只在9.0上实现了)。比如说 Activity 这个 class,你可以通过此接口在字节码层面往里面直接添加方法/修改方法,然后虚拟机会为你重新加载这个类,之后这个被改过的类就是原来那个货真价值的 Activity 类。所以,这个技术跟 Instant Run/Robust 编译期字节码编织 / ClassLoader 替换 / AndFix 方法替换那种动态修改完全不是一个层面的东西,这是 运行时动态字节码编织

2)插件化

1、Android App Bundles:不支持四大组件代理,依赖 Play Service,我们也可以实现插件化的四大组件代理。但是具体实现上依然需要在 AndroidManifest 中预先注册四大组件,然后具体的替换规则可以在我们自定义的 AppComponentFactory 实现类中埋好

来源:

美团Android热更新方案Robusthttps://tech.meituan.com/2016/09/14/android-robust.html

美团美团App 插件化实践https://tech.meituan.com/2017/10/12/android-hydra.html

美团热更新插桩:http://www.easemob.com/news/729

微信热修复的变迁:https://mp.weixin.qq.com/s/2xBnlmESZjq7UTtcfzqhcA

tinker热修复之路:https://mp.weixin.qq.com/s/tlDy6kx8qVHQOZjNpG514w

腾讯bugly插件化原理::https://www.jianshu.com/p/0a2501328e0e

深度理解Android InstantRun原理以及源码分析:https://www.baidu.com/link?url=9NzevROjgkaKaWK6CWNXDAgqFtcUe4LGdeNZ-CNvvwz67VW8AUJigh8jtRSqt0hm7zn8dy3__oOOFcBR2lKhDTwLETfsEwjHj2liaoe6q_C&wd=&eqid=f6531d2300073578000000065ebb9ea3

京东:当插件化遇上安卓P:https://myslide.cn/slides/9961

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值