Android界的荀彧和荀攸:Service和Activity
根据Context的族谱,Service 是 Activity 的叔叔,结合作用来看,二者有太多相似,但是备份不同,类似三国时期的荀彧和荀攸。不过,二者的区别也挺明显:
- Activity 是面向用户的,有大量的用户交互的方法,而Service 是后台运行的,生命周期函数很少
- Activity 中有LaunchMode 的概念,每个Activity启动时都会放在栈顶,根据不同的 LaunchMode 可能会有复用以前的实例或者不复用以前的实例。但是Service不同,同一个 Service 调用多次startService并不会启动多个实例,只会有一个实例,所以,只用一个StubActivity 是应付不了多个插件的Service 的
- ActivityThread 最终通过Instrumentation 启动一个Activity。而ActivityThread 启动Service 并不借助于 Instrumentation ,而是直接把Service 反射出来就启动了
注意一点,Service 有两种形式: 由 startService 启动的服务;由 bindService 绑定的服务。二者的区别在于:startService 以及对应的 stopService ,就是简单地启动和停止 Service ;bindService 执行时会传递一个 ServiceConnection 对象给 AMS ,接下来 Service 在执行 onBind 时,可以把生成的 binder 对象返回给 App 调用端,这个值存于 ServiceConnection 对象的 onServiceConnected 回调函数的第二个参数中。
预先占位
前面说过,Service 与 Activity 不一样,它只会存在一个实例,所以只用一个StubService 是应付不了多个插件Service 的。考虑到在绝大部分App中Service 数据不会超过10个,所以我们完全可以在宿主App 中创建 10 个 StubService ,StubService1,StubService2…StubService10 ,每个 StubService 只对应插件中的一个Service,如下图:
接下来就是让每个插件Service匹配一个宿主中的 StubService 了,有两种匹配方式:
- 服务器下发一个 JSON 字符串,给出二者的一一对应关系
- 在每个插件 App 的 assets 目录中,创建一个 plugin_config 配置文件,把这个 JSON 字符串放进去
第2种做法更自然,不需要和服务器交互,json文件解析类似如下代码:
1 | String strJson = Utils.readZipFileString(dexFile.getAbsolutePath(), "assets/plugin_config.json"); |
我们将 JSON 转化为一个 HashMap ,以插件的类名作为key,以宿主的替身作为value,这个HashMap 存放在宿主App 的 UPFApplication 的 plutinServices 中,是一个全局变量。
startService 的解决方案
Service 的插件化机制和Activity 很像,因为它们是亲戚。我们首先从简单的startService 和 stopService 的插件化做起。
首先,把插件和宿主的dex合并,之前有封装过 BaseDexClassLoaderHookHelper 类,合并后才能随心所欲加载类;其次,采用“欺骗AMS”方法:
1 | //即 Hook AMN 的 gDefault,它是一个Singleton 对象,之后创建它的代理对象 MockClass1 ,然后替换这个字段 |
主要流程分析一下:
Hook AMN,让 AMS 启动 StubService,这次要拦截的是 startServcie 和 stopService 这两个方法(也即在AMN中就拦截这两个方法,将要操作的目标Service替换成相应的StubService)。不过,这次不再需要把 Intent 缓存了,因为有了 UPFApplication 中的 plutinServices ,我们可以根据插件 Service 找到对应的 StubService,也可以根据 StubService 反向找到 Service
AMS 被“欺骗”之后,它原本会通知App启动StubService,而我们要Hook掉ActivityThread 的 mH 对象的 mCallback 对象,仍然截获它的 handleMessage 方法,只不过这次截获的是 “CREATE_SERVICE” 分支,这个分支执行 ActivityThread 的 handleCreateService 方法。在 handleCreateService 中,并不能获取到 App 发送给 AMS 的 Intent,AMS 要启动那个Service ,这个信息是存在 handleCreateService 方法的 dat 参数中,是 CreateServiceData 类型的。Android系统的实现如下:
1 | private void handleCreateService (CreateServiceData data) { |
上面代码中,data.info.name 就是Service 的名称,所以我们只需要将这个值 Hook 为插件的Service即可。至此,一个支持 startService 的插件化框架就完成了。
bindService 的解决方案
有了前面的基础,Service 的 bind 与 unbind 就非常简单了,只要在 AMN 的Hook 中添加一个分支,在 “bindService” 的时候 “欺骗AMS” 就行:
1 | else if (”bindService”.equals(method . getName())) { |
这个过程就完成了,接下来就是使用:
1 | //unbind |
看了上面的使用方法,可能会有疑惑了:
- 上半场,对于unbind行为,为什么不像bind一样在 unbind 的时候做“欺骗AMS”?
- 下半场,为什么不用在MockClass2中写代码,把StubService2 换回 MyService2?
关于第一个问题,因为我们 unbind 的语法是这样的 unbindService(conn) ,只需要一个 ServiceConnection 类型的参数 conn 即可,这个 conn 在前面的bindService 时用到了,AMS 会根据这个conn 来找到对应的Service,所以并不需要在unbind的时候去做欺骗。
第二个问题,这要从Android 系统源码说起,bindService 过程在 AMS通知 App 下半场的流程图如下所示:
也就是说,bindService 先走114(handleCreateSrvice)分支,再走 121 (handleBindService) ,在 handleCreateSrvice 中已经把我们要启动的 MyService2 放到了 mService 这个集合中了,那么,在 handleBindService 和 handleUnbindService 中,都会从 mService 集合中找到 Service2 。在之前章节,为了解决 createService ,已经拦截了 114 分支把 StubService2 换回了 MyService2 了,所以我们不需要要拦截 121 和 122 了, 无需再MockClass2 添加额外代码。
小结
这里给出Service第一种解决方案——预先占位,在宿主中预先声明若干个StubService。