保活实现原理长期以来,App进程保活一直是各大厂商,尤其是顶级应用开发者永恒的追求。毕竟App进程死了,什么都做不了;一旦App进程死掉,就无法再在用户手机上开展任何业务,所有的业务模式在【用户端】都将无用武之地。早期Android系统的不完善,导致很多APP存在着大量的漏洞可以利用,于是就有了各种姿势保活。例如,在Android5.0之前,应用程序中的原生fork进程不受系统控制。系统杀死app进程时,只会杀死app启动的Java进程。因此,大量的“肿瘤”诞生了。他们forknative进程,当App的Java进程被kill掉后,使用am命令将自己拉起来,达到永生。那个时候的Android,妖孽横行,群魔乱舞;系统根本无法控制应用程序,因此长期被诟病耗电和卡顿。同时,系统的弱点导致了Xposed框架、拦截操作、绿卫士、黑域、冰箱等一系列控制系统后台进程的框架和APP的出现。不过随着Android系统的发展,这一切都在朝着好的方向发展。Android5.0及以上版本,系统杀掉以uid为标识的进程,是通过杀掉整个进程组来杀掉进程的,所以native进程逃不过系统的法眼。Android6.0引入了待机模式(打瞌睡),一旦用户拔下设备并在屏幕关闭后保持一段时间不活动,设备就会进入低功耗模式,设备会尝试让系统进入休眠状态。Android7.0强化了之前无用的待机模式(不再要求设备处于静止状态),同时启用了ProjectSvelte。ProjectSvelte是一个专门用来优化Android系统后台的项目。Android7.0直接去掉了一些隐式广播,App无法再通过收听这些广播来拉起自己。Android8.0进一步收紧应用后台执行限制:一旦应用进入缓存状态,如果没有活动组件,系统将释放应用拥有的任何唤醒锁。此外,系统限制了非前台运行的应用程序的某些行为,例如限制访问应用程序的后台服务,不能使用Mainifest注册大多数隐式广播。Android9.0进一步完善了省电模式功能,增加了应用待机组。长时间不用的应用会被打入冷宫;另外,当系统检测到某个应用程序占用过多资源时,系统会通知并询问用户是否对该应用程序进行限制。应用程序的后台活动。然而,道高一尺,魔高一尺。系统在不断发展,保活方法也在不断发展。大约4年前,MarsDaemon出现了。这个库是通过双进程守护保活的,一时风头正劲。但好景不长。进入Android8.0时代后,这个库就逐渐消亡了。一般来说,Android进程保活分为两个方面:保证进程不被系统杀死。进程被系统杀死后,可以复活。随着安卓系统越来越完善,靠自己活下去也渐渐变得不可能了;所以,后面所谓的“keepalive”基本上就是两种方式:提高自己进程的优先级,让系统不会轻易kill自己;应用程序彼此结盟,一个兄弟死了,另一个兄弟把它拉了起来。当然还有一个终极方法,就是和各大系统厂商建立PY【朋友,但我一直认为是(piyan)】关系,把自己加入系统内存清理白名单;比如国民App微信。当然,普通人是没有资格走这条路的。大约一年前,gityuan大神在他的博客上公布了TIM使用的一种堪称“终极长生不死”的活法;这种方法在目前Android内核的实现中可以大大提高进程的存活率。作者研究了这种keep-alive思想的实现原理,并提供了Leoric的参考实现。接下来给大家分享一下这个极致保活黑科技的实现原理。KeepAlive的底层技术原理知己知彼,百战不殆。既然要活着,就必须先知道自己是怎么死的。一般来说,系统杀进程有两种方式,都是ActivityManagerService提供的:killBackgroundProcessesforceStopPackage在原生系统上,常使用第一种方式杀进程,除非用户主动点击“强行停止”。强行停止,不过国内厂商和一加、三星等ROM现在普遍采用第二种方式。第一种方法过于温和,无法控制想要做事的应用程序。第二种方法更强大。一般来说,被强制停止后,app只能乖乖等死。因此,为了保持活力,我们需要知道强制停止是如何工作的。在这种情况下,我们来跟踪下系统的forceStopPackage方法的执行过程:首先,ActivityManagerService中的forceStopPackage方法:ActivityManagerServiceforceStopPackage在这里我们可以知道系统是以uid为单位强制停止进程的,那么无论你是nativeProcess或Java进程,强行停止会杀死你们。我们继续跟踪forceStopPackageLocked方法:forceStopPackageLocked方法的实现很明确:先kill掉App内部的所有进程,然后清理残留在system_server中的四大组件信息;我们关心进程是如何被kill掉的,所以继续跟踪killPackageProcessesLocked,这个方法最终会调用ProcessList里面的removeProcessLocked方法,removeProcessLocked会调用ProcessRecord的kill方法,我们来看看这个kill:这里可以看到目标先杀掉进程,然后以uidGroup为单位杀掉目标进程。如果只有目标进程被kill掉,那么我们可以通过双进程守护来保活;关键就在这个killProcessGroup。继续跟踪后发现这是一个native方法。它的最终实现在libprocessgroup中。代码如下:注意这里有一个奇怪的数字:40。我们继续跟进:看看我们的系统做了什么花样?循环40次连续kill进程,每次kill后等待5ms,循环完成后pass。看到这段代码,想必有人会有疑问:假设连续杀了40个进程后,如果App中还有一个进程,岂不是侥幸逃脱了?如何实现那么,如何实现这个目标呢?我们来看看这个关键的5ms。假设App进程kill后,能够足够快的启动一堆新进程(5ms以内),那么系统一个周期kill掉所有老进程后,sleep5ms后会遇到一堆新进程.过程;如此循环40次,只要我们每次都能拉起一个新的进程,那么我们的App就可以逃脱系统的追杀,实现永生。是的,炼狱200ms,只要熬过这200ms,就一定能够顺利渡过劫难,升华成道。不知道大家有没有玩过打地鼠游戏。整个过程非常相似。当你按下时,一个弹出。只要每次弹出的速度足够快,我们就赢了。现在问题的症结在于:如何在5ms内启动一堆新进程?回头看看原来的保活方式,他们是通过am命令启动进程的。这个命令实际上是一个java程序。它会启动一个进程,然后启动一个ART虚拟机,然后获取ams的binder代理。然后与ams进行binder同步通信。这个过程实在是太慢了。在这场与死亡赛跑的5ms中,它的速度实在不敢恭维。后来MarsDaemon提出了一种新的方法,使用binder引用直接将Parcel发送给ams。这个过程比am命令快很多,从而大大提高了成功率。其实这里还有改进的余地。毕竟这还是Java层调用的。Java语言在这种对实时性要求极高的场合有一个很受诟病的特性:垃圾回收(GC);虽然我们直接遇到gc引起的暂停的可能性很小,但是由于GC的存在,在ART中的Java代码中有很多检查点;想象一下,你现在是一名有重要军事情报要报告的使者,但途中遇到重重阻碍,而且很可能会被勒令暂时停止,这种情况是不能接受的。因此,最好的方式是通过nativecode向ams发送binder调用;当然,如果再低一点,我们甚至可以通过ioctl直接向binder驱动发送数据来完成调用,但这种方式兼容性较差,用native方式省心。通过在native层向ams发送binder消息启动进程,解决了“快速启动进程”的问题。但这还不够。让我们回到打地鼠游戏。假设你按下一只仓鼠,一个新的会弹出。那么你每次按下都有比较高的获胜概率;但是如果你按下下一个地鼠,其他所有地鼠会从哪里弹出?这个难度要高很多。如果我们的进程能在任何一个进程死掉后拉起所有其他进程,系统就很难杀掉我们。在新的保活技术中,使用了两种机制来保证进程相互拉起:两个进程通过监视对方的文件锁来感知对方的死亡。通过fork产生子进程,fork进程属于同一个进程组。一个被杀死后,会触发另一个进程被杀死,这是文件锁所感知的。具体来说,创建了两个进程p1和p2。这两个进程通过文件锁相互关联。一个被杀,一个被拉上来;同时p1经过2次fork后生成孤儿进程c1,p2经过2次fork后生成孤儿进程c1。c2,在c1和c2之间建立文件锁关联。假设p1被kill掉,那么p2会马上感知到,那么p1和c1属于同一个进程组,p1就会触发c1被kill掉。c1死后,c2会立马感应到并拉起p1,所以四个进程三对三之间形成了一个铁三角,从而保证了存活率。既然分析了这个方案的大致原理,我们就已经说的很清楚了。基于以上原理,我写了一个简单的PoC,代码在这里:https://github.com/xinjianteng/Leoric有兴趣的可以看看。AMS在执行查杀进程时从ProcessRecord中一一获取(https://android.googlesource.com/platform/frameworks/base/+/4f868ed/services/core/java/com/android/server/am/ActivityManagerService.java#5766),即libprocessgroup中的killProcessgroup最终会被多次执行。这样只要属于某个cgroup的进程被杀死,一旦android:process是另一个进程,另一个进程只要启动成功就可以存活。因为new对应的是new的ProcessRecord,所以不会在上面的循环中被kill掉。此外,循环四十次给了很长的时间来开始一个新的。观察日志可以发现,killProcessgroup的间隔时间在几十到一百多ms。改进空间该方案原理比较简单直观,但要实现稳定的keepalive,需要补充很多细节;尤其是与死亡赛跑的5ms,需要不惜一切代价优化,提高成功率。具体来说,当前的实现是在java层通过binder调用的,我们应该在native层完成。笔者之前已经实现过这个方案,但是这个库本质上是有损用户利益的,所以不打算公开代码。这里简单介绍一下实现思路,供大家学习:native层如何与binder通信?libbinder是一个NDK公共库,获取对应的头文件动态链接即可。难点:依赖较多,剥离头文件比较费力。Binder通信的数据如何组织?通信数据实际上是二进制流;具体表现为一个(C++/Java)Parcel对象。native层没有对应的IntentParcel,兼容性差。解决方案:Java层创建一个Parcel(包括Intent),获取Parcel对象的mNativePtr(nativepeer),传递给Native层。native层直接将mNativePtr转为结构体指针。fork子进程,建立管道,准备传输parcel数据。子进程读取管道,获取二进制流,并重新组装成包裹。如何回应?今天把这个实现原理公开,并提供PoC代码,不是鼓励大家用这个方法保命,而是希望各大系统厂商能够感知到这个黑科技的存在,推广自己的系统彻底解决这个问题.我两年前就知道这个程序的存在,但当时鲜为人知。最近一个月,发现很多app都用这个方案,害得我的安卓手机苦不堪言;毕竟,我的手机上安装了将近800个应用程序。如果每个应用程序都使用这个解决方案来保持活力,那么这个系统将无法正常工作。法是用的。系统如何响应?如果把系统查杀过程比作斩首,那么这种保活方案的本质就是可以快速长出一个新的脑袋;所以,解决的办法也很简单,只要我们杀掉一个进程,让其他进程老去,老老实实什么都不做就可以了。具体实现方法有多种,在此不再赘述。用户如何回应?在厂商推出解决方案之前,用户可以有一些解决方案来缓解利用该解决方案保活的流氓应用。这里推荐两款应用给大家:冰箱岛可以完全防止app通过冰箱的冰冻和小岛的深度睡眠来维持生命。当然,如果你喜欢其他“冰冻”类型的应用,比如小黑屋或者太极阴阳门,也是可以的。其他不通过“冻结”这种机制来抑制背景的应用程序理论上对这种保活方案的影响非常有限。总结1.就技术而言,黑科技并没有什么黑的,只是深入理解系统底层原理来反制系统的一种手段。很多人会说,了解系统底层有什么用,这篇文章应该可以给出答案:它可以实现别人永远无法实现的功能,通过技术推广产品,从而产生巨大的商业价值。2、黑科技虽然强大,但在这个世界上不应该存在。没有规则,没有标准。黑科技黑一时,黑不了一辈子。提高产品的存活率,归根结底还是要落在产品本身。尊重用户,提升体验才是正道。
