上一篇 唯一插件化Replugin源码及原理深度剖析–唯一Hook点原理
在Replugin的初始化过程中,我将他们分成了比较重要3个模块,整体框架的初始化、hook系统ClassLoader、插件的加载,3个模块已经说了两个,在第一篇的最后是插件的加载,当时没有说,一个是因为篇幅的原因,另一个原因是想从插件的安装、加载、插件apk初始化、整体的流程梳理下来,这里虽然没有分析插件的卸载,但是当看完安装和加载的过程,就自然能明白卸载了。

提示:请不要忽略代码注释,由于通畅上下逻辑思维,不太重要的部分跳转代码不会全部进去一行行的看,但是会将注释写出来,所以请务必不要忽略注释,而且最好是跟着文章一起看源码。

概要:
一、插件的安装,在分析前会先简单介绍系统安装的过程

二、插件的加载及插件apk的初始化,在分析前会简单介绍系统启动应用的过程

等分析完后你会觉得Replugin的安装加载和系统的安装启动原理是那么的类似

一、插件的安装:
Replugin插件的安装并不会真正的处理插件apk中的dex、so库、资源等,只是将插件移动到需要的位置,这个位置默认是宿主的context.getFilesDir(),然后将插件信息包装成Plugin对象并绑定宿主Context、宿主ClassLoader、负责和宿主通信的PluginCommImpl类,最后将Plugin对象存入插件管理进程统一管理。如果你看过PMS的安装过程源码会发现两者及其的类似。

在分析Replugin的安装源码前,先简单描述一下系统PackManagerService安装apk的过程,完了我们在看的时候,可以对比两者是否真的很相似。

apk安装过程描述(5.1源码):
apk的安装可以分为系统自动扫描安装和主动触发PackManagerService的接口安装,但是无论是哪种安装方式,原理都是一样的。

系统扫描安装:

在PMS被创建的时候,在构造方法中会调用scanDirLI方法扫描固定几个文件夹,例如: /system/framework、/system/app、/data/app等目录,在scanDirLI方法中遍历文件夹判断是否是apk文件,如果是会执行scanPackageLI方法,在scanPackageLI方法中为apk文件创建一个PackageParser对象并执行该对象的parsePackage,在PackageParser的parsePackage方法中去解析这个apk文件,主要是解析AndroidManifest.xml,并封装PackageParser.Package对象返回,接着会回到scanPackageLI中最后又调用了一个scanPackageLI的重载方法,这个方法释放apk文件的lib库,优化dex文件,最后将解析得到的数据缓存起来,例如四大组件,Rermission等,系统扫描安装的大概步骤就是这样。

调用PMS接口安装:

入口是PMS的installPackage,这个方法中直接跳到了installPackageAsUser方法中,这个方法中创建了InstallParams对象,这个类继承自HandlerParams,他们都是PMS的内部类,然后发送一条消息到PMS的内部类PackageHandler中调用params.startCopy(),这个方法执行的是HandlerParams中的方法,在这个方法中又执行了InstallParams的handleStartCopy方法,startCopy方法在执行handleStartCopy方法时有失败重试的机制,最多4次,handleStartCopy中会创建InstallArgs对象并执行它的copyApk方法将apk复制到data/app目录下,这一步完成后会再回到startCopy()的方法继续执行handleReturnCode方法,方法中判断如果复制成功后会调用processPendingInstall方法,在这个方法中也会调用解析apk的方法scanPackageLI方法来解析apk,最后会发送一个广播通知安装完成。

Replugin中的插件分为内置插件和外置插件:

内置的插件的安装时在初始化的时候就自动安装和加载了,当时在第一篇文章分析插件框架中的Server端要做的事情时,就有扫描内置插件的过程,在插件管理进程初始化的时候会扫描assest目录下的一个叫plugins-builtin.json的文件,并将插件信息封装成Plugin对象存入PmBase中
的一个叫mPlugins的ConcurrentHashMap中统一管理

外置插件的安装需要调用Replugin.install()方法来安装插件,这个过程和内置插件类似,区别就是内置插件是通过assest目录下的json文件来生成插件对象,外置插件则是通过获取插件apk的PackageInfo来生成插件对象,但是并不会处理apk中的dex、so库、资源等,只有当真正使用这个插件中的类时才会去真正的解析加载这个插件。

本文就只说外置插件的安装了,而且在安装方法的注释中说了p-n类型的插件即将废弃,所有这里只说纯apk插件,如果想知道内置插件的安装过程请看第一篇文章

1.插件的安装需要调用Replugin.install()来完成,从这个方法开始
源码路径:com.qihoo360.replugin.RePlugin

public static PluginInfo install(String path) {if (TextUtils.isEmpty(path)) {throw new IllegalArgumentException();}// 判断文件合法性File file = new File(path);if (!file.exists()) {return null;} else if (!file.isFile()) {return null;}//省略p-n判断。。。//安装插件return MP.pluginDownloaded(path);
}

2.首先判断了一下需要安装的插件这个文件是否合法,最后直接返回了MP.pluginDownloaded(path)。

源码路径:com.qihoo360.loader2.MP

public static final PluginInfo pluginDownloaded(String path) {if (LOG) {LogDebug.d(PLUGIN_TAG, "MP.pluginDownloaded ... path=" + path);}//针对p-n类型的进程锁,忽略ProcessLocker lock = null;try {//省略p-n类型判断和创建进程锁。。。//获取IPluginHost类型对象并调用它的pluginDownloaded方法PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);//回调if (info != null) {RePlugin.getConfig().getEventCallbacks().onInstallPluginSucceed(info);}return info;} catch (Throwable e) {if (LOGR) {LogRelease.e(PLUGIN_TAG, "mp.pded: " + e.getMessage(), e);}} finally {// 去锁if (lock != null) {lock.unlock();}}return null;
}

3.上面其实主要的就一句代码,如果看过第一篇的朋友看到这个PluginProcessMain.getPluginHost()方法应该能联想到返回的是谁,我们还是先进去看看

源码路径:com.qihoo360.loader2.PluginProcessMain

public static final IPluginHost getPluginHost() {if (sPluginHostLocal != null) {return sPluginHostLocal;}// 可能是第一次,或者常驻进程退出了if (sPluginHostRemote == null) {        // 再次唤起常驻进程connectToHostSvc();}return sPluginHostRemote;
}

4.上面不管是返回sPluginHostLocal还是sPluginHostRemote,其实逻辑不会变,只不过一个是插件管理进程,另一个是其他进程,但是最终获得的都是IPluginHost,如果看过第一篇分析的朋友应该能知道,他是一个Binder对象,它的具体实现类是PmHostSvc,我记得还说这个这个类有点像AMS和ServerManager,如果不太了解的朋友建议先看第一篇框架初始化,那么我们下面就去看PmHostSvc中的pluginDownloaded方法

源码路径:com.qihoo360.loader2.PmHostSvc

 public PluginInfo pluginDownloaded(String path) throws RemoteException {// 通过路径来判断是采用新方案,还是旧的P-N(即将废弃,有多种)方案PluginInfo pi;String fn = new File(path).getName();if (fn.startsWith("p-n-") || fn.startsWith("v-plugin-") || fn.startsWith("plugin-s-") || fn.startsWith("p-m-")) {//忽略p-n类型pi = pluginDownloadedForPn(path);} else {//执行插件安装pi = mManager.getService().install(path);}//后面会再回来分析这里if (pi != null) {// 通常到这里,表示“安装已成功”,这时不管处于什么状态,都应该通知外界更新插件内存表syncPluginInfo2All(pi);}return pi;
}

5.执行安装的是mManager.getService().install(path),这个mManager是PluginManagerServer类型,我们在第一篇的框架初始化中也分析了,它是在PmHostSvc的构造方法中被创建,是用来管理插件的安装、卸载、更新、获取等功能的,我们先看一下mManager.getService()

系统源码:com.qihoo360.replugin.packages.PluginManagerServer

public IPluginManagerServer getService() {//这个mStub是IPluginManagerServer类型return mStub;
}

6.返回的是mStub类型,这个mStub是IPluginManagerServer类型,是一个Binder对象,在第一篇框架初始化中也介绍了,它是在PluginManagerServer的构造中被创建的,而且这个Stub是PluginManagerServer的内部类,这个类中的install方法就一句代码,直接调用了外部类PluginManagerServer中的installLocked方法,我们就直接看installLocked方法了

源码路径:com.qihoo360.replugin.packages.PluginManagerServer

private PluginInfo installLocked(String path) {//是否开启签名验证final boolean verifySignEnable = RePlugin.getConfig().getVerifySign();      final int flags = verifySignEnable ? PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES : PackageManager.GET_META_DATA;// 1. 读取APK内容PackageInfo pi = mContext.getPackageManager().getPackageArchiveInfo(path, flags);if (pi == null) {if (LogDebug.LOG) {LogDebug.e(TAG, "installLocked: Not a valid apk. path=" + path);}RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.READ_PKG_INFO_FAIL);return null;}// 2. 校验插件签名if (verifySignEnable) {if (!verifySignature(pi, path)) {return null;}}// 3. 通过PackageInfo解析插件apk中的包名、AndroidManifest.xml中的metaData标签、根据这些信息new一个PluginInfoPluginInfo instPli = PluginInfo.parseFromPackageInfo(pi, path);      //标记未安装instPli.setType(PluginInfo.TYPE_NOT_INSTALL);//获取之前是否已经安装过这个插件,false代表是返回原对象,true返回clone后的对象PluginInfo curPli = MP.getPlugin(instPli.getName(), false);if (curPli != null) {// 若要安装的插件版本小于或等于当前版本,则安装失败// 检查版本,版本较老?直接返回final int checkResult = checkVersion(instPli, curPli);if (checkResult < 0) {RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.VERIFY_VER_FAIL);return null;} else if (checkResult == 0){//设置同版本覆盖instPli.setIsPendingCover(true);}}// 4. 将合法的APK改名后,移动到指定位置if (!copyOrMoveApk(path, instPli)) {RePlugin.getConfig().getEventCallbacks().onInstallPluginFailed(path, RePluginEventCallbacks.InstallResult.COPY_APK_FAIL);return null;}// 5. 从插件中释放 So 文件,注意不是加载,只是将需要是so放到指定文件下PluginNativeLibsHelper.install(instPli.getPath(), instPli.getNativeLibsDir());// 6. 若已经安装旧版本插件,则尝试更新插件信息,否则直接加入到列表中if (curPli != null) {//如果已经安装过该插件,会设置待更新信息、和指定上一个版本对象updateOrLater(curPli, instPli);} else {mList.add(instPli);}// 7. 保存插件信息到文件中,下次可直接使用mList.save(mContext);return instPli;
}

7.这个方法主要就是通过插件apk生成PluginInfo并保存插件信息,至于每一步的具体实现不去看了,有兴趣可以自己去看每一步中的代码,
现在回到第4步中,接着看通知其他进程更新信息

源码路径:com.qihoo360.loader2.PmHostSvc

private void syncPluginInfo2All(PluginInfo pi) {// PS:若更新了“正在运行”的插件(属于“下次重启进程后更新”),则由于install返回的是“新的PluginInfo”,为防止出现“错误更新”,需要使用原来的//// 举例,有一个正在运行的插件A(其Info为PluginInfoOld)升级到新版(其Info为PluginInfoNew),则:// 1. mManager.getService().install(path) 的返回值为:PluginInfoNew// 2. PluginInfoOld在常驻进程中的内容修改为:PluginInfoOld.mPendingUpdate = PendingInfoNew// 3. 同步到各进程,这里存在两种可能://    a) (有问题)同步的是PluginInfoNew,则所有进程的内存表都强制更新到新的Info上,因此【正在运行的】插件信息将丢失,会出现严重问题//    b) (没问题)同步的是PluginInfoOld,只不过这个Old里面有个mPendingUpdate指向PendingInfoNew,则不会有问题,旧的仍被使用,符合预期// 4. 最终install方法的返回值是PluginInfoNew,这样最外面拿到的就是安装成功的新插件信息,符合开发者的预期//如果在上一步中是走更新逻辑这里获取到的parent是上一个插件信息对象PluginInfo needToSyncPi;PluginInfo parent = pi.getParentInfo();if (parent != null) {needToSyncPi = parent;} else {needToSyncPi = pi;}// 在常驻进程内更新插件内存表,mPluginMgr是PmBasemPluginMgr.newPluginFound(needToSyncPi, false);// 通知其它进程去更新,发送了一个广播到PmBase中Intent intent = new Intent(PmBase.ACTION_NEW_PLUGIN);intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, mNeedRestart);intent.putExtra("obj", needToSyncPi);IPC.sendLocalBroadcast2AllSync(mContext, intent);if (LOG) {LogDebug.d(TAG, "syncPluginInfo2All: Sync complete! syncPi=" + needToSyncPi);}
}

8.在这个方法中Replugin的注释中说到了更新插件可能会出现的问题,简单的说就是如果这个插件正在运行则会记录这个要更新插件的信息到原插件信息对象上,直到所有“正在使用插件”的进程结束并重启后才会生效,我们先来看一下在常驻进程中更新插件的内存表

源码路径:com.qihoo360.loader2.PmBase

final void newPluginFound(PluginInfo info, boolean persistNeedRestart) {// 更新最新插件表,这里将插件存入了PluginTable中一个叫PLUGINS的HashMap中PluginTable.updatePlugin(info);// 更新可加载插件表,封装成Plugin对象,绑定了Context、ClassLoader和PluginCommImplinsertNewPlugin(info);// 清空插件的状态(解禁)PluginStatusController.setStatus(info.getName(), info.getVersion(), PluginStatusController.STATUS_OK);//设置常驻进程是否需要重启if (IPC.isPersistentProcess()) {//mNeedRestart在第insertNewPlugin方法中做了判断并赋值persistNeedRestart = mNeedRestart;}// 通知给外部使用者,例如在插件中注册了安装完成监听,这个广播只有在不是常驻进程中才会被注册,注册的代码在RePluginApplication的onCreate中Intent intent = new Intent(RePluginConstants.ACTION_NEW_PLUGIN);intent.putExtra(RePluginConstants.KEY_PLUGIN_INFO, info);intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, persistNeedRestart);intent.putExtra(RePluginConstants.KEY_SELF_NEED_RESTART, mNeedRestart);LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);
}

9.上面我们主要看一下更新插件表的方法,因为这里将插件信息对象封装成了Plugin插件对象并绑定了Context、ClassLoader、PluginCommImpl

源码路径:com.qihoo360.loader2.PmBase

final void insertNewPlugin(PluginInfo info) {synchronized (LOCKER) {// 检查插件是否已经被禁用if (RePlugin.getConfig().getCallbacks().isPluginBlocked(info)) {return;}//查看之前是否已经存在这个插件,也就是是否是要更新Plugin p = mPlugins.get(info.getName());// 如果是内置插件,新插件extract成功,则直接替换if (p != null && p.mInfo.getType() == PluginInfo.TYPE_BUILTIN && info.getType() == PluginInfo.TYPE_PN_INSTALLED) {// next} else if (p != null && p.isInitialized()) {// 检查该插件是否已加载// 设置是否需要重启标志mNeedRestart = true;return;}// 直接new了一个PluginPlugin plugin = Plugin.build(info);//绑定了Context、ClassLoader和PluginCommImplplugin.attach(mContext, mClassLoader, mLocal);// 这个方法在框架初始化的时候也出现过,就是将插件对象存入到叫mPlugins的ConcurrentHashMap中,如果有别名存入的是别名,没有别名存入包名putPluginObject(info, plugin);}
}

10.//在回去看一下第7步中发送了一个广播到PmBase中去通知其他进程更新插件信息,这个方法在PmBase的registerReceiverAction方法中,
这个广播只有在不是常驻进程中才会被注册,注册的代码在RePluginApplication的onCreate中

源码路径:com.qihoo360.loader2.PmBase

private final void registerReceiverAction(final String action) {IntentFilter filter = new IntentFilter(action);LocalBroadcastManager.getInstance(mContext).registerReceiver(new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {if (action.equals(intent.getAction())) {                PluginInfo info = intent.getParcelableExtra("obj");if (info != null) {switch (action) {case ACTION_NEW_PLUGIN:// 这个方法和第8步中的一样,只是执行的进程不同而已newPluginFound(info, intent.getBooleanExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, false));break;case ACTION_UNINSTALL_PLUGIN://卸载插件pluginUninstalled(info);break;}}}}}, filter);
}

好了,到这里插件的安装就结束了,如开头所说,安装并没有真正的处理插件apk中的dex、so库、资源文件等,只是在为真正使用插件时做准备工作,只要记住插件的安装最重要的是移动插件apk到指定目录,然后将插件封装成Plugin对象并存储在插件管理进程中统一管理。

二、插件的加载

插件的加载其实就是对插件启动运行的一个过程,在加载的过程中会对dex、so库、资源文件等做初始化的处理,处理完成之后会初始化插件中所依赖replugin-plugin-lib库并调用插件apk中Application的onCreate方法,虽然这时并未启动插件apk中的任何组件,但是插件apk中Application已经创建并运行了,所以说加载插件就是启动运行插件apk的过程。

系统应用启动简述:
首先说明一点,启动一个应用并不一定是要打开一个Activity才行,四大组件都可以启动一个应用,其实就是启动应用进程的过程,当我们触发要启动一个应用的时候,首先AMS会通过调用Process中的start来开启一个新的进程,进程启动后调用ActivityThread的main方法,在这里会开启消息循环,获取AMS并调用attachApplication和AMS绑定,绑定过程中AMS会通过ActivityThread中的ApplicationThread再调ActivityThread中的bindApplication方法,bindApplication中会再发送一条消息到ActivityThread中的Handler中调用handleBindApplication,在这个方法中会创建LoadedApk对象、ContextImpl、PathClassLoader、Resource等,然后创建Application调用attach方法,接着查询该应用的所有Provider列表先初始化Provider,最后调用Application的onCreate方法,然后AMS会接着走和ActivityThread的绑定方法,下面会再判断是否需要执行或者启动Activity、Service、BroadcastReceiver等相关方法。

1.在第一篇框架初始化的最后插件的加载没有详细的讲,但是我们知道插件的真正加载会调用Plugin.load方法开始,也就是上面插件安装的时候最后封装成的Plugin对象,插件的加载就不分内置插件和外置插件了,都是调用Plugin.load来完成的,我们就从这个方法开始

源码路径:com.qihoo360.loader2.Plugin

final boolean load(int load, boolean useCache) {//插件信息对象PluginInfo info = mInfo;//解析加载apkboolean rc = loadLocked(load, useCache);// 尝试在此处调用Application.onCreate方法if (load == LOAD_APP && rc) {//创建插件ApplicationcallApp();}// 如果info改了,通知一下常驻// 只针对P-n的Type转化来处理,一定要通知,这样Framework_Version也会得到更新if (rc && mInfo != info) {UpdateInfoTask task = new UpdateInfoTask((PluginInfo) mInfo.clone());Tasks.post2Thread(task);}return rc;
}

2.上面方法很清晰,加载插件apk调用了loadLocked方法,初始化插件apk调用了callApp,这个环节我们只分析loadLocked方法,省略了源码中所有打印log的地方

源码路径:com.qihoo360.loader2.Plugin

private boolean loadLocked(int load, boolean useCache) {// 判断插件是否是禁用状态int status = PluginStatusController.getStatus(mInfo.getName(), mInfo.getVersion());if (status < PluginStatusController.STATUS_OK) {return false;}//是否已经加载过,判断需要加载哪些数据,返回相应的结果,这里有四种情况,可以看一下Plugin的字段声明       if (mInitialized) {if (mLoader == null) {if (LOG) {LogDebug.i(MAIN_TAG, "loadLocked(): Initialized but mLoader is Null");}return false;}if (load == LOAD_INFO) {boolean rl = mLoader.isPackageInfoLoaded();if (LOG) {LogDebug.i(MAIN_TAG, "loadLocked(): Initialized, pkginfo loaded = " + rl);}return rl;}if (load == LOAD_RESOURCES) {boolean rl = mLoader.isResourcesLoaded();if (LOG) {LogDebug.i(MAIN_TAG, "loadLocked(): Initialized, resource loaded = " + rl);}return rl;}if (load == LOAD_DEX) {boolean rl = mLoader.isDexLoaded();if (LOG) {LogDebug.i(MAIN_TAG, "loadLocked(): Initialized, dex loaded = " + rl);}return rl;}boolean il = mLoader.isAppLoaded();if (LOG) {LogDebug.i(MAIN_TAG, "loadLocked(): Initialized, is loaded = " + il);}return il;}//标记加载mInitialized = true;// 判断之前是否存在缓存if (useCache) {boolean result = loadByCache(load);// 如果缓存命中,则直接返回if (result) {return true;}}Context context = mContext;ClassLoader parent = mParent;PluginCommImpl manager = mPluginManager;//String logTag = "try1";String lockFileName = String.format(Constant.LOAD_PLUGIN_LOCK, mInfo.getApkFile().getName());//创建进程锁ProcessLocker lock = new ProcessLocker(context, lockFileName);//加锁if (!lock.tryLockTimeWait(5000, 10)) {// 此处仅仅打印错误if (LOGR) {LogRelease.w(PLUGIN_TAG, logTag + ": failed to lock: can't wait plugin ready");}}long t1 = System.currentTimeMillis();//加载插件apkboolean rc = doLoad(logTag, context, parent, manager, load);//解锁        lock.unlock();if (rc) {           try {// 至此,该插件已开始运行PluginManagerProxy.addToRunningPluginsNoThrows(mInfo.getName());} catch (Throwable e) {}return true;}//加载失败尝试再次加载,代码和上面类似,省略。。。。return true;
}

3.这个方法先判断了插件是否是禁用状态,是否已经加载过,是否存在缓存,都没成功则进入到doLoad方法开始加载插件,我们默认当这个插件是第一次加载,毕竟我们主要还是要看如何加载插件的

源码路径:com.qihoo360.loader2.Plugin

private final boolean doLoad(String tag, Context context, ClassLoader parent, PluginCommImpl manager, int load) {if (mLoader == null) {//1. 判断是否为内置插件,如果是内置插件,在检查是否已经释放了so库到指定位置,如果没有先释放到指定位置,并重新构造PluginInfo//2. 判断是否为p-n类型并且未执行安装插件步骤,如果是p-n类型并且未安装,先执行插件的安装操作,并重新构造PluginInfo。。。。//创建插件加载类mLoader = new Loader(context, mInfo.getName(), mInfo.getPath(), this);//加载插件数据if (!mLoader.loadDex(parent, load)) {return false;}。。。。// 初始化插件工程框架if (load == LOAD_APP) {// NOTE Entry对象是可以在任何线程中被调用到if (!loadEntryLocked(manager)) {return false;}}}if (load == LOAD_INFO) {return mLoader.isPackageInfoLoaded();} else if (load == LOAD_RESOURCES) {return mLoader.isResourcesLoaded();} else if (load == LOAD_DEX) {return mLoader.isDexLoaded();} else {return mLoader.isAppLoaded();}
}

4.判断了插件apk的类型,然后做响应的处理,接着创建了一个插件加载类Loader,并调用loadDex方法开始加载插件,插件加载完成后调用插件apk所依赖的replugin-plugin-lib库进行初始化,我们先来看如何加载插件

源码路径:com.qihoo360.loader2.Loader

 final boolean loadDex(ClassLoader parent, int load) {try {//获取PMPackageManager pm = mContext.getPackageManager();//查看是否有缓存的PackageInfomPackageInfo = Plugin.queryCachedPackageInfo(mPath);if (mPackageInfo == null) {// 通过PM获取插件apk的包信息对象PackageInfomPackageInfo = pm.getPackageArchiveInfo(mPath,PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS | PackageManager.GET_META_DATA);if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {                   mPackageInfo = null;return false;}//指定插件apk的资源路径mPackageInfo.applicationInfo.sourceDir = mPath;mPackageInfo.applicationInfo.publicSourceDir = mPath;// 添加针对SO库的加载// 此属性最终用于ApplicationLoaders.getClassLoader,在创建PathClassLoader时成为其参数// 这样findLibrary可不用覆写,即可直接实现SO的加载,这里不明白的可以看上一篇的唯一hook点分析系统PathClassLoader的部分PluginInfo pi = mPluginObj.mInfo;File ld = pi.getNativeLibsDir();mPackageInfo.applicationInfo.nativeLibraryDir = ld.getAbsolutePath();// 缓存表: pkgName -> pluginNamesynchronized (Plugin.PKG_NAME_2_PLUGIN_NAME) {Plugin.PKG_NAME_2_PLUGIN_NAME.put(mPackageInfo.packageName, mPluginName);}// 缓存表: pluginName -> fileNamesynchronized (Plugin.PLUGIN_NAME_2_FILENAME) {Plugin.PLUGIN_NAME_2_FILENAME.put(mPluginName, mPath);}// 缓存表: fileName -> PackageInfosynchronized (Plugin.FILENAME_2_PACKAGE_INFO) {Plugin.FILENAME_2_PACKAGE_INFO.put(mPath, new WeakReference<PackageInfo>(mPackageInfo));}}// 创建或获取ComponentList表mComponents = Plugin.queryCachedComponentList(mPath);if (mComponents == null) {// ComponentList在构造方法中保存了插件apk中的四大组件,并解析插件apk的AndroidManifest.xml文件来生成//组件与IntentFilter的对应关系mComponents = new ComponentList(mPackageInfo, mPath, mPluginObj.mInfo);// 动态注册插件中apk中在AndroidManifest中声明的 receiverregReceivers();// 缓存表:ComponentListsynchronized (Plugin.FILENAME_2_COMPONENT_LIST) {Plugin.FILENAME_2_COMPONENT_LIST.put(mPath, new WeakReference<>(mComponents));}/* 只调整一次 */// 调整插件apk中四大组件的自定义进程名称为宿主坑位进程名称adjustPluginProcess(mPackageInfo.applicationInfo);// 调整插件中 Activity 的 TaskAffinityadjustPluginTaskAffinity(mPluginName, mPackageInfo.applicationInfo);}//如果只加载组件信息就返回了if (load == Plugin.LOAD_INFO) {return isPackageInfoLoaded();}//查看是否有缓存Resource对象mPkgResources = Plugin.queryCachedResources(mPath);if (mPkgResources == null) {// Resourcestry {if (BuildConfig.DEBUG) {// 如果是Debug模式的话,防止与Instant Run冲突,资源重新New一个Resources r = pm.getResourcesForApplication(mPackageInfo.applicationInfo);mPkgResources = new Resources(r.getAssets(), r.getDisplayMetrics(), r.getConfiguration());} else {//获取插件apk的Resource对象mPkgResources = pm.getResourcesForApplication(mPackageInfo.applicationInfo);}} catch (NameNotFoundException e) {return false;}if (mPkgResources == null) {return false;}// 缓存表: Resourcessynchronized (Plugin.FILENAME_2_RESOURCES) {Plugin.FILENAME_2_RESOURCES.put(mPath, new WeakReference<>(mPkgResources));}}//如果只加载资源信息就直接返回了if (load == Plugin.LOAD_RESOURCES) {return isResourcesLoaded();}//查看是否有缓存的插件自己的ClassLoader对象mClassLoader = Plugin.queryCachedClassLoader(mPath);if (mClassLoader == null) {// dex文件路径String out = mPluginObj.mInfo.getDexParentDir().getPath();if (BuildConfig.DEBUG) {// 因为Instant Run会替换parent为IncrementalClassLoader,所以在DEBUG环境里// 需要替换为BootClassLoader才行parent = ClassLoader.getSystemClassLoader();} else {// 这里直接用父类加载器parent = getClass().getClassLoader().getParent();  }//设置so库路径String soDir = mPackageInfo.applicationInfo.nativeLibraryDir;//创建插件自己的PluginDexClassLoadermClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPluginObj.mInfo, mPath, out, soDir, parent);if (mClassLoader == null) {if (LOG) {LogDebug.d(PLUGIN_TAG, "get dex null");}return false;}// 缓存表:ClassLoadersynchronized (Plugin.FILENAME_2_DEX) {Plugin.FILENAME_2_DEX.put(mPath, new WeakReference<>(mClassLoader));}}//只加载dex直接返回了if (load == Plugin.LOAD_DEX) {return isDexLoaded();}// 创建插件apk使用的Context对象mPkgContext = new PluginContext(mContext, android.R.style.Theme, mClassLoader, mPkgResources, mPluginName, this);} catch (Throwable e) {return false;}return true;
}

5.loadDex这个方法主要是对运行插件apk的准备工作,这里做的事情主要如下几点:

1)创建ComponentList对象
这个对象在创建的时候解析了AndroidManifest.xml文件获取四大组件信息,intent-filter,category,action,data等标签下的信息,并将这些信息和四大组件生成对应表关系并缓存,缓存插件apk的Application信息对象,接着动态注册插件apk中在AndroidManifest中声明的receiver,调整插件apk中四大组件的自定义进程名称为宿主坑位进程名称和Activity的TaskAffinity

2)创建插件apk的要使用的Resources对象
每一个apk中都会使用资源,要使用这些资源必要有一个Resource对象,平时我们虽然可以直接R.xx什么的,这些其实也是通过Resource对象获取的,而这个Resource对象是系统帮我创建好的,这个对象的创建时机是在ContextImpl的构造方法中通过LoadedApk的getResource方法,方法中又调用了ActivityThread中的getTopLevelResources方法,最后又调用了ResourcesManager的getTopLevelResources方法中。而未安装的apk系统当然不会帮我们创建这个Resource对象,所以要自己创建

3)创建插件自己的ClassLoader对象:有了属于插件自己的ClassLoader后自然可以加载插件用的类要使用dex文件中的类,需要有类加载器,安装的应用系统会帮忙创建一个PathClassLoader,而要加载外置的dex则需要用DexClassLoader,对于这里的原理在上一篇hook系统ClassLoader一文中已经讲了。

4)创建插件用的Context对象
android应用的上下文是Context,在应用被启动的时候会创建一个BaseContext对象,这个对象的创建过程在上一篇hook系统ClassLoader一文中也讲了。

接着返回去看第3步中,初始化插件工程框架,调用了loadEntryLocked方法

源码路径:com.qihoo360.loader2.Plugin

private boolean loadEntryLocked(PluginCommImpl manager) {if (mDummyPlugin) {mLoader.mPlugin = new IPlugin() {@Overridepublic IModule query(Class<? extends IModule> c) {return null;}};} else {//尝试反射调用插件工程中Entry的create方法if (mLoader.loadEntryMethod2()) {if (!mLoader.invoke2(manager)) {return false;}} else if (mLoader.loadEntryMethod(false)) {if (!mLoader.invoke(manager)) {return false;}} else if (mLoader.loadEntryMethod3()) {//反射获取Entry的create方法//执行Entry的create方法if (!mLoader.invoke2(manager)) {return false;}} else {return false;}}return true;
}

6.注意:这里是反射调用了插件工程中的Entry的create方法,那么我们现在去看插件工程中这个方法,replugin-plugin-lib

源码路径:com.qihoo360.replugin.Entry

 public static final IBinder create(Context context, ClassLoader cl, IBinder manager) {// 初始化插件框架,就是反射主工程框架中的类或者方法,方便插件工程之后直接调用RePluginFramework.init(cl);// 初始化主工程传递过来的Context和ClassLoaderRePluginEnv.init(context, cl, manager);//返回插件工程中的服务管理Binder对象return new IPlugin.Stub() {@Overridepublic IBinder query(String name) throws RemoteException {return RePluginServiceManager.getInstance().getService(name);}};
}

7.插件工程框架的初始化很简单,就是反射了主工程框架中的一些方法,可以让插件功能也可以使用主工程框架的功能,接着就是缓存持有主工程传递过来的插件Context对象,这个Context是插件工程自己用的,还通过这个Context获取了宿主的Context对象,这个对象主要是为了用来获取一些宿主中的资源,反射类等一些信息的,还缓存了宿主的ClassLoader对象,也是用来方便反射宿主中的一些类的,最后返回了插件工程中管理服务的Binder对象,插件工程框架初始化完毕了,接着返回主工程中继续看第一步的代码,创建插件功能Application的方法callApp()

源码路径:com.qihoo360.loader2.Plugin

private void callApp() {//调用了callAppLocked()if (Looper.myLooper() == Looper.getMainLooper()) {callAppLocked();} else {// 确保一定在UI的最早消息处调用mMainH.postAtFrontOfQueue(new Runnable() {@Overridepublic void run() {callAppLocked();}});}
}

8.这里要确保是在ui线程中执行,调用了callAppLocked()

源码路径:com.qihoo360.loader2.Plugin

private void callAppLocked() {// 获取并调用Application的几个核心方法if (!mDummyPlugin) {// NOTE 不排除A的Application中调到了B,B又调回到A,或在同一插件内的onCreate开启Service/Activity,而内部逻辑又调用fetchContext并再次走到这里// NOTE 因此需要对mApplicationClient做判断,确保永远只执行一次,无论是否成功if (mApplicationClient != null) {// 已经初始化过,无需再次处理return;}//创建Application并包装成PluginApplicationClient返回mApplicationClient = PluginApplicationClient.getOrCreate(mInfo.getName(), mLoader.mClassLoader, mLoader.mComponents, mLoader.mPluginObj.mInfo);//调用Application的核心方法if (mApplicationClient != null) {//执行反射获取的attach方法mApplicationClient.callAttachBaseContext(mLoader.mPkgContext);//调用onCreate方法mApplicationClient.callOnCreate();}} else {if (LOGR) {LogRelease.e(PLUGIN_TAG, "p.cal dm " + mInfo.getName());}}
}

9.创建Application并包装成PluginApplicationClient返回,接着调用Application的核心方法,主要看看如何创建的就好了

源码路径:com.qihoo360.replugin.component.app.PluginApplicationClient

public static PluginApplicationClient getOrCreate(String pn, ClassLoader plgCL, ComponentList cl, PluginInfo pi) {if (pi.getFrameworkVersion() <= 1) {// 仅框架版本为2及以上的,才支持Application的加载          return null;}//查看是否已经初始化了PluginApplicationClient pac = getRunning(pn);if (pac != null) {// 已经初始化过Application?直接返回         return pac;}try {//只是反射Application的attach方法,没有调用,因为还没有创建Application类initMethods();} catch (Throwable e) {if (BuildConfig.DEBUG) {e.printStackTrace();}return null;}//构造方法中创建Application对象,如果插件中有自定义的Application创建自定义的(之前已经解析过AndroidManifest.xml),如果没有直接new一个Application,final PluginApplicationClient pacNew = new PluginApplicationClient(plgCL, cl, pi);//判断刚才的Application是否创建成功if (pacNew.isValid()) {//存入已经初始化过的ArrayMap中sRunningClients.put(pn, new WeakReference<>(pacNew));if (Build.VERSION.SDK_INT >= 14) {//使用宿主的Application注册监听,在回调中使用插件Application回调RePluginInternal.getAppContext().registerComponentCallbacks(new ComponentCallbacks2() {@Overridepublic void onTrimMemory(int level) {pacNew.callOnTrimMemory(level);}@Overridepublic void onConfigurationChanged(Configuration newConfig) {pacNew.callOnConfigurationChanged(newConfig);}@Overridepublic void onLowMemory() {pacNew.callOnLowMemory();}});}return pacNew;} else {// Application对象没有初始化出来,则直接按失败处理return null;}
}

反射获取Application的attach方法,接着创PluginApplicationClient,在构造中创建了Application对象,最后使用宿主的Application注册ComponentCallbacks监听,在回调中再使用插件的Application回调。
在返回去看上一步最后执行了Application的onCreate方法,是不是可以说这个插件就已经被启动了,这里验证了开头说的,插件的加载过程可以理解成就是插件apk的启动过程。

而且现在再去细细品味和回想我简述的系统安装和启动的过程,两者是不是非常的相似呢!本系列博客从分析到写到这里一共用了将近一个月的时间,因为还要工作,所有耽误了本人很多自己的时间,现在拿出来分享,如果你都读完了还希望可以支持一下,本来还想继续写四大组件的运行过程,但是由于工作最近较忙实在没有经历了,目前分析到这里基本上Replugin的原理都通了,剩下的相信也可以自己看了。

唯一插件化Replugin源码及原理深度剖析--插件的安装、加载原理相关推荐

  1. c++插件化 NDD源码的插件机制实现解析

    插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能.它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用.插件机制允许管理员在需要时轻松安装 ...

  2. 从源码分析Android的Glide库的图片加载流程及特点

    转载:http://m.aspku.com/view-141093.html 这篇文章主要介绍了从源码分析Android的Glide库的图片加载流程及特点,Glide库是Android下一款人气很高的 ...

  3. Spring源码之ResourceLoader(二):PathMatchingResourcePatternResolver实现getResources加载多文件

    Spring源码之ResourceLoader二:PathMatchingResourcePatternResolver实现getResources加载多文件 findAllClassPathReso ...

  4. PTMs:QLoRA技巧之源码解读(qlora.py文件)—解析命令与加载参数→数据预处理→模型训练+评估+推理

    PTMs:QLoRA技巧之源码解读(qlora.py文件)-解析命令与加载参数→数据预处理→模型训练+评估+推理 目录 QLoRA技巧之源码解读(qlora.py文件)-解析命令与加载参数→数据预处理 ...

  5. 短视频直播源码,显示和隐藏 类似淘宝加载

    短视频直播源码,显示和隐藏 类似淘宝加载的相关代码 1:当点击HomeActity中的Tab时,首先 case 1:case 2:if ( fg2== null) {fg2 = new Fragmen ...

  6. replugin源码解析之replugin-plugin-gradle(插件的gradle插件)

    前言 replugin-plugin-gradle 是 RePlugin 插件框架中提供给replugin插件用的gradle插件,是一种动态编译方案实现. 主要在插件应用的编译期,基于Transfo ...

  7. spring源码解析之IOC容器(二)------加载和注册

    上一篇跟踪了IOC容器对配置文件的定位,现在我们继续跟踪代码,看看IOC容器是怎么加载和注册配置文件中的信息的.开始之前,首先我们先来了解一下IOC容器所使用的数据结构-------BeanDefin ...

  8. skynet源码分析(11)--skynet的配置加载

    作者:shihuaping0918@163.com,转载请注明作者 skynet中的源码已经分析得差不多了,还有启动过程没有分析.skynet的配置文件是以lua格式来写的.使用过skynet的都清楚 ...

  9. Spring源码分析(1) —— 从Xml的加载到解析

    题外话: 接口&多态 我有一辆自行车,每天骑着它去上班 package com.zhao.SpringIoc;public class Bike {public void go() {Syst ...

最新文章

  1. 【js】通过js代码改变html表单中的数据
  2. 实战 用户登录、session校验、分布式存储session
  3. python零基础怎么学-零基础如何学习Python?老男孩Python入门培训
  4. 客户网页WIZnet无线解决方案 之 太阳能逆变器
  5. 基于SAML2.0的SAP云产品Identity Authentication过程介绍
  6. 客户端的socket是否需要bind?
  7. 光纤收发器的工作原理以及使用方法
  8. 【Sublime】使用 Sublime 工具时运行python文件
  9. 7个理由,给你推荐这款“秒杀Excel”的分析神器!
  10. net start mysql 失败_net start mysql出错,显示错误1067
  11. 【转】requests、BeautifulSoup使用总结
  12. OpenCV3.0.0 + VS2012 的环境搭建
  13. Pycharm 主题字体推荐(亮色)
  14. 百度文库免费复制文字_如何复制百度文库上的内容——解答!
  15. linux flex安装包,安装flex builder for Linux在Ubuntu
  16. java 解析josn数组
  17. OPTEE学习笔记 - IPC
  18. pygame模块学习
  19. Django建网站教程
  20. centos 开发套件_替代的Laravel套件开发工作流程

热门文章

  1. win8.1怎样打开计算机名,Win8怎么打开cmd命令窗口_Win8.1打开命令提示符的方法-192路由网...
  2. Linux(1) 概要、安装 、文件系统基本认知
  3. Nginx服务器支持.htaccess的方法
  4. ASCII Grid
  5. 漏洞建议:实施 TLS_FALLBACK_SCSV。此外,要么完全禁用 SSLv3,要么禁用以通过 SSLv3 的 CBC 模式操作的所有密码套件
  6. Java为什么要有基本数据类型和包装类型
  7. php留言板的简单编写
  8. 计算机网络如何新建vlan和划分vlan,[单选] 划分VLAN后,不同VLAN的计算机之间不能实现二层通信。如果在VLAN间通信,需要建立()...
  9. 程序员如何巧用Excel提高工作效率
  10. 机械革命极光Pro 评测