插件化-插件Service的运行管理

本文出自: https://github.com/SusionSuc/AdvancedAndroid

在继续看VirtualApk中如何启动一个插件的Service之前,先简单的看一下Android如何启动一个Service, 主要是有个印象。

下面的源码参考自Android8.0。 贴的源码只是包含一些关键点。

Service启动的大体流程

我们从ContextImpl.startService()开始看。 为什么从这里开始看呢? 如果你看过前面的文章插件Activity的启动, 你应该知道在Activity创建时,其Context的实例就是ContextImpl,因此我们看一下它的startService()

    public ComponentName startService(Intent service) {
        .....
        return startServiceCommon(service, false, mUser);
    }

    private ComponentName startServiceCommon(Intent service, boolean requireForeground,UserHandle user) {
        ..
        ActivityManager.getService().startService(mMainThread.getApplicationThread(), service, ....);
       ......
    }

即直接向ActivityManager请求启动Service。这是一次通过Binder跨进程调用调用。最后调用到ActivityManagerService.startService(),然后它又调用了ActiveServices.startServiceLocked(), 来看一下关键步骤:

 ComponentName startServiceLocked(....){
    //创建 ServiceRecord, 并缓存起来
    ServiceLookupResult res = retrieveServiceLocked(.....); //这里会检测Service是否在 manifest文件中注册
    ServiceRecord r = res.record;
    ....一系列权限相关检查
    //继续启动流程
    ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
 }

在后续流程中,真正启动Service的方法是ActiveServices.bringUpServiceLocked():

    private String bringUpServiceLocked(ServiceRecord r, .....) {
        ProcessRecord app;
        ....
        //是不是要单独开了一个进程来启动这个Service
        final boolean isolated = (r.serviceInfo.flags&ServiceInfo.FLAG_ISOLATED_PROCESS) != 0;
        ...
        if (!isolated) {
            app = mAm.getProcessRecordLocked(procName, r.appInfo.uid, false);
            .....
            realStartServiceLocked(r, app, execInFg); //真正启动Service
            return null;
        } 

        //启动一个对应的进程
        app = mAm.startProcessLocked(procName, r.appInfo, true, intentFlags,
                    hostingType, r.name, false, isolated, false)
        .....

        //等会启动
        if (!mPendingServices.contains(r)) {
            mPendingServices.add(r);
        }
    }

ActiveServices.realStartServiceLocked()这个方法做的事情就很熟悉了:

    app.thread.scheduleCreateService(r, r.serviceInfo,....);  //这个方法会导致 service.onCreate()调用
    ....
    sendServiceArgsLocked(r, execInFg, true);  //这个方法会导致 service.onStartCommand()调用

即切换到了我们应用的进程,接下来就是在ActivityThread中启动Service了:

    //ActivityThread.java
    private void handleCreateService(CreateServiceData data) {
        ......
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        service = (Service) cl.loadClass(data.info.name).newInstance();
        ......
        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        context.setOuterContext(service); 
    
        Application app = packageInfo.makeApplication(false, mInstrumentation);    //不为null, 直接返回
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());  
        service.onCreate();
        mServices.put(data.token, service);
        ......
    }

ok分析到这里,我们大致知道了Service是如何启动的。 至于Service的绑定这里先不做分析。 那分析这一遍源码有什么意义呢?当然是为了方便接下来的分析插件化Service的启动

VirtualApk-插件化Service的启动

  • 服务端对于Service的启动有校验机制吗 ?

前面在分析源码时注释已经标注了。ActiveServices.retrieveServiceLocked()这个方法在构造ServiceRecord的时候会做这个校验。即必须在manifest文件中注册。 所以启动一个插件的Service,我们也需要类似插件Activity来启动一个占坑的Service

动态代理ActivityManagerService

所以VirtualApk对于插件Service启动的第一步就是需要绕过系统校验,原理类似于插件Activity的启动,只不过由于Service的启动和Instrumentation关系不大,它hook的是ActivityManagerService

    protected void hookSystemServices() {
        Singleton<IActivityManager> defaultSingleton;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get();
        } else {
            defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get();
        }

        //通过java动态代理 hook 
        IActivityManager activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
            createActivityManagerProxy(defaultSingleton.get()));
        Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy); // hook for android8.0以下版本
    }

hook之后目的当然是为了绕过系统对插件Service的校验。类似于Activity的启动,在插件Service启动时会把占坑ServiceActivityManagerService校验。在VirtualApk中也定义了两个占坑Service:

    <service android:exported="false" android:name="com.didi.virtualapk.delegate.LocalService" />
    <service android:exported="false" android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon"/>

现在我们看VirtualApk是如何在Service启动时替换插件Service为这两个占坑的Service:

public class ActivityManagerProxy implements InvocationHandler { //前面已经标注,对ActivityManagerService的hook是通过java的动态代理
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startService".equals(method.getName())) { 
            return startService(proxy, method, args);  
        }

       if ("stopService".equals(method.getName())) {
           return stopService(proxy, method, args);
        }
    }

即如果调用的是ActivityManagerService.startService,那就做一些处理:

    protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {
        Intent target = (Intent) args[1];
        ...如果这个intent不是插件的Service直接返回 
        Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, RemoteService.EXTRA_COMMAND_START_SERVICE);
        return mPluginManager.getHostContext().startService(wrapperIntent);
    }

    protected Object stopService(Object proxy, Method method, Object[] args) throws Throwable {
        ....流程同   startService
        startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_STOP_SERVICE);
    }

    protected Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
        target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
        String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

        //插件启动的service是 本地的还是远程的
        Class<? extends Service> delegate = PluginUtil.isLocalService(serviceInfo) ? LocalService.class : RemoteService.class;
        Intent intent = new Intent();
        intent.setClass(mPluginManager.getHostContext(), delegate);
        intent.putExtra(RemoteService.EXTRA_TARGET, target);
        intent.putExtra(RemoteService.EXTRA_COMMAND, command); // 注意这个command。 这里是: EXTRA_COMMAND_START_SERVICE
        intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
        .....
        return intent;
    }

即如果启动的是插件Service, 就把要启动的插件Serivice的信息保存在intent中,然后启动占坑Service。并且对于不同的操作在intent中标记为一个不同的command,比如上面的 EXTRA_COMMAND_START_SERVICEEXTRA_COMMAND_STOP_SERVICE。其余还有EXTRA_COMMAND_BIND_SERVICEEXTRA_COMMAND_UNBIND_SERVICE

接下来还要像启动插件Activity那样再把Activity换回来吗? VirtualApk并没有这么做,它就是直接启动了LocalService or RemoteService, 只不过在这两个Service中对不同的command做了不同的处理,比如EXTRA_COMMAND_START_SERVICE:

public class LocalService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int command = intent.getIntExtra(EXTRA_COMMAND, 0);
        switch (command) {
            case EXTRA_COMMAND_START_SERVICE: {
                ActivityThread mainThread = ActivityThread.currentActivityThread();
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service;
                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                    service = this.mPluginManager.getComponentsHandler().getService(component); //如果这个Service已经运行
                } else {
                    //实例化插件Service
                    service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();
                    Application app = plugin.getApplication();
                    IBinder token = appThread.asBinder();
                    Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                    IActivityManager am = mPluginManager.getActivityManager();
                    attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                    service.onCreate();

                    //缓存Service,避免重复实例化
                    this.mPluginManager.getComponentsHandler().rememberService(component, service);
                }
                //调用插件Service的 onStartCommand
                service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
                break;
        }
    }
}

即它在LocalServiceonStartCommand中,模拟了Service的生命周期方法的调用,具体生命周期方法调用步骤可以回顾前面看过的Service源码。对于其他stopServicebindService也是由LocalServiceonStartCommand中来模拟的。对于RemoteService这里就不列了,处理其实和LocalService是一样的,只不过RemoteService是跑在另一个进程中的。

所以这里来总结一下VirtualApk中的插件Serivice的真正运行情况吧,这里以主进程中的Service为例:

  1. 插件Service都是运行在LocalService中的。其实这无所谓,都是跑在主进程中的。
  2. 插件Service的相关生命周期方法都是LocalService来模拟调用的(在主线程中)。
  3. 即真正在后台一直跑的Service是LocalService

所以从源代码来看,VirtualApk对于插件Service通过依赖于系统Service,创建了一套自己的插件Service运行模型。有没有什么缺点呢?当然有

  1. 自己模拟的Service生命周期方法毕竟不是系统,随着系统版本的更新需要不断维护
  2. 没有真正支持开多进程Service。并且远程Service的名称的修改,需要修改源代码。

好,对于VirtualApk插件Service的管理源码的解读就看到这里。 欢迎关注我的Android进阶计划 : https://github.com/SusionSuc/AdvancedAndroid

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

推荐阅读更多精彩内容