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);
}
注意: 修改 oom_adj 需要 root 权限或系统签名。在用户版本中,这个操作可能被 SELinux 拦截。建议在系统编译时直接固化,不要运行时动态修改。

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();
}
小技巧: 状态文件不要写太大。我一般限制在 4KB 以内,只保存关键信息。如果文件损坏,直接丢弃,不要阻塞重启流程。

25.4 实战:搭建一个完整的稳定性框架

说了这么多,我们来画一张图,看看这三板斧怎么组合在一起。

MediaServer 稳定性框架 SIGKILL 保护层 oom_adj 固化 | 内存泄漏检测 | 系统白名单注册 看门狗机制 主线程看门狗 | 子服务看门狗 | 全局看门狗 异常恢复层 L1 轻量恢复 | L2 中度恢复 | L3 重度恢复 稳定可靠的 MediaServer 服务

你看,这三层是层层递进的。SIGKILL 保护在最外层,挡住系统级别的杀进程。如果没挡住,看门狗发现内部异常,触发恢复。恢复也分等级,尽量做到「小问题小修,大问题大修」。

最后说一句:稳定性不是写几行代码就能搞定的。我建议你在项目初期就把这些机制加进去,不要等线上出事故了再补。补丁式的稳定性,永远不如原生设计的好。

总结: SIGKILL 保护是「防杀」,看门狗是「防死」,异常恢复是「善后」。三者缺一不可。

公众号:蓝海资料掘金营,微信deep3321