插件化知识梳理(10) - Service 插件化实现及原理


相关阅读

插件化知识梳理(1) - Small 框架之如何引入应用插件
插件化知识梳理(2) - Small 框架之如何引入公共库插件
插件化知识梳理(3) - Small 框架之宿主分身
插件化知识梳理(4) - Small 框架之如何实现插件更新
插件化知识梳理(5) - Small 框架之如何不将插件打包到宿主中
插件化知识梳理(6) - Small 源码分析之 Hook 原理
插件化知识梳理(7) - 类的动态加载入门
插件化知识梳理(8) - 类的动态加载源码分析
插件化知识梳理(9) - 资源的动态加载示例及源码分析
插件化知识梳理(10) - Service 插件化实现及原理


一、Service 插件化思路

很可惜,Small不支持Service的插件化,但是在项目中我们确实有这样的需求,那么就需要研究一下如何自己来实现Service的插件化。在讨论如何实现Service的插件化之前,必须有三点准备:

插件化知识梳理(6) - Small 源码分析之 Hook 原理 这篇文章中,我们一起学习了如何实现Activity的插件化,简单地来说,实现原理就是:

  • 在调用startActivity启动插件Activity后,通过替换mInstrumentation成员变量,拦截这一启动过程,在后台偷偷地把startActivity时传入的intent中的component替换成为在AndroidManifest.xml中预先注册的占坑Activity,再通知ActivityManagerService
  • ActivityManagerService完成调度后,有替换客户端中的ActivityThread中的mH中的mCallback,将占坑的Activity重新恢复成插件的Activity

Service的插件化也可以采用类似的方式,大体的思路如下:

  • 在调用startService启动插件Service时,通过拦截ActivityManagerProxy的对应方法,将Intent中的插件Service类型替换成预先在AndroidManifest.xml中预先注册好的占坑Service
  • AMS通过ApplicationThreadProxy回调占坑Service对应的生命周期时,我们再在占坑Service中的onStartCommand中,去创建插件Service的实例,如果是第一次创建,那么先调用它的onCreate方法,再调用它的onStartCommand方法,否则,就只调用onStartCommand方法就可以了。
  • 在调用stopService停止插件Service时,同样通过拦截ActivityManagerProxy的对应方法,去调用插件ServiceonDestroy,如果此时发现没有任何一个与占坑Service关联的插件Service运行时,那么就可以停止插件Service了。

二、具体实现

传了一个简单的例子到仓库,大家可以简单地对照着看一下,下面,我们开始分析具体的实现。

2.1 准备工作

初始化的过程如下所示:

    public void setup(Context context) {
        try {
            //1.通过反射获取到ActivityManagerNative类。
            Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
            Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object gDefault = gDefaultField.get(activityManagerNativeClass);

            //2.获取mInstance变量。
            Class<?> singleton = Class.forName("android.util.Singleton");
            Field instanceField = singleton.getDeclaredField("mInstance");
            instanceField.setAccessible(true);

            //3.获取原始的对象。
            Object original = instanceField.get(gDefault);

            //4.动态代理,用于拦截Intent。
            Class<?> iActivityManager = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{ iActivityManager }, new IActivityManagerInvocationHandler(original));
            instanceField.set(gDefault, proxy);

            //5.读取插件当中的Service。
            loadService();

            //6.占坑的Component。
            mStubComponentName = new ComponentName(ServiceManagerApp.getAppContext().getPackageName(), StubService.class.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这里最主要的就是做了两件事:

2.1.1 对 ActivityManagerNative.getDefault() 调用的拦截

这里应用到了动态代理的知识,我们用Proxy.newProxyInstance所创建的proxy对象,替代了ActivityManagerNative中的gDefault静态变量。在 Framework 源码解析知识梳理(1) - 应用程序与 AMS 的通信实现 中,我们分析过,它其实是一个AMS在应用程序进程中的代理类。通过这一替换过程,那么当调用ActivityManagerNative.getDefault()方法时,就会先经过IActivityManagerInvocationHandler类,我们就可以根据invoke所传入的方法名,在方法真正被调用之前插入一些自己的逻辑(也就是前文所说的,将启动插件ServiceIntent替换成启动占坑ServiceIntent),最后才会通过Binder调用到达AMS端,以此达到了“欺骗系统”的目的。

下面是InvocationHandler的具体实现,构造函数中传入的是原始的ActivityManagerProxy对象。

    private class IActivityManagerInvocationHandler implements InvocationHandler {

        private Object mOriginal;


        public IActivityManagerInvocationHandler(Object original) {
            mOriginal = original;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            switch (methodName) {
                case "startService":
                    Intent matchIntent = null;
                    int matchIndex = 0;
                    for (Object object : args) {
                        if (object instanceof Intent) {
                            matchIntent = (Intent) object;
                            break;
                        }
                        matchIndex++;
                    }
                    if (matchIntent != null && ServiceManager.getInstance().isPlugService(matchIntent.getComponent())) {
                        Intent stubIntent = new Intent(matchIntent);
                        stubIntent.setComponent(getStubComponentName());
                        stubIntent.putExtra(KEY_ORIGINAL_INTENT, matchIntent);
                        //将插件的Service替换成占坑的Service。
                        args[matchIndex] = stubIntent;
                    }
                    break;
                case "stopService":
                    Intent stubIntent = null;
                    int stubIndex = 0;
                    for (Object object : args) {
                        if (object instanceof Intent) {
                            stubIntent = (Intent) object;
                            break;
                        }
                        stubIndex++;
                    }
                    if (stubIntent != null) {
                        boolean destroy = onStopService(stubIntent);
                        if (destroy) {
                            //如果需要销毁占坑的Service,那么就替换掉Intent进行处理。
                            Intent destroyIntent = new Intent(stubIntent);
                            destroyIntent.setComponent(getStubComponentName());
                            args[stubIndex] = destroyIntent;
                        } else {
                            //由于在onStopService中已经手动调用了onDestroy,因此这里什么也不需要做,直接返回就可以。
                            return null;
                        }
                    }
                    break;
                default:
                    break;
            }
            Log.d("ServiceManager", "call invoke, methodName=" + method.getName());
            return method.invoke(mOriginal, args);
        }
    }

先看startServiceargs参数中保存了startService所传入的实参,这里面就包含了启动插件ServiceIntent,我们将目标IntentComponent替换成为占坑的Component,然后将原始的Intent保存在KEY_ORIGINAL_INTENT字段当中,最后,通过原始的对象调用到ActivityManagerService端。

2.1.2 加载插件 Service 类

这里其实就是用到了前面介绍的DexClassLoader的知识,详细的可以看一下前面的这两篇文章 插件化知识梳理(7) - 类的动态加载入门插件化知识梳理(8) - 类的动态加载源码分析
最终,我们会将插件ServiceClass对象保存在一个mLoadServicesMap当中,它的Key就是插件Service的包名和类名。

    private void loadService() {
        try {
            //从插件中加载Service类。
            File dexOutputDir = ServiceManagerApp.getAppContext().getDir("dex2", 0);
            String dexPath = Environment.getExternalStorageDirectory().toString() + PLUG_SERVICE_PATH;
            DexClassLoader loader = new DexClassLoader(dexPath, dexOutputDir.getAbsolutePath(), null, ServiceManagerApp.getAppContext().getClassLoader());
            try {
                Class clz = loader.loadClass(PLUG_SERVICE_NAME);
                mLoadedServices.put(new ComponentName(PLUG_SERVICE_PKG, PLUG_SERVICE_NAME), clz);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.2 启动插件 Service

接下来,在宿主中通过下面的方式启动插件Service类:

    public void startService(View view) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(ServiceManager.PLUG_SERVICE_PKG, ServiceManager.PLUG_SERVICE_NAME));
        startService(intent);
    }

按照前面的分析,首先会走到我们预设的“陷阱”当中,可以看到,这里面的Intent还是插件ServiceComponent名字:


然而,经过替换,最终调用时的Intent就变成了占坑的Service

如果一切正常,接下来占坑Service就会启动,依次调用它的onCreateonStartCommand方法,我们在onStartCommand中,再去回调插件Service对应的生命周期:

public class StubService extends Service {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("StubService", "onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d("StubService", "onStartCommand");
        ServiceManager.getInstance().onStartCommand(intent, flags, startId);
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        ServiceManager.getInstance().onDestroy();
        Log.d("StubService", "onDestroy");
    }
}

这里,我们取出之前保存在KEY_ORIGINAL_INTENT中原始的Intent,通过它找到对应插件Service的包名和类名,以此为key,在mLoadedServices中找到前面从插件apk中加载的Service类,并通过反射实例化该对象,如果是第一次创建,那么先执行它的onCreate方法,并将它保存在mAliveServices中,之后再执行它的onStartCommand方法。


这一过程的打印如下图所示:

2.3 停止插件 Service

当我们通过stopService方法,停止插件Service时,也会和前面类似,先走到拦截的逻辑当中:


而在onStop方法中,我们判断它是否是需要停止插件Service,如果是那么就调用插件ServiceonDestory()方法,并且判断与占坑Service相关联的插件Service是否都已经结束了,如果是,那么就返回true,让占坑Service也销毁。

销毁的时候,就是将插件ServiceIntent替换成占坑Service

这时的打印为:

三、总结

以上,就是实现插件化Service的核心思路,实现起来并不简单,需要涉及到很多的知识,这已经是插件化学习的第十篇文章了。如果大家能一路看下来,可以发现,其实插件化并没有什么神秘的地方,如果我们希望实现任意一个组件的插件化,无非就是以下几点:

  • 组件的生命周期。
  • 组件的启动过程,最主要就是和ActivityManagerService的交互过程,这也是最难的地方,要花很多的时间去看源码,而且各个版本的API也可能有所差异。
  • 插件化常用技巧,也就是Hook,动态代理之类的知识。
  • 类动态加载的知识。

掌握了以上几点,对于市面上大厂的插件框架基本能够看懂个六七成,但是对于大多数人而言,并没有这么多的时间和条件,去分析一些细节问题。我写的这些文章,也只能算是入门水平,和大家一起学习基本的思想。真正核心的东西,还是需要有机会能应用到生产环境中才能真正掌握。

很可惜,我也没有这样的机会,感觉每天工作的时间都是在调UI、解Bug、浪费时间,只能靠着晚上的时间,一点点摸索,写Demo,哎,说出来都是泪,还有半年,继续加油吧!


更多文章,欢迎访问我的 Android 知识梳理系列:

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

推荐阅读更多精彩内容