前一篇讲了背景和路线选择,这篇讲最终方案是怎么落地的。
hybrid 怎么合成
先把结论摆出来:normal 内核加 TWRP ramdisk。normal 内核驱动全,TWRP ramdisk 不跑 Android 框架。
为什么要这么组合,看一组对照就清楚了。normal 环境跑框架,大核 offline,内存约 2GB,驱动全。TWRP 环境不跑框架,大核也 offline,内存约 447MB,但缺 fuel gauge 和 WiFi 驱动。hybrid 就是 normal 内核加 TWRP ramdisk,大核 offline,内存约 447MB,驱动全。
合成方法不复杂:把 TWRP 的 ramdisk(10666488 字节)和 normal 内核(8151028 字节,md50018ac)拼起来,bootsize 改 0x2000000。
合成后实测验证了三件事。调度没问题,cpu0 在线,cpu4-7 睡死,内存 156-200M,governor 是 mtk_governor。fuelgauged 能跑,capacity 读到 100,这就证明了之前 capacity 是 -1 不是内核驱动缺失,是用户态守护没起来。WiFi 也通,网关 0% 丢包,外网 223.5.5.5 通,39ms。
三个内核的对比:normal 内核 md5 是0018ac,驱动全;TWRP 内核 md5 是8fb9c4,缺 fuel gauge 和 WiFi 驱动;hybrid 用的是 normal 内核,所以驱动全。
钩子机制怎么设计的
思路是 Magisk 式的:开机自动执行/data/local/tmp/twrp-hooks/*.sh,放脚本即跑,不改镜像。
具体实现靠 init.rc 里的一个 service 声明。开机时 init 跑 init.rc,启动service twrphook /sbin/twrp-hook.sh,这是 class default 加 oneshot,异步执行,不阻塞启动。
然后/sbin/twrp-hook.sh这个 runner 干活。它先等/data挂载,最多等 60 秒。然后挂载/system(只读),给脚本提供 wpa_supplicant 这些工具。接着建/data/local/tmp/twrp-hooks目录。最后按文件名顺序遍历目录下的*.sh,逐个执行。
脚本内容是这样的:
sh
#!/sbin/sh LOG=/data/local/tmp/twrp-hook.log HOOK_DIR=/data/local/tmp/twrp-hooks i=0 while [ "$i" -lt 60 ]; do if [ -d /data/local ]; then break; fi sleep 1; i=$((i + 1)) done mkdir -p "$HOOK_DIR" 2>/dev/null mount /system 2>/dev/null || mount -o ro /system 2>/dev/null export PATH=/sbin:/system/bin export LD_LIBRARY_PATH=/sbin:/system/lib echo "=== twrp-hook start $(date) ===" >> "$LOG" for f in "$HOOK_DIR"/*.sh; do [ -f "$f" ] || continue echo "--- run $f ---" >> "$LOG" sh "$f" 2>&1 | grep -v "^Aborted" >> "$LOG" done echo "=== twrp-hook done $(date) ===" >> "$LOG"
那个grep -v "^Aborted"后面会讲,是用来过滤 wpa_cli segfault 刷屏的。
六个钩子脚本
钩子按文件名顺序执行,耗时长的放后面。最终跑通的是六个脚本,加上一个可选的 zram。
01-fuelgauged.sh
电池守护。这个脚本有点特殊,fuelgauged 用&后台启动,不用 setsid。因为实测发现 setsid 在 hybrid 里启动失败,进程数是 0,改成简单&就成功了。
脚本逻辑是先确认/system挂载了,没有的话挂上。然后把/system/vendor软链到/vendor,因为有些路径依赖。检查/dev/wmtWifi,没有的话跑一下 wmt_loader。设置好LD_LIBRARY_PATH,killall 掉旧的 fuelgauged,等一秒,然后后台启动。
验证下来 capacity 读到 100,成功。
02-wifi.sh
WiFi 拉起。这个脚本是踩坑最多的地方。
逻辑分七步。先挂/system,软链/vendor,跑 wmt_loader(只能跑一次)。然后后台起 wmt_launcher,等 10 秒。写/dev/wmtWifi,等 3 秒,ip link set wlan0 up。接着建/data/misc/wifi/sockets目录,权限 777,写/data/wpa.conf配置文件。killall 掉旧的 wpa_supplicant,等一秒,用 64 位的库路径启动 wpa_supplicant。然后timeout 3 wpa_cli reassociate触发一次关联。最后等 20 秒。
关键在LD_LIBRARY_PATH要设成/system/lib64:/system/vendor/lib64:/system/lib,因为 wpa_supplicant 是 64 位的,runner 默认设的只有 32 位库,不设 lib64 会静默失败不关联。
验证下来 WiFi 连上 COMPLETED,外网通。
20-chroot-mount.sh
挂 Debian chroot。这个照抄面具的 chroot-mount 模块,去掉等框架的循环。逻辑是等/data/debian/bin出现,最多 30 秒,然后用 busybox 挂 proc、sysfs、bind /dev、devpts。
30-chroot-services.sh
起 supervisord。这个也照抄面具的 chroot-services 模块,同样去掉等框架的循环。
逻辑是等/data/debian/proc/version出现,然后 unset 掉LD_PRELOAD和LD_LIBRARY_PATH,用setsid busybox chroot ... supervisord启动,最多重试三次,每次用supervisorctl pid检查是否起来了。
这里踩了个坑:我一开始瞎加了 rm socket、killall、&、重定向,还加了网络配置,结果 supervisord 起不来。后来照抄面具原文才成功。教训是照抄验证过的原文,不要瞎加。
40-adbd-tcp.sh
开无线 adb。很简单,setprop service.adb.tcp.port 5555,kill 掉 adbd,等三秒让它自己重启。
验证下来 adbd 监听 5555,adb connect 192.168.31.213:5555能连上。
50-static-ip.sh
配静态 IP 和路由。这个脚本的关键是轮询ip link等LOWER_UP,最多等 180 秒,因为 WiFi 关联时间不定,60 秒不够。等到之后等三秒,ip addr add和ip route add。
这里也踩了个坑:一开始用&后台配 IP,结果 adb shell 退出后进程被杀了。改成阻塞式就好了。
还有一个坑是ip命令也要显式设 lib64,否则会 segfault。
03-zram.sh
zram swap,内存一半。这个脚本踩了三个坑。
第一个坑是用awk '{print $2}'算 disksize,但 recovery 的 PATH 里没有 awk,直接报awk: not found,算失败,disksize 变成 0,后面 mkswap 和 swapon 全失败。
第二个坑是改用纯 shell$(( MemTotal * 1024 / 2 )),但 shell 是 32 位整数,2855084 乘以 1024 除以 2 等于 1461795328,超过 2^31 就变负数了,写负的 disksize 失败。
第三个坑是grep MemTotal /proc/meminfo输出是MemTotal: 2855084 kB,带 kB 单位,tr -d ' '只删空格没删 kB,变成bad number '2855084kB'。
修法是分步算,KB * 512这样中间结果不会溢出,同时用tr -d 'kB'去掉单位。验证下来 swap 1427540 kB,priority 100。
这个坑挺有代表性,写 recovery 脚本要考虑三件事:PATH 里有什么命令、shell 是 32 位会不会溢出、/proc 输出带不带单位。
踩的坑
除了上面说的,还有几个。
wpa_cli 会 segfault
这是最烦的坑。wpa_cli reassociate和wpa_cli status在 hybrid 里偶尔会 segfault,stderr 输出 "Aborted",用2>&1都吞不掉,会直接进钩子日志刷屏。
修法是钩子里只跑一次 reassociate,不循环,runner 加grep -v "^Aborted"过滤。
后来还试过用 wpa_cli action 脚本做事件驱动,也不可靠,因为 wpa_cli 本身会 segfault。最终的稳定方案是轮询ip link。
教训是 wpa_cli 在 hybrid 里任何形式(reassociate、action、status)都会 segfault,唯一稳定的方案是轮询ip link。
wpa_supplicant 不会自动关联
去掉 wpa_cli 之后发现 wpa_supplicant 不会自动关联,状态一直是 DISCONNECTED。所以还是要加回timeout 3 wpa_cli reassociate一次触发。
IP 配置拆到独立钩子
02-wifi.sh 里原本有 IP 配置,但 wpa_cli 循环会 abort,IP 配置没跑到。所以拆到 50-static-ip.sh,不依赖 wpa_cli。
不要照搬 Magisk 模块
Magisk 模块开头都有while [ "$(getprop sys.boot_completed)" != "1" ],在 recovery 里会死循环,因为 recovery 没有框架,sys.boot_completed永远不变成 1。所以照搬的时候要去掉这些循环。
最终验证
重启什么都不做,等 120 秒,ssh meilan-phone1就能连上,uptime 显示 1 分钟,load average 9.56。
完整状态:fuelgauged 正常,capacity 是 100;WiFi 正常,LOWER_UP 加 COMPLETED;chroot 文件系统挂好了;supervisord 里 cpa、freellmapi、sshd、batt 全 RUNNING;sshd 能连;静态 IP 和路由正常;无线 adb 能连;省电调度正常,cpu4-7 睡死;日志干净,0 个 Aborted。
外网也通,ping 223.5.5.5 是 39ms。
部署
刷镜像只刷 recovery 分区:
bash
adb push hybrid-hooked.img /data/local/tmp/ adb shell su -c "dd if=/data/local/tmp/hybrid-hooked.img of=/dev/block/mmcblk0p1; sync"
放钩子脚本:
bash
adb push 01-fuelgauged.sh /data/local/tmp/twrp-hooks/ adb push 02-wifi.sh /data/local/tmp/twrp-hooks/ adb push 03-zram.sh /data/local/tmp/twrp-hooks/ adb push 20-chroot-mount.sh /data/local/tmp/twrp-hooks/ adb push 30-chroot-services.sh /data/local/tmp/twrp-hooks/ adb push 40-adbd-tcp.sh /data/local/tmp/twrp-hooks/ adb push 50-static-ip.sh /data/local/tmp/twrp-hooks/ adb shell chmod 755 /data/local/tmp/twrp-hooks/*.sh
重启进 recovery,等 120 秒,ssh meilan-phone1就行。
几个额外实验
跑通之后又做了几个实验,记录一下。
冻结 TWRP UI
kill -STOP $(pidof recovery)可以冻结 TWRP UI,屏幕冻结,触屏无响应,但后台服务全不受影响。kill -CONT $(pidof recovery)可以恢复。
试过 kill recovery,init 会立刻重启它,PID 变了但进程又回来。chmod -x 也没用,init 以 root 跑忽略权限。所以 recovery 不能 kill 掉,它是 class default 服务,on boot 时 class_start default 拉起它,死掉会自动重启。
想真正禁用 recovery UI 要改 init.rc,重打包 hybrid 镜像,但风险高,36MB 内存换可能不稳定的 recovery 环境不划算。
屏幕控制
echo 0 > /sys/class/leds/lcd-backlight/brightness可以关屏,echo 255可以开屏,中间亮度 0-255 随便调。TWRP 不覆盖手动设置的值,关屏后不会自动开屏。
关屏实测温度降 0.6 度,CPU ticks 减少,但省电效果有限,主要省屏幕功耗。
试过echo 1 > /sys/class/graphics/fb0/blank走 fbdev blank 接口,写不进去,TWRP 不响应。只能靠背光控制。
U盘挂载
插 U 盘能识别,/dev/block/sda和sda1,120GB,vfat 格式。挂载mount -t vfat -o rw /dev/block/sda1 /mnt/usb就行。中文目录名会乱码,挂载时加-o utf8可以解决。
8 核压测
空闲时 cpu0 是 1 核,负载约 0,温度 35°C。压测时 cpu0-7 全开,loadavg 14.93,温度 76.5°C,小核 1GHz 加大核 1.8GHz。峰值 8 核满载,loadavg 15.98,温度 82.7°C。停后 10 秒回到 cpu0,温度降到 42.5°C。
之前误判过要手动唤醒 cpu1-7,实际上 HPS 根据负载自动开核。手动唤醒的问题在于 HPS 主线程会顶回去关核,循环唤醒对抗 HPS 是错的做法。正确做法是给足够负载,让 HPS 自己开核。
压测脚本很简单:
bash
for i in 0 1 2 3 4 5 6 7; do (while :; do :; done) > /dev/null 2>&1 & done
这验证了 TWRP 环境下 8 核全功能,不只是小核。
framebuffer 直接绘制
fb0 参数是virtual_size = 1088x3840,bpp 32,stride 4352,可视分辨率 1080x1920。
recovery 会 mmap fb0 持续重绘,所以必须先kill -STOP recovery冻结,才能写入不被覆盖。
用 Python 生成 RGB565 转 32bpp 的数据,画三个三角形(红绿蓝),推到设备,冻结 recovery,cat /tmp/fb_tri.rgb > /dev/graphics/fb0,恢复 recovery。
实测屏幕显示了三个三角形加黑底加顶部灰条加白色边框,恢复后 recovery 状态正常。
关键点是 fb0 是线性 framebuffer,offset 是y*stride + x*4,MTK 的 fbdev 是 BGRA,recovery 会重绘必须先冻结,double buffer 虚拟 1088x3840 里 1920 是 back buffer。
这说明图形栈完全可控,不需要 Android 框架,任意图像都可以显示。
最轻桌面:Xorg + jwm
chroot 里装 Xorg 加 jwm 加 xterm,--no-install-recommends省空间,总共约 20MB。
xorg.conf 用 fbdev 驱动,指向/dev/graphics/fb0,DefaultDepth 24,Modes "1080x1920"。
启动脚本后台跑 Xorg,等 5 秒,后台跑 jwm,等 2 秒,后台跑 xterm。
实测屏幕显示 jwm 桌面,深灰背景,顶部任务栏,xterm 窗口。Xorg 占 fb0,写 jwm 深灰背景。
这说明 fb0 可以直接给 Xorg 用,不需要 Android 框架,最轻桌面框架跑起来了。
远程桌面:x11vnc
背景是手机爆屏,触屏碎了,需要远程桌面。
scrcpy 用不了,因为它需要 Android 框架(SurfaceFlinger 加 MediaProjection),TWRP 没框架连不上。爆屏本身不影响 scrcpy,因为 scrcpy 是软件镜像不走物理屏,但 TWRP 环境没框架。
解决方案是 x11vnc 加 SSH 隧道加 vncviewer。
x11vnc 装了之后,启动命令是x11vnc -display :0 -nopw -noshm -forever -listen 0.0.0.0 -rfbport 5900 -noxdamage。
这里踩了两个坑。一个是-nopass是无效选项,正确的是-nopw。另一个是shmget: Function not implemented,内核没 SYSV IPC 共享内存,要加-noshm禁用 shm。
SSH 隧道是ssh -f -N -L 5900:localhost:5900 meilan-phone1,把本地 5900 转发到设备的 5900。
电脑上用 vncviewer 连localhost:5900,-SecurityTypes None无密码。
实测设备上 Xorg、jwm、xterm、x11vnc 全跑,电脑上 vncviewer 连上,显示 jwm 桌面加 xterm 窗口。vncviewer 鼠标点击等于设备触屏,键盘输入等于设备键盘。
这套方案适合爆屏手机、无头服务器、远程协助。
反思
这次开发犯了不少错,记录一下。
反复在同一坑改。wpa_cli 改了五版,setsid、&、timeout、reassociate 一次、无 wpa_cli,每次都以为这次对了,其实还在同一个坑。该查资料总结的时候不要瞎试。
瞎改 supervisord。我瞎加了 rm socket、killall、&、重定向,应该照抄验证过的原文。教训是照抄面具原文,不要瞎加。
不该跑诊断命令干扰系统。我跑wpa_cli status和ip addr诊断时,干扰了系统状态。教训是不要跑诊断命令干扰系统,直接测最终结果(SSH、ping)。
总结
hybrid recovery 加钩子,等于 normal 内核(驱动全)加 TWRP ramdisk(调度好)加六个钩子脚本(fuelgauged、wifi、chroot-mount、chroot-services、adbd-tcp、static-ip)加 runner 过滤 Aborted。
全自动化,重启什么都不做,等 120 秒,ssh meilan-phone1即可。
关键踩坑:fuelgauged 用&不用 setsid;wpa_supplicant 是 64 位要显式设 lib64;wpa_cli 会 segfault 要用 timeout 兜住只跑一次 runner 过滤 Aborted;wpa_supplicant 不会自动关联需要 reassociate 一次触发;IP 配置拆成独立钩子轮询ip link不用 wpa_cli;supervisord 照抄面具原文不要瞎加;ip命令也要显式设 lib64;zram 有 64 位溢出、recovery 没 awk、/proc/meminfo 带单位三个坑。
额外实验验证了 TWRP UI 可以冻结但不能 kill,背光可以控制但 fb0/blank 写不进去,U 盘能挂载但中文目录要加 utf8,8 核压测 HPS 自动开核不需要手动唤醒,fb0 可以直接绘制图像,Xorg 加 jwm 最轻桌面能跑,x11vnc 可以做爆屏手机的远程桌面。