一直以来,App 进程保活都是各大厂商,特别是头部应用开发商永恒的追求。java
毕竟App 进程死了,就什么也干不了了;一旦 App 进程死亡,那就再也没法在用户的手机上开展任何业务,全部的商业模型在用户侧都没有立锥之地了。android
早期的 Android 系统不完善,致使 App 侧有不少空子能够钻,所以它们有着有着各类各样的姿式进行保活。git
譬如说在 Android 5.0 之前,App 内部经过 native 方式 fork 出来的进程是不受系统管控的,系统在杀 App 进程的时候,只会去杀 App 启动的 Java 进程。程序员
所以诞生了一大批“毒瘤”,他们经过 fork native 进程,在 App 的 Java 进程被杀死的时候经过 am命令拉起本身从而实现永生。github
那时候的 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,这个库经过双进程守护的方式实现保活,一时间风头无两。app
不过好景不长,进入 Android 8.0 时代以后,这个库就逐渐消亡。
通常来讲,Android 进程保活分为两个方面:
保持进程不被系统杀死。
进程被系统杀死以后,能够从新复活。
随着 Android 系统变得愈来愈完善,单单经过本身拉活本身逐渐变得不可能了;所以后面的所谓「保活」基本上是两条路:
提高本身进程的优先级,让系统不要轻易弄死本身;
App 之间互相结盟,一个兄弟死了其余兄弟把它拉起来。
固然,还有一种终极方法,那就是跟各大系统厂商创建 PY 关系,把本身加入系统内存清理的白名单;好比说国民应用微信。固然这条路通常人是没有资格走的。
大约一年之前,大神 gityuan 在其博客上公布了 TIM 使用的一种能够称之为「终极永生术」的保活方法;这种方法在当前 Android 内核的实现上能够大大提高进程的存活率。笔者研究了这种保活思路的实现原理,而且提供了一个参考实现 Leoric。
接下来就给你们分享一下这个终极保活黑科技的实现原理。
知己知彼,百战不殆。
既然咱们想要保活,那么首先得知道咱们是怎么死的。
通常来讲,系统杀进程有两种方法,这两个方法都经过 ActivityManagerService 提供:
killBackgroundProcesses
forceStopPackage
在原生系统上,不少时候杀进程是经过第一种方式,除非用户主动在 App 的设置界面点击「强制中止」。
不过国内各厂商以及一加三星等 ROM 如今通常使用第二种方法。
第一种方法太过温柔,根本治不住想要搞事情的应用。
第二种方法就比较强力了,通常来讲被 force-stop 以后,App 就只能乖乖等死了。
所以,要实现保活,咱们就得知道 force-stop 究竟是如何运做的。既然如此,咱们就跟踪一下系统的 forceStopPackage 这个方法的执行流程:
首先是 ActivityManagerService里面的 forceStopPackage 这方法:
public void forceStopPackage(final String packageName, int userId) { // .. 权限检查,省略 long callingId = Binder.clearCallingIdentity(); try { IPackageManager pm = AppGlobals.getPackageManager(); synchronized(this) { int[] users = userId == UserHandle.USER_ALL ? mUserController.getUsers() : new int[] { userId }; for (int user : users) { // 状态判断,省略.. int pkgUid = -1; try { pkgUid = pm.getPackageUid(packageName, MATCH_DEBUG_TRIAGED_MISSING, user); } catch (RemoteException e) { } if (pkgUid == -1) { Slog.w(TAG, "Invalid packageName: " + packageName); continue; } try { pm.setPackageStoppedState(packageName, true, user); } catch (RemoteException e) { } catch (IllegalArgumentException e) { Slog.w(TAG, "Failed trying to unstop package " + packageName + ": " + e); } if (mUserController.isUserRunning(user, 0)) { // 根据 UID 和包名杀进程 forceStopPackageLocked(packageName, pkgUid, "from pid " + callingPid); finishForceStopPackageLocked(packageName, pkgUid); } } } } finally { Binder.restoreCallingIdentity(callingId); } }
在这里咱们能够知道,系统是经过 uid 为单位 force-stop 进程的,所以不论你是 native 进程仍是 Java 进程,force-stop 都会将你通通杀死。咱们继续跟踪 forceStopPackageLocked 这个方法:
final boolean forceStopPackageLocked(String packageName, int appId, boolean callerWillRestart, boolean purgeCache, boolean doit, boolean evenPersistent, boolean uninstalling, int userId, String reason) { int i; // .. 状态判断,省略 boolean didSomething = mProcessList.killPackageProcessesLocked(packageName, appId, userId, ProcessList.INVALID_ADJ, callerWillRestart, true /* allowRestart */, doit, evenPersistent, true /* setRemoved */, packageName == null ? ("stop user " + userId) : ("stop " + packageName)); didSomething |= mAtmInternal.onForceStopPackage(packageName, doit, evenPersistent, userId); // 清理 service // 清理 broadcastreceiver // 清理 providers // 清理其余 return didSomething; }
这个方法实现很清晰:
先杀死这个 App 内部的全部进程,而后清理残留在 system_server 内的四大组件信息;咱们关心进程是如何被杀死的,所以继续跟踪 killPackageProcessesLocked,这个方法最终会调用到 ProcessList 内部的 removeProcessLocked 方法, removeProcessLocked 会调用 ProcessRecord 的 kill 方法,咱们看看这个 kill:
void kill(String reason, boolean noisy) { if (!killedByAm) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill"); if (mService != null && (noisy || info.uid == mService.mCurOomAdjUid)) { mService.reportUidInfoMessageLocked(TAG, "Killing " + toShortString() + " (adj " + setAdj + "): " + reason, info.uid); } if (pid > 0) { EventLog.writeEvent(EventLogTags.AM_KILL, userId, pid, processName, setAdj, reason); Process.killProcessQuiet(pid); ProcessList.killProcessGroup(uid, pid); } else { pendingStart = false; } if (!mPersistent) { killed = true; killedByAm = true; } Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } }
这里咱们能够看到,首先杀掉了目标进程,而后会以 uid为单位杀掉目标进程组。
若是只杀掉目标进程,那么咱们能够经过双进程守护的方式实现保活;
关键就在于这个 killProcessGroup,继续跟踪以后发现这是一个 native 方法,它的最终实如今 libprocessgroup中,代码以下:
int killProcessGroup(uid_t uid, int initialPid, int signal) { return KillProcessGroup(uid, initialPid, signal, 40 /*retries*/); } 注意这里有个奇怪的数字:40。 咱们继续跟踪: static int KillProcessGroup(uid_t uid, int initialPid, int signal, int retries) { // 省略 int retry = retries; int processes; while ((processes = DoKillProcessGroupOnce(cgroup, uid, initialPid, signal)) > 0) { LOG(VERBOSE) << "Killed " << processes << " processes for processgroup " << initialPid; if (retry > 0) { std::this_thread::sleep_for(5ms); --retry; } else { break; } } // 省略 }
瞧瞧咱们的系统作了什么骚操做?循环 40 遍不停滴杀进程,每次杀完以后等 5ms,循环完毕以后就算过去了。
看到这段代码,我想任何人都会蹦出一个疑问:假设经历连续 40 次的杀进程以后,若是 App 还有进程存在,那不就侥幸逃脱了吗?
那么,如何实现这个目的呢?
咱们看这个关键的 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 个机制来保证进程之间的互相拉起:
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,所以这四个进程三三之间造成了铁三角,从而保证了存活率。
分析到这里,这种方案的大体原理咱们已经清晰了。
基于以上原理,我写了一个简单的 PoC,代码在这里:
https://github.com/tiann/Leoric
有兴趣的能够看一下。
为了文章的严谨性(注一位读者Rikka的回复):
文章中说须要“在 5ms 内启动一堆新的进程”,但其实并不须要。
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 是另外的的进程便可活下来。由于新对应新的 ProcessRecord,不会在上面那个循环里被杀死。此外,循环四十次反而给了超长的时间来启动新的,观察 log 能够发现 killProcessgroup 的间隔长达几十到一百多 ms。
本方案的原理仍是比较简单直观的,可是要实现稳定的保活,还须要不少细节要补充;特别是那与死神赛跑的 5ms,须要不计一切代价去优化才能提高成功率。
具体来讲,就是当前的实现是在 Java 层用 binder 调用的,咱们应该在 native 层完成。笔者曾经实现过这个方案,可是这个库本质上是有损用户利益的,所以并不打算公开代码,这里简单提一下实现思路供你们学习:
如何在 native 层进行 binder 通讯?
libbinder 是 NDK 公开库,拿到对应头文件,动态连接便可。
难点:依赖繁多,剥离头文件是个体力活。
如何组织 binder 通讯的数据?
通讯的数据其实就是二进制流;具体表现就是 (C++/Java) Parcel 对象。native 层没有对应的 Intent Parcel,兼容性差。
方案:
Java 层建立 Parcel (含 Intent),拿到 Parcel 对象的 mNativePtr(native peer),传到 Native 层。
native 层直接把 mNativePtr 强转为结构体指针。
fork 子进程,创建管道,准备传输 parcel 数据。
子进程读管道,拿到二进制流,重组为 parcel。
今天我把这个实现原理公开,而且提供 PoC 代码,并非鼓励你们使用这种方式保活,而是但愿各大系统厂商能感知到这种黑科技的存在,推进本身的系统完全解决这个问题。
两年前我就知道了这个方案的存在,不过当时不为人知。
最近一个月我发现不少 App 都使用了这种方案,把个人 Android 手机折腾的惨不忍睹;毕竟本人手机上安装了将近 800 个 App,假设每一个 App 都用这个方案保活,那这系统就无法用了。
系统如何应对?
若是咱们把系统杀进程比喻为斩首,那么这个保活方案的精髓在于能快速长出一个新的头;所以应对之法也很简单,只要咱们在斩杀一个进程的时候,让别的进程老老实实呆着别搞事情就 OK 了。具体的实现方法多种多样,不赘述。
用户如何应对?
在厂商没有推出解决方案以前,用户能够有一些方案来缓解使用这个方案进行保活的流氓 App。
这里推荐两个应用给你们:
冰箱
Island
经过冰箱的冻结和 Island 的深度休眠能够完全阻止 App 的这种保活行为。固然,若是你喜欢别的这种“冻结”类型的应用,好比小黑屋或者太极的阴阳之门也是能够的。
其余不是经过“冻结”这种机制来压制后台的应用理论上对这种保活方案的做用很是有限。
对技术来讲,黑科技没有什么黑的,不过是对系统底层原理的深刻了解从而反过来对抗系统的一种手段。不少人会说,了解系统底层有什么用,本文应该能够给出一个答案:能够实现别人永远也没法实现的功能,经过技术推进产品,从而产生巨大的商业价值。
黑科技虽强,可是它不应存在于这世上。没有规矩,不成方圆。黑科技黑的了一时,黑不了一世。要提高产品的存活率,终归要落到产品自己上面来,尊重用户,提高体验方是正途。
最后小编想说:不论之后选择什么方向发展,目前重要的是把Android方面的技术学好,毕竟其实对于程序员来讲,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提高本身,历来都是咱们去适应环境,而不是环境来适应咱们!
当程序员容易,当一个优秀的程序员是须要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每一个阶段都须要掌握不一样的能力。早早肯定本身的职业方向,才能在工做和能力提高中甩开同龄人。
想要拿高薪实现技术提高薪水获得质的飞跃。最快捷的方式,就是有人能够带着你一块儿分析,这样学习起来最为高效,因此为了你们可以顺利进阶中高级、架构师,我特意为你们准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了之后保证薪资上升一个台阶。(如下是一小部分,获取更多其余精讲进阶架构视频资料能够关注【个人主页】或者【简信我】获取免费领取方式)
当你有了学习线路,学习哪些内容,也知道之后的路怎么走了,理论看多了总要实践的。
如下是今天给你们分享的一些独家干货:
【Android开发核心知识点笔记】
【Android思惟脑图(技能树)】
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【Android高级架构视频学习资源】
Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!如今都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现现在市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破本身涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
【Android进阶学习视频】、【全套Android面试秘籍】【简信我学习】查看免费领取方式!
分享不易!喜欢的朋友别忘了关注+点赞!
原文做者:鸿洋
原文连接:https://mp.weixin.qq.com/s/bkHP-BiwTeQhqKvze2jtdQ