0%

第10章:Service的插件化解决方案

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,如下图:

StubService占位示意

接下来就是让每个插件Service匹配一个宿主中的 StubService 了,有两种匹配方式:

  • 服务器下发一个 JSON 字符串,给出二者的一一对应关系
  • 在每个插件 App 的 assets 目录中,创建一个 plugin_config 配置文件,把这个 JSON 字符串放进去

第2种做法更自然,不需要和服务器交互,json文件解析类似如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String strJson = Utils.readZipFileString(dexFile.getAbsolutePath(), "assets/plugin_config.json");

if (strJson == null || TextUtils.isEmpty(strJson)) {
return;
}

JSONObject jobject = new JSONObject(strJson.replaceAll("\r|\n", ""));
JSONArray jsonArray = jobject.getJSONArray("plugins");
for(int i = 0;i< jsonArray.length(); i++) {
JSONObject jsonObject = (JSONObject) jsonArray.get(i);
UPFApplication.pluginServices.put(
jsonObject.optString("PluginService"),
jsonObject.optString("StubService"));
}

我们将 JSON 转化为一个 HashMap ,以插件的类名作为key,以宿主的替身作为value,这个HashMap 存放在宿主App 的 UPFApplication 的 plutinServices 中,是一个全局变量。

startService 的解决方案

Service 的插件化机制和Activity 很像,因为它们是亲戚。我们首先从简单的startService 和 stopService 的插件化做起。

首先,把插件和宿主的dex合并,之前有封装过 BaseDexClassLoaderHookHelper 类,合并后才能随心所欲加载类;其次,采用“欺骗AMS”方法:

1
2
3
//即 Hook AMN 的 gDefault,它是一个Singleton 对象,之后创建它的代理对象 MockClass1 ,然后替换这个字段

//之后,再Hook 到 Handler 类型的 H 类的 mCallback 字段,替换为 MockClass2

主要流程分析一下:

  1. Hook AMN,让 AMS 启动 StubService,这次要拦截的是 startServcie 和 stopService 这两个方法(也即在AMN中就拦截这两个方法,将要操作的目标Service替换成相应的StubService)。不过,这次不再需要把 Intent 缓存了,因为有了 UPFApplication 中的 plutinServices ,我们可以根据插件 Service 找到对应的 StubService,也可以根据 StubService 反向找到 Service

  2. AMS 被“欺骗”之后,它原本会通知App启动StubService,而我们要Hook掉ActivityThread 的 mH 对象的 mCallback 对象,仍然截获它的 handleMessage 方法,只不过这次截获的是 “CREATE_SERVICE” 分支,这个分支执行 ActivityThread 的 handleCreateService 方法。在 handleCreateService 中,并不能获取到 App 发送给 AMS 的 Intent,AMS 要启动那个Service ,这个信息是存在 handleCreateService 方法的 dat 参数中,是 CreateServiceData 类型的。Android系统的实现如下:

1
2
3
4
5
6
7
private void handleCreateService (CreateServiceData data) { 
LoadedApk packageinfo = getPackageinfoNoCheck(data.info.applicationinfo, data.compatInfo);
Service service = null;
java.lang.ClassLoader cl= packageinfo.getClassLoader();
service = (Service) cl.loadClass(data.info.name).newinstance();
//省略无关代码
service . onCreate ();

上面代码中,data.info.name 就是Service 的名称,所以我们只需要将这个值 Hook 为插件的Service即可。至此,一个支持 startService 的插件化框架就完成了。

bindService 的解决方案

有了前面的基础,Service 的 bind 与 unbind 就非常简单了,只要在 AMN 的Hook 中添加一个分支,在 “bindService” 的时候 “欺骗AMS” 就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
else if (”bindService”.equals(method . getName())) { 
//找到参数里面的第一个 Intent 对象
int index = 0;
for (inti= 0; i < args . length ; i++) {
if (args [i) instanceof Intent) {
index = i ;
break;
}
}

Intent rawintent = (Intent) args[index);
String rawServiceName = rawintent.getComponent().getClassName();
String stubServiceName = UPFApplication.pluginServces.get(rawServiceName)
//替换 Plugin Service of StubService
ComponentName componentName =new ComponentName(stubPackage, stubServiceName);
Intent newintent = new Intent();
newintent.setComponet(componentName);
//替换 Intent ,欺骗 AMS
args[index] = newintent ;
Log.d(TAG, ” hook success ");
return method.invoke(mBase , args);
}

这个过程就完成了,接下来就是使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//unbind
btnBind.setOnClickListener(new View.OnclickListener(){
@Override
public void onClick(View v) {
final Intent = new Intent();
intent.setComponent(new ComponentName("jianqiang.com.testservice1"), "jianqiang.com.testservice1.MyService2");
bindService(intent, conn, Service.BIND_AUTO_CREATE);
}
});

//unbind
btnUnbind.setOnClickListener(new View.OnclickListener(){
@Override
public void onClick(View v) {
unbindService(conn);
}
});

看了上面的使用方法,可能会有疑惑了:

  • 上半场,对于unbind行为,为什么不像bind一样在 unbind 的时候做“欺骗AMS”?
  • 下半场,为什么不用在MockClass2中写代码,把StubService2 换回 MyService2?

关于第一个问题,因为我们 unbind 的语法是这样的 unbindService(conn) ,只需要一个 ServiceConnection 类型的参数 conn 即可,这个 conn 在前面的bindService 时用到了,AMS 会根据这个conn 来找到对应的Service,所以并不需要在unbind的时候去做欺骗

第二个问题,这要从Android 系统源码说起,bindService 过程在 AMS通知 App 下半场的流程图如下所示:

bindService过程AMS通知App这下半场

也就是说,bindService 先走114(handleCreateSrvice)分支,再走 121 (handleBindService) ,在 handleCreateSrvice 中已经把我们要启动的 MyService2 放到了 mService 这个集合中了,那么,在 handleBindService 和 handleUnbindService 中,都会从 mService 集合中找到 Service2 。在之前章节,为了解决 createService ,已经拦截了 114 分支把 StubService2 换回了 MyService2 了,所以我们不需要要拦截 121 和 122 了, 无需再MockClass2 添加额外代码。

小结

这里给出Service第一种解决方案——预先占位,在宿主中预先声明若干个StubService。

谢谢你的鼓励