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

Posted 刘镓旗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了唯一插件化Replugin源码及原理深度剖析--插件的安装加载原理相关的知识,希望对你有一定的参考价值。

上一篇 唯一插件化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一个PluginInfo
    PluginInfo 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是PmBase
    mPluginMgr.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和PluginCommImpl
    insertNewPlugin(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了一个Plugin
        Plugin plugin = Plugin.build(info);

        //绑定了Context、ClassLoader和PluginCommImpl
        plugin.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() 

        @Override
        public 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;

    //解析加载apk
    boolean rc = loadLocked(load, useCache);

    // 尝试在此处调用Application.onCreate方法
    if (load == LOAD_APP && rc) 
        //创建插件Application
        callApp();
    
    // 如果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();

    //加载插件apk
    boolean 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 

        //获取PM
        PackageManager pm = mContext.getPackageManager();

        //查看是否有缓存的PackageInfo
        mPackageInfo = Plugin.queryCachedPackageInfo(mPath);
        if (mPackageInfo == null) 

            // 通过PM获取插件apk的包信息对象PackageInfo
            mPackageInfo = 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 -> pluginName
            synchronized (Plugin.PKG_NAME_2_PLUGIN_NAME) 
                Plugin.PKG_NAME_2_PLUGIN_NAME.put(mPackageInfo.packageName, mPluginName);
            

            // 缓存表: pluginName -> fileName
            synchronized (Plugin.PLUGIN_NAME_2_FILENAME) 
                Plugin.PLUGIN_NAME_2_FILENAME.put(mPluginName, mPath);
            

            // 缓存表: fileName -> PackageInfo
            synchronized (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中声明的 receiver
            regReceivers();

            // 缓存表:ComponentList
            synchronized (Plugin.FILENAME_2_COMPONENT_LIST) 
                Plugin.FILENAME_2_COMPONENT_LIST.put(mPath, new WeakReference<>(mComponents));
            

            /* 只调整一次 */
            // 调整插件apk中四大组件的自定义进程名称为宿主坑位进程名称
            adjustPluginProcess(mPackageInfo.applicationInfo);

            // 调整插件中 Activity 的 TaskAffinity
            adjustPluginTaskAffinity(mPluginName, mPackageInfo.applicationInfo);
        

        //如果只加载组件信息就返回了
        if (load == Plugin.LOAD_INFO) 
            return isPackageInfoLoaded();
        

        //查看是否有缓存Resource对象
        mPkgResources = Plugin.queryCachedResources(mPath);

        if (mPkgResources == null) 
            // Resources
            try 
                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;
            

            // 缓存表: Resources
            synchronized (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;

            //创建插件自己的PluginDexClassLoader
            mClassLoader = RePlugin.getConfig().getCallbacks().createPluginClassLoader(mPluginObj.mInfo, mPath, out, soDir, parent);

            if (mClassLoader == null) 
                if (LOG) 
                    LogDebug.d(PLUGIN_TAG, "get dex null");
                
                return false;
            

            // 缓存表:ClassLoader
            synchronized (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() 
            @Override
            public 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和ClassLoader
    RePluginEnv.init(context, cl, manager);

    //返回插件工程中的服务管理Binder对象
    return new IPlugin.Stub() 
        @Override
        public 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() 
            @Override
            public 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() 
                @Override
                public void onTrimMemory(int level) 
                    pacNew.callOnTrimMemory(level);
                

                @Override
                public void onConfigurationChanged(Configuration newConfig) 
                    pacNew.callOnConfigurationChanged(newConfig);
                

                @Override
                public void onLowMemory() 
                    pacNew.callOnLowMemory();
                
            );
        
        return pacNew;
     else 
        // Application对象没有初始化出来,则直接按失败处理
        return null;
    

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

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

以上是关于唯一插件化Replugin源码及原理深度剖析--插件的安装加载原理的主要内容,如果未能解决你的问题,请参考以下文章

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

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

唯一插件化RePlugin源码及原理深度剖析--工程职责及大纲

唯一插件化Replugin源码及原理深度剖析--初始化之框架核心

Replugin 源码分析------replugin-host-gradle插件源码分析

Replugin 源码分析------replugin-host-gradle插件源码分析