Replugin 全面解析 (4)

在前两篇分析的基础上,这篇我们来看看Replugin是如何支持Service组件的。

本篇会包含以下内容:

  • Service 启动流程
  • PluginServiceServer子系统的设计
  • Replugin自定义进程实现原理
  • Replugin自定义进程启动流程

注意,本文中多处用PSS表示PluginServiceServer

Service 启动流程

在上一篇有提到插件中的Activity以及Applicatioin中的Context对象被替换成了PluginContext,并且在Android中启动Service职能通过Context对象的startServicebindService函数,因此Replugin在PluginContext中重写了这两个函数,以调用自己设计的启动流程。

本篇我们以startService函数为例来讲解,bindService就不赘述了,大家有兴趣的话可以参考本篇的内容,跟一跟代码。

PluginContext.startService中有一点值得注意的是mContextInjector,它为用户提供了能够自定义Service启动前后的附加操作的途径。

public ComponentName startService(Intent service) {
    if (mContextInjector != null) {
        mContextInjector.startServiceBefore(service);
    }
    try {
        return PluginServiceClient.startService(this, service, true); //Replugin的启动逻辑
    } catch (PluginClientHelper.ShouldCallSystem e) {
        return super.startService(service); // 若打开插件出错,则直接走系统逻辑
    } finally {
        if (mContextInjector != null) {
            mContextInjector.startServiceAfter(service);
        }
    }
}

PluginServiceClient.startService首先获取ComponentName,接着找到运行Service的进程,然后找到PSS的对象,它是一个IPluginServiceServer的一个实例化对象,并用它执行启动Service的动作。

public static ComponentName startService(Context context, Intent intent, boolean throwOnFail) {
    ComponentName cn = getServiceComponentFromIntent(context, intent); //获取 ComponentName
    int process = getProcessByComponentName(cn); // 获取Service运行的进程
    ......
    intent.setComponent(cn);

    IPluginServiceServer pss = sServerFetcher.fetchByProcess(process);
    ......   
    try {
        return pss.startService(intent, sClientMessenger); // 开启服务
    } catch (Throwable e) {
    }
    return null;
}

PluginServiceClient.getProcessByComponentName最终会调用 PluginClientHelper.getProcessInt,你可以看到Service可以在运行在UI进程,Persistent进程以及自定义进程中。

public static Integer getProcessInt(String processName) {
    if (!TextUtils.isEmpty(processName)) {
        // 插件若想将组件定义成在"常驻进程"中运行,则可以在android:process中定义:
        // 1. ":GuardService"。这样无论宿主的常驻进程名是什么,都会定向到"常驻进程"
        String pntl = processName.toLowerCase();
        String ppdntl = RePluginConstants.PERSISTENT_NAME_DEFAULT.toLowerCase();
        if (pntl.contains(ppdntl)) {
            return IPluginManager.PROCESS_PERSIST;
        }
        // 2. 和宿主常驻进程名相同,这样也会定向到"常驻进程",但若移植到其它宿主上则会出现问题
        String ppntl = IPC.getPersistentProcessName().toLowerCase();
        if (TextUtils.equals(pntl, ppntl)) {
            return IPluginManager.PROCESS_PERSIST;
        }
        // 3. 自定义进程
        processName = PluginProcessHost.processTail(processName.toLowerCase());
        if (PROCESS_INT_MAP.containsKey(processName)) {
            return PROCESS_INT_MAP.get(processName);
        }
    }
    return IPluginManager.PROCESS_UI;
}

PluginServiceServerFetcher.fetchByProcess用于获取PSS对象,如果是运行在Persistent进程,用IPluginHost来获取PSS,如果是其他进程,则使用IPluginClient来获取。

稍后会详细讲解IPluginHostIPluginClient,他们的用法有一点tricky。

public IPluginServiceServer fetchByProcess(int process) {
    ......
    try {
        if (process == IPluginManager.PROCESS_PERSIST) {
            IPluginHost ph = PluginProcessMain.getPluginHost();
            pss = ph.fetchServiceServer();
        } else {
            PluginBinderInfo pbi = new PluginBinderInfo(PluginBinderInfo.NONE_REQUEST);
            IPluginClient pc = MP.startPluginProcess(null, process, pbi);
            pss = pc.fetchServiceServer();
        }
        ......
    } catch (Throwable e) {
    }
    ......
    return pss;
}

最后一个大的步骤就是通过Binder通信机制远程调用PSS服务启动ServicePluginServiceServer.startServiceLocked先通过retrieveServiceLocked获取或者创建一个ServiceRecord对象,然后用installServiceIfNeededLocked开始运行Service的生命周期函数

ComponentName startServiceLocked(Intent intent, Messenger client) {
        intent = cloneIntentLocked(intent);
        ComponentName cn = intent.getComponent();
        final ServiceRecord sr = retrieveServiceLocked(intent);
        ......
        if (!installServiceIfNeededLocked(sr)) {  
            return null;
        }
        try {
            final Intent finalIntent = intent;
            ThreadUtils.syncToMainThread(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return sr.service.onStartCommand(finalIntent, 0, 0);
                }
            }, 6000);
        } catch (Throwable e) {
            return null;
        }
        sr.startRequested = true;
        mServicesByName.put(cn, sr);  // 加入到列表中,统一管理
        return cn;
    }

PluginServiceServer.installServiceIfNeededLocked调用installServiceLocked函数,加载Service类,通过反射创建Service对象,并利用attachBaseContextLocked函数反射调用Service.attachBaseContext,注意这里会将插件的全局PluginContext赋值给Service,然后还调用了Service.onCreate函数,最后一步将开启Service坑位,启动Service坑位只是为了让进程被回收的可能性降低(请参考android系统资源回收的优先级)。

private boolean installServiceLocked(ServiceRecord sr) {
    Context plgc = Factory.queryPluginContext(sr.plugin);
    ClassLoader cl = plgc.getClassLoader();
    ......
    Service s;
    try {
        s = (Service) cl.loadClass(sr.serviceInfo.name).newInstance();
    } catch (Throwable e) {
        return false;
    }
    try {
        attachBaseContextLocked(s, plgc); // 复写Context
    } catch (Throwable e) {
        return false;
    }
    s.onCreate();
    sr.service = s;
    startPitService();   // 开启“坑位”服务
    return true;
}

Replugin只是简单的将Service作为普通的类在运行,手动的调用Service的生命周期函数。当然本质上原生系统在启动Service的的时候,在ActivityThread类中也是这样做的,只不过原生系统用ActivityManagerService来管理Service,Replugin用PSS来管理,它们都为每一个Service产生了一个ServiceRecord对象。

在我目前对Replugin的了解程度来看,我觉得这里的处理稍微的复杂了一点,为什么一定要采取这么反直觉的方式来启动Service,而不是采用跟启动Activity同样的方式呢?

PluginServiceServer 子系统的设计

在上一个节中提到的IPluginHostIPluginClientPluginManagerService是什么关系?先来看一个并不那么标准的UML图,图中我故意简化了一下,将一些中间类省去了,所以如果你在跟代码,请知晓!

service.png
  • Replugin启动过程中,在UI进程或者自定义进程创建了一个PluginProcessPer对象,继承自IPluginClient并持有一个PluginServiceServer对象,这个PluginProcessPer对象会在应用启动时被注册到Persistent进程中的PluginProcessMain中(用一个Map类型的ALL成员变量缓存)。当需要使用PluginServiceServer的时候,会通过PluginServiceServerFetcherPersistent进程中查找并返回之前注册的IPluginClient对象,也就是PluginProcessPer的实例,然后用它所持有的PluginServiceServer对象来完成启动Service的工作,也就是前面讲到的流程。

    这里注册PluginProcessPer的动作是通过PmBase.attach函数完成的,也就是第一篇中讲到的UI进程启动的过程注册的。

    final void attach() {
        try {
            mDefaultPluginName = PluginProcessMain.getPluginHost().attachPluginProcess(IPC.getCurrentProcessName(), PluginManager.sPluginProcessIndex, mClient, mDefaultPluginName);
        } catch (Throwable e) {
        }
    }
    

    PluginServiceServerFetcher查询PSS对象的代码这里再贴出来看看。

    public IPluginServiceServer fetchByProcess(int process) {
        ......
        try {
            if (process == IPluginManager.PROCESS_PERSIST) {
                IPluginHost ph = PluginProcessMain.getPluginHost();
                pss = ph.fetchServiceServer();
            } else {
                PluginBinderInfo pbi = new PluginBinderInfo(PluginBinderInfo.NONE_REQUEST);
                IPluginClient pc = MP.startPluginProcess(null, process, pbi);
                pss = pc.fetchServiceServer();
            }
            ......
        } catch (Throwable e) {
        }
        ......
        return pss;
    }
    
  • 如果Service本身是要运行在Persistent进程中,那么就直接使用IPluginHost对象来启动Service,实质上它是PmHostSvc的实例,PmHostSvcIPluginHost的实现类,并且它只存在于Persistent进程中。

    PmHostSvc的创建在PmBase.init函数内完成,正是在Persistent进程启动并初始化是完成的。

    void init() {
        if (IPC.isPersistentProcess()) {
            mHostSvc = new PmHostSvc(mContext, this);
            PluginProcessMain.installHost(mHostSvc);
            ......
        } else {
        ......
    }
    

从上面的分析可以知道,在每一个进程里面都有一个PluginServiceServer在运行,并且负责各自进程内部的Service启动工作。PSS相关的其他代码逻辑并不复杂,大家有兴趣可以去看一看代码。

Replugin自定义进程实现原理

既然提到了进程,也该讲一讲进程相关的东西了,拿到这个框架大家肯定会自然而然的想到如何启动新的进程。在Repugin中默认会启动两个进程,就是前面提到过的UI进程和Persistent进程。那如果我要启动其他的进程来运行四大组件呢?

Replugin也提供了对进程的支持, 它允许用户启动最多三个自定义进程,它们的进程名将是固定的p0p1p2。自定义进程都是由UI进程启动起来的,UI进程或者自定义进程想要与其它进程联系,比如向其他进程发送广播,在其它进程中国启动Activity或者Service等,都要先通过常驻进程。

process.jpg

PluginProcessHost中又对自定义进程的的定义:

/**
 * 自定义插件的数量,暂时只支持3个自定义进程
 */
public static final int PROCESS_COUNT = 3;

/**
 * 自定义进程,int 标识,从 -100 开始,每次加 1;
 * 目前只支持 3 个进程,即到 -98
 */
public static final int PROCESS_INIT = -100;

/**
 * 插件中,进程名称后缀,
 * 进程名称类似 xxx.xx:p0, xxx.xx:p1, xxx.xx:p2
 */
public static final String PROCESS_PLUGIN_SUFFIX = "p";

大家知道要启动一个新的进程需要在AndroidManifest文件中給组件加上属性anroid:process=,但这并不需要你给出的属性值是p0,p1或者p2,你根据自己的意愿给出进程名,然后将你所给定的进程名与Replugin提供的三个进程名建立映射关系。这点是通过MetaData来实现的,在插件的AndroidManifest文件中你需要这样写(作为Appication标签的字标签):

<meta-data
    android:name="process_map"   //用来在Application.mMetaData中找到映射关系
    android:value="[{'from':'com.qihoo360.replugin.sample.demo1:bg','to':'$p1'}]"
/>

在上一篇分析中我们分析过插件Dex文件的加载过程Loader.loadDex,在这个函数中有一个步骤叫做adjustPluginProcess,在这个函数中,Replugin会从PackageInfo中的ApplicationInfo.mMetaData找到自定义进程名与Replugin提供的进程名的映射关系(以meta-data标签中的android:name作为key)。

private void adjustPluginProcess(ApplicationInfo appInfo) throws Exception {
    ......
    Bundle bdl = appInfo.metaData;
    if (bdl == null || TextUtils.isEmpty(bdl.getString("process_map"))) {
        return;
    }

    HashMap<String, String> processMap = new HashMap<>();
    try {
        String processMapStr = bdl.getString("process_map");
        JSONArray ja = new JSONArray(processMapStr);
        for (int i = 0; i < ja.length(); i++) {
            JSONObject jo = (JSONObject) ja.get(i);
            if (jo != null) {
                String to = jo.getString("to").toLowerCase();
                if (to.equals("$ui")) {
                    to = IPC.getPackageName();
                } else {
                    if (to.contains("$" + PluginProcessHost.PROCESS_PLUGIN_SUFFIX)) {
                        to = PluginProcessHost.PROCESS_ADJUST_MAP.get(to);
                    }
                }
                processMap.put(jo.getString("from"), to);
            }
        }
    } catch (JSONException e) {
        e.printStackTrace();
    }

    if (!processMap.isEmpty()) {
        doAdjust(processMap, mComponents.getActivityMap());
        doAdjust(processMap, mComponents.getServiceMap());
        doAdjust(processMap, mComponents.getReceiverMap());
        doAdjust(processMap, mComponents.getProviderMap());
    }
}

最后通过doAdjust函数用Replugin内置的进程名替换掉用户在插件中自定义的进程名,并保存在ComponentList中的ComponentIinfo中。

private void doAdjust(HashMap<String, String> processMap, HashMap<String, ? extends ComponentInfo> infos) throws Exception {
    for (HashMap.Entry<String, ? extends ComponentInfo> entry : infos.entrySet()) {
        ComponentInfo info = entry.getValue();
        if (info != null) {
            String targetProcess = processMap.get(info.processName);

            if (!TextUtils.isEmpty(targetProcess)) {
                info.processName = targetProcess;
            }
        }
    }
}

一切就绪以后,当然就是要启动进程啦!

Replugin自定义进程启动流程

启动流程我们从MP.startPluginProcess开始说起,它会通过AIDL接口IPluginHost.startPluginProcess调用Persistent进程中的PmHostSvc.startPluginProcess函数,最后会调用PmBase.startPluginProcessLocked函数。

  • 第一步先尝试从缓存中查找
  • 缓存中未找到,先分配一个进程坑位(这个分支需要优化)
  • 启动进程
  • 获取IPluginClient对象,用于在这个进程中启动组件
final IPluginClient startPluginProcessLocked(String plugin, int process, PluginBinderInfo info) {
    ......
    PluginProcessMain.schedulePluginProcessLoop(PluginProcessMain.CHECK_STAGE1_DELAY);

    // 尝试从缓存中查找
    IPluginClient client = PluginProcessMain.probePluginClient(plugin, process, info);
    ......
    int index = IPluginManager.PROCESS_AUTO;
    try {//分支A,分配进程坑位,这里的代码在2.1.7以后的版本会优化,大部分情况下 process == index
        index = PluginProcessMain.allocProcess(plugin, process); 
    } catch (Throwable e) {
    }
    // 启动
    boolean rc = PluginProviderStub.proxyStartPluginProcess(mContext, index);
    ......
    // 再次获取
    client = PluginProcessMain.probePluginClient(plugin, process, info);
    ......
    return client;
}

PluginProviderStub.proxyStartPluginProcess会使用第一篇分析中启动Persitent进程同样的方式来启动自定义进程: 尝试去访问一个ContentProvider,如果它没有运行,则在新的进程中启动它,而这个进程刚好就是上一节中通过PackageInifo.mMetaData匹配出来的进程(Replugin默认提供的三个自定义进程之一)。

static final boolean proxyStartPluginProcess(Context context, int index) {
    ContentValues values = new ContentValues();
    values.put(KEY_METHOD, METHOD_START_PROCESS);
    values.put(KEY_COOKIE, PMF.sPluginMgr.mLocalCookie);
    Uri uri = context.getContentResolver().insert(ProcessPitProviderBase.buildUri(index), values);
    if (uri == null) {
        return false;
    }
    return true;
}

这里之所以能准确找到PackageInifo.mMetaData中匹配的进程,要归功于上面这段代码中的index参数,来看看ProcessPitProviderBase.buildUri的代码:

public static final String AUTHORITY_PREFIX = IPC.getPackageName() + ".loader.p.main";

public static final Uri buildUri(int index) {
    String str = "";
    if (index < 0) {
        str += "N";
        index *= -1;
    }
    str += index;
    Uri uri = Uri.parse("content://" + AUTHORITY_PREFIX + str + "/main");
    return uri;
}

这里的Uri最终会是这样的:content://com.qihoo360.replugin.sample.host.loader.p.mainN99/main,中间的com.qihoo360.replugin.sample.host.loader.p.mainN99ContentProviderauthority(如果你不知道这个东西就需要去补课咯)。但是当你去AndroidManifest文件中搜索的时候,发现并没有与之匹配的ContentProvidder被注册,这是怎么回事?想了半天没想通,反编译(使用apktool)官方Demo中的Host代码,打开AndroidManifest文件,惊喜地发现了以下这些内容(这正是我们要找的东西):

<provider
    android:name="com.qihoo360.replugin.component.process.ProcessPitProviderP0"
    android:authorities="com.qihoo360.replugin.sample.host.loader.p.mainN100"
    android:exported="false"
    android:process=":p0" />
<provider
    android:name="com.qihoo360.replugin.component.process.ProcessPitProviderP1"
    android:authorities="com.qihoo360.replugin.sample.host.loader.p.mainN99"
    android:exported="false"
    android:process=":p1" />
<provider
    android:name="com.qihoo360.replugin.component.process.ProcessPitProviderP2"
    android:authorities="com.qihoo360.replugin.sample.host.loader.p.mainN98"
    android:exported="false"
    android:process=":p2" />

查看repugin-host-plutin源码,打开ComponentGenerator.groovy你会看到以下这段代码,原来是通过插件写到AndroidManifest文件中的,为什么要这样做呢?因为这里的applicationID字段是会根据你的项目的包名不同而变化的。

provider("${name}": "com.qihoo360.replugin.component.process.ProcessPitProviderP${p}",
        "android:authorities": "${applicationID}.loader.p.mainN${100 - p}",
        "${process}": ":p${p}",
        "${exp}": "${expV}")

好了,到这里自定义进程的启动流程就讲解完毕了!

总结

这一篇内容中比较不好理解的是PluginServiceServer子系统的设计,如果你有兴趣可以多看看代码,结合上面的讲解再体会体会!本篇的内容到此为止,下一篇Replugin 全面解析(5)我们来分析ContentProviderBroadcastReceiver相关的内容!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容