横向浅析Small,RePlugin两个插件化框架(三) RePlugin宿主原理解析

·  阅读 54

RePlugin

为什么拿出RePlugin呢?因为在我看来RePlugin虽然看起来都是360的,很可能和DroidPlugin相似。但是实际上RePlugin实现上在我看来打开了新世界的大门,其思路另辟蹊径。同时其思想,在本人看来又是一个另外一个层面的上的。最能让人另眼相看的有两点,首先打出了入侵最小的旗号,其次其工程的架构,将明确的区分了宿主和插件而且没有太多的代码入侵。宿主和插件双方能够单独运行。第三,灵活的使用了ContentProvider进行了跨进程的通信。第三点的技巧很值得我们学习。

让我们看看吧。这个插件化框架究竟是怎么回事。

老规矩先看用法。 先贴上github的wiki: github.com/Qihoo360/Re…

用法很简单,分为宿主和插件两块。

宿主配置

首先依赖

android {
    // ATTENTION!!! Must CONFIG this to accord with Gradle's standard, and avoid some error
    defaultConfig {
        applicationId "com.qihoo360.replugin.sample.host"
        ...
    }
    ...
}

// ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationId
apply plugin: 'replugin-host-gradle'

/**
 * 配置项均为可选配置,默认无需添加
 * 更多可选配置项参见replugin-host-gradle的RepluginConfig类
 * 可更改配置项参见 自动生成RePluginHostConfig.java
 */
repluginHostConfig {
    /**
     * 是否使用 AppCompat 库
     * 不需要个性化配置时,无需添加
     */
    useAppCompat = true
    /**
     * 背景不透明的坑的数量
     * 不需要个性化配置时,无需添加
     */
    countNotTranslucentStandard = 6
    countNotTranslucentSingleTop = 2
    countNotTranslucentSingleTask = 3
    countNotTranslucentSingleInstance = 2
}

dependencies {
    compile 'com.qihoo360.replugin:replugin-host-lib:2.2.4'
    ...
}
复制代码

首先我们继承RePlugin的Application

public class SampleApplication extends RePluginApplication
复制代码

接着可以添加各种自定义配置

 /**
     * RePlugin允许提供各种“自定义”的行为,让您“无需修改源代码”,即可实现相应的功能
     */
    @Override
    protected RePluginConfig createConfig() {
        RePluginConfig c = new RePluginConfig();

        // 允许“插件使用宿主类”。默认为“关闭”
        c.setUseHostClassIfNotFound(true);

        // FIXME RePlugin默认会对安装的外置插件进行签名校验,这里先关掉,避免调试时出现签名错误
        c.setVerifySign(!BuildConfig.DEBUG);

        // 针对“安装失败”等情况来做进一步的事件处理
        c.setEventCallbacks(new HostEventCallbacks(this));

        // FIXME 若宿主为Release,则此处应加上您认为"合法"的插件的签名,例如,可以写上"宿主"自己的。
        // RePlugin.addCertSignature("AAAAAAAAA");

        // 在Art上,优化第一次loadDex的速度
        // c.setOptimizeArtLoadDex(true);
        return c;
    }

    @Override
    protected RePluginCallbacks createCallbacks() {
        return new HostCallbacks(this);
    }

    /**
     * 宿主针对RePlugin的自定义行为
     */
    private class HostCallbacks extends RePluginCallbacks {

        private static final String TAG = "HostCallbacks";

        private HostCallbacks(Context context) {
            super(context);
        }

        @Override
        public boolean onPluginNotExistsForActivity(Context context, String plugin, Intent intent, int process) {
            // FIXME 当插件"没有安装"时触发此逻辑,可打开您的"下载对话框"并开始下载。
            // FIXME 其中"intent"需传递到"对话框"内,这样可在下载完成后,打开这个插件的Activity

            return super.onPluginNotExistsForActivity(context, plugin, intent, process);
        }

        /*
        @Override
        public PluginDexClassLoader createPluginClassLoader(PluginInfo pi, String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
            String odexName = pi.makeInstalledFileName() + ".dex";
            if (RePlugin.getConfig().isOptimizeArtLoadDex()) {
                Dex2OatUtils.injectLoadDex(dexPath, optimizedDirectory, odexName);
            }

            long being = System.currentTimeMillis();
            PluginDexClassLoader pluginDexClassLoader = super.createPluginClassLoader(pi, dexPath, optimizedDirectory, librarySearchPath, parent);


            return pluginDexClassLoader;
        }
        */
    }

    private class HostEventCallbacks extends RePluginEventCallbacks {

        private static final String TAG = "HostEventCallbacks";

        public HostEventCallbacks(Context context) {
            super(context);
        }

        @Override
        public void onInstallPluginFailed(String path, InstallResult code) {
            // FIXME 当插件安装失败时触发此逻辑。您可以在此处做“打点统计”,也可以针对安装失败情况做“特殊处理”
            // 大部分可以通过RePlugin.install的返回值来判断是否成功

            super.onInstallPluginFailed(path, code);
        }

        @Override
        public void onStartActivityCompleted(String plugin, String activity, boolean result) {
            // FIXME 当打开Activity成功时触发此逻辑,可在这里做一些APM、打点统计等相关工作
            super.onStartActivityCompleted(plugin, activity, result);
        }
    }
复制代码

插件的配置

只需要添加配置

apply plugin: 'replugin-plugin-gradle'

dependencies {
    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.4'
    ...
}
复制代码

从上面可以清楚,实际上RePlugin对我们的代码入侵性十分低,同时也通过依赖不同的类库来区分出了宿主和插件。

了解如何使用,我们来思考这个问题,如何才能做到入侵性最少,换句说就是反射最少的系统源码下,能够完成插件的类与资源的读取。

当时我看到这个RePlugin的宣言时候,也在好奇怎么样才能做到最小的入侵性。确实如果真的要实现一个Activity的跳转,我一开始给插件化的基础模型是一个Activity跳转必备的流程,当时确实无法想到更加简单的办法。

反过来思考,在阅读了这么多的插件化源码,变化的永远是我们对不同版本以及对Android源码的理解而进行反射,让系统帮忙完成资源和类的加载。不变的是,我们永远需要通过ClassLoader来查找类。从变化中找不变,难道是对ClassLoader进行处理?在ClassLoader的loadClass的时候找我们想要的类,加载插件?很大胆的想法,而这种大胆的想法,还真的给RePlugin的团队实现了。

不得不说一声,一年前我学习DroidPlugin的时候为这个团队对源码的熟悉程度献上了膝盖。而现在对源码还算熟悉的我,再一次为RePlugin团队的极具开创性的想法再度献上膝盖。看了这么开源库能让我读着,读着就跳起来的,也就这两个库。

##Host-Library宿主库

###Host中的RePlugin的关键类 源码将分为宿主的host库和插件的plugin库分别分析,同时将会根据RePugin的插件服务进程与宿主进程区分来说明。在分析之前,我先对源码中几个重要的类先列出来,这里尽可能的少列出来。个人看法和大佬们的理解角度不一样,列出来的类或许不一样。

####PmBase 作为整个RePlugin的核心类之一。控制了RePlugin的初始化。保存着从包中解析解析出来的占坑信息,类加载器,插件信息,以及其他核心类的实例。换句话说就是插件管理中心

Plugin

代表着RePlugin中插件在内存中的对象,这个类如同Small一样也会解析plugin-buildin.json生成对应的Plugin,同时也会解析外部插件的信息生成Plugin类

Loader

代表着RePlugin实际加载插件数据的执行器。

PmHostSvc

这个是指RePlugin插件管理器的进程总控制器。

PluginServiceServer

是指插件管理进程服务端的服务,只要是控制插件服务端的生命周期。

PluginManagerServer

是指插件管理进程服务端的插件管理中心,主要是通过它来完成跨进程插件操作。

PluginLibraryInternalProxy

是指插件的在宿主进程中实际的操作实现类。

PluginDexClassLoader

是用于插件寻找插件内类的类加载器

RePluginClassLoader

是用于宿主的类加载器。

接下来,我将围绕这几个类来对RePlugin的初始化和启动Activity展开讨论。

RePlugin的启动

这里先给出两幅时序图以及进程初始化的图,第一幅是宿主的,第二幅是插件进程的。注意,这里的时序图只会根据关键信息给出主要流程。下面的源码分析,默认按照多进程框架进行分析。

RePlugin的宿主进程(UI进程)的启动

RePlugin的宿主进程的启动.png

RePlugin的插件管理进程的启动

RePlugin插件管理进程的启动.png

标红的地方就是RePlugin宿主进程和插件管理进程的分割点。在RePlugin启动的时候,就区分出了所谓的UI进程和常驻进程。UI进程也就是我们的宿主主进程,而常驻进程是指插件管理器的进程。这里对启动做进一步的划分。

RePlugin多进程初始化.png

通过两个进程初始化的比较,其实双方的相似度十分高,变化是从PmBase开始。那么两者之间的进程是怎么联系,UI进程是宿主可以默认启动,但是插件进程又是何时启动呢?接下来我将一一分析。提示,这里将不会对AIDL的原理进行分析,想要了解的,可以看看我csdn中对Binder的解析,或者网上也有很多优秀的文章。

实际上插件管理进程的初始化其中还有很多的细节。这里我就以Plugin为主要线索画出的建议流程图。实际上初始化的核心模块几乎都在PmBase中完成。所以我们其实可以先去PmBase中看看初始化的init的方法。

###UI进程的初始化

void init() {

        RePlugin.getConfig().getCallbacks().initPnPluginOverride();

        if (HostConfigHelper.PERSISTENT_ENABLE) {
            // (默认)“常驻进程”作为插件管理进程,则常驻进程作为Server,其余进程作为Client
            if (IPC.isPersistentProcess()) {
                // 初始化“Server”所做工作
                initForServer();
            } else {
                // 连接到Server
                initForClient();
            }
        } else {
            // “UI进程”作为插件管理进程(唯一进程),则UI进程既可以作为Server也可以作为Client
            ...
        }

        // 最新快照
        PluginTable.initPlugins(mPlugins);

        // 输出
     ...
    }
复制代码

开始的时候默认HostConfigHelper.PERSISTENT_ENABLE为打开,允许使用插件进程来维护插件信息。刚开始我们通过Application的attachBaseContext进来的,也就是说此时一定是UI进程,那么一定会走下面的initForClient方法的分支。

从注释可以清楚,常驻进程为插件管理进程,其余的如插件和宿主统统都是客户端进程。

initForClient

 /**
     * Client(UI进程)的初始化
     *
     */
    private final void initForClient() {


        // 1. 先尝试连接
        PluginProcessMain.connectToHostSvc();

        // 2. 然后从常驻进程获取插件列表
        refreshPluginsFromHostSvc();
    }
复制代码

第一个方法是核心。当前作为UI进程会尝试的连接常驻进程。

/**
     * 非常驻进程调用,获取常驻进程的 IPluginHost
     */
    static final void connectToHostSvc() {
        Context context = PMF.getApplicationContext();

        //
        IBinder binder = PluginProviderStub.proxyFetchHostBinder(context);

        if (binder == null) {
            // 无法连接到常驻进程,当前进程自杀
            
            System.exit(1);
        }

        //
        try {
            binder.linkToDeath(new IBinder.DeathRecipient() {

                @Override
                public void binderDied() {
                    
                    // 检测到常驻进程退出,插件进程自杀
                    if (PluginManager.isPluginProcess()) {
                        
                        System.exit(0);
                    }
                    sPluginHostRemote = null;

                    // 断开和插件化管理器服务端的连接,因为已经失效
                    PluginManagerProxy.disconnect();
                }
            }, 0);
        } catch (RemoteException e) {
            // 无法连接到常驻进程,当前进程自杀
            
            System.exit(1);
        }

        //
        sPluginHostRemote = IPluginHost.Stub.asInterface(binder);


        // 连接到插件化管理器的服务端
        // Added by Jiongxuan Zhang
        try {
            PluginManagerProxy.connectToServer(sPluginHostRemote);

            // 将当前进程的"正在运行"列表和常驻做同步
            // TODO 若常驻进程重启,则应在启动时发送广播,各存活着的进程调用该方法来同步
            PluginManagerProxy.syncRunningPlugins();
        } catch (RemoteException e) {
            // 获取PluginManagerServer时出现问题,可能常驻进程突然挂掉等,当前进程自杀
            
            System.exit(1);
        }

        // 注册该进程信息到“插件管理进程”中
        PMF.sPluginMgr.attach();
    }
复制代码

这个方法做两个事情,第一,尝试着通过ContentProvider来查找有没有连接插件进程的PmHostSvc这个插件进程的总控制器的aidl的IBinder。

/**
     * @param context
     * @param selection
     * @return
     */
    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);
            if (cursor == null) {
                
                return null;
            }
            while (cursor.moveToNext()) {
                //
            }
            IBinder binder = BinderCursor.getBinder(cursor);
            
            return binder;
        } finally {
            CloseableUtils.closeQuietly(cursor);
        }
    }
复制代码

第二,尝试的连接着插件进程的服务,通过上面给予的IBinder,这个IBinder指代的是常驻进程的远程端的代理,换句话说就是通过调用常驻进程调用fetchManagerServer,获取常驻进程的插件服务。

/**
     * 连接到常驻进程,并缓存IPluginManagerServer对象
     *
     * @param host IPluginHost对象
     * @throws RemoteException 和常驻进程通讯出现异常
     */
    public static void connectToServer(IPluginHost host) throws RemoteException {
        if (sRemote != null) {
            
            return;
        }

        sRemote = host.fetchManagerServer();
    }
复制代码

是怎么连到插件进程的,这里先埋个伏笔。先假设我们都连接成功了。

refreshPluginsFromHostSvc

从PmHostSvc中刷新插件数据,PmHostSvc现在这里说明了,由于实现了IPluginHost.Stub,所以实际上就是常驻进程的总控制器。

private void refreshPluginsFromHostSvc() {
        List<PluginInfo> plugins = null;
        try {
            plugins = PluginProcessMain.getPluginHost().listPlugins();
        } catch (Throwable e) {
            
        }

        // 判断是否有需要更新的插件
        // FIXME 执行此操作前,判断下当前插件的运行进程,具体可以限制仅允许该插件运行在一个进程且为自身进程中
        List<PluginInfo> updatedPlugins = null;
        if (isNeedToUpdate(plugins)) {
            
            try {
                updatedPlugins = PluginManagerProxy.updateAllPlugins();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        if (updatedPlugins != null) {
            refreshPluginMap(updatedPlugins);
        } else {
            refreshPluginMap(plugins);
        }
    }
复制代码

做了两件事情,先从PluginProcessMain获取常驻进程总控制器的远程代理。读取常驻进程中需要加载的插件信息列表。一旦发现我们有需要加载插件,则立即调用updateAllPlugins,来控制常驻进程的PluginManagerServer来加载安装进来的插件,最后再同步到UI进程的mPluginsList集合中。

PatchClassLoaderUtils.patch

接着也是RePlugin的核心之一,为宿主创造了宿主的ClassLoader,也是整个RePlugin体系唯一Hook的地方。

public static boolean patch(Application application) {
        try {
            // 获取Application的BaseContext (来自ContextWrapper)
            Context oBase = application.getBaseContext();
            ...


            ClassLoader oClassLoader = (ClassLoader) ReflectUtils.readField(oPackageInfo, "mClassLoader");
            if (oClassLoader == null) {
                
                return false;
            }

            // 外界可自定义ClassLoader的实现,但一定要基于RePluginClassLoader类
            ClassLoader cl = RePlugin.getConfig().getCallbacks().createClassLoader(oClassLoader.getParent(), oClassLoader);

            // 将新的ClassLoader写入mPackageInfo.mClassLoader
            ReflectUtils.writeField(oPackageInfo, "mClassLoader", cl);

            // 设置线程上下文中的ClassLoader为RePluginClassLoader
            // 防止在个别Java库用到了Thread.currentThread().getContextClassLoader()时,“用了原来的PathClassLoader”,或为空指针
            Thread.currentThread().setContextClassLoader(cl);

          
        } catch (Throwable e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
复制代码

主要的想法是自己创造一个ClassLoader来替代掉Android系统中用来寻找类的类加载器。根据上面插件化基础框架中可以得知,所有的类查找都是通过获取LoadedAPk中的ClassLoader来查找类。那么这段话的意思就很简单了,就是替换掉系统的ClassLoader。让我们看看Applicaion中Context的mPackageInfo究竟是不是LoadedApk吧。Context的实现类是ContextImpl,我们直接看看里面是什么

final LoadedApk mPackageInfo;
复制代码

确实思路是正确的,这里埋下第二个伏笔,将会在Activity的启动中让我们聊聊这个创建的RePluginClassLoader究竟在整个RePlugin起了什么作用。

callAttach

根据我们的时序图初始化接下来会走到callAttach这个方法,我们看看

final void callAttach() {
        //
        mClassLoader = PmBase.class.getClassLoader();

        // 挂载
        for (Plugin p : mPlugins.values()) {
            p.attach(mContext, mClassLoader, mLocal);
        }

        // 加载默认插件
        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) {
                        
                    }
                    if (rc) {
                        mDefaultPlugin = p;
                        mClient.init(p);
                    }
                }
            }
        }
    }
复制代码

下面部分是核心,如果是插件则会从Plugin中解析一次插件的信息,这里先不谈,到了常驻进程的时候会详细说说这个Plugin.load方法。

###onCreate

final void callAppCreate() {
        // 计算/获取cookie
        if (IPC.isPersistentProcess()) {
            mLocalCookie = PluginProcessMain.getPersistentCookie();
        } else {
...
        if (!IPC.isPersistentProcess()) {
            // 由于常驻进程已经在内部做了相关的处理,此处仅需要在UI进程注册并更新即可
            registerReceiverAction(ACTION_NEW_PLUGIN);
            registerReceiverAction(ACTION_UNINSTALL_PLUGIN);
        }
    }

复制代码

这里则是注册了插件的安装和卸载的监听。

所以,先不论RePlugin的RePluginClassLoader究竟做了什么。实际上,如果启用了多进程框架的RePlugin的管理模式,其实插件的解析和加载都是在常驻进程中完成,而UI进程只是做一次插件信息的同步处理。

RePlugin从UI进程启动常驻进程

还记得我上面的第一个伏笔吧。现在我们回到宿主在连接常驻进程的方法,proxyFetchHostBinder。

//selection
private static final String SELECTION_MAIN_BINDER = "main_binder";
private static final String PROJECTION_MAIN[] = {
        "main"
    };
    private static final String AUTHORITY_PREFIX = IPC.getPackageName() + ".loader.p.main";

//ProcessPitProviderPersist.URI
    public static final Uri URI = Uri.parse("content://" + AUTHORITY_PREFIX + "/main");

    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);
复制代码

还记得ContentProvider的吧。这是内容提供器,因为开发中用的不多,我都几乎都忘记这个Android的四大组件之一的原理,但是用法还是记得的。

首先需要拿到和AndroidManifest中注册的权限和这里对应,找找看注册在注册文件中对应的内容提供器有什么猫腻。

  <provider android:name='com.qihoo360.replugin.component.process.ProcessPitProviderPersist' 
android:authorities='com.qihoo360.replugin.sample.host.loader.p.main'
 android:exported='false' 
android:process=':GuardService' />
复制代码

你会发现这个用来注册在AndroidManifest的内容提供器,是位于GuardService进程的。从这里我们得知,我们在调用getContentResolver().query的方法,从这个GuardService进程中的内容提供器获取我们想要的IBinder。

这里我们可以进一步的猜测,整个常驻进程是不是就是指GuardService呢?

####ContentProvider跨进程启动 这里我们就需要分析一下四大组件ContentProvider的源码了。 在整个App启动进程的时候,会从Zygote.cpp中fork一个新的进程出来,目标类是AppThread。换句话说就是,main方法为进程第一个运行的方法。 ContentProvider安装与跨进程的启动.png

如果不熟悉源码的可以根据我上面给的时序图读一遍源码。这里我稍微解释一下,每一次在进程启动的时候,都会绑定一次Application。加载ContentProvider的时期,这个时候会创建好Instrumentation,并且在makeApplication之后,Instrumentation的onCreate之前。换句话说就是在Application的attchBaseContext和onCreate之后。

            Application app = data.info.makeApplication(data.restrictedBackupMode, null);
            mInitialApplication = app;

            // app's custom Application class
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);

                    mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
                }
            }


            try {
                mInstrumentation.onCreate(data.instrumentationArgs);
            }
            catch (Exception e) {
                throw new RuntimeException(
                    "Exception thrown in onCreate() of "
                    + data.instrumentationName + ": " + e.toString(), e);
            }
复制代码

这里多说一句也很重要,你自己往源码深处查看的时候,你会发现实际上这个加载的内容提供器,实际上会从PMS中解析的数据找出和当前进程名一致的内容提供器,而不一致的会被筛选掉。

我们可以看到PMS中这段源码

@Override
    public @NonNull ParceledListSlice<ProviderInfo> queryContentProviders(String processName,
            int uid, int flags) {
        final int userId = processName != null ? UserHandle.getUserId(uid)
                : UserHandle.getCallingUserId();
        if (!sUserManager.exists(userId)) return ParceledListSlice.emptyList();
        flags = updateFlagsForComponent(flags, userId, processName);

        ArrayList<ProviderInfo> finalList = null;
        // reader
        synchronized (mPackages) {
            final Iterator<PackageParser.Provider> i = mProviders.mProviders.values().iterator();
            while (i.hasNext()) {
                final PackageParser.Provider p = i.next();
                PackageSetting ps = mSettings.mPackages.get(p.owner.packageName);
                if (ps != null && p.info.authority != null
                        && (processName == null
                                || (p.info.processName.equals(processName)
                                        && UserHandle.isSameApp(p.info.applicationInfo.uid, uid)))
                        && mSettings.isEnabledAndMatchLPr(p.info, flags, userId)) {
                    if (finalList == null) {
                        finalList = new ArrayList<ProviderInfo>(3);
                    }
                    ProviderInfo info = PackageParser.generateProviderInfo(p, flags,
                            ps.readUserState(userId), userId);
                    if (info != null) {
                        finalList.add(info);
                    }
                }
            }
        }

        if (finalList != null) {
            Collections.sort(finalList, mProviderInitOrderSorter);
            return new ParceledListSlice<ProviderInfo>(finalList);
        }

        return ParceledListSlice.emptyList();
    }
复制代码

结合源码解析出来的结果,以及RePlugin的源码。我们可以很清楚的明白,以下两点:

  1. 实际上当我们第一次加载ContentProvider的时候是没有标记为GuardService进程的内容提供器。必须是通过我们ContentProvider.query的操作调起我们的常驻进程。
context.getContentResolver().query(uri, PROJECTION_MAIN, selection, null, null);
复制代码

这段代码可说是UI进程初始化的核心。负担了两个角色,第一调起常驻进程,第二获取常驻进程的总控制器的远程代理。

  1. 当我们重新拉起进程的时候,会重新走一边Application的初始化,也就是说会再走一次我们RePlugin的初始化代码,不同的是这一次是常驻进程,所以将会走到了不同的分支。

RePlugin的常驻进程的启动

通过ContentProvider的源码的阅读,也就能够明白为什么顺序是上面的时序图样子。

还是一样我们直奔PmBase的initForServer

initForServer

private final void initForServer() {

        mHostSvc = new PmHostSvc(mContext, this);
        PluginProcessMain.installHost(mHostSvc);
        PluginProcessMain.schedulePluginProcessLoop(PluginProcessMain.CHECK_STAGE1_DELAY);

        // 兼容即将废弃的p-n方案 by Jiongxuan Zhang
        mAll = new Builder.PxAll();
        Builder.builder(mContext, mAll);
        refreshPluginMap(mAll.getPlugins());

        // [Newest!] 使用全新的RePlugin APK方案
        // Added by Jiongxuan Zhang
        try {
            List<PluginInfo> l = PluginManagerProxy.load();
            if (l != null) {
         
                refreshPluginMap(l);
            }
        } catch (RemoteException e) {

        }
    }
复制代码

做了几件事情,这里分别实例化了PmHostSvc,这个服务端的总控制器,以及内部的PluginServiceServer和PluginManagerServer。

installHost

static final void installHost(IPluginHost host) {
        sPluginHostLocal = host;
        // 连接到插件化管理器的服务端
        // Added by Jiongxuan Zhang
        try {
            PluginManagerProxy.connectToServer(sPluginHostLocal);
        } catch (RemoteException e) {
            // 基本不太可能到这里,直接打出日志
            
        }
    }
复制代码

    public static void connectToServer(IPluginHost host) throws RemoteException {
        if (sRemote != null) {
            
            return;
        }

        sRemote = host.fetchManagerServer();
    }
复制代码
 @Override
    public IPluginManagerServer fetchManagerServer() throws RemoteException {
        return mManager.getService();
    }
复制代码

这里很有意思,我们会再一次的尝试着通过PmHostSvc去获取IPluginManagerServer对象,但是实际上我们IPluginManagerServer这个对象现在指的是PluginManagerServer,已经在在PmHostSvc中实例化出来。

PmHostSvc(Context context, PmBase packm) {
        mContext = context;
        mPluginMgr = packm;
        mServiceMgr = new PluginServiceServer(context);
        mManager = new PluginManagerServer(context);
    }
复制代码

虽然并不影响使用,但是按照我们多进程插件框架来说,这里的意思是在常驻进程中通过AIDL再一次的和常驻进程的服务端进行通信。相当于对着常驻进程再度切开两个接口PluginServiceServer和PluginManagerServer让外部进行跨进程通信。

这么做是为了在关闭常驻进程模式的时候,UI进程将会作为服务端和客服端,让其他插件的进程链接进来。

Builder.builder

static final void builder(Context context, PxAll all) {
        // 搜索所有本地插件和V5插件
        Finder.search(context, all);

        // 删除不适配的PLUGINs
        for (PluginInfo p : all.getOthers()) {
            // TODO 如果已存在built-in和V5则不删除
            
            boolean rc = p.deleteObsolote(context);
            if (!rc) {
                
            }
        }

        // 删除所有和PLUGINs不一致的DEX文件
        deleteUnknownDexs(context, all);

        // 删除所有和PLUGINs不一致的SO库目录
        // Added by Jiongxuan Zhang
        deleteUnknownLibs(context, all);

        // 构建数据
    }
复制代码

这个方法中,我们着重看看Finder.search(context, all);扫描所有的插件数据并且转化为Plugin类

/**
     * 扫描插件
     */
    static final void search(Context context, PxAll all) {
        // 扫描内置插件
        FinderBuiltin.loadPlugins(context, all);

        // 扫描V5插件
        File pluginDir = context.getDir(Constant.LOCAL_PLUGIN_SUB_DIR, 0);
        V5Finder.search(context, pluginDir, all);

        // 扫描现有插件,包括刚才从V5插件文件更新过来的文件
        HashSet<File> deleted = new HashSet<File>();
        {
            
            searchLocalPlugins(pluginDir, all, deleted);
        }

        // 删除非插件文件和坏的文件
        for (File f : deleted) {
            
            boolean rc = f.delete();
            if (!rc) {
                
            }
        }
        deleted.clear();
    }
复制代码

让我们先看看内置插件的逻辑

static final void loadPlugins(Context context, PxAll all) {
        InputStream in;

        // 读取内部配置
        in = null;
        try {
            in = context.getAssets().open("plugins-builtin.json");

            readConfig(in, all);
        } catch (FileNotFoundException e0) {
            
        } catch (Throwable e) {
            
        }
        CloseableUtils.closeQuietly(in);
    }

复制代码

有点意思的地方是,这里实际上和Small的思路很像。也是通过读取Asset文件夹下面的plugins-builtin.json文件来获取插件信息。这个json文件并不需要我们自己编写实际上会通过host-gradle的gradle插件自己生成的。最后把数据保存到PxAll缓存中。

我的gradle插件这部分不太熟悉,但是好在gradle插件的语法简单,我们可以简单的明白这个json是通过读取插件中AndroidManifest里面的内容,生成的json文件

public PluginInfoParser(File pluginFile, def config) {

        pluginInfo = new PluginInfo()

        ApkFile apkFile = new ApkFile(pluginFile)

        String manifestXmlStr = apkFile.getManifestXml()
        ByteArrayInputStream inputStream = new ByteArrayInputStream(manifestXmlStr.getBytes("UTF-8"))

        SAXParserFactory factory = SAXParserFactory.newInstance()
        SAXParser parser = factory.newSAXParser()
        parser.parse(inputStream, this)

        String fullName = pluginFile.name
        pluginInfo.path = config.pluginDir + "/" + fullName

        String postfix = config.pluginFilePostfix
        pluginInfo.name = fullName.substring(0, fullName.length() - postfix.length())
    }
复制代码

plugins-builtin.json里面的json是这样的。包含了插件的包名,路径名,版本以及其他信息。

{"high":null,"frm":null,"ver":104,"low":null,"pkg":"com.qihoo360.replugin.sample.demo1","path":"plugins/demo1.jar","name":"demo1"}
复制代码

通过这些初步的生成了在Asset文件夹中内置的插件信息。和Small不同的,Small的build.json除了可以制定插件名还可以制定模块名以及相应的规则。

我们在看看所谓的V5插件也就是不存在Asset的外部插件,当然也有目录限制,就在“plugins_v3”这里,这里一般是指从外部下载进来的插件。

static final void search(Context context, File pluginDir, PxAll all) {
        // 扫描V5下载目录
        ArrayList<V5FileInfo> v5Plugins = new ArrayList<V5FileInfo>();
        {
            File dir = RePlugin.getConfig().getPnInstallDir();
            
            searchV5Plugins(dir, v5Plugins);
        }

        // 同步V5原始插件文件到插件目录
        for (V5FileInfo p : v5Plugins) {

            ProcessLocker lock = new ProcessLocker(RePluginInternal.getAppContext(), p.mFile.getParent(), p.mFile.getName() + ".lock");

            if (lock.isLocked()) {
                // 插件文件不可用,直接跳过
                continue;
            }

            PluginInfo info = p.updateV5FileTo(context, pluginDir, false, true);
            // 已检查版本
            if (info == null) {
               
            } else {
                all.addV5(info);
            }
        }
    }
复制代码

这里的思路会筛选出能够使用的下载哈见接着再更新到“plugins_v3”这个目录中,并且把数据保存到PxAll缓存中。这个下载路径我们可以通过RePlugin初始化配置下调配,这里就不多说了。

searchLocalPlugins(pluginDir, all, deleted);
复制代码

这一句将会做一次对加载进来的信息,做一次筛选,找出是否版本号不一致需要更新的,是否有最新的需要更新等。

PluginManagerProxy.load();

我们再看看

PluginManagerProxy.load();
复制代码

这个方法实际上调用的是PluginManagerServer远程端的load方法。我们看看load方法。

@Override
        public List<PluginInfo> load() throws RemoteException {
            synchronized (LOCKER) {
                return PluginManagerServer.this.loadLocked();
            }
        }

    private List<PluginInfo> loadLocked() {
        if (!mList.load(mContext)) {
            return null;
        }

        // 执行“更新或删除Pending”插件,并返回结果
        return updateAllLocked();
    }

public boolean load(Context context) {
         try {
            // 1. 新建或打开文件
            File d = context.getDir(Constant.LOCAL_PLUGIN_APK_SUB_DIR, 0);
            File f = new File(d, "p.l");
            if (!f.exists()) {
                // 不存在?直接创建一个新的即可
                if (!f.createNewFile()) {
                    
                    return false;
                } else {
                    
                    return true;
                }
            }

            // 2. 读出字符串
            String result = FileUtils.readFileToString(f, Charsets.UTF_8);
            if (TextUtils.isEmpty(result)) {
               
                return false;
            }

            // 3. 解析出JSON
            mJson = new JSONArray(result);

        } 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);
                if (pi == null) {
                    
                    continue;
                }
                addToMap(pi);
            }
        }
        return true;
    }
复制代码

这里是指RePlugin将会检测通过install进来的安装进来的apk插件的安装信息,读取其中的json数据,再更新插件列表信息,同时生成PluginInfo这个外部插件的信息,并且更新到之前的json数据文件中。

通过这种方式,RePlugin控制了宿主内的插件,下载的V5插件以及安装进来的apk插件。

refreshPluginMap


    private final void refreshPluginMap(List<PluginInfo> plugins) {
        if (plugins == null) {
            return;
        }
        for (PluginInfo info : plugins) {
            Plugin plugin = Plugin.build(info);
            putPluginObject(info, plugin);
        }
    }


    private void putPluginObject(PluginInfo info, Plugin plugin) {
        if (mPlugins.containsKey(info.getAlias()) || mPlugins.containsKey(info.getPackageName())) {
            

            // 找到已经存在的
            Plugin existedPlugin = mPlugins.get(info.getPackageName());
            if (existedPlugin == null) {
                existedPlugin = mPlugins.get(info.getAlias());
            }

            if (existedPlugin.mInfo.getVersion() < info.getVersion()) {
               

                // 同时加入PackageName和Alias(如有)
                mPlugins.put(info.getPackageName(), plugin);
                if (!TextUtils.isEmpty(info.getAlias())) {
                    // 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已
                    mPlugins.put(info.getAlias(), plugin);
                }
            } else {
                
            }
        } else {
            // 同时加入PackageName和Alias(如有)
            mPlugins.put(info.getPackageName(), plugin);
            if (!TextUtils.isEmpty(info.getAlias())) {
                // 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已
                mPlugins.put(info.getAlias(), plugin);
            }
        }
    }
复制代码

此时,无论是UI还是常驻进程将会通过refreshPluginMap通过PluginManagerServer生成的消息把安装的apk插件同步到内存中。

callAttach

由于不是插件进程,所以并没有在意的地方。时序图后面的将会在Activity启动拿出来详细。

总结:初始化,实际上做的工作主要有两点: 第一,连接常驻进程,并且初始化相关的工作,如ClassLoader的替换等 第二,通过常驻进程解析插件信息,并且同步到UI进程。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改