夜雨聆风学习资料网

ARTICLE · 1030300

手机 APP 突然"冻住"点不动?它没崩,只是被堵住了

手机 APP 突然"冻住"点不动?它没崩,只是被堵住了

你有没有过这种经历:正刷着手机,突然屏幕就"冻"住了。点按钮没反应,滑动没动静,过了几秒还弹出一个对话框:“应用未响应”,问你"等待"还是"关闭"。

你第一反应多半是:这 APP 是不是崩了?

其实不一定。它可能还活着,只是"卡住不动"了。在鸿蒙 HarmonyOS 里,这类问题有个专门的名字,叫应用冻屏(APP_FREEZE)。今天我们就用大白话聊聊:手机到底是怎么"冻"住的,又是谁把它"冻"住的。


一、先分清:崩了,还是冻了?

崩溃和冻屏,是两码事。

崩溃,是进程死了。就像一个人突然倒下,彻底没气了。系统会立刻处理,给你弹个"应用已停止"。

冻屏,是进程还活着,但不干活了。就像一个人睁着眼坐在那儿,你喊他、推他,他都没反应,可他还喘着气。界面卡死、点不动,就是这种状态。

冻屏还分两种,治法完全不同:

  • 阻塞型:主线程被"堵住"了,卡在原地动不了。等锁、等跨进程调用、等输入输出,都属于这一类。
  • 繁忙型:主线程没被堵,而是"忙不过来",任务太多太重,做不完。

怎么区分?靠一把"金钥匙"——3 秒/6 秒栈顶对比。这个后面细说。


二、系统怎么知道应用"冻"住了?三个"监工"

要分析冻屏,先得懂系统怎么判定冻屏。鸿蒙里有三种判定机制,相当于三个"监工"。

1. THREAD_BLOCK_6S:看门狗判活

每个应用进程里,系统都安插了一个"监工"线程,叫 watchdog(看门狗)。它干一件事:每隔一段时间,往主线程的任务队列里塞一个小任务,相当于问一句"你还活着吗?"

主线程正常的话,会顺手把这个小任务做掉。可要是主线程被堵住了,这个小任务就没人理。

  • 超过 3 秒没执行,看门狗上报一次告警,抓一张"现场照片"(warning 栈)。
  • 超过 6 秒还没执行,上报故障,再抓一张照片(error 栈)。

两张照片一对,就生成了完整的冻屏日志。

2. APP_INPUT_BLOCK:输入回执超时

你点了一下屏幕,输入系统把点击事件发给应用,应用得回个"收到"。如果这个回执超时了——5 秒(新版系统放宽到 8 秒)——系统就判定故障。因为事件能发到应用,所以这类故障一定发生在前台

3. LIFECYCLE_TIMEOUT:生命周期切换超时

应用在切换状态(比如从后台切到前台)时,系统会发指令,等应用回应。等太久没回应,就报超时。加载阶段给 10 秒,切前台给 5 秒

三种机制的检测时长,汇总成一张表:

故障类型
前台
后台
说明
THREAD_BLOCK_6S 主线程卡死
6 秒
21 秒
3 秒告警 + 6 秒故障
APP_INPUT_BLOCK 输入超时
5 秒(新版 8 秒)
必为前台
回执超时即报
LIFECYCLE_TIMEOUT 生命周期
加载 10 秒 / 前台 5 秒
同左
过半告警 + 完整故障

小知识:用 DevEco 的 Debug 按钮启动应用时,系统会自动关掉这些超时检测,免得调试干扰。所以调测冻屏要用 Run 方式。


三、金钥匙:3 秒/6 秒栈顶对比

这是整个排查里最关键的一步。

系统在 3 秒和 6 秒各抓了一次主线程的"现场照片"(栈)。把两张照片的栈顶对比一下:

  • 两张照片一样 → 主线程卡住没动,是阻塞型。它要么在等锁,要么在等跨进程调用,要么卡在某个业务调用上。
  • 两张照片不一样 → 主线程还在跑,只是任务太重做不完,是繁忙型

一句话记忆:“一致堵,不一致忙。”

先分型,再下药。阻塞型去追"被谁堵住了",繁忙型去找"哪里太忙了"。


四、阻塞型三种形态:看栈顶一眼认出

阻塞型冻屏,主线程卡在原地,具体卡在哪,看栈顶就能认出来。三种形态:

形态
栈顶特征
下一步
等锁卡死
栈顶是 Mutex::Lock(),往下是数据库连接池、ResultSet 关闭
找"拿着钥匙的人":谁持锁不放
IPC 卡死
栈顶是 ioctl,往下是 Binder 写入、等待完成
查对端进程:你在等谁,它为什么不回
业务栈帧
栈顶是 pread,往下是数据库读页
主线程做了重 IO,优化或挪到子线程

这里有个判读要领:栈顶是系统函数并不可怕,关键是往下找到第一个业务代码帧——它回答"主线程在替谁等"。

形态一:等锁,找"拿着钥匙的人"

锁(lock)可以理解成一把"钥匙"。主线程想用某个资源,得先拿到钥匙;钥匙被别人拿着,它就只能干等。

排查路径四步:

  1. 确认主线程栈顶在 lock / wait,确实在等锁。
  2. 顺着栈往下,找到这把锁属于哪个模块(数据库连接池、单例对象、全局缓存)。
  3. 翻全量线程栈,找哪个线程正拿着这把锁干活,它还卡在别的什么事上。
  4. 还原等待链:A 等 B 的锁,B 又在等 C……链的末端就是根因。

典型的死锁长相:3 秒/6 秒栈相同,主线程在 wait,另一个线程持锁不释放,而那个线程又在等主线程手里的东西——互相等,谁都动不了

修复方向:减小锁的粒度(别抱着锁做 IO 和网络)、统一加锁顺序、能不用锁就不用锁。

形态二:Binder,看"对端进程"

Binder 是鸿蒙里进程间通信的机制,可以理解成进程之间"打电话"。主线程打了一个电话出去,对方一直不接,它就卡在电话旁干等。

排查看两个东西:

  • BinderCatcher:进程间通信的"通话记录",格式是"谁 to 谁,等了多久"。等超过几秒的对端,就是重点怀疑对象。
  • PeerBinder Stacktrace:对端进程的栈。你在等它,它在干什么,一目了然。

还有个概念叫 Binder 线程池:每个进程最多 16 个线程专门处理跨进程调用。同步调用会占着线程等对端返回,对端慢,线程就被占住;16 个全占满,后续调用全部排队,表现为大面积卡顿。这就是"级联阻塞"。

形态三:业务栈帧,重 IO 挪子线程

栈顶是 pread(读文件)、数据库读写这类,说明主线程在做重 IO。主线程本该轻装上阵,结果扛着大石头跑,自然跑不动。优化方向:把重 IO、重计算挪到子线程。


五、排查五步走:别一上来就追凶手

分析冻屏,顺序很重要,不能乱。官方总结了一套五步闭环:

  1. 先排整机假象:先看是不是整机的问题(低内存、CPU 打满、过热降频)。整机问题会让应用"躺枪",先排除误判再谈分析。
  2. 划窗口:看 Reason 和前后台,推出检测时长;用上报时间反推故障发生的时间区间,只看这个窗口的日志。
  3. 分类型:3 秒/6 秒栈顶对比,一致是阻塞型,不一致是繁忙型。
  4. 追链条:等锁找持锁线程,Binder 看对端,业务调用找第一个业务栈帧。
  5. 定根因:在任务队列里找超时任务,定位到具体业务代码,修复。

为什么第一步是排整机?因为从 API 20 起,整机资源告警时,日志里会直接输出一行 NOTE,大意是"这个故障可能是低内存或热限频引起的,可以忽略"。系统都提醒你了,就别再冤枉应用了。


六、三板斧:栈、trace、日志

当栈顶落在方舟运行时(JS 代码都在里面跑)时,栈顶看不出业务,官方总结出"三板斧":

  • 一板斧,看 freeze 栈:栈顶代码行告诉你 3 秒/6 秒在跑什么;栈相同查死锁;留意典型长耗时函数。
  • 二板斧,看 trace:瞬时栈只有 3 秒/6 秒两张照片,trace 能还原冻屏前整个 6 秒的进程状态,相当于把"主线程这 6 秒干了什么"拍成电影。
  • 三板斧,看日志:栈和 trace 都定不了界时,看 freeze 前的日志。日志里反复出现的接口名,往往就是凶手签名。

七、两个真实案例

案例一:3 秒/6 秒栈相同,不一定是死锁

某应用冻屏,3 秒和 6 秒的栈完全一致,栈顶都是同一个引擎接口。剥掉引擎栈帧,两次抓到的应用代码行都是同一行

按前面的判据,栈一致应该是"堵住了"。但仔细一看,这个接口流程无锁,排除死锁。那真相是什么?

同栈不一定是在"等",也可能是在"算"。 3 秒里主线程一直在执行同一行代码,典型的大 for 循环——循环遍历查找,数据量一大,循环耗时过长。

官方的规避方案很有意思:给循环装一个"超时保险丝"。用 setTimeout 设一个 100 毫秒的定时器,到点就把一个标志位置真;循环每轮检查这个标志,超时直接跳出,保住主线程。根治方向是优化查找算法,或整体挪到子线程。

案例二:栈和 trace 都失灵,日志兜底

另一个服务类进程卡死,trace 显示 GC 循环调用,但每次占比不高、符合规律,排除 GC;freeze 栈也无法定界。两板斧都失效了。

第三板斧上场:按线程加时间窗口过滤日志。发现应用一直在循环调用某个搜索接口,日志反复刷。与应用方确认,就是循环调用导致的。

这课教的是"退路":不要在一棵树上吊死。 栈不行换 trace,trace 不行换日志,三招组合,覆盖绝大多数场景。


八、给普通用户的几点提醒

如果你是普通用户,遇到"应用未响应",可以记住这几点:

  • 别急着卸载。冻屏不一定是应用"坏"了,可能是整机内存不足、手机过热降频,甚至系统调度问题。先重启试试,往往就好了。
  • 冻屏久了,系统会"杀"掉应用保流畅。所以冻屏不只是体验问题,还可能让应用被误杀。
  • 后台应用也会冻屏,只是系统放宽了检测时长(21 秒),因为后台不直接和你交互。

如果你是开发者,记住五条军规:

  1. 拿到冻屏日志,先看 NOTE 行和整机资源,再开始分析。
  2. 3 秒/6 秒栈对比先行:一致追链条,不一致查繁忙,别混着治。
  3. 等锁必找持锁方,Binder 必看对端——凶手常在别的线程、别的进程
  4. 主线程三不放:重 IO、重计算、同步跨进程调用
  5. 线上靠 uuid 聚类看冻屏构成,别一条条翻原始日志。

相关学习资料

返回首页浏览学习资料