内容介绍、背景
这次我在三星OneUI系统发现的BUG,影响范围大,存在时间久,我觉得有几个原因:
- 从0复现的难度高,彻底理清机制之前很难用测试机稳定复现,只有日常使用三星才容易遇到
- 未经过梆梆企业版加固的包,不容易直观发现这个问题
- 由于这个BUG的机制,被结束的App不属于崩溃,APM平台统计不到数据,难以引起重视
- 这个BUG代码进行了判断,只有大陆用户触发,三星在大陆地区市场占有率很低
也因此才终于有这么一篇我认为值得分享的分析记录,上次写的分析证明系统BUG的博客是小米手机MIUI的setSystemGestureExclusionRects不生效的问题,都过去两年了。
这个BUG最开始是在云闪付App遇到的,为了领取国补,不得不下载和注册,但是当我接收验证码的时候,下拉状态栏查看验证码,云闪付App就直接闪退了。在App内的客服页面,想发录屏,选择文件之后又闪退。和App内的客服沟通的最后一句话,是问我手机是不是安装了Xposed等软件。我是绝对没有安装的。但是频繁闪退,我已经没心思在App回复客服了。
购买必需品,领不到国补很冤,像是硬亏几百块。想起工信部经常通报App违规收集信息,我先是给工信部反馈此问题,得到回应是它们不负责这方面。我又在云闪付App里面发现了中国银联的客服电话,向银联客服反馈几天后,接到回电,我又听到电话另一头云闪付的开发人员让客服问我是不是安装了Xposed等软件。又过了几天,客服通过了我的微信请求,我发过去录屏,客服给了我10元红包补偿。
再后来,我又在其他App遇到了崩溃问题,但是这款App性质和云闪付类似,我还只是怀疑是不是它们有什么公共SDK存在问题(就像有些App检测frida,实际上只是某OAID相关的库libmsaoaidsec.so在检测frida)。再后来听说它们没有特有的公共SDK,然后我发现它们的特征是都使用了梆梆加固企业版,我又找了几个梆梆加固企业版的样本,发现确实和此有关,怀疑梆梆的检测环境逻辑在三星手机存在误判。
但是现象又和以往遇到的触发加固壳检测不太像,我印象中,梆梆的加固壳检测到环境异常,一般是在native层崩溃,并且给某些寄存器设置成错误编号,便于它们加固厂商排查问题和加白名单等,还有些壳是直接kill自身进程,或者固定延迟10秒再kill自身进程。而这些App,多点几次,比如尝试打开10次,会有一次不闪退,然后下拉状态栏或者切换App前后台,又闪退。
网络上有一些偏方,比如关闭RAM Plus,或者是进入开发者选项开启“禁用子进程限制”,确实可以解决这个问题,但是前者只是临时解决问题,没多久还会闪退,只是临时降低了存储空间占用+重启手机,临时避开了BUG逻辑而已。而后者,只是掩盖了问题,确实不再闪退,但是BUG仍然在对系统造成消耗,很快就有几百,甚至上千个没意义的进程默默在后台浪费系统资源,甚至由于文件描述符过多占用,造成系统进程异常,手机突然黑屏重启。还有一些其他偏方此处不提了,也不过是误打误撞,只有这篇文章才真正首次分析出根本原因。
排查此问题走的弯路还蛮多的:
- 系统刷新和回收幻影进程有特定的时机,对寻找稳定复现的路径造成了一点点误导
- 创建最终会被泄露的进程有特定的冷却时间,对寻找稳定复现的路径造成了一点点误导
- 最初没找到相应逻辑位置,中途误以为在vdex文件中,消耗了一点时间编译windows版本的vdexExtracter
- 手机没有Root,纯静态分析很麻烦,所以中途又去研究了是否有不存在变砖风险的临时Root漏洞可以利用
- 反编译了"智能管理器"之类的三星特有的系统App找线索
为了测试能否在更多设备上复现,我跑了两家三星体验店,一家得知我想测试低存储空间的情况,直接说"所有手机品牌剩余3GB的时候都会打不开软件,不是三星的问题",怕我测出BUG,直接劝我走,说我"在这里会浪费时间影响他人体验"。然后我又去了5公里外的另一家体验店,刚把剩余存储空间小于3GB的触发条件达成,才发现展示机不能修改手机时间,如果不改时间,想自然复现此bug,至少需要大约3小时,而且一但屏幕关闭,体验店的展示机就会还原数据,一切白费,时间成本太高了,然后云测平台虽然免费,但是里面的测试机都是海外版,而从代码来看,只有大陆版本才会触发此BUG。
不过最后还是找到了一个大陆版本的云测平台。找了一部OneUI8.5的设备提取了相关文件,确定三星已在OneUI8.5版本修复此问题。 但是BUG代码属于apex模块,apex模块的版本和系统版本不一定完全对应。后续或许更低的OneUI版本也会收到相关模块的OTA。
从日志找线索
接下来的步骤再换到另一个App:用铁路12306演示。
不要对logcat过滤,放开全部级别、全部标签、全部包名的日志,搜索即将闪退的包名,比如铁路12306的com.MobileTicket,从最后一个倒着往前看。
这里AMS只是说进程退出了,但AMS这条日志不包含进程退出的原因,但是可以根据这个日志确定退出前pid是1760,继续搜索1760。
倒着找,可以看到进程被结束的原因是
Killing PhantomProcessRecord {c4b77f2 1815:1760:com.MobileTicket/u0a295}: Trimming phantom processes
进程是AMS主动结束的,这里提到了一个关键词phantom processes,本文这里就统一称作“幻影进程”。这里提到“Trimming phantom processes”,后文会根据此在AOSP中搜索,展开具体的清理逻辑。
Android普通应用App想创建进程,有两种方式,一种是在manifest里面给某个组件声明processName,另一种是fork创建子进程,加固壳或者是需要快速拷贝一份内存的时候(比如xcrash在捕获到崩溃时)常用到,还有就是结合execve一起用,比如执行某个命令,本质就是fork+execve,例如常见的捕获日志的方案就是用Runtime.exec或者ProcessBuilder启动logcat。
App进程通过fork创建的子进程,就是幻影进程。注意这里说的是普通应用App,所以Zygote调用fork创建出来的普通App进程不被定义成幻影进程。
梆梆企业版加固壳会通过fork创建子进程执行检测相关代码,如果fork创建的进程被结束,被保护的App进程也会跟随退出。
manifest里processName是给四大组件声明的,所以AMS本来就能管理它们的生命周期,但是native层调用fork创建出来的进程,framework没办法直接感知到,不容易管理,为了避免滥用,Android12开始限制了幻影进程的数量,默认32个。
进入adb shell之后,用dumpsys activity processes可以打印出来当前有哪些幻影进程。在可以复现崩溃的手机上执行之后:
e1q:/ $
e1q:/ $ dumpsys activity processes | grep -A 5 "PhantomProcessRecord"
proc #0: PhantomProcessRecord {ec6ced2 1743:2588:top/1000}
user #0 uid=1000 pid=1743 ppid=2588 knownSince=-22m26s620ms killed=false
lastCpuTime=0 oom adj=-900 seq=3963
proc #1: PhantomProcessRecord {6163ba3 2655:2588:top/1000}
user #0 uid=1000 pid=2655 ppid=2588 knownSince=-10h16m30s253ms killed=false
lastCpuTime=10 timeUsed=+60ms oom adj=-900 seq=3963
proc #2: PhantomProcessRecord {b53ea0 4792:2588:top/1000}
user #0 uid=1000 pid=4792 ppid=2588 knownSince=-8h19m1s625ms killed=false
lastCpuTime=10 timeUsed=+40ms oom adj=-900 seq=3963
proc #3: PhantomProcessRecord {6009a59 5690:2588:top/1000}
user #0 uid=1000 pid=5690 ppid=2588 knownSince=-13h28m38s82ms killed=false
lastCpuTime=10 timeUsed=+100ms oom adj=-900 seq=3963
proc #4: PhantomProcessRecord {71f061e 8009:2588:top/1000}
user #0 uid=1000 pid=8009 ppid=2588 knownSince=-6h16m7s358ms killed=false
lastCpuTime=10 timeUsed=+20ms oom adj=-900 seq=3963
proc #5: PhantomProcessRecord {546eeff 8181:2588:top/1000}
user #0 uid=1000 pid=8181 ppid=2588 knownSince=-5h7m32s244ms killed=false
lastCpuTime=10 timeUsed=0 oom adj=-900 seq=3963
proc #6: PhantomProcessRecord {43fa4cc 8980:2588:top/1000}
user #0 uid=1000 pid=8980 ppid=2588 knownSince=-4h37m15s947ms killed=false
lastCpuTime=0 oom adj=-900 seq=3963
proc #7: PhantomProcessRecord {5178315 10626:2588:top/1000}
user #0 uid=1000 pid=10626 ppid=2588 knownSince=-13h51m27s689ms killed=false
lastCpuTime=10 timeUsed=+90ms oom adj=-900 seq=3963
proc #8: PhantomProcessRecord {c31662a 12436:2588:top/1000}
user #0 uid=1000 pid=12436 ppid=2588 knownSince=-13h9m24s922ms killed=false
lastCpuTime=10 timeUsed=+90ms oom adj=-900 seq=3963
proc #9: PhantomProcessRecord {ca57c1b 14163:2588:top/1000}
user #0 uid=1000 pid=14163 ppid=2588 knownSince=-8h52m42s943ms killed=false
lastCpuTime=10 timeUsed=+50ms oom adj=-900 seq=3963
proc #10: PhantomProcessRecord {62841b8 15184:2588:top/1000}
user #0 uid=1000 pid=15184 ppid=2588 knownSince=-10h42m34s355ms killed=false
lastCpuTime=10 timeUsed=+60ms oom adj=-900 seq=3963
proc #11: PhantomProcessRecord {f7a3b91 15367:2588:top/1000}
user #0 uid=1000 pid=15367 ppid=2588 knownSince=-11h53m21s647ms killed=false
lastCpuTime=10 timeUsed=+80ms oom adj=-900 seq=3963
proc #12: PhantomProcessRecord {857baf6 16294:2588:top/1000}
user #0 uid=1000 pid=16294 ppid=2588 knownSince=-5h1m13s120ms killed=false
lastCpuTime=10 timeUsed=0 oom adj=-900 seq=3963
proc #13: PhantomProcessRecord {895bef7 17702:2588:top/1000}
user #0 uid=1000 pid=17702 ppid=2588 knownSince=-9h18m23s947ms killed=false
lastCpuTime=10 timeUsed=+60ms oom adj=-900 seq=3963
proc #14: PhantomProcessRecord {1fbc164 17916:2588:top/1000}
user #0 uid=1000 pid=17916 ppid=2588 knownSince=-12h26m38s906ms killed=false
lastCpuTime=10 timeUsed=+70ms oom adj=-900 seq=3963
proc #15: PhantomProcessRecord {475ffcd 18682:2588:top/1000}
user #0 uid=1000 pid=18682 ppid=2588 knownSince=-5h49m46s190ms killed=false
lastCpuTime=10 timeUsed=+20ms oom adj=-900 seq=3963
proc #16: PhantomProcessRecord {fbc9082 19708:2588:top/1000}
user #0 uid=1000 pid=19708 ppid=2588 knownSince=-7h4m42s286ms killed=false
lastCpuTime=10 timeUsed=+20ms oom adj=-900 seq=3963
proc #17: PhantomProcessRecord {ddd5393 19949:2588:top/1000}
user #0 uid=1000 pid=19949 ppid=2588 knownSince=-11h31m48s133ms killed=false
lastCpuTime=10 timeUsed=+60ms oom adj=-900 seq=3963
proc #18: PhantomProcessRecord {d658fd0 20100:2588:top/1000}
user #0 uid=1000 pid=20100 ppid=2588 knownSince=-13h41m24s354ms killed=false
lastCpuTime=10 timeUsed=+100ms oom adj=-900 seq=3963
proc #19: PhantomProcessRecord {efcbc9 20546:2588:top/1000}
user #0 uid=1000 pid=20546 ppid=2588 knownSince=-10h59m58s866ms killed=false
lastCpuTime=10 timeUsed=+60ms oom adj=-900 seq=3963
proc #20: PhantomProcessRecord {ca732ce 20678:2588:top/1000}
user #0 uid=1000 pid=20678 ppid=2588 knownSince=-6h9m4s943ms killed=false
lastCpuTime=10 timeUsed=+10ms oom adj=-900 seq=3963
proc #21: PhantomProcessRecord {dff95ef 22114:2588:top/1000}
user #0 uid=1000 pid=22114 ppid=2588 knownSince=-8h25m48s257ms killed=false
lastCpuTime=10 timeUsed=+40ms oom adj=-900 seq=3963
proc #22: PhantomProcessRecord {b5bd8fc 22234:2588:top/1000}
user #0 uid=1000 pid=22234 ppid=2588 knownSince=-4h55m37s266ms killed=false
lastCpuTime=0 oom adj=-900 seq=3963
proc #23: PhantomProcessRecord {1205b85 22569:2588:top/1000}
user #0 uid=1000 pid=22569 ppid=2588 knownSince=-12h2m43s261ms killed=false
lastCpuTime=10 timeUsed=+70ms oom adj=-900 seq=3963
proc #24: PhantomProcessRecord {487adda 25387:2588:top/1000}
user #0 uid=1000 pid=25387 ppid=2588 knownSince=-12h16m17s235ms killed=false
lastCpuTime=10 timeUsed=+70ms oom adj=-900 seq=3963
proc #25: PhantomProcessRecord {559a20b 26665:2588:top/1000}
user #0 uid=1000 pid=26665 ppid=2588 knownSince=-10h52m53s85ms killed=false
lastCpuTime=10 timeUsed=+70ms oom adj=-900 seq=3963
proc #26: PhantomProcessRecord {60b88e8 26831:2588:top/1000}
user #0 uid=1000 pid=26831 ppid=2588 knownSince=-5h44m9s538ms killed=false
lastCpuTime=10 timeUsed=+20ms oom adj=-900 seq=3963
proc #27: PhantomProcessRecord {2102b01 27440:2588:top/1000}
user #0 uid=1000 pid=27440 ppid=2588 knownSince=-6h25m40s471ms killed=false
lastCpuTime=10 timeUsed=+20ms oom adj=-900 seq=3963
proc #28: PhantomProcessRecord {1c2cda6 28494:2588:top/1000}
user #0 uid=1000 pid=28494 ppid=2588 knownSince=-9h0m16s439ms killed=false
lastCpuTime=10 timeUsed=+40ms oom adj=-900 seq=3963
proc #29: PhantomProcessRecord {efe53e7 30564:2588:top/1000}
user #0 uid=1000 pid=30564 ppid=2588 knownSince=-4h49m9s391ms killed=false
lastCpuTime=10 timeUsed=0 oom adj=-900 seq=3963
proc #30: PhantomProcessRecord {b844b94 31655:2588:top/1000}
user #0 uid=1000 pid=31655 ppid=2588 knownSince=-12h39m2s871ms killed=false
lastCpuTime=10 timeUsed=+80ms oom adj=-900 seq=3963
proc #31: PhantomProcessRecord {ed3763d 31761:2588:top/1000}
user #0 uid=1000 pid=31761 ppid=2588 knownSince=-11h19m4s341ms killed=false
lastCpuTime=120 timeUsed=+70ms oom adj=-900 seq=3963
Foreground Processes:
PID #12330: ImportanceToken { 2468a7a setProcessImportant() 12330 android.os.BinderProxy@a96522b }
e1q:/ $
可以发现有一堆top进程出现,而且knownSince字段可以看到启动时间没有什么周期性的规律,好像在使用手机期间更容易创建出"top"进程,ppid竟然都是2588,在我设备上就是system_server进程,uid也和system_server一样,是1000。
我这里写了个Demo,发现fork之后,dumpsys activity processes | grep -A 5 "PhantomProcessRecord"并不会立即打印出来我fork出的进程,而下拉手机状态栏的时候,才会被系统统计到,说明AMS统计幻影进程是有某些时机才触发的,系统在发现了幻影进程数量达到限制才会执行回收清理。所以下拉状态栏的时候会闪退,这也导致从现象看来太像是App自身的BUG了。
如果开发者选项里面勾选了"禁用子进程限制",确实不再闪退了,所有App都能正常打开,正常使用,并且不久后top进程的数量就会突破32。这里调试的时候遇到了个坑,就是如果想再复现此BUG,再次关闭此选项之后,需要重启手机,否则幻影进程数量就不会再回收了。所以还有个更好的测试命令:
device_config set_sync_disabled_for_tests persistent
device_config put activity_manager max_phantom_processes 64
提升幻影进程数量上限后,短暂不再出现闪退问题,至此已经可以初步确定是系统出了BUG,一切都能说得通了,system_server进程oom_adj非常低,所以清理优先级很低,系统清理进程时是根据oom_adj清理的,所以反而是普通App的进程被清理掉了,具体的清理逻辑下文会提到。但是至此还不能确定触发top进程泄露的具体的触发时机,以及为什么top进程会出现泄露,所以还要继续分析。
尝试找top进程创建时机
top进程创建时机不太好确定,可以在adb shell里面执行一段脚本,然后正常使用手机的同时留意一下我们哪些操作触发了top进程的创建。
p=" "; while true; do for i in $(pgrep -x -u 1000 top; pgrep -x -P 2588 top); do case "$p" in *" $i "*) ;; *) T=$(date "+%Y-%m-%d %H:%M:%S.%N" | cut -c1-23); echo "$T $i"; p="$p$i ";; esac; done; sleep 0.5; done
其中2588是system_server进程pid。
通过这种方法收集到了更多信息:从launcher冷启动App、安装新的App时,会触发top进程创建。但是,并不是每次都创建,而是有几分钟的间隔时间。
反编译system_server,静态分析
为了猜测system_server为什么执行top命令,可以看一下top启动时候的参数,即使设备没有root,也可以通过/proc/[pid]/cmdline确定app启动时候的命令行参数,可以看到命令和参数是top -b -n 1:
(后来发现,其实top -b -n 1这个命令本身也可以看到top进程的命令行参数)。
用adb pull导出手机的/system/framework文件夹。由于jadx反编译出来的java代码可能有误、反编译失败、不完整,这里用baksmali,把所有jar转为smali代码。 以smali版本为准。寻找何处调用了“top”进程,搜索的关键词主要是ProcessBuilder、Runtime.exec、top,
然后发现确实没有地方启动top进程,所有调用exec的参数,都追踪了来源,确定没有地方调用top,此处几种可能:相关代码做了混淆、system_server还加载了其他位置的jar/dex、或者是逻辑位于native层。但是由于没有root权限,难以直观得知system_server进程加载了哪些dex/jar/so。
根据以往的一些积累,知道设备的/apex目录也存放了一些包含dex/so的模块。Android10开始引入apex机制,把一些模块放到/apex目录,而不是/system/framework,初衷是便于像在应用商店升级app一样动态升级某些系统模块,而不必升级整个手机操作系统。
但是由于没有root权限,也无法直接用ls获得当前设备的/apex目录列表,而/system/apex目录里面的dex在img格式文件当中,但这里不用考虑如何处理这个img文件,有个更快的技巧。system_server和普通app进程都是由zygote进程fork出来的,大部分模块早在zygote进程就加载好了。不用写代码,直接在adb shell里面run-as一个debuggable的app,再去打印相应的maps文件,如下图,通过这种方式先确定一些已经被zygote进程加载过的模块的路径,然后先把已知的模块导出,继续反编译查看是否有top相关的调用。
然而仍然没有找到关键的执行top命令的信息,但是如上图,我们看到了三星设备特有的apex路径/apex/com.samsung.android.shell/,虽然对于adb shell,/apex/不能直接ls,然而它子目录是可以的,如下图,就这样找到了存在BUG的关键文件:service-samsung-shell.jar。
就这样终于找到了调用top命令的位置:
1个函数里面的3个严重bug
对照了smali代码,确定jadx反编译出来的伪代码基本准确,直接贴出来:
private double getCpuUsage(String processName) {
double result = 0.0d;
try {
Process process = Runtime.getRuntime().exec("top -b -n 1");
BufferedReader in1 = new BufferedReader(new InputStreamReader(process.getInputStream()));
int cnt = 8;
while (true) {
String str1 = in1.readLine();
if (str1 == null) {
break;
}
int cnt2 = cnt - 1;
if (cnt <= 0) {
break;
}
if (processName == null) {
if (!str1.contains("%idle")) {
cnt = cnt2;
} else {
String cpuTotal = str1.split("%cpu")[0];
String cpuIdle = str1.split(" +")[4].split("%")[0];
result = (Double.valueOf(cpuTotal).doubleValue() - Double.valueOf(cpuIdle).doubleValue()) / Double.valueOf(cpuTotal).doubleValue();
break;
}
} else if (!str1.contains(processName)) {
cnt = cnt2;
} else {
result = Double.valueOf(str1.split(" +")[9]).doubleValue() / 100.0d;
break;
}
return result;
}
} catch (IOException e) {
Log.e(this.TAG, "Error getCpuUsage: \n", e);
}
return result;
}
我在我Android 16设备执行top -b -n 1的输出结果:
BUG 1
注意,代码里用str1.split(" +")[9]获取第10列,然而,第1列是PID,第10列是MEM,第9列才是CPU!导致获取到的CPU百分比完全错误。
或许在Android16之前的某个版本,这不是BUG,可能这是历史遗留问题,或许Android12、13还是正常的呢,但这里我觉得没必要去测试了。反正这一定是非常不好的做法,凭什么假定top命令的输出格式永远不变呢?
联想到以前的一个经历
我想起了以前一份工作中非常不好的经历,那就是领导非让用sha256sum命令获取文件的哈希值,因为领导说,“linux命令极度高效,应该会比你java效率高!”,由于领导没有系统学过计算机,甚至不了解进程和线程的关系,解释不通,也不听解释。
前同事还因此奉命使用系统自带的tar命令实现文件压缩,由于部分vivo系统对tar命令参数做了修改,导致部分用户功能异常,我们自己的测试机却无法复现,由于反馈比例较少,没有人重视,最终是随着这些机型被淘汰而不了了之,这些不好的经历还是不要展开了...。
BUG 2(本文重点)
此处使用Runtime.getRuntime().exec创建子线程,用BufferedReader读取输出结果,然而,由于cnt = 8,根本就不会把子进程的输出读取完毕,函数就退出了,子进程仍然在运行中,没有调用destroy,也没有调用close,这就是造成top进程泄露,最终引发App异常,甚至大量App闪退的根本原因。
如果用户根据网络上的教程勾选了“禁用子线程限制”,虽然表面上解决了闪退问题,但是top进程的泄露依然存在,文件描述符泄露依然存在,Android系统对单个进程的文件描述符数量做了限制,通常是1024,如果超过这个数字,会造成system_server进程异常,导致手机突然黑屏重启。
BUG 3
就算没有BUG1,也根本读取不到参数进程的CPU占用!这里cnt是8,然而根据我设备上的输出,除了读取top进程之外,只能额外读取到一个进程的CPU占用,很难刚好就是目标进程!
稳定触发BUG的测试路径
到这一步,就很容易找到触发BUG的必现路径了。
前面发现冷启动时会触发,以及偶然会触发,确实如此,其中一条链路梳理如下,因为三星做了一点定制,所以这里从设备导出的famework源码分析,而不能是AOSP源码。
经典面试题,App冷启动流程:用户点Launcher上的图标,ActivityStarter.execute内部调用startActivityUnchecked,调用到resolveReusableTask,获取PkgPredictorService服务,调用dexFilePreload:
直接来到刚才找到的jar中,dexFilePreload内部调用appTouchDownEvent:
然后内部用handler调用handleAppBoostTask:
handleAppBoostTask内部调用isSystemBusy,如下图,
其中的isSystemBusy调用getCpuUsage,至此BUG闭环了,如下图。另外果然App安装的时候会有地方触发它的调用,如上图。
这里贴出来,
private long getAvailRomSize() {
File file = Environment.getDataDirectory();
StatFs statFs = new StatFs(file.getPath());
long blockSize = statFs.getBlockSize();
long availableBlocks = statFs.getAvailableBlocksLong();
long available = (((availableBlocks * blockSize) / 1024) / 1024) / 1024;
this.SYSTEM_BUSY_UPDATE_TIME = 30000 * available;
if (this.SYSTEM_BUSY_UPDATE_TIME < 300000) {
this.SYSTEM_BUSY_UPDATE_TIME = 300000L;
}
return available;
}
public boolean isSystemBusy() {
if (!this.mAppBoostInit) {
return false;
}
if (System.currentTimeMillis() - this.mSystemBusyTime <= this.SYSTEM_BUSY_UPDATE_TIME) {
return this.mSystemBusy;
}
this.mSystemBusyTime = System.currentTimeMillis();
if (getAvailRomSize() <= this.APPBOOST_AVAILABLE_ROM_SIZE && getCpuUsage("com.google.android.providers.media.module") >= this.MP_CPU_LIMITATION) {
this.mSystemBusy = true;
return true;
}
this.mSystemBusy = false;
return false;
}
这个函数里面用currentTimeMillis时间对上次判断结果做了缓存,这个有效期是动态计算的,在getAvailRomSize里面对缓存有效期做了更新,用手机可用空间的GB数量乘以30000毫秒(30秒),也就是说,剩余10GB的用户,缓存300秒,但是,这里还有最低阈值是300000毫秒(300秒,5分钟),也就是说最低也是5分钟才会调用到一次getCpuUsage函数。不过如果我们为了测试方便,可以直接去系统设置里面改手机时间,但是三星手机体验店里面的展示机不能改时间,云测机不太容易进入设置。
并不是说最低5分钟就一定会调用getCpuUsage,此处还有一个限制就是可用Rom空间(GB单位)必须小于APPBOOST_AVAILABLE_ROM_SIZE,这里是常量值是3,也就是说只有内存低于3GB的用户才会出现top进程泄露导致许多App闪退或者异常的问题。
所以稳定复现的路径:不要在开发者选项勾选“禁用子进程限制”(如果勾选了,需要取消勾选,然后重启手机),然后将可用内存空间调整到3GB以下,然后冷启动App大约32次,每次间隔5分钟(可以修改手机系统时间缩短间隔时间)。
还有一个前提条件,必须是中国版系统:
dexFilePreload内部没机会调用到appTouchDownEvent,也就不会触发此BUG,因为非中国版系统,mIpmAntiAgingController不被赋值:
Android16 清理幻影进程的顺序
最初和同事提到此问题时,有同事说:“...我觉得不太可能是三星系统的BUG,App在前台,系统回收进程的时候不是优先回收后台的进程吗?照你这么说是Android系统有BUG吗?不太可能...”。
直接看代码,首先看前文提到的那个日志,位于trimPhantomProcessesIfNecessary函数中:
/**
* Clamp the number of phantom processes to
* {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the
* order of the oom adjs of their parent process.
*/
void trimPhantomProcessesIfNecessary() {
if (!mService.mSystemReady || !FeatureFlagUtils.isEnabled(mService.mContext,
SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS)) {
return;
}
synchronized (mService.mProcLock) {
synchronized (mLock) {
mTrimPhantomProcessScheduled = false;
if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) {
for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i));
}
synchronized (mService.mPidsSelfLocked) {
Collections.sort(mTempPhantomProcesses, (a, b) -> {
final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid);
if (ra == null) {
// parent is gone, this process should have been killed too
return 1;
}
final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid);
if (rb == null) {
// parent is gone, this process should have been killed too
return -1;
}
if (ra.getCurAdj() != rb.getCurAdj()) {
return ra.getCurAdj() - rb.getCurAdj();
}
if (a.mKnownSince != b.mKnownSince) {
// In case of identical oom adj, younger one first
return a.mKnownSince < b.mKnownSince ? 1 : -1;
}
return 0;
});
}
for (int i = mTempPhantomProcesses.size() - 1;
i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) {
final PhantomProcessRecord proc = mTempPhantomProcesses.get(i);
proc.killLocked("Trimming phantom processes", true);
}
mTempPhantomProcesses.clear();
}
}
}
}
看这里Collections.sort,直接根据mPpid(父进程的pid)的oom adj排序,然后从oom obj较大的一端开始kill,直到幻影进程数量小于MAX_PHANTOM_PROCESSES。system_server的oom obj被定义为-900,从前面的adb输出也可以看到确实如此,而普通app的oom obj,可见前台是0,可见非前台是100,上一个可见app是700,普通app的oom obj,再怎么也不能比0更小了。
另外,从源码来看,还有另一种情况就是连带父进程一起结束,代码如下,这里不继续探索了。
/**
* Kill the given phantom process, all its siblings (if any) and their parent process
*/
@GuardedBy("mService")
void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc,
@Reason int reasonCode, @SubReason int subReason, String msg) {
synchronized (mLock) {
int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
if (index >= 0) {
final SparseArray<PhantomProcessRecord> array =
mAppPhantomProcessMap.valueAt(index);
for (int i = array.size() - 1; i >= 0; i--) {
final PhantomProcessRecord r = array.valueAt(i);
if (r == proc) {
r.killLocked(msg, true);
} else {
r.killLocked("Caused by sibling process: " + msg, false);
}
}
}
}
// Lastly, kill the parent process too
app.killLocked("Caused by child process: " + msg, reasonCode, subReason, true);
}
OneUI8.5的修复
如图所示,用proc文件系统的方式获取cpu占用率,不再用top命令获取了。
其实还没太写完,也还没来得及研究IPM这里在做什么,时间太晚了,有时间再完善吧,这周末再补充点细节,就先这样发出来了,技术交流或者别的事可以联系我微信HKHSTECH(可能会改微信号,但几周内都不会换号)。