Android 插件化框架经过多年的发展已经出现很多成熟的方案。依然记得自己最早接触的DL框架,在能够让APP不经过安装就可以加载功能新模块,别提有多兴奋。再到之后的360的 DroidPlugin 等等,感谢开发者们无私的奉献,让大家受益匪浅。
接下来会有一个插件化系列的文章主要介绍一下当下一些插件化框架的设计思想和代码逻辑,深入理解插件化开发。
Replugin 框架是360开源的一款插件化框架,其实现思路走的还是占坑的思想,我们知道Android的组件需要在AndroidManifest.xml中注册,占坑的思想就是预先注册坑位,然后用插件中的组件替换坑位注册的组件欺骗系统从而实现组件的加载,这个和360 张勇的DroidPlugin思想是一样的,两者的一大区别就是 Replugin 宣传的唯一 hook点。只 hook了系统的ClassLoader。而DroidPlugin则几乎是完全自己实现了一套Framework层中对Service和Activity加载的方案,替换掉了系统几乎全部相关的Binder
。 这样的话Replugin的系统侵入性更低,维护成本更低,问题更少更加稳定。
Replugin为了实现插件的管理会开启一个常驻进程作为service 端,其他插件进程都是client 端,而service端管理着所有插件的 安装,卸载,更新,状态等等功能,当插件进程client 需要实现(安装,卸载,更新,状态等等)这些功能的时候是通过进程间通信Binder机制实现的。这样的思想和Android 系统的 AMS有点类似。
Replugin中有一个RepluginApplication类直接继承这个类就可以实现框架的注册,也可以在自己Application类中回调的方法中调用相关方法,首先要调用的就是attachBaseContext()方法,要在这个方法中完成框架的初始化。
//com.qihoo360.replugin.RePlugin
public static void attachBaseContext(Application app, RePluginConfig config) {
//初始化一次
if (sAttached) {
return;
}
//这个类主要缓存application对象
RePluginInternal.init(app);
sConfig = config;
//初始化默认值
sConfig.initDefaults(app);
IPC.init(app);
// 初始化HostConfigHelper(通过反射HostConfig来实现)
// NOTE 一定要在IPC类初始化之后才使用
HostConfigHelper.init();
// FIXME 此处需要优化掉
AppVar.sAppContext = app;
//PluginStatusController用来管理插件的运行状态:
//用来管理插件的状态:正常运行、被禁用,等情况
//设置Application的引用
PluginStatusController.setAppContext(app);
PMF.init(app);
PMF.callAttach();
sAttached = true;
}
//com.qihoo360.replugin.RePluginConfig
void initDefaults(Context context) {
if (pnInstallDir == null) {
pnInstallDir = context.getFilesDir();
}
if (callbacks == null) {
callbacks = new RePluginCallbacks(context);
}
if (eventCallbacks == null) {
eventCallbacks = new RePluginEventCallbacks(context);
}
}
1.获取pn插件安装目录
2.创建RePluginCallbacks
3.创建RePluginEventCallbacks
首先RePluginCallbacks 这个类很重要主要用来生成宿主和插件的ClassLoader,是插件化的关键,这个类中还有相关的回调。下图是主要的几个方法。
RePluginEventCallbacks:插件化框架对外事件回调接口集,宿主需继承此类,并复写相应的方法来自定义插件框架的事件处理机制,其中有安装插件成功、失败、启动插件Activity时的一些事件回调等。我们可以重写这些方法然后再相应的回调中做相应的操作,比如插件安装失败弹出提示框等。
//com.qihoo360.replugin.base.IPC;
public static void init(Context context) {
//获取当前进程名称
sCurrentProcess = SysUtils.getCurrentProcessName();
//获取当前进程ID
sCurrentPid = Process.myPid();
//获取当前包名
sPackageName = context.getApplicationInfo().packageName;
// 设置最终的常驻进程名
//------------------------------------------------------------
// RePlugin 坑位默认配置项
// 是否使用“常驻进程”(见PERSISTENT_NAME)作为插件的管理进程
// public static boolean PERSISTENT_ENABLE = true;
// 常驻进程名
// public static String PERSISTENT_NAME = ":GuardService";
//------------------------------------------------------------
//如果设置常驻进程
if (HostConfigHelper.PERSISTENT_ENABLE) {
//cppn : :GuardService
String cppn = HostConfigHelper.PERSISTENT_NAME;
if (!TextUtils.isEmpty(cppn)) {
if (cppn.startsWith(":")) {
//常驻进程名称为 包名:GuardService
sPersistentProcessName = sPackageName + cppn;
} else {
sPersistentProcessName = cppn;
}
}
} else {
//如果不使用常驻进程管理插件,则使用当前app进程名称
sPersistentProcessName = sPackageName;
}
//判断当前进程是否是主进程
sIsUIProcess = sCurrentProcess.equals(sPackageName);
//判断当前线程是不是常驻进程
sIsPersistentProcess = sCurrentProcess.equals(sPersistentProcessName);
}
IPC 的初始化主要是 获取当前进程的名称 ID 宿主的包名,然后设置“插件管理进程”的名称。这里框架默认是开启一个新的进程“常驻进程”作为“插件管理进程”PERSISTENT_ENABLE 的值默认是true。但是这个按钮是可以关闭的。关闭后不会新开一个进程而是以主进程作为插件管理进程。这样就只有一个进程。Replugin 这样灵活的设计都是为了应用考虑。不同的APP需求不一样,我们可以根据APP自己灵活使用,如需切换到以“主进程”作为“插件管理进程”,则需要在宿主的app/build.gradle中添加下列内容,用以设置 persistentEnable 字段为False
apply plugin: 'replugin-host-gradle'
repluginHostConfig {
// ... 其它RePlugin参数
// 设置为“不需要常驻进程”
persistentEnable = false
}
RePlugin默认的“常驻进程”名为“:GuardService”,通常在后台运行,存活时间相对较久。各进程启动时,插件信息的获取速度会更快(因直接通过Binder从常驻进程获取),只要常驻进程不死,其它进程杀掉重启后,仍能快速启动(热启动,而非“冷启动”)但是若应用为“冷启动”(无任何进程时启动),则需要同时拉起“常驻进程”,时间可能有所延长。若应用对“进程”数量比较敏感,则此模式会无形中“多一个进程” 。主进程也可以作为“插件管理进程”无需额外启动任何进程,例如你的应用只有一个进程的话,那采用此模型后,也只有一个进程,应用冷启动(无任何进程时启动)的时间会短一些,因为无需再拉起额外进程。但是“冷启动”的频率会更高,更容易被系统回收,再次启动的速度略慢于“热启动”。
通过上面的介绍我们已经知道了Replugin有插件管理进程这一设计,接下来我们可以看看是如何实现这一设计思想的。也是整个插件最重要的地方PMF.init(app);
// com.qihoo360.loader2.PMF;
public static final void init(Application application) {
setApplicationContext(application);
PluginManager.init(application); // 1
sPluginMgr = new PmBase(application); //2
sPluginMgr.init();
Factory.sPluginManager = PMF.getLocal();
Factory2.sPLProxy = PMF.getInternal();
PatchClassLoaderUtils.patch(application);
}
首先 “1” 部分PluginManager.init(application); 初始化比较简单创建了一个叫Tasks的类,在里面中创建了一个主线程的Hanlder。方便后面执行任务,不必担心Handler为空的情况。
“2 ”部分 创建了Pmbase
// com.qihoo360.loader2.PmBase;
PmBase(Context context) {
mContext = context;
//判断当前进程是UI进程(主进程)或者插件进程
if (PluginManager.sPluginProcessIndex == IPluginManager.PROCESS_UI || PluginManager.isPluginProcess()) {
String suffix;
//如果是主进程 设置suffix = N1;
if (PluginManager.sPluginProcessIndex == IPluginManager.PROCESS_UI) {
suffix = "N1";
} else {
//设值为 是0或1
suffix = "" + PluginManager.sPluginProcessIndex;
}
//CONTAINER_PROVIDER_PART = .loader.p.Provider
mContainerProviders.add(IPC.getPackageName() + CONTAINER_PROVIDER_PART + suffix);
//CONTAINER_SERVICE_PART = ".loader.s.Service"
mContainerServices.add(IPC.getPackageName() + CONTAINER_SERVICE_PART + suffix);
}
mClient = new PluginProcessPer(context, this, PluginManager.sPluginProcessIndex, mContainerActivities);// "1"
mLocal = new PluginCommImpl(context, this);// "2"
mInternal = new PluginLibraryInternalProxy(this);// "3"
}
在PmBase 的构造函数中首先UI进程带有N1标识,其他标识为 0 或 1 。然后拼接service 和 provider 名称 将其存在hashset 中。不同的进程拼接的名称是不一样的。主要就是之前这个标识起到区分作用,然后这个service 和 provider 名字会在编译的时候通过gradle自动添加到我们的AndroidManifest.xml文件中完成注册。
“1” 创建了PluginProcessPer 对象,这个类 extends IPluginClient.Stub 。这是熟悉的AIDL的味道,那么我们知道这个PluginProcessPer是个Binder对象,我们通过这个类来和服务端进行通信。在这个类的构造函数中我们看到初始化了PluginServiceServer,这个类主要负责Server端的服务调度、提供等工作,是服务的提供方,核心类之一。另一个是PluginContainers,用来管理Activity坑位信息的容器,初始化了多种不同启动模式和样式Activity的坑位信息。
“2” 创建PluginCommImpl对象。这个类主要负责宿主与插件、插件间的互通,可通过插件的Factory直接调用,也可通过RePlugin来跳转
“3” 创建PluginLibraryInternalProxy对象。通过“反射”调用的内部逻辑(如PluginActivity类的调用、Factory2等)
这 “1” “2” “3”的内容涉具体的会在之后讲。
在构建好PmBase对象后执行其init();
// com.qihoo360.loader2.PmBase;
void init() {
RePlugin.getConfig().getCallbacks().initPnPluginOverride();
if (HostConfigHelper.PERSISTENT_ENABLE) {
// (默认)“常驻进程”作为插件管理进程,则常驻进程作为Server,其余进程作为Client
if (IPC.isPersistentProcess()) {
//如果当前进程是插件管理进程
// 初始化“Server”所做工作
initForServer();// 1
} else {
// 如果当前进程是client 初始化client
initForClient(); //2
}
} else {
// “UI进程”作为插件管理进程(唯一进程),则UI进程既可以作为Server也可以作为Client
if (IPC.isUIProcess()) {
// 1. 尝试初始化Server所做工作,
initForServer();
// 2. 注册该进程信息到“插件管理进程”中
// 注意:这里无需再做 initForClient,因为不需要再走一次Binder
PMF.sPluginMgr.attach();
} else {
// 其它进程?初始化Client
initForClient();
}
}
// 从mPlugins中将所有插件信息取出,保存到PLUGINS中,PLUGINS是一个HashMap,保存的key是包名或者别名,value是PluginInfo
PluginTable.initPlugins(mPlugins);
}
这里首先判断是否启用了“常驻进程”作为插件管理进程,如果启用了就判断是否是插件管理进程,之前我们介绍过,插件管理进程是服务端,其他进程都是客户端。会根据不同的进程做不同的初始化,如果没用启用“常驻进程”那么UI 进程就是服务端,那么UI进程既可以是server 也可以是client 。这个时候不需要再initForClient() 因为两个逻辑在同一个进程中不需要再一次Binder进行进程间通信。最后把所有的插件信息取出来保存到HashMap中。
"1" initForServer,服务端的初始化
// com.qihoo360.loader2.PmBase;
private final void initForServer() {
// 1 创建PmHostSvc
mHostSvc = new PmHostSvc(mContext, this);
// 2 创建进程管理类 PluginProcessMain
PluginProcessMain.installHost(mHostSvc);
StubProcessManager.schedulePluginProcessLoop(StubProcessManager.CHECK_STAGE1_DELAY);
mAll = new Builder.PxAll();
Builder.builder(mContext, mAll);
//将刚扫描的本地插件封装成Plugin添加进mPlugins中,mPlugins代表所有插件的集合
refreshPluginMap(mAll.getPlugins());
try {
// 调用常驻进程的Server端去加载插件列表,方便之后使用
List l = PluginManagerProxy.load();
if (l != null) {
// 将"纯APK"插件信息并入总的插件信息表中,方便查询
// 这里有可能会覆盖之前在p-n中加入的信息。本来我们就想这么干,以"纯APK"插件为准
refreshPluginMap(l);
}
} catch (RemoteException e) {
}
}
“1”创建PmHostSvc对象。extends IPluginHost.Stub 又是一个binder 对象,在他的构造函数中
// com.qihoo360.loader2.PmHostSvc;
PmHostSvc(Context context, PmBase packm) {
mContext = context;
mPluginMgr = packm;
//创建一个service管理者
mServiceMgr = new PluginServiceServer(context);
//创建一个插件管理者
mManager = new PluginManagerServer(context);
}
“2”创建进程管理类PluginProcessMain 插件管理进程调用,缓存自己的 IPluginHost
// com.qihoo360.loader2.PluginProcessMain;
static final void installHost(IPluginHost host) {
sPluginHostLocal = host;
// 连接到插件化管理器的服务端
try {
PluginManagerProxy.connectToServer(sPluginHostLocal);
} catch (RemoteException e) {
// 基本不太可能到这里,直接打出日志
if (LOGR) {
e.printStackTrace();
}
}
}
//com.qihoo360.replugin.packages.PluginManagerProxy
/**
* 连接到常驻进程,并缓存IPluginManagerServer对象
*
* @param host IPluginHost对象
* @throws RemoteException 和常驻进程通讯出现异常
*/
public static void connectToServer(IPluginHost host) throws RemoteException {
if (sRemote != null) {
return;
}
//host 是IPluginHost 真正调用fetchManagerServer方法的是我们继承IPluginHost.Stub的Binder对象PmHostSvc
sRemote = host.fetchManagerServer();
}
// com.qihoo360.loader2.PmHostSvc;
//PmHostSvc 类中的fetchManagerServer()方法
@Override
public IPluginManagerServer fetchManagerServer() throws RemoteException {
//mManager 是在PmHostSvc构造方法中初始化的PluginManagerServer
return mManager.getService();
}
这里PluginManagetServer.getService()得到的是 private class Stub extends IPluginManagerServer.Stub 这个Stub Binder对象。又是AIDL 那么我们找找aidl文件看看服务端提供了哪些方法。
//com.qihoo360.replugin.packages.IPluginManagerServer.aidl
interface IPluginManagerServer {
/**
* 安装一个插件
*/
PluginInfo install(String path);
/**
* 卸载一个插件
*/
boolean uninstall(in PluginInfo info);
/**
* 加载插件列表,方便之后使用
*/
List load();
/**
* 更新所有插件列表
*/
List updateAll();
/**
* 设置isUsed状态,并通知所有进程更新
*/
void updateUsed(String pluginName, boolean used);
......
}
首先服务端也就是插件管理进程创建了一个Binder对象PmHostSvc 然后PmHostSvc创建PluginManagerServer 插件管理者通过内部的Binder对象Stub(实现IPluginManagerServer接口)提供了插件安装,卸载,更新等操作。因此我们的插件管理进程创建了进程管理类PluginProcessMain缓存了IPluginManagerServer对象(也就是Stub)用来提供插件安装,卸载,更新等服务。在缓存完毕后管理进程开始扫描本地的插件,然后同步所有的插件信息,最后判断是否有插件需要更新或删除,对应的执行一些操作。服务端也就是插件管理进程的初始化就完成了。
另外PmHostSvc 还创建了一个service 管理者PluginServiceServer
然后initForClient ,客户端的初始化
//com.qihoo360.loader2.PmBase
/**
* Client(UI进程)的初始化
*
*/
private final void initForClient() {
// 1. 先尝试连接
PluginProcessMain.connectToHostSvc();
// 2. 然后从常驻进程获取插件列表
refreshPluginsFromHostSvc();
}
/**
* 非常驻进程调用,获取常驻进程的 IPluginHost
*/
static final void connectToHostSvc() {
Context context = PMF.getApplicationContext();
// 1
IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);
。。。。
//通过调用asInterface方法确定是否需要返回远程代理
sPluginHostRemote = IPluginHost.Stub.asInterface(binder);
// 连接到插件化管理器的服务端
try {
PluginManagerProxy.connectToServer(sPluginHostRemote);
// 将当前进程的"正在运行"列表和常驻做同步
// TODO 若常驻进程重启,则应在启动时发送广播,各存活着的进程调用该方法来同步
PluginManagerProxy.syncRunningPlugins();
} catch (RemoteException e) {
// 获取PluginManagerServer时出现问题,可能常驻进程突然挂掉等,当前进程自杀
System.exit(1);
}
// 注册该进程信息到“插件管理进程”中
PMF.sPluginMgr.attach();
}
"1" PluginProviderStub.proxyFetchHostBinder(context);
//com.qihoo360.loader2.PluginProviderStub
private static final IBinder proxyFetchHostBinder(Context context, String selection) {
//
Cursor cursor = null;
try {
//uri 经过拼接后: content://包名.loader.p.main/main
Uri uri = ProcessPitProviderPersist.URI;
cursor = context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
if (cursor == null) {
return null;
}
...
//通过cursor得到Binder对象
IBinder binder = BinderCursor.getBinder(cursor);
return binder;
} finally {
CloseableUtils.closeQuietly(cursor);
}
}
这里我们看到IBinder 对象是通过getBinder(cursor)获取到的,而cursor 是通过provider query获得的,进入代码我们看到cursor是BinderCursor.queryBinder(PMF.sPluginMgr.getHostBinder()),里面传入的参数是PMF.sPluginMgr.getHostBinder(),sPluginMgr是最开始初始化的PmBase 。而getHostBinder 返回的对象就是之前在initForServer()中创建的mHostSvc = new PmHostSvc(mContext, this);所以从cursor中取出的Binder就是PmHostSvc。
另外我们在使用AIDL的时候回创建一个Service 客户端通过bindService 来获取IBinder, 但是通过bindServive 是一个异步的过程。我们必须等到ServiceConnection
拿到 onServiceConnected
回调。这个过程是异步的,这样的话,必须等待才能执行之后的连接服务端,和插件管理进程进行信息同步。为了规避这个问题。采用ProviderContent 直接得到PmHostSvc就更加合适。
得到PmHostSvc通过调用asInterface方法确定是否需要返回远程代理。然后连接服务端,把Client的信息和插件管理进程进行信息同步。然后把client进程注册到插件管理进程中。initForClient()工作就完成了。在上面各进程操作完后会遍历mPlugins中的所有插件信息,并将他们存入到一个叫PluginTable类中一个叫PLUGINS的HashMap中,存入的key是包名或者别名,value是PluginInfo。
到这里我们讲完了PMF.init(app) ,中 PmBase.init(); 在PMF.init(app) 中还有两行代码,我们新开一章继续看。