OrangeFox 适配 Redmi K60 Pro:从源头解决 Recovery 点击卡顿和振动问题
日期:2026-05-10
设备:Redmi K60 Pro,代号 socrates
系统环境:WSL Ubuntu,源码目录 /home/builduser/fox_12.1
目标:OrangeFox/TWRP Recovery
背景
在 Redmi K60 Pro 的 OrangeFox 适配过程中,Recovery 已经可以正常刷入和启动,但交互体验有一个很明显的问题:切换选项卡、点击按钮等操作非常卡顿,像是每次点击都会被阻塞几秒;但滑动列表并不算卡,最多只是有掉帧感。
这个现象很关键。列表滑动和按钮点击走的不是完全一样的路径,如果 GPU、触摸驱动或整体渲染性能有问题,滑动列表通常也会一起严重卡顿。现在只有“点击类操作”特别卡,就要优先怀疑点击事件里附带的同步动作,比如振动、声音、存储写入、页面动作前置检查等。
最终定位到的问题是:Recovery 点击时触发了振动逻辑,而当前设备树启用了 AIDL haptics 路径。这个路径会同步等待一个在 Recovery 环境里不存在或没有正确启动的 vibrator 服务,导致 UI 线程被阻塞。
现象和第一轮判断
点击按钮或切换选项卡时,logcat 中可以看到类似信息:
Waiting for service 'android.hardware.vibrator.IVibrator/vibratorfeature'...
Service android.hardware.vibrator.IVibrator/vibratorfeature didn't start. Returning NULL
这条日志几乎已经把问题说透了:每次点击都会触发一次振动,振动代码去 ServiceManager 里同步取 AIDL vibrator service;但 Recovery 环境没有这个服务,于是等待超时。用户看到的就是按钮响应卡顿。
为了验证这个判断,先做了一个最小化修复:在设备树中关闭 haptics。
TW_NO_HAPTICS := true
重新构建刷入后,点击卡顿消失。这一步不是最终方案,但它证明了问题链路:
点击按钮 -> 触发振动 -> 等待不存在的 AIDL vibrator service -> UI 阻塞 -> 体感卡顿
也就是说,真正要修的是振动路径,而不是触摸、显示或 OrangeFox 主题层。
为什么不直接启动 Xiaomi/QCOM AIDL 振动 HAL
理论上可以把系统里的 Xiaomi/QCOM vibrator HAL 搬进 Recovery,让 android.hardware.vibrator.IVibrator/vibratorfeature 这个服务真的跑起来。但这个方向风险比较高:
- Recovery 环境比 Android system/vendor 环境小很多,很多 HAL 的依赖不一定齐全。
- AIDL HAL 需要正确的 service binary、init rc、VINTF manifest、权限和依赖库。
- 当前卡顿来自同步等待服务,任何服务名、manifest 或 init 时序不一致,都会让卡顿回来。
- 为了一个点击振动引入完整 binder HAL,维护成本偏高。
Recovery 的需求其实很简单:按钮点击时让马达短震一下。既然内核已经暴露了 haptics 设备,就没有必要绕一大圈去起完整 HAL。
关键发现:设备已经暴露 input force-feedback 节点
在设备上检查 input 设备,发现了 qcom-hv-haptics:
N: Name="qcom-hv-haptics"
H: Handlers=event1
B: EV=200001
B: FF=120270000 0
用 getevent -il /dev/input/event1 也能看到它支持 force-feedback:
name: "qcom-hv-haptics"
events:
FF: FF_RUMBLE FF_PERIODIC FF_CONSTANT FF_DAMPER FF_CUSTOM FF_GAIN
这说明内核侧的振动能力已经在那里了。QCOM 的 HAL 最终也会落到类似的底层设备控制上。对 Recovery 来说,直接通过 Linux input force-feedback 驱动它,是更短、更稳定的路径。
最终方案就是:保留 TWRP/OrangeFox 原有的 vibrate() 调用入口,但不要再走 AIDL binder service;改成在普通 sysfs 振动节点不存在时,自动扫描 /dev/input/event*,找到 qcom-hv-haptics 或 qti-haptics,通过 EVIOCSFF 上传振动效果,再写入 EV_FF 事件播放。
最终实现思路
实现放在 bootable/recovery/minuitwrp/events.cpp,核心逻辑分成三层:
- 打开 haptics input 设备,并缓存 fd。
- 根据设备能力选择
FF_CONSTANT或FF_RUMBLE。 - 每次振动前移除旧 effect,上传新 effect,然后播放。
关键代码如下,省略了非核心分支:
static int qcom_input_ff_open()
{
if (qcom_haptics_fd >= 0)
return qcom_haptics_fd;
if (qcom_haptics_scanned)
return -1;
qcom_haptics_scanned = true;
DIR *dir = opendir("/dev/input");
if (!dir)
return -1;
struct dirent *de;
while ((de = readdir(dir))) {
if (strncmp(de->d_name, "event", 5))
continue;
int fd = openat(dirfd(dir), de->d_name, O_RDWR | O_CLOEXEC);
if (fd < 0)
continue;
char name[64] = {0};
if (ioctl(fd, EVIOCGNAME(sizeof(name)), name) < 0) {
close(fd);
continue;
}
if (strcmp(name, "qcom-hv-haptics") && strcmp(name, "qti-haptics")) {
close(fd);
continue;
}
unsigned long ff_bitmask[NBITS(FF_MAX)];
memset(ff_bitmask, 0, sizeof(ff_bitmask));
if (ioctl(fd, EVIOCGBIT(EV_FF, sizeof(ff_bitmask)), ff_bitmask) < 0) {
close(fd);
continue;
}
if (test_bit(FF_CONSTANT, ff_bitmask) || test_bit(FF_RUMBLE, ff_bitmask)) {
qcom_haptics_fd = fd;
qcom_haptics_uses_rumble = !test_bit(FF_CONSTANT, ff_bitmask);
closedir(dir);
return qcom_haptics_fd;
}
close(fd);
}
closedir(dir);
return -1;
}
播放振动时,用标准 input force-feedback ioctl:
static int qcom_input_ff_vibrate(int timeout_ms)
{
int fd = qcom_input_ff_open();
if (fd < 0)
return -1;
qcom_input_ff_remove_effect();
struct ff_effect effect;
memset(&effect, 0, sizeof(effect));
effect.id = -1;
effect.replay.length = timeout_ms;
effect.replay.delay = 0;
if (qcom_haptics_uses_rumble) {
effect.type = FF_RUMBLE;
effect.u.rumble.strong_magnitude = 0x7fff;
} else {
effect.type = FF_CONSTANT;
effect.u.constant.level = 0x5fff;
}
if (ioctl(fd, EVIOCSFF, &effect) < 0)
return -1;
qcom_haptics_effect_id = effect.id;
struct input_event play;
memset(&play, 0, sizeof(play));
play.type = EV_FF;
play.code = qcom_haptics_effect_id;
play.value = 1;
return write(fd, &play, sizeof(play)) == sizeof(play) ? 0 : -1;
}
最后把它接回原来的 vibrate() fallback 链路:
if (std::ifstream(LEDS_HAPTICS_ACTIVATE_FILE).good()) {
write_to_file(LEDS_HAPTICS_DURATION_FILE, tout);
write_to_file(LEDS_HAPTICS_ACTIVATE_FILE, "1");
} else if (qcom_input_ff_vibrate(timeout_ms) != 0) {
write_to_file(VIBRATOR_TIMEOUT_FILE, tout);
}
这样做的好处是,不破坏其他设备原本能用的 sysfs haptics。如果设备有 /sys/class/leds/vibrator/activate,仍然优先走旧逻辑;只有旧逻辑不可用时,才尝试 socrates 这类 QCOM input FF 设备。
BoardConfig 调整
设备树里不再关闭全部振动,也不再启用 AIDL haptics:
# Haptics
# Socrates exposes qcom-hv-haptics as an input force-feedback device in recovery.
# Use minuitwrp's direct input-ff fallback instead of waiting on an absent AIDL HAL.
# TW_SUPPORT_INPUT_AIDL_HAPTICS := true
# TW_SUPPORT_INPUT_AIDL_HAPTICS_FQNAME := "IVibrator/vibratorfeature"
# TW_SUPPORT_INPUT_AIDL_HAPTICS_FIX_OFF := true
这里的重点是:不要再定义 TW_NO_HAPTICS := true,否则所有按钮振动都会在编译期被去掉;也不要启用 TW_SUPPORT_INPUT_AIDL_HAPTICS,否则又会回到等待 binder service 的老问题。
TW_NO_HAPTICS := true已验证可彻底消除点击卡顿(证明问题在振动路径),但代价是失去全部振动反馈。最终方案不保留该行。
init rc 调整
之前 init 中尝试启动过 vibratorfeature-hal-service。既然现在 Recovery 直接驱动 qcom-hv-haptics,就不需要再主动启动这个服务:
# Recovery drives qcom-hv-haptics directly through input force-feedback.
# Do not start the absent Xiaomi AIDL vibrator service here.
# start vibratorfeature-hal-service
这一步的目的不是“让振动可用”,而是避免未来有人看到 vendor 里有 vibratorfeature rc 后又把服务启动回来,导致 binder 路径重新混进来。
构建过程中的一个坑:ramdisk 里的 libminuitwrp 可能是旧的
第一次构建 direct input FF 版本后,检查发现 out/target/product/socrates/system/lib64/libminuitwrp.so 已经包含新代码,但 out/target/product/socrates/recovery/root/system/lib64/libminuitwrp.so 仍然是旧的,里面还能搜到 AIDL vibrator 相关字符串。
这会导致一个很迷惑的情况:源码和 system 输出都对了,但打进 recovery ramdisk 的库还是旧的。
手动同步 recovery root 里的库后再重新打包:
bootable/recovery/prebuilt/relink.sh \
out/target/product/socrates/recovery/root/system/lib64 \
out/target/product/socrates/system/lib64/libminuitwrp.so
bootable/recovery/prebuilt/relink.sh \
out/target/product/socrates/recovery/root/system/lib \
out/target/product/socrates/system/lib/libminuitwrp.so
cp device/xiaomi/socrates/recovery/root/init.recovery.qcom.rc \
out/target/product/socrates/recovery/root/init.recovery.qcom.rc
然后删除旧的 ramdisk/recovery 镜像产物,重新构建 recoveryimage。
提示:增量编译时,
relink.sh可能不会自动覆盖 recovery root 里的旧.so。根本解决方案是rm -rf out/target/product/socrates/recovery后再 rebuild,或者直接 clean build。
构建和刷入
本次构建使用已有脚本:
wsl.exe -e bash /mnt/f/twrp/build_minimal_twrp.sh
刷入流程:
adb reboot bootloader
fastboot flash recovery output\recovery_v18_ffhaptics_fixedramdisk.img
fastboot reboot recovery
最终产物:
recovery_v18_ffhaptics_fixedramdisk.img
OrangeFox-R12.0_260510-Unofficial-socrates-ffhaptics-fixedramdisk.img
OrangeFox-R12.0_260510-Unofficial-socrates-ffhaptics-fixedramdisk.zip
其中 recovery_v18_ffhaptics_fixedramdisk.img 的 MD5:
C0FAAF49C11DD2BFAA3BF10F276F875C
验证结果
刷入后设备成功回到 Recovery:
c913c50a recovery product:fox_socrates model:Redmi_K60_Pro device:socrates
再次检查日志,没有再出现原来的阻塞等待:
Waiting for service 'android.hardware.vibrator.IVibrator/vibratorfeature'
实际体验验证:
- 切换选项卡不再卡顿。
- 点击按钮不再出现几秒等待。
- 振动已经恢复正常。
- 列表滑动保持正常。
这说明最终路径已经从:
UI 点击 -> AIDL vibrator service -> 等待失败 -> 卡顿
变成了:
UI 点击 -> minuitwrp vibrate() -> /dev/input/event1 qcom-hv-haptics -> 立即振动
为什么有时看不到 input FF 的日志
实现中有一条日志:
LOGI("Using input FF haptics device '%s' at /dev/input/%s\n", name, de->d_name);
但实际验证时不一定能在 logcat 里看到它。这不代表代码没有生效,可能原因有几个:
- 这条日志只在第一次扫描并打开 haptics 设备时打印一次,后续会复用 fd。
- Recovery 里的
LOGI不一定稳定进入 logcat,可能只进入/tmp/recovery.log或被 OrangeFox 的日志系统处理。 - 如果第一次振动发生在清日志或 grep 之前,后续点击不会再次打印“Using input FF haptics device”。
判断是否成功,最可靠的指标不是这条日志,而是:
- 没有
Waiting for service 'android.hardware.vibrator...'。 - UI 点击不再阻塞。
- 实机振动正常。
总结
这次问题的本质不是 Recovery 渲染慢,也不是触摸驱动慢,而是点击时的振动路径把 UI 线程拖进了一个不存在的 AIDL HAL 等待里。
短期关闭 TW_NO_HAPTICS 可以证明问题,但会牺牲振动体验。最终修复选择直接使用内核已经暴露的 qcom-hv-haptics input force-feedback 设备,绕过 Recovery 环境里不可靠的 binder HAL 依赖。
这个方案更适合 Recovery 场景:路径短,依赖少,失败时也能自然 fallback 到旧的 sysfs 逻辑,不需要维护一套完整的 Xiaomi/QCOM AIDL vibrator 服务启动链路。
对类似设备的适配也有参考价值:当 Recovery 中点击卡顿,而日志里出现 vibrator service 等待时,不要一开始就从显示或触摸方向排查。先关掉 haptics 做 A/B 验证,再看内核是否已经提供 /dev/input/event* force-feedback 节点,通常能更快找到真正的源头。
快速诊断:判断设备是否适用此方案
用以下命令检查内核是否暴露了 haptics 输入设备的 force-feedback 能力:
adb shell getevent -il /dev/input/event* | grep -A5 -i "haptic\|vibra"
如果输出中包含 FF: 行(如 FF_RUMBLE、FF_CONSTANT),说明内核侧已就绪,可以直接使用 input FF 路径,无需部署完整 AIDL vibrator HAL。
已知限制
FF_CONSTANT 和 FF_RUMBLE 的强度值(0x5fff / 0x7fff)目前是硬编码的,未从 BoardConfig 读取。如需按设备调节振感强度,可改为编译期变量:
# 可选:自定义 input FF 振动强度(0x0001 ~ 0x7fff)
TW_HAPTICS_FF_CONSTANT_LEVEL := 0x5fff
TW_HAPTICS_FF_RUMBLE_MAGNITUDE := 0x7fff
然后在 events.cpp 中用编译宏或 BoardConfig 导出属性替换硬编码值。