Replugin源码解析——进程的管理

1,864 阅读15分钟
原文链接: mp.weixin.qq.com

奇技指南

本文将从源码角度分析Replugin的进程管理机制。我们将重点分析进程的启动和插件的加载2个过程。

本文转载自奇卓社。

本文将从源码角度分析Replugin的进程管理机制。我们将重点分析进程的启动和插件的加载2个过程。

1

进程的启动

首先系统调用RePluginApplication的attachBaseContext方法。

接着调用RePlugin.App.attachBaseContext。

代码如下:

/**

* (推荐)当Application的attachBaseContext调用时需调用此方法 <p>

* 可自定义插件框架的行为。参见RePluginConfig类的说明

* @param app Application对象

@see Application#attachBaseContext(Context)

* @see RePluginConfig

* @since 1.2.0

*/

public static void attachBaseContext(Application app, RePluginConfig config) {

    if (sAttached) {//避免重复初始化

        if (LogDebug.LOG) {

            LogDebug.d(TAG, "attachBaseContext: Already called");

        }

        return;

    }

    RePluginInternal.init(app); //设置AppContext引用,方便框架内部调用。

    sConfig = config;

    sConfig.initDefaults(app);

    IPC.init(app); //初始化IPC进程通信类,初始化设置当前进程的名称、pid、包名、常驻进程名称、是否是UI进程、是否是常驻进程等属性值。

    // 打印当前内存占用情况

    // 只有开启“详细日志”才会输出,防止“消耗性能”

    if (LOG && RePlugin.getConfig().isPrintDetailLog()) {

        LogDebug.printMemoryStatus(LogDebug.TAG, "act=, init, flag=, Start, pn=, framework, func=, attachBaseContext, lib=, RePlugin");

    }

    // 初始化HostConfigHelper(通过反射HostConfig来实现)

    // NOTE 一定要在IPC类初始化之后才使用

    HostConfigHelper.init();

    // FIXME 此处需要优化掉

    AppVar.sAppContext = app;

    // Plugin Status Controller

    PluginStatusController.setAppContext(app); //PluginStatusController是插件状态管理类

    PMF.init(app);//初始化

    PMF.callAttach();

    sAttached = true;

}

这里包括以下几个主要过程:

1)RePluginInternal.init(app); 设置AppContext引用,方便框架内部调用。

2)框架config相关配置初始化。

3)初始化IPC进程通信类,初始化设置当前进程的名称、pid、包名、常驻进程名称、是否是UI进程、是否是常驻进程等属性值。

4)PMF的初始化。

5)PMF调用callAttach()。

我们将详细分析以上过程。

1

RePluginInternal的init()方法

static void init(Application app) {

    sAppContext = app;

}

该方法非常简单,报错app的引用,以便在框架中方便的获取APP的Context对象和宿主注册时的ClassLoader对象。

2

初始化框架配置

sConfig = config;

sConfig.initDefaults(app);

3

初始化IPC进程通信类

IPC类,用于“进程间通信”的类。插件和宿主可使用此类来做一些跨进程发送广播、判断进程等工作。

/**

* [HIDE] 外界请不要调用此方法

*/

public static void init(Context context) {

    sCurrentProcess = SysUtils.getCurrentProcessName();

    sCurrentPid = Process.myPid();

    sPackageName = context.getApplicationInfo().packageName;

    // 设置最终的常驻进程名

    if (HostConfigHelper.PERSISTENT_ENABLE) {

        String cppn = HostConfigHelper.PERSISTENT_NAME;

        if (!TextUtils.isEmpty(cppn)) {

            if (cppn.startsWith(":")) {

                sPersistentProcessName = sPackageName + cppn;

            } else {

                sPersistentProcessName = cppn;

            }

        }

    } else {

        sPersistentProcessName = sPackageName;

    }

    sIsUIProcess = sCurrentProcess.equals(sPackageName);

    sIsPersistentProcess = sCurrentProcess.equals(sPersistentProcessName);

}

初始化设置当前进程的名称、pid、包名、常驻进程名称、是否是UI进程、是否是常驻进程等属性值。

4

PMF的初始化方法init()

/**

* @param application

*/

public static final void init(Application application) {

    setApplicationContext(application); //保存appliction的引用

    PluginManager.init(application); //获取进程的uid和进程index值(进程在插件化框架中对应的int值,默认值是Integer.MIN_VALUE)。

    sPluginMgr = new PmBase(application); //创建PmBase实例。详情见下文“PmBase类”

    sPluginMgr.init();//初始化PmBase实例。详情见下文“PmBase类”

    Factory.sPluginManager = PMF.getLocal();

    Factory2.sPLProxy = PMF.getInternal();

    PatchClassLoaderUtils.patch(application);//hook宿主的classLoader,替换为RePluginClassLoader

}

该方法做了很多Replugin初始化相关的工作,包括框架核心类之一的PmBase的初始化,宿主classLoader的替换等。

我们根据初始化的次序依次来看重要的调用过程。

4.1

PluginManager初始化

PluginManager每个进程都是独有一份,PluginManager初始化方法init(Context context):

1)初始化当前进程的sUid。

2)初始化当前进程的sPluginProcessIndex,

规则:默认值为Integer.MIN_VALUE;UI进程值为-1;如果是自定义进程p1~p3,则对应的值为-100~-98;如果进程名包含loader(Stub进程),则值为进程名正则后包含的数字内容(0~1)。

4.2

4.2 PmBase类相关初始化

1、构造函数

 context赋值;   

     初始化Provider和Services前缀:

     Provider(UI进程) = 包名+".loader.p.Provider"+N1; Provider(Stub进程) = 包名+".loader.p.Provider"+PluginManager.sPluginProcessIndex;

     Services(UI进程) = 包名+".loader.s.Service"+N1; Services(Stub进程) = 包名+" .loader.s.Service"+PluginManager.sPluginProcessIndex;

     创建PluginProcessPer(继承IPluginClient.Stub,用于常驻进程与客户端进程之间的通信) 、PluginCommImpl(宿主与插件、插件间通信使用)、PluginLibraryInternalProxy(用于插件对主框架的内部调用接口处理)实例。

2、init()

 初始化常驻及客户端进程,注册进程到进程管理列表中。

 if(使用常驻进程){

     if(当前进程是常驻进程){

          Persistent(常驻)进程的初始化 //见3)

     }else{

          初始化Client进程端 //见4

     }

 }else{

     if(UI进程){

          Persistent(常驻)进程的初始化 //见3)

          调用PMF.sPluginMgr.attach(),注册该进程信息到“插件管理进程”中 

     }else{

          初始化Client进程端 //见4

     }

 }

// 最新快照

PluginTable.initPlugins(mPlugins);

3、常驻进程初始化

创建PmHostSvc

调用PluginProcessMain.installHost,使用PmHostSvc缓存自己的 IPluginHost。

PluginProcessMain.schedulePluginProcessLoop注册Stub进程退出检查。

if(当前是常驻进程){

     清空plugins_up.xml  

}

读取p-n插件信息。

读取apk方案插件信息。//先处理p-n插件,再处理apk插件,优先使用apk插件。

更新插件列表信息(更新plugins_up.xml) 

4、初始化Client进程端

  • 尝试连接

PluginProcessMain.connectToHostSvc(){

     获取常驻进程的IPluginHost

     注册常驻死亡监听(常驻binder死亡监听,如果常驻退出,Stub进程自杀)

     调用PluginManagerProxy.syncRunningPlugins();将当前进程的"正在运行"列表和常驻做同步

     调用PmBase的attach() //注册该进程信息到“插件管理进程”中

}

  • 从常驻进程获取插件列表

refreshPluginsFromHostSvc() {

     从常驻进程中获取插件列表,并检查是否存在插件更新。

     更新插件信息到"plugins_up"文件。 

}

5、把进程注册到“插件管理进程”中(PmBase的attach() 方法)

final void attach() {

    try {

        mDefaultPluginName = PluginProcessMain.getPluginHost().attachPluginProcess(IPC.getCurrentProcessName(), PluginManager.sPluginProcessIndex, mClient, mDefaultPluginName);

    } catch (Throwable e) {

        if (LOGR) {

            LogRelease.e(PLUGIN_TAG, "c.n.a: " + e.getMessage(), e);

        }

    }

}

调用PmHostSvc的attachPluginProcess方法:

@Override

public String attachPluginProcess(String process, int index, IBinder binder, String def) throws RemoteException {

    int pid = Binder.getCallingPid();

    IPluginClient client = null;

    try {

        client = IPluginClient.Stub.asInterface(binder);

    } catch (Throwable e) {

        if (LOGR) {

            LogRelease.e(PLUGIN_TAG, "a.p.p pc.s.ai: " + e.getMessage(), e);

        }

    }

    if (client == null) {

        return null;

    }

    return PluginProcessMain.attachProcess(pid, process, index, binder, client, def, mManager);

}

创建IPluginClient对象,然后调用PluginProcessMain.attachProcess()。

static final String attachProcess(int pid, String process, int index, IBinder binder, IPluginClient client, String def, PluginManagerServer pms) {

    final String plugin;

    synchronized (PROCESSES) {

        plugin = attachProcessLocked(pid, process, index, binder, client, def);

    }

    final ProcessClientRecord pr = new ProcessClientRecord(pms);

    pr.name = process;

    pr.plugin = plugin;

    pr.pid = pid;

    pr.index = index;

    pr.binder = binder;

    pr.client = client;

    try {

        pr.binder.linkToDeath(pr, 0);

    } catch (Throwable e) {

        if (LOGR) {

            LogRelease.e(PLUGIN_TAG, "ap l2d: " + e.getMessage(), e);

        }

    }

    writeProcessClientLock(new Action<Void>() {

        @Override

        public Void call() {

            ALL.put(pr.name, pr);

            return null;

        }

    });

    return plugin;

}

PluginProcessMain的attachProcess方法:1)获取插件名称,如果是stub进程,则更新stub进程信息及状态。2)创建ProcessClientRecord对象并put到ALL列表中。3)注册进程死亡通知。

5

PMF的callAttach()方法

直接调用PmBase的callAttach()方法。

final void callAttach() {

    mClassLoader = PmBase.class.getClassLoader();

    // 挂载

    for (Plugin p : mPlugins.values()) {

        p.attach(mContext, mClassLoader, mLocal);

    }

    // 加载默认插件

    if (PluginManager.isPluginProcess()) {

        Log.d("process manager", "bu01 PmBase callAttach : 加载默认插件 mDefaultPluginName = " + mDefaultPluginName);

        if (!TextUtils.isEmpty(mDefaultPluginName)) {

            //

            Plugin p = mPlugins.get(mDefaultPluginName);

            if (p != null) {

                boolean rc = p.load(Plugin.LOAD_APP, true);

                if (!rc) {

                    if (LOG) {

                        LogDebug.d(PLUGIN_TAG, "failed to load default plugin=" + mDefaultPluginName);

                    }

                }

                if (rc) {

                    mDefaultPlugin = p;

                    mClient.init(p);

                }

            }

        }

    }

}

1)首先给mClassLoader赋值。

2)传递给插件宿主相关信息。

3)如果进程是stub进程,则加载默认插件。

附:

进程在Replugin中所对应的id值如下:

进程名

UI进程

-1

常驻进程

-2

loader0~loader1

0~1

p1~p3

-100~-98

2

插件的加载(插件列表的更新)

进程信息初始化完毕后,当有新插件需要加载时,会执行以下流程

1

Replugin的install方法开始

public static PluginInfo install(String path) {

    // 判断文件合法性

    ……

    // 若为p-n开头的插件,则必须是从宿主设置的“插件安装路径”上(默认为files目录)才能安装,其余均不允许

    ……

    return MP.pluginDownloaded(path);

}

2

MP的pluginDownloaded方法

public static final PluginInfo pluginDownloaded(String path) {

    /**

    * 问题描述:

    * 对于正在生效的插件,如果当前时机pluginHost没有存活,那么这里会先启动pluginHost,然后再调用它的PluginHost进程的pluginDownloaded接口

    * 这里的问题是:pluginHost进程在启动过程会通过扫描文件的方式将当前即将生效的插件识别到,

    * 而在进程ready后,再去调用pluginDownloaded接口的时候会认为它不是新插件,从而不会通过NEW_PLUGIN广播来周知所有进程新插件生效了

    * 因此,当前进程也不会同步新插件生效的逻辑。

    * so,问题就来了,当前进程新下载的插件由于pluginHost的逻辑无法正常生效。

    * 当然该问题只针对p-n格式的插件,而纯APK格式的插件不再使用进程启动的时候通过扫描文件目录的方式来来识别所有准备好的插件

    * 解决办法:

    * 对于处于该流程的插件文件(p-n插件)加上lock文件,以表示当前插件正在生效,不需要plugHost进程再次扫描生效了,也就不存在新插件在新进程中成为了老插件

    */

    ProcessLocker lock = null;

    try {

        //文件锁初处理

        ……

        PluginInfo info = PluginProcessMain.getPluginHost().pluginDownloaded(path);

        if (info != null) {

            RePlugin.getConfig().getEventCallbacks().onInstallPluginSucceed(info);

        }

        return info;

    } catch (Throwable e) {

    } finally {

        // 去锁

    }

    return null;

}

3

调用PmHostSvc的pluginDownloaded方法

(转到常驻进程中处理)

@Override

public PluginInfo pluginDownloaded(String path) throws RemoteException {

    if (LOG) {

        LogDebug.d(PLUGIN_TAG, "pluginDownloaded: path=" + path);

    }

    // 通过路径来判断是采用新方案,还是旧的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-")) {

        pi = pluginDownloadedForPn(path);

    } else {

        pi = mManager.getService().install(path);

    }

    if (pi != null) {

        // 通常到这里,表示“安装已成功”,这时不管处于什么状态,都应该通知外界更新插件内存表

        syncInstalledPluginInfo2All(pi);

    }

    return pi;

}

1)进程插件安装,p-n插件和apk插件执行不同的逻辑。

2)插件释放后通知进程更新插件列表。接下来重点分析下该过程。

4

PmHostSvc的syncInstalledPluginInfo2All方法

(常驻进程中)

private void syncInstalledPluginInfo2All(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,这样最外面拿到的就是安装成功的新插件信息,符合开发者的预期

    PluginInfo needToSyncPi;

    PluginInfo parent = pi.getParentInfo();

    if (parent != null) {

        needToSyncPi = parent;

    } else {

        needToSyncPi = pi;

    }

    // 在常驻进程内更新插件内存表

    mPluginMgr.newPluginFound(needToSyncPi, false);

    // 通知其它进程去更新

    Intent intent = new Intent(PmBase.ACTION_NEW_PLUGIN);

    intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, mNeedRestart);

    intent.putExtra("obj", (Parcelable) needToSyncPi);

    IPC.sendLocalBroadcast2AllSync(mContext, intent);

    if (LOG) {

        LogDebug.d(TAG, "syncInstalledPluginInfo2All: Sync complete! syncPi=" + needToSyncPi);

    }

}

1)首先检查该插件释放正在运行,如果正在运行则获取正在运行插件版本的PluginInfo对象。

2)通知常驻进程更新插件列表。

3)通过广播的方式,通知其他进程更新插件列表。广播是在进程onCreate时进行注册的(所有的非常驻进程都会进行注册)。

以下步骤发生在所有进程中(常驻进程先调用的)

5

PmBase的newPluginFound方法

final void newPluginFound(PluginInfo info, boolean persistNeedRestart) {

    // 更新最新插件表

    PluginTable.updatePlugin(info);

    // 更新可加载插件表

    insertNewPlugin(info);

    // 清空插件的状态(解禁)

    PluginStatusController.setStatus(info.getName(), info.getVersion(), PluginStatusController.STATUS_OK);

    if (IPC.isPersistentProcess()) {

        persistNeedRestart = mNeedRestart;

    }

    // 输出一个日志

    if (LOGR) {

        LogRelease.i(PLUGIN_TAG, "p.m. n p f n=" + info.getName() + " b1=" + persistNeedRestart + " b2=" + mNeedRestart);

    }

    // 通知本进程:通知给外部使用者

    Intent intent = new Intent(RePluginConstants.ACTION_NEW_PLUGIN);

    intent.putExtra(RePluginConstants.KEY_PLUGIN_INFO, (Parcelable) info);

    intent.putExtra(RePluginConstants.KEY_PERSIST_NEED_RESTART, persistNeedRestart);

    intent.putExtra(RePluginConstants.KEY_SELF_NEED_RESTART, mNeedRestart);

    LocalBroadcastManager.getInstance(mContext).sendBroadcast(intent);

}

1)更新PluginTable中的插件列表。

2)更新可加载插件列表。

3)清空插件的状态。

4)通知本进程:通知给外部使用者。(当前进程内部的消息通知,可以通过Replugin的registerInstalledReceiver方法进行注册)

6

PmBase的insertNewPlugin方法

final void insertNewPlugin(PluginInfo info) {

    synchronized (LOCKER) {

        // 检查插件是否已经被禁用

        if (RePlugin.getConfig().getCallbacks().isPluginBlocked(info)) {

            if (LOG) {

                LogDebug.d(PLUGIN_TAG, "insert new plugin: plugin is blocked, in=" + info);

            }

            return;

        }

        Plugin p = mPlugins.get(info.getName());

        // 如果是内置插件,新插件extract成功,则直接替换

        // TODO 考虑加锁?

        if (p != null && p.mInfo.getType() == PluginInfo.TYPE_BUILTIN && info.getType() == PluginInfo.TYPE_PN_INSTALLED) {

            // next

        } else if (p != null && p.isInitialized()) {

            // 检查该插件是否已加载

            if (LOG) {

                LogDebug.d(PLUGIN_TAG, "insert new plugin: failed cause plugin has loaded, plugin=" + info);

            }

            // 设置是否需要重启标志

            mNeedRestart = true;

            return;

        }

        // 此处直接使用该插件,没有考虑是否只采用最新版

        if (LOG) {

            LogDebug.d(PLUGIN_TAG, "insert new plugin: ok: plugin=" + info);

        }

        Plugin plugin = Plugin.build(info);

        plugin.attach(mContext, mClassLoader, mLocal);

        // 同时加入PackageName和Alias(如有)

        putPluginObject(info, plugin);

    }

}

最终调用putPluginObject方法添加插件到插件列表中。

7

PmBase的putPluginObject方法

/**

* 把插件Add到插件列表

* @param info  待add插件的PluginInfo对象

* @param plugin 待add插件的Plugin对象

*/

private void putPluginObject(PluginInfo info, Plugin plugin) {

    if (HostConfigHelper.PERSISTENT_ENABLE && IPC.isPersistentProcess()) {

        if (RePluginInternal.FOR_DEV) {

            Log.d("plugins_up", "此时,常驻进程中的:" + info.getName() + "插件,版本号是:" + plugin.mInfo.getVersion());

        }

        SharedPreferences sharedPreferences = Pref.getSharedPreferences("plugins_up");

        SharedPreferences.Editor editor = sharedPreferences.edit();

        editor.putInt(info.getName(), plugin.mInfo.getVersion());

        editor.apply();

    }

    if (mPlugins.containsKey(info.getAlias()) || mPlugins.containsKey(info.getPackageName())) {

        if (LOG) {

            LogDebug.d(PLUGIN_TAG, "当前内置插件列表中已经有" + info.getName() + ",需要看看谁的版本号大。");

        }

        // 找到已经存在的

        Plugin existedPlugin = mPlugins.get(info.getPackageName());

        if (existedPlugin == null) {

            existedPlugin = mPlugins.get(info.getAlias());

        }

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

            if (LOG) {

                LogDebug.d(PLUGIN_TAG, "新传入的纯APK插件, name=" + info.getName() + ", 版本号比较大,ver=" + info.getVersion() + ",以TA为准。");

            }

            // 同时加入PackageName和Alias(如有)

            mPlugins.put(info.getPackageName(), plugin);

            if (!TextUtils.isEmpty(info.getAlias())) {

                // 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已

                mPlugins.put(info.getAlias(), plugin);

            }

        } else {

            if (LOG) {

                LogDebug.d(PLUGIN_TAG, "新传入的纯APK插件" + info.getName() + "版本号还没有内置的大,什么都不做。");

            }

        }

    } else {

        // 同时加入PackageName和Alias(如有)

        mPlugins.put(info.getPackageName(), plugin);

        if (!TextUtils.isEmpty(info.getAlias())) {

            // 即便Alias和包名相同也可以再Put一次,反正只是覆盖了相同Value而已

            mPlugins.put(info.getAlias(), plugin);

        }

    }

}

把新插件put到插件列表中。如果是常驻进程,则更新plugins_up缓存文件。

好了,到这里,相信大家已经对Replugin进程的管理过程有所了解了,接下来我会继续进行Replugin其他模块的源码解析,敬请期待~~~

界世的你当不

只作你的肩膀

 360官方技术公众号 

技术干货|一手资讯|精彩活动

空·