Android最强保活黑科技的最强技术实现

大家好,我是老玩童。今天来跟大家分享TIM最强保活思路的几种实现方法。这篇文章我将通过ioctl跟binder驱动交互,实现以最快的方式唤醒新的保活服务,最大程度防止保活失败。同时,我也将跟您分享,我是怎么做到在不甚了解binder的情况下,快速实现ioctl binder这种高级操作。

随着Android阵营的各大手机厂商对于续航的高度重视,两三年前的手机发布会更是把反保活作为一个系统的卖点,不断提出了各种反保活的方案,导致现在想实现应用保活简直难于上青天,甚至都需要一个团队来专门研究这个事情。连微信这种超级APP,也要拜倒在反保活的石榴裙下,允许后台启动太费电,不允许后台启动就收不到消息。。Android发现了一个保活野路子就堵一条,然而很多场景是有保活的强需求的,有木有考虑过我们开发者的感受,自己人何必为难自己人。

我觉得这是一个Android设计的不合理的地方,路子可以堵,但还是有必要留一个统一的保活接口的。这个接口由Google实现也好,厂商来实现也好,总好过现在很笨拙的系统自启动管理或者是JobScheduler。我觉得本质上来说,让应用开发者想尽各种办法去做保活,这个事情是没有意义的,保活的路子被封了,但保活还是需要做,保活的成本也提高了,简直浪费生命。(仅代表个人观点)

黑科技进程保活原理

大概2个月前,Gityuan大佬放出了一份分析TIM的黑科技保活的博客史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术(后来不知道什么原因又删除了),顿时间掀起了一阵波澜,仿佛让开发者们又看到了应用保活的一丝希望。Gityuan大佬通过超强的专业技术分析,为我们解开了TIM保活方案的终极奥义。

后来,为数不多的维术大佬在Gityuan大佬的基础上,发布了博客Android 黑科技保活实现原理揭秘又进行了系统进程查杀相关的源码分析。为我们带来的结论是,Android系统杀应用的时候,会去杀进程组,循环 40 遍不停地杀进程,每次杀完之后等 5ms。

总之,引用维术的话语,原理如下:

利用Linux文件锁的原理,使用2个进程互相监听各自的文件锁,来感知彼此的死亡。通过 fork 产生子进程,fork 的进程同属一个进程组,一个被杀之后会触发另外一个进程被杀,从而被文件锁感知。具体来说,创建 2 个进程 p1, p2,这两个进程通过文件锁互相关联,一个被杀之后拉起另外一个;同时 p1 经过 2 次 fork 产生孤儿进程 c1,p2 经过 2 次 fork 产生孤儿进程 c2,c1 和 c2 之间建立文件锁关联。这样假设 p1 被杀,那么 p2 会立马感知到,然后 p1 和 c1 同属一个进程组,p1 被杀会触发 c1 被杀,c1 死后 c2 立马感受到从而拉起 p1,因此这四个进程三三之间形成了铁三角,从而保证了存活率。

按照维术大佬的理论,只要进程我复活的足够快,系统它就杀不死我,嘿嘿。

维术大佬写了一个简单的实现,代码在这里:github.com/tiann/Leori…,这个方案是当检测到进程被杀时,会通过JNI的方式,调用Java层的方法来复活进程。为了实现稳定的保活,尤其是系统杀进程只给了5ms复活的机会,使用JNI这种方式复活进程现在达不到最优的效果。

Java 层复活进程

复活进程,其实就是启动指定的Service。当native层检测到有进程被杀时,为了能够快速启动新Service。我们可以通过反射,拿到ActivityManager的remote binder,直接通过这个binder发送数据,即可实现快速启动Service。

Class amnCls = Class.forName("android.app.ActivityManagerNative");amn = activityManagerNative.getMethod("getDefault").invoke(amnCls);Field mRemoteField = amn.getClass().getDeclaredField("mRemote");mRemoteField.setAccessible(true);mRemote = (IBinder) mRemoteField.get(amn);启动Service的Intent:

Intent intent = new Intent();ComponentName component = new ComponentName(context.getPackageName(), serviceName);intent.setComponent(component);封装启动Service的Parcel:

Parcel mServiceData = Parcel.obtain();mServiceData.writeInterfaceToken("android.app.IActivityManager");mServiceData.writeStrongBinder(null);mServiceData.writeInt(1);intent.writeToParcel(mServiceData, 0);mServiceData.writeString(null); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);启动Service:

mRemote.transact(transactCode, mServiceData, null, 1);在 native 层进行 binder 通信

在Java层做进程复活的工作,这个方式是比较低效的,最好的方式是在 native 层使用纯 C/C++来复活进程。方案有两个。

其一,维术大佬给出的方案是利用libbinder.so, 利用Android提供的C++接口,跟ActivityManagerService通信,以唤醒新进程。

Java 层创建 Parcel (含 Intent),拿到 Parcel 对象的 mNativePtr(native peer),传到 Native 层。native 层直接把 mNativePtr 强转为结构体指针。fork 子进程,建立管道,准备传输 parcel 数据。子进程读管道,拿到二进制流,重组为 parcel。其二,Gityuan大佬则认为使用 ioctl 直接给 binder 驱动发送数据以唤醒进程,才是更高效的做法。然而,这个方法,大佬们并没有提供思路。

那么今天,我们就来实现这两种在 native 层进行 Binder 调用的骚操作。

方式一 利用 libbinder.so 与 ActivityManagerService 通信

上面在Java层复活进程一节中,是向ActivityManagerService发送特定的封装了Intent的Parcel包来实现唤醒进程。而在native层,没有Intent这个类。所以就需要在Java层创建好Intent,然后写到Parcel里,再传到Native层。

Parcel mServiceData = Parcel.obtain();mServiceData.writeInterfaceToken("android.app.IActivityManager");mServiceData.writeStrongBinder(null);mServiceData.writeInt(1);intent.writeToParcel(mServiceData, 0);mServiceData.writeString(null); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);查看Parcel的源码可以看到,Parcel类有一个mNativePtr变量:

privatelong mNativePtr; // used by native code// android4.4 mNativePtr是int类型可以通过反射得到这个变量:

privatestaticlonggetNativePtr(Parcel parcel) {try { Field ptrField = parcel.getClass().getDeclaredField("mNativePtr"); ptrField.setAccessible(true);return (long) ptrField.get(parcel); } catch (Exception e) { e.printStackTrace(); }return0;}这个变量对应了C++中Parcel类的地址,因此可以强转得到Parcel指针:

Parcel *parcel = (Parcel *) parcel_ptr;然而,NDK中并没有提供binder这个模块,我们只能从Android源码中扒到binder相关的源码,再编译出libbinder.so。腾讯TIM应该就是魔改了binder相关的源码。

提取libbinder.so

为了避免libbinder的版本兼容问题,这里我们可以采用一个更简单的方式,拿到binder相关的头文件,再从系统中拿到libbinder.so,当然binder模块还依赖了其它的几个so,要一起拿到,不然编译的时候会报链接错误。

adb pull /system/lib/libbinder.so ./adb pull /system/lib/libcutils.so ./adb pull /system/lib/libc.so ./adb pull /system/lib/libutils.so ./如果需要不同SDK版本,不同架构的系统so库,可以在 Google Factory Images 网页里找到适合的版本,下载相应的固件,然后解包system.img(需要在windows或linux中操作),提取出目标so。

binder_libs├── arm64-v8a│ ├── libbinder.so│ ├── libc.so│ ├── libcutils.so│ └── libutils.so├── armeabi-v7a│ ├── ...├── x86│ ├── ...└── x86_64 ├── ...为了避免兼容问题,我这里只让这些so参与了binder相关的头文件的链接,而没有实际使用这些so。这是利用了so的加载机制,如果应用lib目录没有相应的so,则会到system/lib目录下查找。

SDK24以上,系统禁止了从system中加载so的方式,所以使用这个方法务必保证targetApi <24。否则,将会报找不到so的错误。可以把上面的so放到jniLibs目录解决这个问题,但这样就会有兼容问题了。

CMake修改:

# 链接binder_libs目录下的所有so库link_directories(binder_libs/${CMAKE_ANDROID_ARCH_ABI})# 引入binder相关的头文件include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)# libbinder.so libcutils.so libutils.so libc.so等库链接到libkeep_alive.sotarget_link_libraries( keep_alive ${log-lib} binder cutils utils c)进程间传输Parcel对象

C++里面还能传输对象?不存在的。好在Parcel能直接拿到数据地址,并提供了构造方法。所以我们可以通过管道把Parcel数据传输到其它进程。

Parcel *parcel = (Parcel *) parcel_ptr;size_t data_size = parcel->dataSize();int fd[2];// 创建管道if (pipe(fd) < 0) {return;}pid_t pid;// 创建子进程if ((pid = fork()) < 0) {exit(-1);} elseif (pid == 0) {//第一个子进程if ((pid = fork()) < 0) {exit(-1); } elseif (pid > 0) {// 托孤exit(0); }uint8_t data[data_size];// 托孤的子进程,读取管道中的数据int result = read(fd[0], data, data_size);}// 父进程向管道中写数据int result = write(fd[1], parcel->data(), data_size);重新创建Parcel:

Parcelparcel;parcel.setData(data, data_size);传输Parcel数据

// 获取ServiceManagersp sm = defaultServiceManager();// 获取ActivityManager bindersp binder = sm->getService(String16("activity"));// 传输parcelint result = binder.get()->transact(code, parcel, NULL, 0);方式二 使用 ioctl 与 binder 驱动通信

方式一让我尝到了一点甜头,实现了大佬的思路,不禁让鄙人浮想联翩,感慨万千,鄙人的造诣已经如此之深,不久就会人在美国,刚下飞机,迎娶白富美,走向人生巅峰矣……咳咳。不禁想到ioctl的方式我也可以尝试着实现一下。ioctl是一个linux标准方法,那么我们就直奔主题看看,binder是什么,ioctl怎么跟binder driver通信。

Binder介绍

Binder是Android系统提供的一种IPC机制。每个Android的进程,都可以有一块用户空间和内核空间。用户空间在不同进程间不能共享,内核空间可以共享。Binder就是一个利用可以共享的内核空间,完成高性能的进程间通信的方案。

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。如图:可以看到,注册服务、获取服务、使用服务,都是需要经过binder通信的。

Server通过注册服务的Binder通信把自己托管到ServiceManagerClient端可以通过ServiceManager获取到ServerClient端获取到Server后就可以使用Server的接口了Binder通信的代表类是BpBinder(客户端)和BBinder(服务端)。

ps:有关binder的详细知识,大家可以查看Gityuan大佬的Binder系列文章。

ioctl函数

ioctl(input/output control)是一个专用于设备输入输出操作的系统调用,它诞生在这样一个背景下:

操作一个设备的IO的传统做法,是在设备驱动程序中实现write的时候检查一下是否有特殊约定的数据流通过,如果有的话,后面就跟着控制命令(socket编程中常常这样做)。但是这样做的话,会导致代码分工不明,程序结构混乱。所以就有了ioctl函数,专门向驱动层发送或接收指令。

Linux操作系统分为了两层,用户层和内核层。我们的普通应用程序处于用户层,系统底层程序,比如网络栈、设备驱动程序,处于内核层。为了保证安全,操作系统要阻止用户态的程序直接访问内核资源。一个Ioctl接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通了。函数原型:

intioctl(int fd, int request, …);作用:通过IOCTL函数实现指令的传递

fd 是用户程序打开设备时使用open函数返回的文件描述符request是用户程序对设备的控制命令后面的省略号是一些补充参数,和cmd的意义相关应用程序在调用ioctl进行设备控制时,最后会调用到设备注册struct file_operations结构体对象时的unlocked_ioctl或者compat_ioctl两个钩子上,例如Binder驱动的这两个钩子是挂到了binder_ioctl方法上:

staticconststructfile_operationsbinder_fops = { .owner = THIS_MODULE, .poll = binder_poll, .unlocked_ioctl = binder_ioctl, .compat_ioctl = binder_ioctl, .mmap = binder_mmap, .open = binder_open, .flush = binder_flush, .release = binder_release,};它的实现如下:

staticlongbinder_ioctl(struct file *filp, unsignedint cmd, unsignedlong arg){/*根据不同的命令,调用不同的处理函数进行处理*/switch (cmd) {case BINDER_WRITE_READ:/*读写命令,数据传输,binder IPC通信的核心逻辑*/ ret = **binder_ioctl_write_read**(filp, cmd, arg, thread);break;case BINDER_SET_MAX_THREADS:/*设置最大线程数,直接将值设置到proc结构的max_threads域中。*/break;case BINDER_SET_CONTEXT_MGR:/*设置Context manager,即将自己设置为ServiceManager,详见3.3*/break;case BINDER_THREAD_EXIT:/*binder线程退出命令,释放相关资源*/break;case BINDER_VERSION: {/*获取binder驱动版本号,在kernel4.4版本中,32位该值为7,64位版本该值为8*/break; }return ret;}具体内核层的实现,我们就不关心了。到这里我们了解到,Binder在Android系统中会有一个设备节点,调用ioctl控制这个节点时,实际上会调用到内核态的binder_ioctl方法。

为了利用ioctl启动Android Service,必然是需要用ioctl向binder驱动写数据,而这个控制命令就是BINDER_WRITE_READ。binder驱动层的一些细节我们在这里就不关心了。那么在什么地方会用ioctl 向binder写数据呢?

IPCThreadState.talkWithDriver

阅读Gityuan的Binder系列6—获取服务(getService)一节,在binder模块下IPCThreadState.cpp中有这样的实现(源码目录:frameworks/native/libs/binder/IPCThreadState.cpp):

status_t IPCThreadState::talkWithDriver(bool doReceive) { ... binder_write_read bwr; bwr.write_buffer = (uintptr_t)mOut.data();status_t err;do {//通过ioctl不停的读写操作,跟Binder Driver进行通信if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0) err = NO_ERROR; ... } while (err == -EINTR); //当被中断,则继续执行 ...return err;}可以看到ioctl跟binder driver交互很简单,一个参数是mProcess->mDriverFD,一个参数是BINDER_WRITE_READ,另一个参数是binder_write_read结构体,很幸运的是,NDK中提供了linux/android/binder.h这个头文件,里面就有binder_write_read这个结构体,以及BINDER_WRITE_READ常量的定义。

[惊不惊喜意不意外]

#includestructbinder_write_read {binder_size_t write_size;binder_size_t write_consumed;binder_uintptr_t write_buffer;binder_size_t read_size;binder_size_t read_consumed;binder_uintptr_t read_buffer;};#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)这意味着,这些结构体和宏定义很可能是版本兼容的。

那我们只需要到时候把数据揌到binder_write_read结构体里面,就可以进行ioctl系统调用了!

/dev/binder

再来看看mProcess->mDriverFD是什么东西。mProcess也就是ProcessState.cpp(源码目录:frameworks/native/libs/binder/ProcessState.cpp):

ProcessState::ProcessState(constchar *driver) : mDriverName(String8(driver)) , mDriverFD(open_driver(driver)) , ... {}从ProcessState的构造函数中得知,mDriverFD由open_driver方法初始化。

staticintopen_driver(constchar *driver){int fd = open(driver, O_RDWR | O_CLOEXEC);if (fd >= 0) {int vers = 0;status_t result = ioctl(fd, BINDER_VERSION, &vers); }return fd;}ProcessState在哪里实例化呢?

sp ProcessState::self() {if (gProcess != nullptr) {return gProcess; } gProcess = new ProcessState(kDefaultDriver);return gProcess;}可以看到,ProcessState的gProcess是一个全局单例对象,这意味着,在当前进程中,open_driver只会执行一次,得到的 mDriverFD 会一直被使用。

constchar* kDefaultDriver = "/dev/binder";而open函数操作的这个设备节点就是/dev/binder。

纳尼?在应用层直接操作设备节点?Gityuan大佬不会骗我吧?一般来说,Android系统在集成SELinux的安全机制之后,普通应用甚至是系统应用,都不能直接操作一些设备节点,除非有SELinux规则,给应用所属的域或者角色赋予了那样的权限。

看看文件权限:

~ adb shellchiron:/ $ ls -l /dev/bindercrw-rw-rw- 1 root root 10, 491972-07-0318:46 /dev/binder可以看到,/dev/binder设备对所有用户可读可写。

再看看,SELinux权限:

chiron:/ $ ls -Z /dev/binderu:object_r:binder_device:s0 /dev/binder查看源码中对binder_device角色的SELinux规则描述:

allow domain binder_device:chr_file rw_file_perms;也就是所有domain对binder的字符设备有读写权限,而普通应用属于domain。

既然这样,肝它!

写个Demo试一下

验证一下上面的想法,看看ioctl给binder driver发数据好不好使。

1、打开设备

int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);if (fd < 0) { LOGE("Opening '%s' failed: %s\n", "/dev/binder", strerror(errno));} else { LOGD("Opening '%s' success %d: %s\n", "/dev/binder", fd, strerror(errno));}2、ioctl

Parcel *parcel = new Parcel;parcel->writeString16(String16("test"));binder_write_read bwr;bwr.write_size = parcel->dataSize();bwr.write_buffer = (binder_uintptr_t) parcel->data();int ret = ioctl(fd, BINDER_WRITE_READ, bwr);LOGD("ioctl result is %d: %s\n", ret, strerror(errno));3、查看日志

D/KeepAlive: Opening '/dev/binder' success, fd is35D/KeepAlive: ioctl result is-1: Invalid argument打开设备节点成功了,耶!但是ioctl失败了,失败原因是Invalid argument,也就是说可以通信,但是Parcel数据有问题。来看看数据应该是什么样的。

binder_write_read结构体数据封装

IPCThreadState.talkWithDriver方法中,bwr.write_buffer指针指向了mOut.data(),显然mOut是一个Parcel对象。

binder_write_read bwr;bwr.write_buffer = (uintptr_t)mOut.data();再来看看什么时候会向mOut中写数据:

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer){ binder_transaction_data tr; tr.data.ptr.buffer = data.ipcData(); ... mOut.writeInt32(cmd); mOut.write(&tr, sizeof(tr));return NO_ERROR;}writeTransactionData方法中,会往mOut中写入一个binder_transaction_data结构体数据,binder_transaction_data结构体中又包含了作为参数传进来的data Parcel对象。

writeTransactionData方法会被transact方法调用:

status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {status_t err = data.errorCheck(); // 数据错误检查 flags |= TF_ACCEPT_FDS;if (err == NO_ERROR) {// 传输数据 err = writeTransactionData(BC_TRANSACTION, flags, handle, code, data, NULL); } ...// 默认情况下,都是采用非oneway的方式, 也就是需要等待服务端的返回结果if ((flags & TF_ONE_WAY) == 0) {if (reply) {//等待回应事件 err = waitForResponse(reply); }else { Parcel fakeReply; err = waitForResponse(&fakeReply); } } else { err = waitForResponse(NULL, NULL); }return err;}IPCThreadState是跟binder driver真正进行交互的类。每个线程都有一个IPCThreadState,每个IPCThreadState中都有一个mIn、一个mOut。成员变量mProcess保存了ProcessState变量(每个进程只有一个)。

接着看一下一次Binder调用的时序图:Binder介绍一节中说过,BpBinder是Binder Client,上层想进行进程间Binder通信时,会调用到BpBinder的transact方法,进而调用到IPCThreadState的transact方法。来看看BpBinder的transact方法的定义:

status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {if (mAlive) {status_t status = IPCThreadState::self()->transact(mHandle, code, data, reply, flags);if (status == DEAD_OBJECT) mAlive = 0;return status; }return DEAD_OBJECT;}BpBinder::transact方法的code/data/reply/flags这几个参数都是调用的地方传过来的,现在唯一不知道的就是mHandle是什么东西。mHandle是BpBinder(也就是Binder Client)的一个int类型的局部变量(句柄),只要拿到了这个handle就相当于拿到了BpBinder。

ioctl启动Service分几步?

下面是在依赖libbinder.so时,启动Service的步骤:

// 获取ServiceManagersp sm = defaultServiceManager();// 获取ActivityManager bindersp binder = sm->getService(String16("activity"));// 传输parcelint result = binder.get()->transact(code, parcel, NULL, 0);1、获取到IServiceManager Binder Client;

2、从ServiceManager中获取到ActivityManager Binder Client;

3、调用ActivityManager binder的transact方法传输Service的Parcel数据。

通过ioctl启动Service也应该是类似的步骤:

1、获取到ServiceManager的mHandle句柄;

2、进行binder调用获取到ActivityManager的mHandle句柄;

3、进行binder调用传输启动Service的指令数据。

这里有几个问题:

1、不依赖libbinder.so时,ndk中没有Parcel类的定义,parcel数据哪里来,怎么封装?

2、如何获取到BpBinder的mHandle句柄?

如何封装Parcel数据

Parcel类是Binder进程间通信的一个基础的、必不可少的数据结构,往Parcel中写入的数据实际上是写入到了一块内部分配的内存上,最后把这个内存地址封装到binder_write_read结构体中。Parcel作为一个基础的数据结构,和Binder相关类是可以解耦的,可以直接拿过来使用,我们可以根据需要对有耦合性的一些方法进行裁剪。

c++ Parcel类路径:frameworks/native/libs/binder/Parcel.cpp

jni Parcel类路径:frameworks/base/core/jni/android_os_Parcel.cpp

如何获取到BpBinder的mHandle句柄

具体流程参考Binder系列4—获取ServiceManager。

1、获取ServiceManager的mHandle句柄

defaultServiceManager()方法用来获取gDefaultServiceManager对象,gDefaultServiceManager是ServiceManager的单例。

sp defaultServiceManager() {if (gDefaultServiceManager != NULL) return gDefaultServiceManager;while (gDefaultServiceManager == NULL) { gDefaultServiceManager = interface_cast( ProcessState::self()->getContextObject(NULL)); } }return gDefaultServiceManager;}getContextObject方法用来获取BpServiceManager对象(BpBinder),查看其定义:

sp ProcessState::getContextObject(const sp& /*caller*/) { sp context = getStrongProxyForHandle(0);return context;}可以发现,getStrongProxyForHandle是一个根据handle获取IBinder对象的方法,而这里handle的值为0,可以得知,ServiceManager的mHandle恒为0。

2、获取ActivityManager的mHandle句柄

获取ActivityManager的c++方法是:

sp binder = serviceManager->getService(String16("activity"));BpServiceManager.getService:

virtual sp getService(const String16& name) const { sp svc = checkService(name);if (svc != NULL) return svc;returnNULL;}BpServiceManager.checkService:

virtual sp checkService( const String16& name) const { Parcel data, reply;//写入RPC头data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());//写入服务名data.writeString16(name); remote()->transact(CHECK_SERVICE_TRANSACTION, data, &reply);return reply.readStrongBinder();}可以看到,CHECK_SERVICE_TRANSACTION这个binder调用是有返回值的,返回值会写到reply中,通过reply.readStrongBinder()方法,即可从reply这个Parcel对象中读取到ActivityManager的IBinder。每个Binder对象必须要有它自己的mHandle句柄,不然,transact操作是没办法进行的。所以,很有可能,Binder的mHandle的值是写到reply这个Parcel里面的。

看看reply.readStrongBinder()方法搞了什么鬼:

sp Parcel::readStrongBinder() const { sp val; readNullableStrongBinder(&val);returnval;}status_t Parcel::readNullableStrongBinder(sp* val) const {return unflattenBinder(val);}调用到了Parcel::unflattenBinder方法,顾名思义,函数最终想要得到的是一个Binder对象,而Parcel中存放的是二进制的数据,unflattenBinder很可能是把Parcel中的一个结构体数据给转成Binder对象。

看看Parcel::unflattenBinder方法的定义:

status_t Parcel::unflattenBinder(sp* out) const {const flat_binder_object* flat = readObject(false);if (flat) { ... sp binder = ProcessState::self()->getStrongProxyForHandle(flat->handle); }return BAD_TYPE;}果然如此,从Parcel中可以得到一个flat_binder_object结构体,这个结构体重有一个handle变量,这个变量就是BpBinder中的mHandle句柄。

因此,在不依赖libbinder.so的情况下,我们可以自己组装数据发送给ServiceManager,进而获取到ActivityManager的mHandle句柄。

IPCThreadState是一个被Binder依赖的类,它是可以从源码中抽离出来为我们所用的。上一节中说到,Parcel类也是可以从源码中抽离出来的。

通过如下的操作,我们就可以实现ioctl获取到ActivityManager对应的Parcel对象reply:

Parcel data, reply;// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManagerdata.writeInterfaceToken(String16("android.app.IActivityManager"));data.writeString16(String16("activity"));IPCThreadState::self()->transact(0/*ServiceManger的mHandle句柄恒为0*/, CHECK_SERVICE_TRANSACTION, data, reply, 0);reply变量也就是我们想要的包含了flat_binder_object结构体的Parcel对象,再经过如下的操作就可以得到ActivityManager的mHandle句柄:

const flat_binder_object* flat = reply->readObject(false);return flat->handle;3、传输启动指定Service的Parcel数据

上一步已经拿到ActivityManger的mHandle句柄,比如值为1。这一步的过程和上一步类似,自己封装Parcel,然后调用IPCThreadState::transact方法传输数据,伪代码如下:

Parcel data;// 把Service相关信息写到parcel中writeService(data, packageName, serviceName, sdk_version);IPCThreadState::self()->transact(1/*上一步获取的ActivityManger的mHandle句柄值是1*/, CHECK_SERVICE_TRANSACTION, data, reply, 1/*TF_ONE_WAY*/);4、writeService方法需要做什么事情?

下面这段代码是Java中封装Parcel对象的方法:

Intent intent = new Intent();ComponentName component = new ComponentName(context.getPackageName(), serviceName);intent.setComponent(component);Parcel mServiceData = Parcel.obtain();mServiceData.writeInterfaceToken("android.app.IActivityManager");mServiceData.writeStrongBinder(null);mServiceData.writeInt(1);intent.writeToParcel(mServiceData, 0);mServiceData.writeString(null); // resolvedTypemServiceData.writeInt(0);mServiceData.writeString(context.getPackageName()); // callingPackagemServiceData.writeInt(0);可以看到,有Intent类转Parcel,ComponentName类转Parcel,这些类在c++中是没有对应的类的。所以需要我们参考intent.writeToParcel/ComponentName.writeToParcel等方法的源码的实现,自行封装数据。下面这段代码就是把启动Service的Intent写到Parcel中的方法:

void writeIntent(Parcel &out, constchar *mPackage, constchar *mClass) {// mActionout.writeString16(NULL, 0);// uri mDataout.writeInt32(0);// mTypeout.writeString16(NULL, 0);// // mIdentifierout.writeString16(NULL, 0);// mFlagsout.writeInt32(0);// mPackageout.writeString16(NULL, 0);// mComponentout.writeString16(String16(mPackage));out.writeString16(String16(mClass));// mSourceBoundsout.writeInt32(0);// mCategoriesout.writeInt32(0);// mSelectorout.writeInt32(0);// mClipDataout.writeInt32(0);// mContentUserHintout.writeInt32(-2);// mExtrasout.writeInt32(-1);}继续写Demo试一下

上面已经知道了怎么通过ioctl获取到ActivityManager,可以写demo试一下:

// 打开binder设备int fd = open("/dev/binder", O_RDWR | O_CLOEXEC);Parcel data, reply;// data.writeInterfaceToken(IServiceManager::getInterfaceDescriptor());// IServiceManager::getInterfaceDescriptor()的值是android.app.IActivityManagerdata.writeInterfaceToken(String16("android.app.IActivityManager"));data.writeString16(String16("activity"));IPCThreadState::self()->transact(0/*ServiceManger的mHandle句柄恒为0*/, CHECK_SERVICE_TRANSACTION, data, reply, 0);const flat_binder_object *flat = reply->readObject(false);if (flat) { LOGD("write_transact handle is:%llu", flat->handle);}else { LOGD("write_transact failed, error=%d", status);}给IPCThreadState::transact加上一些日志,打印结果如下:

D/KeepAlive: BR_DEAD_REPLYD/KeepAlive: write_transact failed, error=-32reply中始终读不到数据。这是为什么?现在已经不报Invalid argument的错误了,说明Parcel数据格式可能没问题了。但是不能成功把数据写给ServiceManager,或者ServiceManager返回的数据不能成功写回来。

想到Binder是基于内存的一种IPC机制,数据都是对的,那问题就出在内存上了。这就要说到Binder基本原理以及Binder内存转移关系。

Binder基本原理:Binder的Client端和Server端位于不同的进程,它们的用户空间是相互隔离。而内核空间由Linux内核进程来维护,在安全性上是有保障的。所以,Binder的精髓就是在内核态开辟了一块共享内存。数据发送方写数据时,内核态通过copy_from_user()方法把它的数据拷贝到数据接收方映射(mmap)到内核空间的地址上。这样,只需要一次数据拷贝过程,就可以完成进程间通信。

由此可知,没有这块内核空间是没办法完成IPC通信的。Demo失败的原因就是缺少了一个mmap过程,以映射一块内存到内核空间。修改如下:

#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)int mDriverFD = open("/dev/binder", O_RDWR | O_CLOEXEC);mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);日志:

D/KeepAlive: BR_REPLYD/KeepAlive: write_transact handle is:1搞定!

最后

相关的代码我已经发布到Github(lcodecorex/KeepAlive),master分支是利用 libbinder.so 与 ActivityManagerService 通信的版本,ioctl分支是使用 ioctl 与 binder 驱动通信的版本。

当然,这个保活的办法虽然很强,但现在也只能活在模拟器里了。

说一下我的方法论。

1、确定问题和目标。

研究一个比较复杂的东西的时候,我们比较难有一个大局观。这个时候,就需要明确自己需要什么?有问题,才能推动自己学习,然后顺腾摸瓜,最后弄清自己的模块在系统中的位置。

这篇文章,我们确定了目标是直接通过ioctl进行Binder通信,进而确定Binder通信的关键是拿到mHandle句柄。同时也理清了Binder通信的一个基本流程。

2、时序图很重要。

大佬们画的时序图,可快帮助我们快速理清框架的思路。

3、实践出真知。

纸上得来终觉浅,绝知此事要躬行。我一直践行的一个学习方式是学以致用,可以及时写Demo帮助我们巩固知识以及分析问题。

鸣谢

Gityuan大佬的[Binder系列http://gityuan.com/2015/10/31/binder-prepare/

Android 黑科技保活实现原理揭秘http://weishu.me/2020/01/16/a-keep-alive-method-on-android/

binder Driver (binder IPC) 功能介绍与分析https://blog.csdn.net/vshuang/article/details/88823044

Binder驱动之设备控制binder_ioctlhttps://www.jianshu.com/p/49830c3473b7

Google官方Android源码浏览网站https://cs.android.com/

 

转自:https://baijiahao.baidu.com/s?id=1662900112742214594&wfr=spider&for=pc

你可能感兴趣的:(android开发)