前言
Replugin 已经开源一个月了,最近几天终于抽出时间来研究研究,这里将我的一些心得体会写下来,分享给大家,希望能帮助后来者少走弯路。关于 Replugin 的基本介绍及起优缺点网上已经有一些不错的文章,大家可以搜索一下,很容易就能找到。这篇文章的主要目标是介绍 Replugin 的一些核心概念以及一些核心流程,让大家了解 Replugin 的运作原理。这其中包括 Host 的启动流程,插件的加载和启动流程,坑位的原理等。开发团队利用了一些非常巧妙的方法使得整个框架在只有一个 Hook 点的情况下支持 android 原生的大部分特性,不得不说这一点很厉害,无论系统如何升级,国内厂商如何定制系统,都不会影响这个框架的运行,除非他们连 ClassLoader 都能干掉。当然在阅读源码的过程中,也发现整个代码质量还有提高和优化的空间,另外有一些小设计上有点复杂,如果开发团队有时间能重构优化一下就好了。当然,瑕不掩瑜,这个框架值得大家学习和借鉴!!
阅读提示:
- 这个系列一共有5篇文章,对核心原理和四大组件分别进行讲解
- 文章中的代码都是从 Replugin 源码中搬过来的,但省略了一些部分以便于讲解,代码中的注释大部分是作者本人所加,便于理解代码,也能缩减讲解的篇幅,在阅读时请不要忽略注释。
- 由于代码分支较多,为了方便讲解,我在一些注释中标注了A,B,C等用于标记分支代码
- 要完全了解Replugin的一些源码,你需要能够理解Binder通信机制的原理,android中ClassLoader的原理,以及对四大组件的启动流程有所了解。
目录
- 核心概念
- Hook点
- UI进程,Persistent进程
- 坑位
- Host启动流程
- UI进程启动流程
- Persistent进程启动
核心概念
-
唯一Hook点:
RepluginClassLoader
在应用启动的时候,Replugin使用
RepluginClassLoader
将系统的PathClassLoader
替换掉,并且只篡改了loadClass
方法的行为,用于加载插件的类,后面我们会详细讲解。每一个插件都会有一个PluginDexClassLoader,RepluginClassLoader
会调用插件的PluginDexClassLoader
来加载插件中的类与资源。protected Class> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class> c = null; c = PMF.loadClass(className, resolve); //主力这里就是Hook点,先使用插件的 if (c != null) { //PluginDexClassLoader加载 return c; } //只有在插件没有找到相应的类,才使用系统原来的PathClassLoader加载宿主中的类 try { c = mOrig.loadClass(className); return c; } catch (Throwable e) { } return super.loadClass(className, resolve); }
-
UI进程,Persistent进程
Replugin启动时会默认启动两个进程,一个是UI进程,一个是Persistent进程(常驻进程),在
IPluginManager
接口中定义了两个常量PROCESS_UI
和PROCESS_PERSIST
来表示这两个进程。public interface IPluginManager { int PROCESS_UI = -1; //UI进程 int PROCESS_PERSIST = -2; //Persistent进程 }
UI进程很好理解,就是程序的主进程。
Persistent进程是一个服务器进程,默认用
:GuardService
来标示,它是Replugin的核心之一。所有其他的进程在启动组件的时候都会通过PmHostSvc
与这个进程通信,以下是Persistent进程中运行的两个重要服务:-
PluginManagerServer
用于插件的管理,比如加载插件,更新插件信息,签名验证,版本检查,插件卸载等 -
PluginServiceServer
用于Service
的启动调度等工作
-
-
坑位
坑位是Replugin中设计非常巧妙的一个概念,它的功能是与
RepluginClassLoader
配合才能实现的。所谓坑位就是预先在Host的Manifest中注册的一些组件(Activity, Service, Content Provider,唯独没有Broadcast Receiver),叫做坑位。这些坑位组件的代码都是由gradle插件在编译时生成的,他们实际上并不会被用到。在启动插件的组件时,会用这些坑位去替代要启动的组件,并且会建立一个坑位与真实组件之间的对应关系(用ActivityState
表示),然后在加载类的时候RepluginClassLoader
会根据前文提到的被篡改过的行为偷偷使用插件的PluginDexClassLoader
加载要启动的真实组件类,骗过了系统,这就是唯一hook点的作用。
Host启动流程
Host在启动的时候会先进行UI进程的初始化工作,但在进行到中途的时候会巧妙的将Persistent进程启动起来,以提供服务,不然UI进程将无法正常启动起来,因为有很多东西时运行在Persistent进程的。
-
UI进程启动流程
-
入口位置
RePluginApplication.attachBaseContext
,紧接着调用Replugin.App.attachBaseContext
请注意,下面的代码中有一个注释中标注来“分支A",这个分支会在后面讲到!!!
public static void attachBaseContext(Application app, RePluginConfig config) { ...... RePluginInternal.init(app); sConfig = config; sConfig.initDefaults(app); IPC.init(app); //初始化进程信息,判断当前进程是UI进程还是Persistent进程 ...... PMF.init(app); //初始化当前进程 PMF.callAttach(); //分支A: 将插件与当前进程关联,如果是在单独的进程中运行插件,则会加载并运行插件 sAttached = true; }
-
来看
PMF.init(app)
,这个函数会做两件事情,初始化PmBase以及Hook系统的PathClassLoader。public static final void init(Application application) { setApplicationContext(application); PluginManager.init(application); sPluginMgr = new PmBase(application); sPluginMgr.init(); ...... PatchClassLoaderUtils.patch(application); //Hook系统Loader,这里是系统唯一Hook点 }
-
PmBase.int()
函数在UI进程和Persistent进程中会运行不同的分支,我们这里来看UI进程相关的部分。请注意注释中分支B的存在,后面会见讲到!
void init() { ...... PluginProcessMain.installHost(); // 连接到Persistent进程 initForClient(); //分支B: 初始化UI进程,主要是更新一些插件相关信息 ...... }
-
PluginProcessMain.installHost
首先获取与Persistent进程通信的IBinder对象,然后连接到Persistent进程中的IPluginManagerServer
服务对象(其实就是获取到Binder通信机制中作为客户端的代理对象),到这里运行Replugin的基础设施就已经准备好了。static final void installHost() { Context context = PMF.getApplicationContext() //获取与Persistent进程通信的IBinder对象 IBinder binder = PluginProviderStub.proxyFetchHostBinder(context); ...... sPluginHostRemote = IPluginHost.Stub.asInterface(binder); //连接到插件化管理器的服务端 PluginManagerProxy.connectToServer(sPluginHostRemote); ...... }
-
在上一步中,有一个重点没有讲到,那就是获取IBinder对象这一步
PluginProviderStub.proxyFetchHostBinder
private static final IBinder proxyFetchHostBinder(Context context, String selection) { Cursor cursor = null; try { Uri uri = ProcessPitProviderPersist.URI; cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null); // 访问ProcessPitProviderPersist IBinder binder = BinderCursor.getBinder(cursor); return binder; } finally { CloseableUtils.closeQuietly(cursor); } }
当前进程尝试通过
ContentResolver
去访问ProcessPitProviderPersist
以获取一个与Persistent进程通信的IBinder对象,但是ProcessPitProviderPersist
在第一次被访问时并没有运行起来,于是Android系统会自动启动它。但是请看ProcessPitProviderPersist
在Manifest中的注册代码:注意,
android:process=":GuardService"
表示ProcessPitProviderPersist
会被运行在另外一个叫做GuardService
的进程中,于是Android系统立即通过ActivityManagerService向Zygote进程请求folk一个新的进程,ProcessPitProviderPersist就运行在这个进程中,这个进程就是Persistent进程了。有三点你需要知道:
第一,默认情况下,GuardService会被当作Persistent进程的名字,在
IPC.init()
函数中会用这个名字来判断当前进程是不是Persistent进程。第二,有很多坑位组件使用
android:process=":GuardService"
属性,因此如果Persistent进程不小心被杀掉了,在任何需要启动这些坑位组件的地方都会将Persistent进程重新启动起来。第三,系统在启动新进程的时候,会在新进程中执行
RepluginApplication
的初始化,所以以上提到的流程都会在这个进程中执行一遍,但是因为在PmBase.init()
函数中有一个条件判断IPC.isPersistentProcess()
,Persistent进程会执行和UI进程不同的代码路径。
-
上面我们顺着一条线走通了,接着我们来看看在前面的代码中标记过的代码分支A和B
-
分支B,
PmBase.initForClient()
会通过远程调用向服务端PmHostSvc获取所有插件的信息,这些信息是在Persistent进程的启动流程(后面会讲到)中被加载的,接着会判断是否有更新,如果有插件已经更新了,会通过远程调用让PluginManagerServer
重新加载插件。private final void initForClient() { List
plugins = null; try { plugins = PluginProcessMain.getPluginHost().listPlugins(); // 获取插件 } catch (Throwable e) { } List updatedPlugins = null; if (isNeedToUpdate(plugins)) { try { updatedPlugins = PluginManagerProxy.updateAllPlugins(); // 更新插件 } catch (RemoteException e) { e.printStackTrace(); } } } -
分支A,
PMF.callAttach()
其实就是调用PmBase.callAttach()
,首先将插件与当前进程关联起来,主要是将RepluginClassLoader
和PluginCommImpl
赋值给插件,它们会在插件真正加载运行时被用到。 如果插件启动了自己的进程来运行,那么在插件的进程中会真正的去运行插件,插件运行过程在本文的后面部分会详细讲解。final void callAttach() { mClassLoader = PmBase.class.getClassLoader(); // 获取RepluginClassLoader for (Plugin p : mPlugins.values()) { p.attach(mContext, mClassLoader, mLocal); // 将分支B中获取的插件与当前进程关联 } if (PluginManager.isPluginProcess()) { //如果插件启动了自己单独的进程,就会启动插件 if (!TextUtils.isEmpty(mDefaultPluginName)) { Plugin p = mPlugins.get(mDefaultPluginName); if (p != null) { boolean rc = p.load(Plugin.LOAD_APP, true); if (rc) { mDefaultPlugin = p; mClient.init(p); } } } } }
以上是UI进程启动中的一些重要流程,接着我们来看看Persistent进程启动流程中的一些要点。
-
Persistent进程启动
-
Persitent进程的启动流程前面几个步骤跟UI进程是一样的,这里就不重复,我们开始从不同的地方讲起。还记得上面提高过的
PmBase.init()
函数里面的IPC.isPersistentProcess()
判断吗?在Persistent进程里这个判断返回true,于是Pmbase.init()将执行以下的分支代码:void init() { mHostSvc = new PmHostSvc(mContext, this); //前面提高过的PmHostSvc终于出现啦!!! PluginProcessMain.installHost(mHostSvc); initForPersistent(); }
在Persistent进程中也会通过
PluginProcessMain.installHost(mHostSvc)
连接到IPluginManagerServer
,但因为IPluginManagerServer
就运行在当前进程,因此这里不会进行Binder通信,而是直接调用PmHostSvc端fetchManagerServer
方法。-
initForPersistent
会加载加载插件并保存起来,这样所有作为客户端的进程才能获取到插件信息。private final void initForPersistent() { //这三行识为了兼容儿存在,以后会被废弃掉,所以不用太关注 mAll = new Builder.PxAll(); Builder.builder(mContext, mAll); refreshPluginMap(mAll.getPlugins()); try { List
l = PluginManagerProxy.load(); // 加载插件 if (l != null) { refreshPluginMap(l); // 将获取到的插件信息保存在 PmBase.mPlugins中 } } catch (RemoteException e) { } } -
顺着
PluginManagerProxy.load()
跟踪下去,最后真正做加载工作的是PluginInfoList.load()
函数。Constant.LOCAL_PLUGIN_APK_SUB_DIR
就是插件安装以后的存放目录。public boolean load(Context context) { try { File d = context.getDir(Constant.LOCAL_PLUGIN_APK_SUB_DIR, 0); File f = new File(d, "p.l"); ...... // 从配置文件p.l中读取插件信息,插件信息以JSON格式保存在这个文件中 String result = FileUtils.readFileToString(f, Charsets.UTF_8); //读出字符串 ...... mJson = new JSONArray(result); //解析出JSON } catch (IOException e) { return false; } catch (JSONException e) { return false; } for (int i = 0; i < mJson.length(); i++) { JSONObject jo = mJson.optJSONObject(i); if (jo != null) { PluginInfo pi = PluginInfo.createByJO(jo); //创建PluginInfo对象 if (pi == null) { continue; } addToMap(pi); //保存插件信息 } } return true; }
-
小结
RepluginClassLoader
和坑位机制是 Replugin 最重要的两个基本概念,对四大组件的支持基本都是在此基础上架构起来的!
另外Replugin中的进程关系也有一些复杂,在后面的文章中会详细讲解。
下一篇Replugin 全面解析(2) 会讲解插件Activity加载和启动流程!