三星安卓系统BUG排查记录:云闪付、铁路12306、个人所得税、中国移动等App,长期闪退的根本原因分析

内容介绍、背景

这次我在三星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可以打印出来当前有哪些幻影进程。在可以复现崩溃的手机上执行之后:

bash 复制代码
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,再次关闭此选项之后,需要重启手机,否则幻影进程数量就不会再回收了。所以还有个更好的测试命令:

bash 复制代码
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进程的创建。

bash 复制代码
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"进程,搜索的关键词主要是ProcessBuilderRuntime.exectop

然后发现确实没有地方启动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反编译出来的伪代码基本准确,直接贴出来:

java 复制代码
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安装的时候会有地方触发它的调用,如上图。

这里贴出来,

java 复制代码
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函数中:

源码: cs.android.com/android/pla...

java 复制代码
/**
 * 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更小了。

另外,从源码来看,还有另一种情况就是连带父进程一起结束,代码如下,这里不继续探索了。

java 复制代码
/**
 * 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(可能会改微信号,但几周内都不会换号)。

相关推荐
网安蟹佬霸3 小时前
CTF Web方向解题全攻略:从信息收集到getshell(附实战payload与脚本)
android·前端·网络·安全·web安全·网络安全·网安
又见情义3 小时前
Android 13 Settings 设置界面选项裁剪(RK3568平台)
android
人才瘾大4 小时前
Android录屏内部音频录制完全指南:原理、限制与方案选型
android·音视频
网安蟹佬霸5 小时前
Webshell免杀与检测实战:从一句话木马到加密流量
android·安全·web安全·网络安全·黑客·网安·渗透测试·
阿巴斯甜7 小时前
Android SharedMemory 详细使用
android
sun0077008 小时前
android的dns的常见错误resolv
android
恋猫de小郭8 小时前
Flutter A2UI 的正确用法,怎么把 AI 和动态 UI 结合有效生产
android·前端·flutter
余额瞒着我当琳8 小时前
C++--vector第二讲:手写 C++ STL:vector 源码剖析与迭代器失效分析
android·java·c++
程序员-珍9 小时前
安卓开发:同名 styles.xml 在不同分支/文件夹的合并规则
android·xml·前端