Replugin插件化框架简要分析

题记

写这篇关于Replugin插件化框架的分析,旨在引导读者去快速的了解RePlugin的大概实现原理,文中会抛出需要了解的知识点,并明确的指出大致的流程,指引你去更快速的理解它,避免走过多弯路。因为Replugin的源码中文注释已经够详细了,这里我不贴源码,节省读者的阅读时间,需要具体了解的对照着看源码,想必会更加清晰。同时,想要看具体的Replugin的实现原理详细说明,推荐Replugin 全面解析,讲解的很不错。

插件化的好处

对用户来说:

  1. 一切按需,按需加载,减少内存,存储的消耗。做到小而精。
  2. 随时体验新版,不需要去应用市场更新应用,随时升级更新。
    对开发者来说:
  3. 随时发版
  4. 组织结构灵活,模块之间开发独立性强

Replugin插件化框架的优势

它属于占坑类插件化方案,相比其他插件,它只hook了ClassLoader,最大程度的保证了稳定性,兼容性,和可维护性。它的具体优势参考全面插件化:RePlugin的使命
Replugin只hook ClassLoader,它的原理都是围绕它进行展开。

Replugin项目结构

Replugin有4个相关项目。

  • replugin-host-gradle 作用于宿主项目的脚本项目
  • replugin-host-library 作用于宿主项目的依赖库
  • replugin-plugin-gradle 作用于插件项目的脚本项目
  • replugin-plugin-library 作用于插件项目的依赖库

RePlugin框架的基本原理

其实就是通过占坑替换的方式,欺骗AMS,来完成组件的启动。例如Activity的启动。
1.应用打包时,replugin-host-gradle 这个宿主项目的gradle插件项目会在编译时自动将一些坑位Activity写入注册到AndroidManifest.xml中。
2.我们在启动插件的ActivityA时,Replugin会将它替换成其中的一个坑位Activity,让系统启动坑位Activity。
3.坑位Activity在AMS认证通过后,通知ActivityThread加载和初始化这个坑位Activity,准备开始调用它的生命周期。
4.而在加载的这个过程中,我们的RepluginClassLoader偷偷的转去加载该坑位Activity对应的目标Activity,然后交给ActivityThread继续初始化和调用生命周期。这样AMS以为它启动的是坑位Activity,而ActivityThread这一边实际启动的是插件的目标Activity,这样就完成了加载启动插件Activity的功能了。

ClassLoader相关

Replugin围绕着hook相关的ClassLoader来进行插件化工作,有必要了解ClassLoader相关。
ClassLoader类加载器采用双亲代理模型的加载方式。这里主要注意的是PathClassLoader和DexClassLoader。

  • PathClassLoader 是应用启动时创建的,只能加载内部dex。
  • DexClassLoader 可以加载外部的dex。
    Replugin中存在两个主要的ClassLoader:
  1. RePluginClassLoader: 宿主App中的ClassLoader,继承PathClassLoader,也是唯一Hook住系统的Loader。RePluginClassLoader是在Replugin.attachBaseContext开始初始化的,接着进入PMF.init,然后是PatchClassLoaderUtils.patch,在这里面,获取了宿主APP的Application的Context上下文,然后反射获取Context的mPackageInfo(PackageInfo),接着再反射获取PackageInfo中的mClassLoader(PathClassLoader),通过反射替换这个mClassLoader为RePluginClassLoader,这样,后期的类加载就由它取代负责了。
  2. PluginDexClassLoader: 用于加载插件中类的ClassLoader,继承DexClassLoader。PluginDexClassLoader主要是在Loader.loadDex中进行初始化的,并且在这里填充到创建的PluginContext中,这样插件中的context.getClassLoader获取的就是PluginDexClassLoader了。当加载插件中的一个Activity类时,是由RepluginClassLoader.loadClass进入到PM.loadClass,再进入PmBase.loadClass,这里会加载Plugin,获取Plugin的ClassLoader,也就是PluginDexClassLoader,去加载这个插件Activity类了。

RePlugin的初始化

  1. RePlugin的初始化是从宿主APP的Application的attachBaseContext开始的。这里的Application会在编译时转换成继承自RePluginApplication,所以就进入到RePluginApplication的attachBaseContext。
  2. 然后进入到RePlugin.App.attachBaseContext,进行真正的初始化工作。主要做了初始化IPC进程信息,获取宿主RePluginHostConfig配置信息,调用PMF.init来初始化PmBase和hook系统的ClassLoader为RepluginCLassLoader,调用PMF.callAttach来为所有插件信息填充一些数据,加载默认插件。
  3. 看PMF的init实现,初始化了PmBase,内部会初始化PluginProcessPer,PluginCommImpl,PluginLibraryInternalProxy,然后调用init去做初始化工作,其中分为initForServer和initForClient,如果是常驻进程,走initForServer,如果是非常驻进程,走initClient(正常情况-常驻进程存在的情况下)。
  • initForServer做服务端即persistent的初始化工作,有创建PmHostSvc,PluginProcessMain.installHost(mHostSvc)去缓存自己的IPluginHost,还有Builder.builder扫描出一系列插件信息PluginInfo,保存在PxAll中,然后根据这些插件信息构建出相应的插件对象Plugin缓存起来,更新所有的插件信息。
  • initForClient做Client即UI或者第三方进程的初始化工作,这里通过PluginProcessMain.connectToHostSvc去连接常驻进程,并从常驻进程中获取所有插件信息,并更新缓存起来。
  1. 继续看PMF的callAttach实现,为所有插件Plugin填充一些内容,并加载默认插件,在callAppLocked加载插件时,会创建或获取代表插件Application的PluginApplicationClient,以便主动调用去模拟插件中Application的attach,onCreate生命周期。

RePlugin解析manifest文件

从ManifestParser的parse开始,解析当前插件的AndroidManifest.xml字符串内容,然后到parseManifest,这里采用sax解析方式,传入XmlHandler来处理解析结果,可以得到AndroidManifest.xml中各个节点的的信息。

插件中的Context是什么东东?

插件在编译打包时,会将注册的这些Activity等转换成继承PluginActivity,当系统执行Activity的attachBaseContext时,就会调用插件中的PluginActivity中attachBaseContext,这里会创建PluginContent,并替换了默认的Context。所以插件中所获取的Context都是该插件的PluginContext.当然,包括Application中的Context也是一样的道理。

插件中Activity的实现

  1. 我们知道当调用context.startActivity时,它其实是走的PluginContext.startActivity,然后会调用Factory2.startActivity。
  2. PluginContext.startActivity这个方法会执行两次,第一次是正常外部的调用,这次调用做的操作是找到intent符合对应的坑位Activity,再次执行PluginContext.startActivity方法,这样就进入到了第二次调用,第二次调用,就直接走系统的context.startActivity流程,启动坑位Activity。
  3. 我们主要分析第一次的PluginContext.startActivity,进入Factory2.startActivity,然后是PluginLibraryInternalProxy.startActivity,然后是Factory.startActivityWithNoInjectCN,然后是PluginCommImpl.startActivity,然后是PluginLibraryInternalProxy.startActivity。
  4. 在PluginLibraryInternalProxy.startActivity中,会调用PluginCommImpl.loadPluginActivity去加载插件的Activity,启动插件进程,调用远程接口为其分配坑位Activity,然后调用PluginContext.startActivity进入到第二次的正常启动坑位Activity流程。
  5. 现在坑位Activity就启动起来了。

插件中Activity坑位分配的原理

坑位Activity的分配是在PluginProcessPer.allocActivityContainer中开处理的,接着进入bindActivity,会调用PluginContainers对象mACM的alloc或者alloc2来开始具体的分配。alloc为插件的目标Activity指定在宿主进程时的坑位分配策略,alloc2为插件的目标Activity在自定义进程时的坑位分配策略。

为什么需要坑位Activity来代替目标Activity?

因为插件中的Activity是没有注册到宿主APP的AndroidManifest.xml中的,这样启动这个插件Activity的话,系统是不认识的,会报错。所以为了绕过这个限制,就RePlugin就通过replugin-host-gradle插件预先在APP编译时,将一些坑位Activity写入到宿主APP的AndroidManifest.xml中,这些坑位Activity就作为后面启动插件Activity的代理了。根据Activity不同属性,会内置不同的坑位Activity,以满足不同Activity的需求。

插件中坑位Activity启动后,是如何启动插件中的目标Activity的?

当坑位Activity启动时,ActivityManagerService这边对坑位Activity验证通过并记录了,并且告知ActivityThread这边启动这个Activity实例,这样就会调用ClassLoader去加载这个坑位Activity了,因为我们偷偷替换这个ClassLoader为我们的PluginClassLoader,这样loadClass方法就进入到PluginClassLoader.loadClass了,在里面,调用了PMF.loadClass,接着是PmBase.loadClass,在这里,会从mContainerActivities中查找当前类名是否存在其中,也就是查找这个坑位Activity是否已经记录过了的,如果是,就调用PluginProcessPer.resolveActivityClass去找到该坑位Activity对应的目标Activity,并且加载这个目标Activity,这样,就实现了坑位Activity变换成加载目标Activity的逻辑了。同样,对于Service,ContentProvider也是一样的道理。

插件中的Activity和PluginActivity有什么关系?

总的来说是继承关系,我们在插件中写的Activity,比如ActivityA,它继承自Activity,在编译时,replugin-host-gradle插件会自动将ActivityA替换为继承自PluginActivity,这样ActivityA就具有PluginActivity的功能了,同理继承自AppCompatActivity会转换为继承PluginAppCompatActivity。可以看到PluginActivity内部是交给RePluginInternal来处理的,

插件中Service服务的实现

  1. 我们知道当调用context.startService时,也就会调用PluginContext的startService,接着进入PluginServiceClient的startService。
  2. PluginServiceClient的startService中,将intent中的component设置为插件中的component信息。然后取得IPluginServiceServer接口,这里如果是在persistent进程,则直接通过PluginProcessMain.getPluginHost获取IPluginHost接口(即PmHostSvc),再从IPluginHost获取IPluginServiceServer接口(即PluginServiceServer),如果是非persistent进程,则要先通过MP.startPluginProcess启动插件进程,获取IPluginClient接口(即PluginProcessPer),然后再调用IPluginClient的fetchServiceServer获取IPluginServiceServer接口(也是PluginServiceServer)。
  3. 取得了IPluginServiceServer接口后,调用startService就可以通知通知远端的PluginServiceServer接收了,真实的远端是PluginServiceServer的内部类Stub,继承自IPluginServiceServer.Stub,是binder的服务端,这里就会调用PluginServiceServer的startServiceLocked。
  4. 服务端PluginServiceServer的startServiceLocked会调用installServiceIfNeededLocked,接着进入installServiceLocked,这里就会通过反射加载插件的该Service对象,将pluginContext注入到Service的Context中,并且主动调用onCreate,模拟Service的生命周期。然后再通过系统调用真正的启动一个坑位Service,防止当前的进程容易被杀(因为进程有活动的Service的话,被杀死的概率会低一些)。
  5. 后面在startServiceLocked中会通过Handler发送一个执行OnStart的消息去执行Service的OnStart生命周期回调。
  6. 到这里,插件的Service所在的进程也启动了,插件的Service也加载并反射实例化了,并且也模拟执行了OnCreate,OnStart生命周期,同时对应的坑Service也真正启动了,整个Service也就启动工作了。

插件中自定义进程的启动流程

  1. 自定义进程的启动是从PM的startPluginProcess开始,然后是取得IPluginHost接口,调用得IPluginHost接口的startPluginProcess。
  2. 通过binder,会调用到PmHostSvc(IPluginHost接口的服务端实现)的startPluginProcess,接着进入PmBase的startPluginProcessLocked。
  3. 入PmBase的startPluginProcessLocked中会调用PluginProviderStub.proxyStartPluginProcess启动插件进程,它的实现是找到这个进程对应的ContentProvider,然后构造这个ContentProvider对应的uri,这样就会启动这个ContentProvider,也就会同时启动这个进程了。

插件中广播的实现

广播的注册

  1. 从Loader的regReceivers开始,远程调用IPluginHost接口的regReceiver,经过binder传输之后,调用到Persistent进程中的PmHostSvc的regReceiver(PmHostSvc继承自IPluginHost.Stub,是一个binder对象,实现了IPluginHost接口)。
  2. 的PmHostSvc的regReceiver中,会创建一个PluginReceiverProxy(继承自BroadcastReceiver),它其实就是一个广播,然后遍历参数传来的广播对应的IntentFilter列表的Map,将这些都添加到这个PluginReceiverProxy广播中去,这样这个代理广播就能收到这个插件中所有静态注册的广播的消息了。这里同时还有针对action,来所有插件的广播接收器按action进行分组记录起来,以便后面接收某个action广播时,能迅速找到对应action的所有插件中符合的广播来接收了。

广播的接收

  1. 那广播接收就是由个PluginReceiverProxy这个代理广播来接收了,因为它才是真正注册到系统中的广播,系统只认它,看PluginReceiverProxy的onReceive,先取得action,然后根据action找到符合条件的所有广播接收器(所有插件中的),然后遍历处理。
  2. 遍历中,会取得该广播接收器所在的进程,如果是在persistent进程,则获取远程接口IPluginHost即PluginProcessPer,调用它的onReceive处理,然后交给PluginReceiverHelper的onPluginReceiverReceived处理。这里,会去到当前插件的Context即PluginContext,进而得到该插件的ClassLoader即DexPluginClassLoader,用它去加载当前广播接收器,然后反射实例化这个对象,如果之前有缓存则直接去这个对象,然后就交给这个广播接收器的onReceive处理了,这样插件中的广播接收器就能接收自己想要的广播了。
  3. 遍历中,如果是在非persistent进程,那我就要启动该广播所在的进程,取得该进程对应插件的IPluginClient,因为对于persistent进程来说,其他进程都属于IPluginClient。然后其实也是进入到PluginProcessPer,因为PluginProcessPer就是IPluginClient的实现。这样的话也就跟上面的处理方式一样了。

宿主和插件中广播操作的区别

插件中的PluginLocalBroadcastManager用于插件中广播注册,发送的管理,对应宿主中的LocalBroadcastManager。插件中的注册发送等方法会调用内部的ProxyLocalBroadcastManagerVar的注册发送等方法,也就是反射调用的LocalBroadcastManager对应的方法,实现和宿主一样对广播的管理。

插件中ContentProvider的实现

插件中使用ContentProvider是通过PluginProviderClient操作的,它就相当于我们平时用的ContentResolver,而PluginProviderClient内部其实也是用ContentResolver实现的,这里会做转换,将你传入的uri转换成新的uri,这个新的uri对应了所属的插件信息,也就是会定位到所属插件中定义好了的坑位ContentProvider,这样坑位ContentProvider就能收到请求,然后它还原出原始的uri,然后通过ClassLoader加载目标provider,创建出该实例,然后调用对应的方法。

  1. PluginPitProviderUI,ProcessPitProviderPersist等一些ContentProvider(这些继承PluginPitProviderBase,再往上继承ContentProvier),在编译时被静态注册到宿主APP的AndroidManifest.xml中。
  2. 通过PluginProviderClient(相当于ContentResolver)来访问操作数据,这里会对插件中的uri进行转换,再调用内部的ContentResolver执行增删改查。
  3. 执行了操作之后,系统就会调用到之前注册了的这些坑位ContentProvider了,比如PluginPitProviderUI,它继承自PluginPitProviderBase。
  4. PluginPitProviderBase中的收到uri之后,会还原uri为之前请求的uri,然后通过PluginProviderHelper取得该uri对应的ContentProvider对象。有缓存则取ContentProvider缓存,没有的话,去加载并实例化符合uri条件的ContentProvider对象。
  5. 有了符合uri条件的ContentProvider对象后,在分别调用它的增删改查操作就可以了。

宿主和插件中ContentProvider操作的区别

插件中的PluginLocalBroadcastManager的增删改查操作是通过调用ProxyRePluginProviderClientVar中的对应的MethodInvoker,来反射宿主中的PluginProviderClient对应的增删改成方法,这样插件就能和宿主一样操作了。

插件和宿主中相同的接口是怎么回事?

插件中存在和宿主中相同的接口如Replugin,PluginProviderClient等等,是怎么回事?

答:对比分析宿主中和插件中的Replugin可以发现,两者提供了基本一样的接口,而宿主中的Replugin是真正的实现,插件中的Replugin是通过反射调用宿主中的Replugin来实现对应的方法,这样的话,在插件看来,插件中的方法调用和在宿主中的方法调用好像是一样的。

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

推荐阅读更多精彩内容