美文网首页
插件化笔记

插件化笔记

作者: 卡路fly | 来源:发表于2020-05-17 14:29 被阅读0次

    看这个就够了啊,深入理解Android插件化技术


    插件化技术核心

    类的加载机制和反射机制

    类加载

    https://www.cnblogs.com/muouren/p/11706519.html

    插件化历史

    想要实现插件化,主要是解决下面三个问题:

    1. 插件中代码的加载和与主工程的互相调用
    2. 插件中资源的加载和与主工程的互相访问
    3. 四大组件生命周期的管理

    插件化原理

    1. 类加载

    Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。

    区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件

    而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。我们使用DexClassLoader去加载外部的apk

    1.1 双亲委托机制

    ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。

    1.2 DexPathList

    DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。


    DexPathList的loadClass会去遍历DexFile直到找到需要加载的类。


    (腾讯的qq空间热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。)

    单DexClassLoader与多DexClassLoader

    通过给插件apk生成相应的DexClassLoader便可以访问其中的类分为单DexClassLoader和多DexClassLoader两种结构。

    • 多DexClassLoader (RePlugin)


    对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。

    这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。

    • 单DexClassLoader(Small)


    将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。

    优点:可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。

    2.插件通讯

    插件调用主工程

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

    主工程调用插件

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

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

    3.资源加载

    Android系统通过Resource对象加载资源



    因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。

    具体实现时,由于AssetManager并不是一个public的类,需要通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。

    资源路径的处理

    和代码加载相似,插件和主工程的资源关系也有两种处理方式:

    • 合并式:addAssetPath时加入所有插件和主工程的路径;
    • 独立式:各个插件只添加自己apk路径
    image

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

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

    Context的处理

    通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以VirtualAPK的处理方式为例。

    第一步:创建Resource

    image

    第二步:hook主工程的Resource

    对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码。

    image

    注意下上述代码hook了几个地方,包括以下几个hook点:

    替换了主工程context中LoadedApk的mResource对象。

    将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。

    第三步:关联resource和Activity

    image

    上述代码是在Activity创建时被调用的(后面会介绍如何hook Activity的创建过程),在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。

    做完以上工作后,则可以在插件的Activity中放心的使用setContentView,inflater等方法加载布局了。

    解决资源冲突

    合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。

    资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。如下表所示:

    image

    所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种:

    • 修改aapt源码,编译期修改PP段。
    • 修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。

    四大组件支持

    1. Activity

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


    1. 首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。
    2. ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。
    3. 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方式

    启动Activity主要过程
    1. Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法。

    Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。

    1. 通过跨进程的binder调用,进入ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。
    2. ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。
    3. 主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。

    hook的方式启动插件中的Activity,需要解决以下两个问题:

    1. 插件中的Activity没有在AndroidManifest中注册,如何绕过检测。
    2. 如何构造Activity实例,同步生命周期

    以VirtualAPK为例,核心思路如下:

    1. 先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。
    2. hook第10步,通过插件的ClassLoader反射创建插件Activity
    3. 之后Activity的所有生命周期回调都会通知给插件Activity

    替换系统Instrumentation

    VirtualAPK在初始化时会调用hookInstrumentationAndHandler,该方法hook了系统的Instrumentaiton类,由上文可知该类和Activity的启动息息相关。

    image

    该段代码将主线程中的Instrumentation对象替换成了自定义的VAInstrumentation类。在启动和创建插件activity时,该类都会偷偷做一些手脚。

    hook activity启动过程

    VAInstrumentation类重写了execStartActivity方法,相关代码如下:

    image

    execStartActivity中会先去处理隐式intent,如果该隐式intent匹配到了插件中的Activity,将其转换成显式。之后通过markIntentIfNeeded将待启动的的插件Activity替换成了预先在AndroidManifest中占坑的StubActivity,并将插件Activity的信息保存到该intent中。其中有个dispatchStubActivity函数,会根据Activity的launchMode选择具体启动哪个StubActivity。VirtualAPK为了支持Activity的launchMode在主工程的AndroidManifest中对于每种启动模式的Activity都预埋了多个坑位。

    hook Activity的创建过程

    上一步欺骗了系统,让系统以为自己启动的是一个正常的Activity。当来到图 3.2的第10步时,再将插件的Activity换回来。此时调用的是VAInstrumentation类的newActivity方法。

    image

    由于AndroidManifest中预埋的StubActivity并没有具体的实现类,所以此时会发生ClassNotFoundException。之后在处理异常时取出插件Activity的信息,通过插件的ClassLoader反射构造插件的Activity。

    其他操作

    插件Activity构造出来后,为了能够保证其正常运行还要做些额外的工作。

    image

    这段代码主要是将Activity中的Resource,Context等对象替换成了插件的相应对象,保证插件Activity在调用涉及到Context的方法时能够正确运行。

    经过上述步骤后,便实现了插件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分发。

    相关文章

      网友评论

          本文标题:插件化笔记

          本文链接:https://www.haomeiwen.com/subject/hhajohtx.html