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 中的 RePluginRePluginInternalPluginServiceClient 类方法。

四、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 初始化开始。如下图,这里主要是配置 RePluginConfigRePluginCallbacks ,然后根据 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 类,并初始化了它,创建了内部使用的 PmLocalImplPmInternalImp 接口 ,同时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经历了 FactoryPmLocalImpl ,其实大部分启动的逻辑其实主要在 PmInternalImpl 中。

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

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

说了看图
2、loadPluginActivity

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

其中 getActivityInfo 是通过插件名称,获得插件对象 PluginPlugin可能是初始化中已加载的,如果未加载就加载返回,然后根据 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等的就不多做叙述了,不知道本文对你是否能有些帮助,欢迎留言讨论。

最后说“一”句

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

其他推荐

注意到了吗?最后的总是我!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容