今天来跟大家分享TIM最强保活思路的几种实现方法。这篇文章我将通过ioctl跟binder驱动交互,实现以最快的方式唤醒新的保活服务,最大程度防止保活失败。同时,我也将跟您分享,我是怎么做到在不甚了解binder的情况下,快速实现ioctl binder这种高级操作。
声明:现在这个保活方式在MIUI等定制Android系统中已经不能保活,大部分时候只能活在模拟器中了。但对与我们的轻量定制的Android系统,一些系统级应用的保活,这个方案还是有用的。
随着Android阵营的各大手机厂商对于续航的高度重视,两三年前的手机发布会更是把反保活作为一个系统的卖点,不断提出了各种反保活的方案,导致现在想实现应用保活简直难于上青天,甚至都需要一个团队来专门研究这个事情。连微信这种超级APP,也要拜倒在反保活的石榴裙下,允许后台启动太费电,不允许后台启动就收不到消息。。Android发现了一个保活野路子就堵一条,然而很多场景是有保活的强需求的,有木有考虑过我们开发者的感受,自己人何必为难自己人😭。
我觉得这是一个Android设计的不合理的地方,路子可以堵,但还是有必要留一个统一的保活接口的。这个接口由Google实现也好,厂商来实现也好,总好过现在很笨拙的系统自启动管理或者是JobScheduler。我觉得本质上来说,让应用开发者想尽各种办法去做保活,这个事情是没有意义的,保活的路子被封了,但保活还是需要做,保活的成本也提高了,简直浪费生命。Android的锅。(仅代表个人观点)
黑科技进程保活原理
大概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); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.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); // resolvedType
mServiceData.writeInt(0);
mServiceData.writeString(context.getPackageName()); // callingPackage
mServiceData.writeInt(0);
查看Parcel的源码可以看到,Parcel类有一个mNativePtr变量:
private long mNativePtr; // used by native code
// android4.4 mNativePtr是int类型
可以通过反射得到这个变量:
private static long getNativePtr(Parcel parcel) {
try {
Field ptrField = parcel.getClass().getDeclaredField("mNativePtr");
ptrField.setAccessible(true);
return (long) ptrField.get(parcel);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
这个变量对应了C++中Parcel类的地址,因此可以强转得到Parcel指针:
Parcel *parcel = (Parcel *) parcel_ptr;
然而,NDK中并没有提供binder这个模块,我们只能从Android源码中扒到binder相关的源码,再编译出libbinder.so。腾讯TIM应该就是魔改了binder相关的源码。
提取libbinder.so
为了避免libbinder的版本兼容问