转自

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等的就不多做叙述了,不知道本文对你是否能有些帮助,欢迎留言讨论。

最后说“一”句

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

其他推荐

Android 全面插件化 RePlugin 流程与源码解析的更多相关文章

  1. Android View体系(六)从源码解析Activity的构成

    前言 本来这篇是要讲View的工作流程的,View的工作流程主要指的measure.layout.draw这三大流程,在讲到这三大流程之前我们有必要要先了解下Activity的构成,所以就有了这篇文章 ...

  2. Android 进阶16:IntentService 使用及源码解析

    It's time to start living the life you've only imagined. 读完本文你将了解: IntentService 简介 IntentService 源码 ...

  3. Spark Streaming运行流程及源码解析(一)

    本系列主要描述Spark Streaming的运行流程,然后对每个流程的源码分别进行解析 之前总听同事说Spark源码有多么棒,咱也不知道,就是疯狂点头.今天也来撸一下Spark源码. 对Spark的 ...

  4. Android View体系(七)从源码解析View的measure流程

    前言 在上一篇我们了解了Activity的构成后,开始了解一下View的工作流程,就是measure.layout和draw.measure用来测量View的宽高,layout用来确定View的位置, ...

  5. Android View体系(八)从源码解析View的layout和draw流程

    前言 上一篇文章我们讲了View的measure的流程,接下来我们讲下View的layout和draw流程,如果你理解了View的measure的流程,那这篇文章自然就不在话下了. 1.View的la ...

  6. HDFS追本溯源:HDFS操作的逻辑流程与源码解析

    本文主要介绍5个典型的HDFS流程,这些流程充分体现了HDFS实体间IPC接口和stream接口之间的配合. 1. Client和NN Client到NN有大量的元数据操作,比如修改文件名,在给定目录 ...

  7. Android View体系(四)从源码解析Scroller

    在Android View体系(二)实现View滑动的六种方法这篇文章中我们讲到了用Scroller来实现View的滑动,所以这篇文章我们就不介绍Scroller是如何使用的了,本篇就从源码来分析下S ...

  8. Android View体系(五)从源码解析View的事件分发机制

    1.处理点击事件的方法 View的层级 我们知道View的结构是树形的结构,View可以放在ViewGroup中,这个ViewGroup也可以放到另一个ViewGroup中,这样层层的嵌套就组成了Vi ...

  9. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

随机推荐

  1. 通过调用API在JavaWeb项目中实现证件识别

    本文详细介绍自己如何在JavaWeb项目中通过调用API实现证件识别. 一,Face++使用简介 二,两种方式(图片URL与本地上传)实现证件识别 一,Face++使用简介 Face++旷视人工智能开 ...

  2. 使用CodeBlocks编译64位程序(用的编译器仅仅是windows sdk的)

    需求: -CodeBlocks使用nightly版本: -Windows SDK(我使用的是6.0A,即微软针对vista的,因为这个比较小,你也可以选择其他版本但是要有64位编译器.他也适用于xps ...

  3. 在阿里云上遇见更好的Oracle(二)

    从上一篇文章的反馈来看,大家还是喜欢八卦多过技术细节,那这一篇继续一些题外话,说说我对“去IOE”的看法. 对同一件事情,参与的没参与的人,讨论起来,都会有各自的立场.所以这里先申明一下,以下内容只是 ...

  4. node gyp的问题

    解决 binding.gyp not found (xxx/xxx/xxx) while trying to load binding.gyp 问题 在使用ccap图形验证码模块时遇到这个问题 Err ...

  5. shell功能

    日志切割: function rotate() { logs_path=$ echo Rotating Log: $ cp ${logs_path} ${logs_path}.$(date -d &q ...

  6. 前端JQuery中获取一个div下的多个id值

    获取所有的Id值,方法是通过div.class获取全局的值,然后再提取具体的Id值 方法一:用for循环,因为$("div.class")获取的是一个数组,通过循环读取出数组中的每 ...

  7. nopcommerce商城系统--开发者常遇问题清单

    原址:http://www.nopcommerce.com/docs/74/frequently-asked-development-questions.aspx 以下是开发者常见问题的清单.也介绍了 ...

  8. Spring温故而知新 – bean的装配

    Spring装配机制 Spring提供了三种主要的装配机制: 1:通过XML进行显示配置 2:通过Java代码显示配置 3:自动化装配 自动化装配 Spring中IOC容器分两个步骤来完成自动化装配: ...

  9. 延迟加载(Lazyload)三种实现方式

    定义:延迟加载也称为惰性加载,即在长网页中延迟加载图像.用户滚动到它们之前,视口外的图像不会加载.这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快.在某些情况下,它还可以帮助减少服务器负载. ...

  10. asp.net中的cookie

    一.cookie导读,理解什么是cookie 1.什么是cookie:cookie是一种能够让网站服务器把少量数据(4kb左右)存储到客户端的硬盘或内存.并且读可以取出来的一种技术. 2.当你浏览某网 ...