25、MediaServer 稳定性:SIGKILL 保护、看门狗机制、异常恢复
做多媒体开发这么多年,我见过最多的线上事故,不是功能没实现,而是 MediaServer 挂了。
你想想看,一个正在播放视频的手机,突然画面卡住、声音消失,用户第一反应就是「这手机真垃圾」。其实底层往往是 MediaServer 进程被系统干掉了,或者内部死锁导致整个服务瘫痪。
这一章,我就把 MediaServer 稳定性的三板斧讲清楚:SIGKILL 保护、看门狗机制、异常恢复。这三样东西,说白了就是保命用的。
25.1 SIGKILL 保护:别让系统随便杀我
Android 系统有个特性:当内存不足时,Low Memory Killer(LMK)会优先杀掉那些「不重要」的进程。MediaServer 虽然优先级不低,但架不住系统真的急眼了。
我遇到过一台 2GB 内存的低端机,后台开几个应用再切回相机,MediaServer 就被杀了。结果相机黑屏,用户疯狂投诉。
25.1.1 为什么 MediaServer 容易被杀?
MediaServer 是一个 native 进程,它不像前台 Activity 那样有「免死金牌」。系统判断进程优先级时,主要看 oom_adj 值。这个值越低,越不容易被杀。
| 进程类型 | 典型 oom_adj 值 | 被杀风险 |
|---|---|---|
| 前台 Activity | 0 | 极低 |
| 可见进程 | 1~2 | 低 |
| 服务进程 | 4~6 | 中 |
| MediaServer | 默认 -1000(不可调) | 低(但非绝对) |
| 空进程 | 15 | 极高 |
嗯,这里要注意:MediaServer 的 oom_adj 默认是 -1000,理论上不会被 LMK 杀掉。但实际项目中,我见过厂商修改了 LMK 策略,或者 MediaServer 内部子线程内存泄漏导致 OOM,最终还是被系统强杀。
25.1.2 如何加固 SIGKILL 保护?
我个人习惯的做法是:
- 注册系统服务白名单:在 init.rc 中声明 MediaServer 为 critical 进程。这样系统在杀进程时会跳过它。
- 监控 oom_adj 变化:写一个守护线程,定期读取 /proc/self/oom_adj,如果发现值被篡改(比如变成正数),立刻重置。
- 内存泄漏检测:MediaServer 最怕内存泄漏。我曾在代码里加了一个定时器,每 30 秒检查一次 RSS 内存占用,如果超过阈值就主动触发 dump。
// 示例:监控 oom_adj 并重置
void monitor_oom_adj() {
int fd = open("/proc/self/oom_adj", O_RDWR);
if (fd < 0) return;
char buf[16];
read(fd, buf, sizeof(buf));
int adj = atoi(buf);
if (adj != -1000) {
// 被篡改了,立刻重置
lseek(fd, 0, SEEK_SET);
write(fd, "-1000", 5);
ALOGW("oom_adj was changed to %d, reset to -1000", adj);
}
close(fd);
}
25.2 看门狗机制:别让死锁拖死整个服务
SIGKILL 保护防的是外部杀进程,但 MediaServer 内部也可能出问题。最常见的就是死锁。
我记得有一次,MediaServer 里一个音频解码线程和视频渲染线程互相等待锁,结果整个服务卡死了。系统没杀它,但它自己动不了了。用户看到的画面就是「播放器转圈圈,永远等不到第一帧」。
25.2.1 看门狗的工作原理
看门狗(Watchdog)说白了就是一个心跳检测器。主线程每隔一段时间(比如 5 秒)向看门狗报告「我还活着」。如果看门狗连续几次没收到心跳,就判定主线程卡死,然后执行恢复操作。
Android 原生 MediaServer 其实自带了一个 Watchdog,但说实话,它太简单了。我建议自己实现一个更健壮的版本。
// 自定义看门狗实现
class MediaWatchdog {
public:
void feed() {
std::lock_guard<std::mutex> lock(mMutex);
mLastFeedTime = std::chrono::steady_clock::now();
}
void check() {
auto now = std::chrono::steady_clock::now();
auto diff = std::chrono::duration_cast<std::chrono::seconds>(now - mLastFeedTime);
if (diff.count() > TIMEOUT_SECONDS) {
// 超时了!触发恢复
ALOGE("Watchdog timeout! Last feed was %lld seconds ago", diff.count());
triggerRecovery();
}
}
private:
static constexpr int TIMEOUT_SECONDS = 10;
std::mutex mMutex;
std::chrono::time_point<std::chrono::steady_clock> mLastFeedTime;
void triggerRecovery() {
// 1. 先尝试 graceful 恢复
// 2. 如果失败,直接重启 MediaServer
// 3. 记录 crash log 到 dropbox
}
};
25.2.2 看门狗部署策略
我个人建议分三层部署:
- 主线程看门狗:监控主消息循环是否卡死。如果主线程超过 10 秒没处理消息,就报警。
- 关键服务看门狗:比如 AudioFlinger、CameraService 这些核心子服务,每个都有自己的心跳。
- 全局看门狗:一个独立线程,定期检查所有子服务的心跳。如果某个子服务挂了,全局看门狗负责协调恢复。
25.3 异常恢复:挂了之后怎么办?
前面两招是「防挂」,但万一真的挂了,怎么办?
我见过很多团队,MediaServer 一挂就重启,重启完又挂,陷入死循环。用户手机直接变砖。这不行。
25.3.1 恢复策略分级
我习惯把异常恢复分成三个等级:
| 等级 | 触发条件 | 恢复动作 | 影响范围 |
|---|---|---|---|
| L1 轻量恢复 | 单个服务线程卡死 | 重启该线程,释放资源 | 仅影响当前流 |
| L2 中度恢复 | 子服务(如 AudioFlinger)崩溃 | 重启子服务,重建连接 | 影响音频相关功能 |
| L3 重度恢复 | MediaServer 主进程崩溃 | 重启整个 MediaServer | 所有多媒体功能中断 |
嗯,这里有个坑:L3 恢复时,所有客户端(比如 Camera、MediaPlayer)都会收到服务断开的消息。如果客户端没有做好重连逻辑,就会一直卡住。
25.3.2 重启时的状态保存
我曾经踩过一个坑:MediaServer 重启后,之前打开的摄像头设备没有释放,导致新进程无法打开摄像头。后来我加了一个状态持久化机制。
// 重启前保存关键状态
void save_state_before_restart() {
// 1. 保存当前打开的硬件设备列表
std::vector<std::string> open_devices = get_open_devices();
write_to_file("/data/misc/mediaserver/state", open_devices);
// 2. 保存客户端连接信息(binder 句柄等)
save_client_connections();
// 3. 通知系统服务管理器(servicemanager)即将重启
notify_service_manager();
}
// 重启后恢复状态
void restore_state_after_restart() {
// 1. 读取之前保存的设备列表
auto devices = read_from_file("/data/misc/mediaserver/state");
// 2. 尝试重新打开设备
for (auto& dev : devices) {
if (!try_reopen_device(dev)) {
// 如果打不开,通知上层设备丢失
notify_device_lost(dev);
}
}
// 3. 恢复客户端连接
restore_client_connections();
}
25.4 实战:搭建一个完整的稳定性框架
说了这么多,我们来画一张图,看看这三板斧怎么组合在一起。
你看,这三层是层层递进的。SIGKILL 保护在最外层,挡住系统级别的杀进程。如果没挡住,看门狗发现内部异常,触发恢复。恢复也分等级,尽量做到「小问题小修,大问题大修」。
最后说一句:稳定性不是写几行代码就能搞定的。我建议你在项目初期就把这些机制加进去,不要等线上出事故了再补。补丁式的稳定性,永远不如原生设计的好。
公众号:蓝海资料掘金营,微信deep3321