im即时通讯开发:进程被杀底层原理、APP应对被杀技巧

本文的技术原理讲解透彻、系统源码分享到位、样例代码也很有参考意义,希望能对有同样兴趣爱好的Android开发者、IM开发者、推送系统开发者等,带来对于Android进程保活技术的深入理解。

一直以来,App 进程保活都是各大厂商,特别是头部应用开发商永恒的追求。毕竟App 进程死了,就什么也干不了了。一旦 App 进程死亡,那就再也无法在用户的手机上开展任何业务,所有的商业模型在用户侧都没有立足之地了。

早期的 Android 系统不完善,导致 App 侧有很多空子可以钻,因此它们有着有着各种各样的姿势进行保活。

譬如说在 Android 5.0 以前,App 内部通过 native 方式 fork 出来的进程是不受系统管控的,系统在杀 App 进程的时候,只会去杀 App 启动的 Java 进程。因此诞生了一大批“毒瘤”,他们通过 fork native 进程,在 App 的 Java 进程被杀死的时候通过 am命令拉起自己从而实现永生。

那时候的 Android 可谓是魑魅横行,群魔乱舞,系统根本管不住应用,因此长期以来被人诟病耗电、卡顿。

同时,系统的软弱导致了 Xposed 框架、阻止运行、绿色守护、黑域、冰箱等一系列管制系统后台进程的框架和 App 出现。

不过,随着 Android 系统的发展,这一切都在往好的方向演变。

Android 5.0 以上,系统杀进程以 uid 为标识,通过杀死整个进程组来杀进程,因此 native 进程也躲不过系统的法眼。

Android 6.0 引入了待机模式(doze),一旦用户拔下设备的电源插头,并在屏幕关闭后的一段时间内使其保持不活动状态,设备会进入低电耗模式,在该模式下设备会尝试让系统保持休眠状态。

Android 7.0 加强了之前鸡肋的待机模式(不再要求设备静止状态),同时对开启了 Project Svelte。Project Svelte 是专门用来优化 Android 系统后台的项目,在 Android 7.0 上直接移除了一些隐式广播,App 无法再通过监听这些广播拉起自己。

Android 8.0 进一步加强了应用后台执行限制:一旦应用进入已缓存状态时,如果没有活动的组件,系统将解除应用具有的所有唤醒锁。另外,系统会限制未在前台运行的应用的某些行为,比如说应用的后台服务的访问受到限制,也无法使用 Mainifest 注册大部分隐式广播。

Android 9.0 进一步改进了省电模式的功能并加入了应用待机分组,长时间不用的 App 会被打入冷宫。另外,系统监测到应用消耗过多资源时,系统会通知并询问用户是否需要限制该应用的后台活动。

然而,道高一尺,魔高一丈。系统在不断演进,保活方法也在不断发展。

大约在 4 年前出现过一个 MarsDaemon,这个库通过双进程守护的方式实现保活,一时间风头无两。不过好景不长,进入 Android 8.0 时代之后,这个库就逐渐消亡。

一般来说,Android 进程保活分为两个方面:

1)保持进程不被系统杀死;

2)进程被系统杀死之后,可以重新复活。

随着 Android 系统变得越来越完善,单单通过自己拉活自己逐渐变得不可能了。

因此,后面的所谓「保活」基本上是两条路:

1)提升自己进程的优先级,让系统不要轻易弄死自己;

2)App 之间互相结盟,一个兄弟死了其他兄弟把它拉起来。

当然,还有一种终极方法,那就是跟各大系统厂商建立 PY 关系,把自己加入系统内存清理的白名单——比如说国民应用微信。当然这条路一般人是没有资格走的。

知己知彼,百战不殆。既然我们想要保活,那么首先得知道我们是怎么死的。

一般来说,系统杀进程有两种方法,这两个方法都通过 ActivityManagerService 提供:

1)killBackgroundProcesses;

2)forceStopPackage。

在原生系统上,很多时候杀进程是通过第一种方式,除非用户主动在 App 的设置界面点击「强制停止」。

不过国内各厂商以及一加三星等 ROM 现在一般使用第二种方法。因为第一种方法太过温柔,根本治不住想要搞事情的应用。第二种方法就比较强力了,一般来说被 force-stop 之后,App 就只能乖乖等死了。

因此,要实现保活,我们就得知道 force-stop 到底是如何运作的。

既然如此,我们就跟踪一下系统的 forceStopPackage 这个方法的执行流程。

那么,如何实现逃脱被杀呢?我们看这个关键的 5ms。

假设:App 进程在被杀掉之后,能够以足够快的速度(5ms 内)启动一堆新的进程,那么系统在一次循环杀掉老的所有进程之后,sleep 5ms 之后又会遇到一堆新的进程。

如此循环 40 次,只要我们每次都能够拉起新的进程,那我们的 App 就能逃过系统的追杀,实现永生。

是的:炼狱般的 200ms,只要我们熬过 200ms 就能渡劫成功,得道飞升。不知道大家有没有玩过打地鼠这个游戏,整个过程非常类似,按下去一个又冒出一个,只要每次都能足够快地冒出来,我们就赢了。即时通讯开发

现在问题的关键就在于:如何在 5ms 内启动一堆新的进程?

再回过头来看原来的保活方式:它们拉起进程最开始通过am命令,这个命令实际上是一个 java 程序,它会经历启动一个进程然后启动一个 ART 虚拟机,接着获取 ams 的 binder 代理,然后与 ams 进行 binder 同步通信。这个过程实在是太慢了,在这与死神赛跑的 5ms 里,它的速度的确是不敢恭维。

后来:MarsDaemon 提出了一种新的方式,它用 binder 引用直接给 ams 发送 Parcel,这个过程相比 am命令快了很多,从而大大提高了成功率。

其实这里还有改进的空间,毕竟这里还是在 Java 层调用,Java 语言在这种实时性要求极高的场合有一个非常令人诟病的特性:垃圾回收(GC)。

虽然我们在这 5ms 内直接碰上 gc 引发停顿的可能性非常小,但是由于 GC 的存在,ART 中的 Java 代码存在非常多的 checkpoint。想象一下你现在是一个信使有重要军情要报告,但是在路上却碰到很多关隘,而且很可能被勒令暂时停止一下,这种情况是不可接受的。

因此,最好的方法是通过 native code 给 ams 发送 binder 调用。当然,如果再底层一点,我们甚至可以通过 ioctl 直接给 binder 驱动发送数据进而完成调用,但是这种方法的兼容性比较差,没有用 native 方式省心。

通过在 native 层给 ams 发送 binder 消息拉起进程,我们算是解决了「快速拉起进程」这个问题。但是这个还是不够。

还是回到打地鼠这个游戏,假设你摁下一个地鼠,会冒起一个新的地鼠,那么你每次都能摁下去最后获取胜利的概率还是比较高的;但如果你每次摁下一个地鼠,其他所有地鼠都能冒出来呢?这个难度系数可是要高多了。如果我们的进程能够在任意一个进程死亡之后,都能让把其他所有进程全部拉起,这样系统就很难杀死我们了。

新的黑科技保活中通过 2 个机制来保证进程之间的互相拉起:

    1)2 个进程通过互相监听文件锁的方式,来感知彼此的死亡;

    2)通过 fork 产生子进程,fork 的进程同属一个进程组,一个被杀之后会触发另外一个进程被杀,从而被文件锁感知。

具体来说:

    1)创建 2 个进程 p1、p2,这两个进程通过文件锁互相关联,一个被杀之后拉起另外一个;

    2)同时 p1 经过 2 次 fork 产生孤儿进程 c1,p2 经过 2 次 fork 产生孤儿进程 c2,c1 和 c2 之间建立文件锁关联。

这样假设 p1 被杀,那么 p2 会立马感知到,然后 p1 和 c1 同属一个进程组,p1 被杀会触发 c1 被杀,c1 死后 c2 立马感受到从而拉起 p1,因此这四个进程三三之间形成了铁三角,从而保证了存活率。

分析到这里,这种方案的大致原理我们已经清晰了。基于以上原理,我写了一个简单的验证性代码(代码在下方)有兴趣的可以看一下。

猜你喜欢

转载自blog.csdn.net/wecloud1314/article/details/125215557
今日推荐