Android 全面插件化 RePlugin 流程与源码解析

妖精的绣舞 提交于 2019-12-23 21:32:00

转自

Android 全面插件化 RePlugin 流程与源码解析

RePlugin,360开源的全面插件化框架,按照官网说的,其目的是“尽可能多的让模块变成插件”,并在很稳定的前提下,尽可能像开发普通App那样灵活。那么下面就让我们一起深入♂了解它吧。 (ps :阅读本文请多参考源码图片 ( ̄^ ̄)ゞ )

一、介绍

  RePlugin对比其他插件化,它的强大和特色,在于它只Hook住了ClassLoader。One Hook这个坚持,最大程度保证了稳定性、兼容性和可维护性,详见《全面插件化——RePlugin的使命》。当然,One Hook也极大的提高了实现复杂程度性,其中主要体现在:

  • 增加了Gradle插件脚本,实现开发中自动代码修改与生成。
  • 分割了插件库和宿主库的代码实现。
  • 代码中存在很多不少@deprecatedTODO和临时修改。
  • 初始化、加载、启动等逻辑比较复杂。

 

图一 Replugin项目结构

 

  本篇将竭尽所能,为各位介绍其流程和内部实现,如果存在一些地方存在纰漏,还请指出。文章篇幅较长,需耐心阅读,阅读时可结合图片源码,同时欢迎收藏,或选择感兴趣点阅读,下面主要涉及:

  • 二、ClassLoader基础知识。
  • 三、Replugin项目原理和结构分析。
  • 四、Replugin的ClassLoader。
  • 五、Replugin的相关类介绍。
  • 六、Replugin的初始化。
  • 七、Replugin启动Activity。

 

此处应有图

 

二、ClassLoader基础知识

  既然Replugin选择Hook住ClassLoader,那先简单介绍下ClassLoader的基本知识吧,如熟悉者请略过。

  ClassLoader又叫类加载器,是专门处理类加载,一个APP可以存在多个ClassLoader,它使用的是双亲代理模型,如下图所示,创建一个ClassLoader,需要使用一个已有的ClassLoader对象,作为新建的实例的ParentLoader。

 

抽象基类ClassLoader

 

  这样的条件下,一个App中所有的ClassLoader都联系了起来。当加载类时,如果当前ClassLoader未加载此类,就查询ParentLoader是否加载过,一直往上查找,如果存在就返回,如果都没有,就执行该Loader去执行加载工作。这样避免了类重复加载的浪费。其中常见的Loader有:

  • BootClassLoader 是系统启动时创建的,一般不需要用到。
  • PathClassLoader 是应用启动时创建的,只能加载内部dex。
  • DexClassLoader 可以加载外部的dex。

RePlugin中存在两个主要ClassLoaer:

  • 1、RePluginClassLoader: 宿主App中的Loader,继承PathClassLoader,也是唯一Hook住系统的Loader。

  • 2、PluginDexClassLoader: 加载插件的Loader,继承DexClassLoader。用来做一些“更高级”的特性。

三、Replugin项目原理和结构分析

1、基础原理

  简单来说,其核心是hook住了 ClassLoader,在Activity启动前:

  • 记录下目标页 ActivityA,替换成已自动注册在 AndroidManifest 中的坑位 ActivityNS
  • 在 ClassLoader 中拦截ActivityNS的创建,创建出ActivityA返回。
  • 返回的ActivityA占用着 ActivityNS 这个坑位,坑位由Gradle编译时自动生成在AndroidManifest中。

  在编译时,replugin-replugin-library脚本,会替换代码中的基础类和方法。如下图【官方原理图】所示,替换的基类里会做一些初始化,所以这一块稍微有点入侵性。此外,replugin-host-library会生成AndroidManifest、配置相关信息、打包等,也由Gradle插件自动完成。

  打包独立APK,或者打包为插件,可单可插,这就是RePlugin。

  

官方原理图

 

2、项目结构

  RePlugin整个项目结构,目前分为四个module,其中又分为两个gradle插件module,两个library的java module,详细如开头【图一 Replugin项目结构】,本文主要分析library相关,如果对gradle插件感兴趣的,可以查看结尾其他推荐。

2.1、replugin-host-gradle :

  对应com.qihoo360.replugin:replugin-host-gradle:xxx依赖,主要负责在主程序的编译期中生产各类文件:

  • 根据用户的配置文件,生成HostBuildConfig类,方便插件框架读取并自定义其属性,如:进程数、各类型占位坑的数量、是否使用AppCompat库、Host版本、pulgins-builtin.json文件名、内置插件文件名等。

  • 自动生成带 RePlugin 插件坑位的 AndroidManifest.xml文件,文件中带有如:

    <activity 
      android:theme="@style/Theme.AppCompat" 
      android:name="com.qihoo360.replugin.sample.host.loader.a.ActivityN1STTS0"
      android:exported="false" 
      android:screenOrientation="portrait"
      android:configChanges="keyboard|keyboardHidden|orientation|screenSize" 
    />
2.2、replugin-host-library:

  对应com.qihoo360.replugin:replugin-host-lib:xxx依赖,是一个Java工程,由主程序负责引入,是RePlugin的核心工程,负责初始化、加载、启动、管理插件等。

2.3、replugin-plugin-gradle:

  对应com.qihoo360.replugin:replugin-plugin-gradle:xxx ,是一个Gradle插件,由插件负责引入,主要负责在插件的编译期中:配置插件打包相关信息;动态替换插件工程中的继承基类,如下,修改Activity的继承、Provider的重定向等。

    /* LoaderActivity 替换规则 */
    def private static loaderActivityRules = [
            'android.app.Activity'                    : 'com.qihoo360.replugin.loader.a.PluginActivity',
            'android.app.TabActivity'                 : 'com.qihoo360.replugin.loader.a.PluginTabActivity',
            'android.app.ListActivity'                : 'com.qihoo360.replugin.loader.a.PluginListActivity',
            'android.app.ActivityGroup'               : 'com.qihoo360.replugin.loader.a.PluginActivityGroup',
            'android.support.v4.app.FragmentActivity' : 'com.qihoo360.replugin.loader.a.PluginFragmentActivity',
            'android.support.v7.app.AppCompatActivity': 'com.qihoo360.replugin.loader.a.PluginAppCompatActivity',
            'android.preference.PreferenceActivity'   : 'com.qihoo360.replugin.loader.a.PluginPreferenceActivity',
            'android.app.ExpandableListActivity'      : 'com.qihoo360.replugin.loader.a.PluginExpandableListActivity'
    ]
2.4、replugin-plugin-library:

  对应com.qihoo360.replugin:replugin-plugin-lib:xxx依赖,是一个Java工程,由插件端负责引入,主要提供通过“Java反射”来调用主程序中RePlugin Host Library的相关接口,并提供“双向通信”的能力,以及各种基类Activity等
  
  其中的RePluginRePluginInternalPluginServiceClient都是反射宿主App :replugin-host-library 中的 RePlugin 、 RePluginInternal 、PluginServiceClient 类方法。

四、Replugin的ClassLoader。

  这里主要介绍,宿主和插件使用的ClassLoader,以及它们的创建和Hook住时机。这是RePlugin唯一的Hook点,而其中插件ClassLoader和宿主ClassLoader是相互关系的,如下图。

 

将就的图

 

1、宿主的ClassLoader

  RePluginClassLoader,宿主的ClassLoader,继承 PathClassLoader,构造方法使用原ClassLoader,和原ClassLoader的Parent生成。其中ParentLoader是因为双亲代理模型,创建ClassLoader所需,而原Loader用于保留在后期使用,如下图。

 

 

  如下两图,RePluginClassLoader 在创建时,浅拷贝原Loader的资源到 RePluginClassLoader 中,用于欺骗系统还处于原Loader,并且从原Loader中反射出常用方法,用于重载方法中使用。

 

拷贝资源

 

 

方式方法

 

  宿主Loader中,主要是重载了 loadClass,其中从 PMF(RePlugin中公开接口类)中查找class,如果存在即返回插件class,如果不存在就从原Loader中加载。从而实现了对加载类的拦截。

  这里的 PMF 在加载class时,其实用的是下面【2、插件的ClassLoader 】:PluginDexClassLoader,这个后面流程会讲到。

 

 

2、插件的ClassLoader

  PluginDexClassLoader,继承DexClassLoader,构造时持有了宿主的ClassLoader,从宿主ClassLoader中反射获取loadClass方法,当自己的loadClass方法找不到类时,从宿主Loader中加载。

 

  

 

3、创建和Hook

  创建:上面1、2中两个Loader,是宿主在初始化时创建的,初始化时可以选择配置RePluginCallbacks,callback中提供方法默认创建Loader,你也可以实现自定义的ClassLoader,但是需要继承以上的Loader,如下图。

//初始化方式创建
RePlugin.getConfig().getCallbacks()
.createClassLoader(oClassLoader.getParent(), oClassLoader);

 

RePluginCallbacks

 

  Hook:初始化时,PatchClassLoaderUtils会在Application的attachBaseContext()中,通过patch(application)Hook住宿主的ClassLoader,patch内部如下图。

 

hook ClassLoader

 

五、Replugin的相关类介绍

  提前介绍一些功能类,后面就不做详细介绍。

1、RePlugin :RePlugin的对外入口类,提供install、uninstall、preload、startActivity、fetchPackageInfo、fetchComponentList,fetchClassLoader等等统一的方法入口,用户操作的主要是它。
  
2、RePlugin.App:RePlugin中的内部类,针对Application的入口类,所有针对插件Application的调用应从此类开始和初始化,想象成插件的Application吧。

3、PmBase:RePlugin常用mPluginMgr变量表示,可以看作插件管理者。初始化插件、加载插件等一般都是从它开始。

4、PluginContainers:插件容器管理中心。

5、PmLocalImpl:各种本地接口实现,如startActivity,getActivityInfo,loadPluginActivity等。

6、PmInternalImpl:类似Activity的接口实现,内部实现了真正startActivity的逻辑、还有插件Activity生命周期的接口。
  
-   

 

准备好了吗,骚年

 

六、Replugin的初始化

  那就是从 Application 初始化开始看起,枯燥的流程就要开始了,忍住兄弟,我们能赢。首先我们先看下面这流程图,大致了解启动流程:

 

将就的看吧

 

1、attachBaseContext

  首先是从 Application 的 attachBaseContext 初始化开始。如下图,这里主要是配置 RePluginConfig 和 RePluginCallbacks ,然后根据 Config 去初始化插件。值得注意的是,RePluginConfig 中的 RePluginCallbacks 提供了默认方法创建 RePlugin 的 ClassLoader,还记得上面的介绍吗?

 

看图看图

 

2、插件App.attachBaseContext

  继续上面的流程,进入RePlugin.App.attachBaseContext(this, c),如下图,这里主要是初始化插件相关的进程、配置信息、插件的主框架和接口、根据默认路径、加载默认插件等。插件的初始化从这里开始,其中主要为 PMF.init() 和 PMF.callAttach()

 

继续看图看图

 

3、主程序接口 PMF.init()/PMF.callAttach()

  先进入到 PMF.init() ,如下图,这里主要实例化了 PmBase 类,并初始化了它,创建了内部使用的 PmLocalImpl 和 PmInternalImp 接口 ,同时Hook住主程序的 ClassLoader,替换为 RePluginClassLoader,所以接下来的流程,主要是在 PmBase 。

 

PMF.init(),看图吧

 

  PmBase,按照项目中的变量名 mPluginMgr,可以理解为插件的管理者,它管理内部直接或间接的,管理着坑位分配、ClassLoader、插件、进程、启动\停止页面的接口等,如下图。

 

PmBase创建,还是看图

 

  PmBase 的初始化,也就是插件的初始化,这里会启动各类进程,初始化各种默认插件集合,为后续加载做准备。其中默认插件和配置文件的位置,一般默认是在 assert 的 plugins-builtin.json 和 "plugins" 文件夹下。

 

PmBase.init() 看图看图

 

  接着PMF.callAttach() 其实就是 PmBase.callAttach(),如下图这里开始真正加载插件,初始化插件的 PluginDexClassLoader 、加载插件、初始化插件环境和接口。其中在执行 p.load() 的时候,会通过 Plugind.callAppLocked() 创建插件的 Application,并初始化。

 

PMF.callAttach() 看图呗

 

  以上是在主APP的初始化,深入 PmBase 中,Plugin.load()在加载时,会调用PluginDexClassLoader, 通过类名加载 Entry 类,然后反射出create方法,执行插件的初始化。其中 Entry 位于Plugin-lib库中。这里初始化就去到了插件中了,插件中初始化时,会通过反射的到宿主host类的方法。

 

 

4、Application的onCreate

  这里主要是切换handler到主线程,注册各种广播接收监听,如增加插件、卸载插件、更新插件,可以看出这里设计很多内部进程通信的。

 


   
-      

 

七、Replugin启动Activity

  这里仅描述了Activity启动的其中一个流程,也是简化版的,实际代码逻辑复杂多了,但是万变不离其宗,这里帮你梳理流程,描述一些关键的点,让你快速理解Activity的启动流程。

 

再将就下吧,看图

 

1、startActivity

  从上面的流程图我们知道,启动插件Activity可以从RePlugin.startActivity开始,startActivity经历了 Factory 、 PmLocalImpl ,其实大部分启动的逻辑其实主要在 PmInternalImpl 中。

  具体流程如下图,这里简化了实际代码,关键在于 loadPluginActivity。这里获取了插件对应的坑位,然后保存了目标Activity的信息,通过系统启动坑位。

  因为已经Hook住了ClassLoader,在 loadClass 时再加载出目标Activity,这样坑位中承载的,便是绕过系统打开的目标Activity。下面我们进入 loadPluginActivity

 

说了看图

 

2、loadPluginActivity

  loadPluginActivity 其实是 PmBase 中的 PmLocalImpl 内部方法。如下图,这里主要是根据获取到 ActivityInfo,然后根据坑位去为目标Activity分配坑位。

  其中 getActivityInfo 是通过插件名称,获得插件对象 Plugin, Plugin可能是初始化中已加载的,如果未加载就加载返回,然后根据 Plugin 中缓存的坑位信息,返回 ActivityInfo

  下面进入 allocActivityContainer 看坑位的分配,只有分配到坑位,插件的Activity才可以启动,这是一个IPC过程。

 

看图没?

 

2、allocActivityContainer

  allocActivityContainer 在类 PluginProcessPer 中,还记得我们在 PmBase.init() 时初始化过它么? 分配坑位也是RePlugin的核心之一。

  在 allocActivityContainer 中, 主要逻辑是bindActivity ,如下图,bindActivity 去找到目标Activity匹配的容器,然后加载目标Activity判断是否存在,并建立映射,返回容器。然后分配的逻辑,在 PluginContainers.alloc 中。

 

看我大图

 

3、PluginContainers.alloc

  alloc / alloc2 方法分配坑位,最后都是到了 allocLocked 方法中,其实RePlugin中,如下图,便是坑位分配的逻辑:

  • 如果存在未启动的坑位,就使用它。
  • 如果没有就找最老的:已经被释放的、或者时间最老的。
  • 如果还不行,那么挤掉最老的一个。

 

看图说话

 

4、PulginActivity

  上面的流程总结,是替换目标Activity,加载插件,分配坑位,启动目标坑位,拦截ClassLoader的loadClass去加载返回目标Activity。

  这个时候启动的Activity还不完整,从模块框架中我们知道,在编译时,RePlugin会把继承的Activity替换为如 PluginActivity(当前还有AppComPluginActivity等)。这时候加载启动的目标Activity,其实是继承了 PluginActivity

  如下图, PluginActivity 重载Activity中的一些方法,实现了Activity的补全和自定义操作,如坑位管理,启动宿主Activity等。

 

 

  至此,一个插件Activity就启动起来了,头晕目眩了没?为了实现 One Hook 这个信念,RePlugin 实现了复杂的流程,从代码中可以看出,这些年作者们从中走的的各种坑、各种妥协与坚持、复杂的技术积累、已经经历了多年的严酷考验。

  不知道有多少人能完整看到这,码字不易,如有疏漏还是多多包涵,由于篇(tou)幅(lan)原因,关于Service等的就不多做叙述了,不知道本文对你是否能有些帮助,欢迎留言讨论。

最后说“一”句

  为什么要去了解一个库实现原理呢?学习框架的架构思想?这是一个原因。但是归根结底,是帮助你在使用库的过程中,能靠自己解决各种问题。程序员的日常一般都忙于各种工作,各种技术群中的大佬们,大部分时候,没办法一一解答你的各种咨询,所以使用它、了解它、多尝试靠自己去探索突破吧。

其他推荐

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!