文档章节

8个类,1500行代码搞定插件化

kymjs张涛
 kymjs张涛
发布于 2016/05/22 22:46
字数 2180
阅读 1933
收藏 62
点赞 2
评论 1

写在前面

本文原创,转载请以链接形式注明地址:http://kymjs.com/code/2016/05/22/01

动态加载一个 Service 到应用中,同样采用的是和 Activity 一样的伪装欺骗系统识别的方案。

接上一篇:8个类搞定插件化——Activity实现方案

本篇主要介绍 Android 插件化开发中,如何运行未安装apk中的 Service。同我两年前讲过的那种方案(运行未安装apk中的Service)不同,这次实现的方案是完全没有任何限制的,插件 apk 可以是一个完全独立的应用,而不需要做特殊语法修改。

Android插件化

Service 加载过程

同 Activity 的动态加载原理一样,最首先需要讲讲 Service 的启动与加载过程。主要流程如下图:

Android插件化2

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.CallbackHandle发送了众多的消息类型,其中包括: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中的ActivityService就都解决了,回顾一下,总共就只需要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个类,难道你还有什么疑问吗?

© 著作权归作者所有

共有 人打赏支持
kymjs张涛

kymjs张涛

粉丝 502
博文 63
码字总数 78319
作品 4
普陀
Android工程师
加载中

评论(1)

zabcd117
zabcd117
虽然不懂ad,但是看着确实很简单的样子。
PHP反射机制原理与用法

反射 反射,直观理解就是根据到达地找到出发地和来源。比如,一个光秃秃的对象,我们可以仅仅通过这个对象就能知道它所属的类、拥有哪些方法。 反射是指在PHP运行状态中,扩展分析PHP程序,导...

ganfanghua ⋅ 01/12 ⋅ 0

Eclipse 常用技巧 第一种:直接复制法 解决方法是: 第二种:使用link文

快捷方式 0. Ctrl + 1 (快速修复) Ctrl + D (删除当前行) Ctrl + Alt + ↓(复制当前行到下一行) Alt + / 或者说是 Ctrl + 空格(由于后者与输入法的快捷键冲突,所以,我一般都用前者)...

闪电 ⋅ 2015/08/02 ⋅ 2

Python 10min系列之面试题解析丨Python实现tail -f功能

写这篇文章的初衷是有人去面试遇到了这个笔试题,不知道怎么做,没有什么思路,就发到了Reboot 的交流群里,让大家一起讨论讨论。 关于这道题,简单说一下我的想法吧。当然,也有很好用的 py...

独钓渔 ⋅ 2016/04/20 ⋅ 0

form-binder-java v0.10.2 发布,微型数据绑定和校验框架

form-binder-java v0.10.2 发布,主要更新: 给助手类 Simple 加了一个 data(..) 方法,可以直接把 转换为 form-binder-java 所需的 data (Map)。 form-binder-java 是一个容易使用和定制的微...

Tu_Minglei ⋅ 2015/08/03 ⋅ 0

LucenePlus 1.4,基于 Lucene 的全文搜索框架

LucenePlus 1.4 已发布,基于 lucene 6.5 开发,jdk 要求 1.8。 更新亮点 1、增加动态权重 (做竞价排名使用) 2、增加Jfinal 专属插件 4行代码搞定 3、添加索引效率再次提升 4、增加中文手册...

调调哥 ⋅ 2017/07/15 ⋅ 11

ueditor单独调用上传附件和图片的功能

专业软件定制开发:济南恒软信息技术有限公司(http://www.heng-soft.com) 第一步, 引入文件 <script src="ueditor/ueditor.config.js" type="text/javascript" charset="utf-8"></script><s......

glen_xu ⋅ 2015/07/09 ⋅ 0

为什么 Google 将数十亿行代码储存在单一的源码库

过去16年,Google使用一个中心化源码控制系统去管理一个日益庞大的单一共享源码库。它的代码库包含了约10亿个文件(有重复文件和分支)和3500万行注解,86TB数据,900万唯一源文件中含有大约...

oschina ⋅ 2016/06/29 ⋅ 34

Asp.Net MVC 插件化开发简化方案

Web 管理系统可以庞大到不可想像的地方,如果想就在一个 Asp.Net MVC 项目中完成开发,这个工程将会变得非常庞大,协作起来也会比较困难。为了解决这个问题,Asp.Net MVC 引入了 Areas 的概念...

边城__ ⋅ 2017/09/03 ⋅ 0

循序渐进开发WinForm项目(4)--Winform界面模块的集成使用

随笔背景:在很多时候,很多入门不久的朋友都会问我:我是从其他语言转到C#开发的,有没有一些基础性的资料给我们学习学习呢,你的框架感觉一下太大了,希望有个循序渐进的教程或者视频来学习...

walb呀 ⋅ 2017/12/04 ⋅ 0

MyEclipse常用快捷键+插件大全

(1)Ctrl+M切换窗口的大小 (2)Ctrl+Q跳到最后一次的编辑处 (3)F2当鼠标放在一个标记处出现Tooltip时候按F2则把鼠标移开时Tooltip还会显示即Show Tooltip Description。 F3跳到声明或定义...

帅的不像男的 ⋅ 2016/03/28 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

IDEA PermGen space内存溢出

解决方案: File -> Settings -> Build, Execution, Deployment / Build Tools / Maven / Runner下,找到VM Options选项,默认是空的,改为如下内容(或更大值)...

快乐的小火柴 ⋅ 15分钟前 ⋅ 0

前端常见跨域解决方案

什么是跨域? 跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。 广义的跨域: 1.) 资源跳转: A链接、重定向、表单提交2.) 资源嵌入: <link>、<script>、<im...

临江仙卜算子 ⋅ 16分钟前 ⋅ 0

系统管理命令service

service命令用来控制系统服务的实用工具,例如启动、停止、重启和关闭系统服务,以及当前状态。当然也可以直接操作,例如/etc/init.d/mysqld restart等。 语法 service (选项)(参数) 选项...

Jpchina ⋅ 21分钟前 ⋅ 0

MySQL 联合索引的命中规则

为什么要用联合索引? 对于查询语句“SELECT T.* FROM T WHERE T.c1=1 AND T.c3=2”涉及到两列,这个时候我们一般采用一个联合索引(c1, c3);而不用两个单列索引,这是因为一条查询语句往往应...

hensemlee ⋅ 28分钟前 ⋅ 0

Spring 自动组件扫描

通常情况下都是在XML配置文件中手动声明Bean和组件的。不过Spring也可以自动扫描组件实例化Bean,这样就可以避免在XML文件中繁琐的Bean声明。 手动声明Bean: 这里不再啰嗦,就是简单地在XML...

霍淇滨 ⋅ 33分钟前 ⋅ 0

MapReduce简单需求分析-共同好友及查找互粉的情况

MapReduce的设计,最重要的是要找准key,然后制定一系列的数据处理流程。MapReduce的Map中,会把key相同的分配到同一个reduce中,对于key的选择,可以找到某个相同的因素。以下面的几个例子说...

Jason_typ ⋅ 35分钟前 ⋅ 0

springboot多数据源自动切换

SpringBoot多数据源切换,先上配置文件: 1.pom: <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20......

JackyRiver ⋅ 37分钟前 ⋅ 0

Boost库编译应用

版本:Boost 1.66.0 Windows库编译 官网指南:直接执行bootstrap.bat处理文件即可,可以我却遇到一堆的问题。 环境:Windows 10 + Visual Studio 2017 Boost编译出来库命名 boost库生成文件命...

水海云 ⋅ 41分钟前 ⋅ 0

解决Eclipse发布到Tomcat丢失依赖jar包的问题

如果jar文件是以外部依赖的形式导入的。Eclipse将web项目发布到Tomcat时,是不会自动发布这些依赖的。 可以通过Eclipse在项目上右击 - Propertics - Deployment Assembly,添加“Java Build ...

ArlenXu ⋅ 41分钟前 ⋅ 0

iview tree组件层级过多时可左右滚动

使用vue+iview的tree组件,iview官网iview的tree树形控件 问题描述:tree层级过多时左右不可滚动 问题解决:修改overflow属性值 .el-tree-node>.el-tree-node_children { overflow: vi...

YXMBetter ⋅ 43分钟前 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部