8个类,1500行代码搞定插件化
2016-05-22 22:46
267 查看
写在前面
本文原创,转载请以链接形式注明地址:http://kymjs.com/code/2016/05/22/01动态加载一个 Service 到应用中,同样采用的是和 Activity 一样的伪装欺骗系统识别的方案。
接上一篇:8个类搞定插件化——Activity实现方案
本篇主要介绍 Android 插件化开发中,如何运行未安装apk中的 Service。同我两年前讲过的那种方案(运行未安装apk中的Service)不同,这次实现的方案是完全没有任何限制的,插件 apk 可以是一个完全独立的应用,而不需要做特殊语法修改。
Service 加载过程
同 Activity 的动态加载原理一样,最首先需要讲讲 Service 的启动与加载过程。主要流程如下图:Service的启动与
Activity类似,最终都会调用到
ActivityManagerService里面的方法。
然后是
startServiceLocked()安全监测;
安全校验完成以后,
scheduleCreateService()准备创建
Service
再调用
scheduleServiceArgs()发消息
最终会在
ActivityThread.Callback中处理
Handle发送的消息。
明确
Service启动的一整套流程后,发现尽管与前一篇讲的
Activity的启动流程非常相似,但是不能用 Activity 的那种做法了,因为完全没有用到
Instrumentation这个类。
而且跟 Activity 里一样,我们也没办法覆盖掉校验方法
startServiceLocked()来打到篡改系统校验的目的,因为它运行在另一个系统进程
system_server中。
最后还有一个问题就是,Service 不同于 Activity 可以启动多个实例,同一个 Service 如果执行过后,是不会再次调用
onCreate()方法的。
替换系统的 Service 创建过程
尽管没有办法通过Instrumentation来创建
Service但我们依然有办法替换掉系统创建过程。
首先找到 service 对象是从哪里
new出来的,查看源码知道,在最后的那步
ActivityThread.Callback中
Handle发送了众多的消息类型,其中包括:CREATE_SERVICE、SERVICE_ARGS、BIND_SERVICE 等等…… 不仅是 service 的创建,连 Activity 的生命周期方法也是在这个回调中调用的。
在 CREATE_SERVICE 这个消息中,调用了一个叫
handleCreateService(CreateServiceData data)的方法,其中主要代码为:
private void handleCreateService(CreateServiceData data) { LoadedApk packageInfo = getPackageInfoNoCheck(data.info.applicationInfo, data.compatInfo); Service service = null; try { java.lang.ClassLoader cl = packageInfo.getClassLoader(); service = (Service) cl.loadClass(data.info.name).newInstance(); } catch (Exception e) { } Application app = packageInfo.makeApplication(false, mInstrumentation); service.attach(context, this, data.info.name, data.token, app, ActivityManagerNative.getDefault()); service.onCreate(); mServices.put(data.token, service); try { ActivityManagerNative.getDefault().serviceDoneExecuting(data.token, 0, 0, 0); } catch (RemoteException e) { } }
可以看到,其实
Service也是一个普通的类,在这里就是系统
new出来并执行了他的
onCreate()方法。
所以我们就可以通过替换掉这个 callback 类,并修改其逻辑如果是 CREATE_SERVICE 这条消息,就执行我们自己的
Service创建逻辑。
而我们自己的逻辑,就通过判断,如果正在加载的 service 是一个插件 service 就替换
ClassLoader为插件 classloader,加载出来的类一切照原宿主service的流程走一遍,包括那些
attach()、
onCreate()方法,都手动调用一遍。
替换方法依旧是通过反射,找到原本
ActivityThread类中的
mH这个类。
Field mHField = activityThreadClass.getDeclaredField("mH"); mHField.setAccessible(true); Handler mH = (Handler) mHField.get(currentActivityThread); Field mCallBackField = Handler.class.getDeclaredField("mCallback"); mCallBackField.setAccessible(true); //修改它的callback为我们的,从而HOOK掉 ActivityThreadHandlerCallback callback = new ActivityThreadHandlerCallback(mH); mCallBackField.set(mH, callback);
真的是要吐槽一下 Android 源码,里面充斥着各种奇葩命名,比如这个 mH,它其实是一个
Handle,但是它的类名就一个字母,一个大写的
H,所以他的对象叫 mH。 然后还有,前一个 ActivityInfo 类型的变量叫 aInfo,后面又出现一个 ApplicationInfo 的对象也叫 aInfo,然后时不时还来个 ai,你也不知道到底是啥还得再翻回去找它的类型。
OK,回正题,替换完 callback 后,创建 Service 就可以由我们自己的方法来执行了。但是还有一个问题,就是
onCreate不会多次调用的问题,因此我们同时还要修改
handleMessage()的逻辑,如果是 SERVICE_ARGS 或者 BIND_SERVICE 这两个消息,则首先进行一次判断,如果传入的插件 service 是个没有创建过的,那么就需要再次运行
handleCreateService()方法去创建一次。
@Override public boolean handleMessage(Message msg) { switch (msg.what) { case 114: //CREATE_SERVICE if (!handleCreateService(msg.obj)) { mOldHandle.handleMessage(msg); } break; case 115: //SERVICE_ARGS handleBindService(msg.obj); mOldHandle.handleMessage(msg); break; case 121: //BIND_SERVICE handleBindService(msg.obj); mOldHandle.handleMessage(msg); break; } return true; } /** * startService时调用,如果插件Service是首次启动,则首先执行创建 * * @param data BindServiceData对象 */ private void handleBindService(Object data) { ServiceStruct struct = pluginServiceMap.get(IActivityManagerHook.currentClazzName); //如果这个插件service没有启动过 if (struct == null) { //本来这里应该是传一个CreateServiceData对象,但是由于本方法实际使用的只有CreateServiceData.token //这个token在BindServiceData以及ServiceArgsData中有同名field,所以这里偷个懒,直接传递了 handleCreateService(data); } }
踩坑与爬坑
如果你照着上面的思路实现了整个插件化,你会发现其实还有两个巨大的坑:插件 service 虽然创建了,但是如果启动了多个插件 service,那么除了最后一次启动的那个 service,其他插件 service 的
onCreate()以外的其他生命周期方法一个都没有调用。
插件 service 不会调用
onDestroy()方法。
首先解决第一个问题,生命周期方法。之前说过,每个生命周期方法其实也是通过这个 handle 来处理的。找到相应的消息事件:SERVICE_ARGS、BIND_SERVICE、STOP_SERVICE,发现这三个事件调用的方法都有一句共同的代码:
Service s = mServices.get(data.token);
原来所有创建过的 service 都会被加入到一个 map 中(这个 map 在 4.0 以前是
HashMap,4.0 以后是
ArrayMap),在需要使用的时候就从这个 map 中根据 key 也就是 token 对象来读取,如果读不到,就不会调用生命周期方法了。
再翻回之前的 service 创建的代码
handleCreateService(),那句
mServices.put(data.token, service);原来就是做这个用的。同样也解释了为什么其他 service 不会调用生命周期方法了,因为 map 的值都被覆盖了嘛。 那么简单,这个 key 值 token 我们自己来创建并加入到里面就行了。
第二个坑,
onDestroy()不执行,经过反复测试,发现实际上问题在于带有 STOP_SERVICE 标识的消息就没有被发出,具体原因不得而知,猜测可能是安全校验没通过。解决的办法也很简单,既然系统没有发出,那么就手动发送一次这个消息就行了。
找到一切消息发送的源头——
ActivityManagerService,那么非常简单,通过通过动态代理,就可以替换掉我们关注的方法了。
找到 destroy 相关的两个方法,名字叫:
stopServiceToken()和
unbindService()。在这两个方法执行的时候,调用一下
doServiceDestroy()自己去手动发一下消息。然后在另一边接收的时候接收到这个消息就执行插件的
onDestroy()
public void doServiceDestroy() { Message msg = mH.obtainMessage(); msg.what = ActivityThreadHandlerCallback.PLUGIN_STOP_SERVICE; mH.sendMessage(msg); } private void handleCreateService(CreateServiceData data) { switch (msg.what) { case 116: //STOP_SERVICE case PLUGIN_STOP_SERVICE: if (!handleStopService()) { mOldHandle.handleMessage(msg); } break; } return true; } /** * destroy策略,如果是最后一个service,则停止真实Service */ private boolean handleStopService() { ServiceStruct struct = pluginServiceMap.get(IActivityManagerHook.currentClazzName); if (struct != null) { pluginServiceMap.remove(IActivityManagerHook.currentClazzName); if (pluginServiceMap.size() == 0) { return false; } else { struct.service.onDestroy(); return true; } } return false; }
资源与so文件动态加载
这样,动态加载未安装APK中的Activity和
Service就都解决了,回顾一下,总共就只需要6个类就够了,那么为什么说是8个类搞定插件化呢,因为还有两类是用来处理资源和 so 文件的动态加载的。
先说 so 文件,其实
DexClassLoader原生就支持动态加载的,但是为什么我们传入的 solib 并没有加载出来呢,还是因为权限。在 Android 手机上的 SD 卡是不具备可执行权限的,所以我们必须将 so 文件复制到应用包内存储区域,不管是
getFilesDir()或者是
getCacheDir()都是具有可执行权限的目录,在构造插件
DexClassLoader的时候,第三个参数传入具有可执行权限的路径就可以了。
资源的话就更简单了,由于我们只需要动态加载一个 apk,所以完全涉及不到插件资源冲突问题,只需要一个方法:
public void loadRes(Context context, String resPath) throws Exception { assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, resPath); //在插件的Activity中替换掉原本的resource就可以了 resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration()); }
结尾
结尾没有花絮~就这么简单8个类,难道你还有什么疑问吗?
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories