用 LSPosed 模拟录屏音频编码器卡顿:从思路到落地

用 LSPosed 模拟录屏音频编码器卡顿:从思路到落地

标签:Android / LSPosed / MediaCodec / 音画同步 / 缺陷复现

适用场景:录屏音画不同步、音频断续等缺陷,需要可控地「造」出编码器慢一拍的现象


预备

需要安装kernelSU 和LSPosed 安装kernelSU 和LSPosed

一、为什么要做这个

录屏链路里,视频和音频往往分开编码,最后再封装进同一个文件。

如果音频编码器偶尔变慢,就会出现:

  • 听感上断续、拖沓
  • 口型与画面不同步
  • 严重时录屏进程无响应

真实机器上这类问题偶发、难复现。最理想的做法是:在不改录屏业务代码的前提下,用 Hook 在音频编码收发缓冲时按概率注入随机延迟,把「编码器卡顿」变成可配置的实验条件。

本文介绍一套基于 LSPosed 模块 + 配置 App 的实现:MockEncoder(音频编码卡顿模拟)。

界面示意:

可配置项包括:

含义 示例
开启 / 关闭 是否注入延迟 已开启
触发概率 每次收发缓冲命中概率 40%
最小 / 最大延迟 命中后 sleep 的随机区间 69--317 ms

二、整体架构

bash 复制代码
┌─────────────────┐      Settings.Global       ┌──────────────────────┐
│  配置 App        │  ───────────────────────►  │  录屏进程             │
│  调参 / 开关     │                             │  LSPosed 注入模块     │
└─────────────────┘                             │  Hook MediaCodec     │
                                                │  概率 sleep          │
                                                └──────────────────────┘

分工很清晰:

  1. 配置 App:负责 UI 与参数持久化
  2. LSPosed 模块 :在录屏进程内 Hook MediaCodec
  3. 跨进程传参 :优先写 Settings.Global(任意进程可读),文件 / ContentProvider / XSharedPreferences 作兜底

作用域只勾选录屏应用(如 com.oplus.screenrecorder),避免误伤其他 App。


三、环境前提

  • 已 Root,Magisk + Zygisk
  • 已安装 LSPosed(Zygisk 版)
  • 模块在 LSPosed 中启用,作用域勾选录屏应用
  • 写入系统设置需一次性授权(见下文)
bash 复制代码
adb shell pm grant com.mockencoder.audio android.permission.WRITE_SECURE_SETTINGS

为什么要这个权限?

不是 Hook 需要它,而是「配置 App 把参数写给录屏进程」需要它。在较新的 ColorOS 上:

  • App 私有目录受 SELinux 限制,录屏进程读不到
  • ContentProvider 受应用可见性限制,可能报 Unknown authority
  • 传统 XSharedPreferences 路径也可能被挪走

Settings.Global写入 需权限,读取不需要。录屏侧 Hook 直接读同名键即可。


四、核心实现一:模块入口

目标进程加载时,框架回调入口,再安装 Hook。模块自身进程不注入。

kotlin 复制代码
/**
 * LSPosed 模块入口。
 * 目标 App(如录屏)进程启动时,框架会回调这里,随后在该进程内安装 Hook。
 */
class HookEntry : IXposedHookLoadPackage {
    override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
        // 不 Hook 配置 App 自身,避免无意义注入
        if (lpparam.packageName == MODULE_PACKAGE) {
            return
        }
        runCatching { AudioCodecHook.install(lpparam) }
            .onFailure { /* 打日志 */ }
    }
}

assets/xposed_init 指向该类全名;Manifest 里声明 xposedmodule 等 meta-data。


五、核心实现二:只卡音频,不卡视频

录屏会同时创建视频与音频编码器。必须在 configure 之后用 MIME 区分:

  • audio/mp4a-latm → 加入跟踪集合,后续注入延迟
  • video/avc → 不注入
kotlin 复制代码
/**
 * configure 之后根据 MediaFormat.KEY_MIME 判断是否为音频编码器。
 * audio/mp4a-latm 等加入跟踪集合;video/avc 等从集合移除。
 */
override fun afterHookedMethod(param: MethodHookParam) {
    val format = param.args.firstOrNull { it is MediaFormat } as? MediaFormat ?: return
    val mime = format.getString(MediaFormat.KEY_MIME)
    val isAudio = mime != null && mime.startsWith("audio/")
    if (isAudio) {
        audioCodecs.add(param.thisObject)   // 按对象身份跟踪
    } else {
        audioCodecs.remove(param.thisObject)
    }
}

IdentityHashMap 跟踪 MediaCodec 实例,避免错误地把视频编码也卡住。

queueInputBuffer / dequeueOutputBuffer 前,仅对集合内实例调用卡顿逻辑:

kotlin 复制代码
/** 送入编码输入缓冲前注入延迟,模拟「编码器处理慢」。 */
override fun beforeHookedMethod(param: MethodHookParam) {
    if (audioCodecs.contains(param.thisObject)) {
        LagSimulator.maybeStall("queueInput")
    }
}

成功日志示例:

ini 复制代码
hooks installed configure=3 queue=1 dequeue=1 release=1
codec configured mime=audio/mp4a-latm audio=true
codec configured mime=video/avc audio=false

六、核心实现三:按概率随机延迟

kotlin 复制代码
/**
 * 核心卡顿逻辑:
 * 1. 未开启 / 概率为 0 / 最大延迟为 0 → 直接返回
 * 2. 生成 [0,100) 随机数,小于设定概率则命中
 * 3. 在 [minDelay, maxDelay] 内随机 sleep
 */
fun maybeStall(tag: String) {
    val config = currentConfig()
    if (!config.enabled || config.probabilityPercent <= 0 || config.maxDelayMs <= 0) {
        return
    }
    val hit = ThreadLocalRandom.current().nextInt(100) < config.probabilityPercent
    if (!hit) return

    val min = config.minDelayMs
    val max = max(config.maxDelayMs, min)
    val delay = if (max == min) min else ThreadLocalRandom.current().nextInt(min, max + 1)
    if (delay > 0) {
        Thread.sleep(delay.toLong())
        // 日志:stall injected at queueInput delay=123ms
    }
}

注意:Thread.sleep 堵的是编码线程 。概率过高、延迟过大时,录屏可能像卡死------建议先用 20% + 100--300ms 验证,再加压。

配置约每 1.5 秒刷新一次,避免每次编码都读设置。


七、核心实现四:配置如何传到录屏进程

保存时三路冗余:

kotlin 复制代码
/** 本地保存 + 导出文件 + 写入 Settings.Global,三路冗余提高跨进程可读性。 */
fun save(context: Context, config: MockConfig): Boolean {
    // 1. SharedPreferences:App 自己读
    prefs(context).edit()....commit()
    // 2. 导出文件:部分机型可能被 SELinux 挡住
    exportToFile(context, c, now)
    // 3. Settings.Global:跨进程最稳(需 WRITE_SECURE_SETTINGS)
    writeSettings(context, c, now)
}

录屏侧读取优先级:Settings.Global → 文件 → ContentProvider → XSharedPreferences。

界面底部状态区应显示类似:

matlab 复制代码
磁盘开关 已开启
概率 40%
延迟 69-317ms
写入时间 19:43:45
共享文件已就绪
系统设置(global) 已就绪

看到「系统设置(global) 已就绪」,说明录屏进程有机会读到真实参数。


八、踩坑记录(强烈建议看)

1. 编译期 Xposed API 占位声明返回类型写错

内网若拉不到官方 api:82,常会自写 stub。若把:

  • hookAllMethods 写成 void
  • 而运行时实际是 Set<Unhook>

Android 按「参数 + 返回类型」匹配方法,会直接 NoSuchMethodError,表现为挂接全部失败(configure=0 queue=0)。

stub 的方法签名必须和运行时 LSPosed 一致。

2. 以为保存成功,其实开关一直是关的

早期 UI 开关开/关外观几乎一样,磁盘上 enabled=false,模块永远不注入。

后来改成「开启模拟 / 关闭模拟」大按钮,并在底部回显磁盘真实状态。

4. Hook 成功 ≠ 生效

可能看到:

  • hooks installed ...
  • codec configured mime=audio/...

但若日志是:

ini 复制代码
config[prefs] enabled=false probability=10%

说明配置没传过去,延迟逻辑被短路。目标应是:

ini 复制代码
config[settings] enabled=true probability=40% delay=69-317ms
stall injected at queueInput delay=xxxms

九、使用步骤(精简)

  1. 安装配置 App(即 LSPosed 模块 APK)
  2. LSPosed 启用模块,勾选录屏包名,强制停止录屏
  3. adb shell pm grant ... WRITE_SECURE_SETTINGS
  4. 打开 App → 点「开启模拟」→ 调好概率/延迟 → 确认底部「系统设置(global) 已就绪」
  5. 重新开始录屏,观察音画;用日志确认 stall injected

查看模块日志(LSPosed 模块日志页,或过滤):

bash 复制代码
adb logcat -s MockEncoder
# 或查看 /data/adb/lspd/log/modules_*.log

十、小结

目标 做法
不改录屏源码造故障 LSPosed Hook MediaCodec
只影响音频 configure 后按 MIME 过滤
可调强度 概率 + 随机延迟区间
跨进程配置 优先 Settings.Global
可观测 关键路径打日志,UI 回显磁盘状态

这套方案适合做音画同步 / 音频断续类问题的可控复现。参数要循序渐进:先确认链路通,再加大概率和延迟,否则很容易把录屏直接「卡死」当成模块没生效。


附录:相关声明

  • 仅建议在自有测试设备、合规 Root 环境下使用
  • 勿用于非授权应用或破坏他人设备稳定性
  • 文中包名、截图以 ColorOS 录屏为例,其他 ROM 请自行改作用域

(完)

相关推荐
zzq77971 小时前
别把大模型 API Key 写进 APK:移动 AI 应用接口防盗刷实践
android·人工智能·安全·app加固·御盾安全·安卓加固
xiaopang3 小时前
RK3568 AOSP 编译与快速部署实战
android
码农coding4 小时前
android12 SystemUI之通知
android
summerkissyou19875 小时前
android - 性能 - Perfetto - cpu 分析教程及例子
android·性能
AFinalStone6 小时前
Android 7系统休眠唤醒(七)内核层—wakelock与autosleep机制
android·休眠唤醒
starvapour7 小时前
在关机键损坏的情况下让手机关机
android·adb·手机
AFinalStone7 小时前
Android 7系统休眠唤醒(八)内核层—Alarm定时唤醒与硬件唤醒源
android·电源管理·休眠唤醒
AFinalStone8 小时前
Android 7系统休眠唤醒(五)休眠全链路
android·电源管理·休眠唤醒