刷机圈里有句话流传得挺广:GSI是省事,但省的是做包人的事,不是修包人的事。我上周就碰到一台机器,机主自己把某个通用系统镜像刷进去,卡在开机动画二十分钟不动,截图发过来第一句话是"是不是包坏了,换一个版本试试"。结果我把日志一抓,包一点问题没有,是这套镜像的VNDK版本和机器vendor分区里那批二进制对不上,动态链接器在vendor进程启动阶段直接放弃了。这篇文章就把我这些年做第三方ROM移植、GSI适配和bug修复的思路完整摊开讲一遍,从怎么判断一台机器能不能做,到卡logo怎么定位,到信号、相机、指纹这些功能为什么废掉,再到怎么把一次性修复整理成能长期维护的东西。适合已经会刷机、想往适配和修bug方向走的人看,也适合只是想搞明白"为什么别人刷完能用我刷完就废"的玩家。
1. 摸清移植对象:第三方ROM和GSI到底在改哪一层
1.1 移植ROM和刷GSI是两条完全不同的路子
很多人把这两件事混着说,其实差别很大。移植第三方ROM指的是把某个开源项目为A机型编好的包,改造成能在B机型上跑——包括替换vendor下的闭源二进制、调整内核设备树、修改分区表适配、处理机型和设备指纹。这个过程你可以动vendor、可以动内核、可以动system,自由度很高,工作量也很大,一台冷门机从零开始做往往要几周。
GSI(Generic System Image)的思路完全相反:它把system(以及Android 10之后的product)做成一个"通用件",假设所有符合Treble规范的机器,vendor接口都是标准化的,那system镜像就可以跨机型复用。刷GSI时vendor分区原封不动保留原厂内容,你换的只是system那部分。
这个区别直接决定了修bug的方向。移植ROM出问题,八成在你自己改过的地方;GSI出问题,八成在system和vendor的接口契约上。搞清楚这一点,排查效率能差出好几倍。
1.2 Treble的两道门:VNDK和FCM
Treble规范里最关键的两个概念,一个叫VNDK(Vendor Native Development Kit),一个叫FCM(Framework Compatibility Matrix)。前者管的是"vendor进程能不能加载到它需要的那些系统库",后者管的是"system框架和vendor HAL之间的接口版本对不对得上"。
拿VNDK说,机器出厂时vendor分区的二进制是针对某个VNDK版本编译的。Android 9对应VNDK 28,Android 10是29,往后依次递增。如果GSI的VNDK版本比机器出厂的高太多,vendor里的老二进制就找不到它依赖的库,表现出来就是vendor进程起不来,机器卡第一屏或者反复重启。
查证方法很简单:
adb shell getprop ro.vndk.version adb shell getprop ro.vendor.build.version.sdk adb shell getprop ro.build.version.sdk手机上一般也能在系统信息里看到。三条属性分别对应VNDK版本、vendor那侧的SDK版本、system那侧的SDK版本。GSI一般会做VNDK兼容(比如phhusson那套镜像会把多个版本的VNDK一起打包进去),但也不是万能的,差太远照样翻车。
FCM这边,标准做法是system里带一份兼容性矩阵,vendor里带一份manifest声明自己提供了哪些HAL。开机时系统会做一次匹配,匹配不上会直接把对应HAL标记为"不可用",严重的话直接拒绝启动。
1.3 判断一台机器值不值得动手的前置检查
不是所有机器都适合折腾GSI,动手前先过一遍这几项,能省下不少时间:
| 检查项 | 命令或位置 | 说明 |
|---|---|---|
| 是否支持Treble | getprop ro.treble.enabled | 返回true才算入门 |
| 分区结构 | getprop ro.build.ab_update、recovery里的分区列表 | A/B和A-only处理方式不同 |
| 动态分区 | getprop ro.boot.dynamic_partitions | 是的话刷写方式要用super镜像 |
| 内核版本 | uname -r | 4.4和4.9的兼容性差异明显 |
| 出厂Android版本 | getprop ro.vendor.build.version.release | 决定了VNDK基线 |
实测经验:出厂Android 9、内核4.9、A-only分区、非动态分区的机器,是刷GSI最舒服的一档;出厂Android 8以下还没Treble的机器,基本只能走移植路线,别指望通用镜像开箱能用。
2. 开机阶段的死法分类:卡在第一屏、卡logo、卡动画不是一回事
2.1 三种"卡住"对应的启动阶段
新手最容易混淆的就是这几种"卡死",而它们指向的问题层级完全不同。
卡在第一屏(厂商logo出现前后,通常屏幕是亮的但没有品牌动画),意味着问题发生在bootloader交出控制权之后、内核还没完成挂载根文件系统。这一档基本是内核、设备树、fstab、avb校验的问题。
卡在第二屏或开机动画初期(品牌logo出来了,但动画不动或者动两秒就重启),说明内核起来了,init跑了一部分,但某个关键服务反复崩溃触发重启保护。这一档归init和属性、SELinux、vendor二进制管。
能进开机动画并跑很久,最后停在动画上不重启,这一档一般是zygote起来了但system_server没起完,或者某个核心HAL一直不返回导致开机流程卡在等待。GSI刷完卡这一步特别常见,因为框架侧在等vendor侧回应,而vendor侧压根没提供对应的HAL。
这三档的排查入口完全不同,先归类再动手,别一上来就换包。
2.2 三层日志:logcat、dmesg、pstore
我的排查顺序固定是这三层。
第一层是logcat,能抓到就优先抓:
adb logcat -b all -v threadtime > boot_log.txt-b all会把main、system、crash、events、kernel等所有缓冲区都拉出来。如果机器能连上adb(哪怕卡在动画),这一条命令基本能覆盖大部分问题。
第二层是dmesg,看内核在干什么:
adb shell dmesg > dmesg.txt如果adb连不上(卡第一屏),就用第三层pstore。现在的机器大多开了ramoops,上一次崩溃的内核日志会留在内存里,重启后落到/sys/fs/pstore/:
adb shell ls /sys/fs/pstore/ adb shell cat /sys/fs/pstore/console-ramoops-0很多"卡第一屏然后自动重启"的机器,日志就在这个目录里,比连adbd还靠谱。如果连系统都进不去,那就只能进recovery挂载后去读,或者用线刷工具抓串口输出。
2.3 init阶段的报错怎么读
拿到日志之后,先在init相关的行里找关键词。我一般这么过滤:
grep -iE "init:|Failed|avc: denied|not found|No such file|cannot" boot_log.txt | head -100常见的几类报错和含义:
Failed to start ...后面跟一个服务名,说明这个init服务启动失败,往上看它的exec路径存不存在。avc: denied是SELinux拒绝,这个单独放到第5节讲。Unable to open '/dev/block/by-name/...'说明fstab里的分区名和你机器实际的GPT分区名对不上,这是移植ROM时的经典问题,GSI因为不动vendor所以少见。libc: Fatal signal 11或者CANNOT LINK EXECUTABLE,后面跟so库名,就是前面说的VNDK不匹配,链接器找不到依赖。
补充一个容易被忽略的点:属性覆盖。移植ROM时经常需要改build.prop或者prop.default,但Android 10之后属性被拆到了多个分区(system、vendor、odm、product各有一份),改错了位置不生效。建议用adb shell getprop | grep 关键字确认最终生效值,而不是只看你改的那份文件。
3. 能开机之后的"功能性失能":相机、指纹、蓝牙的修复逻辑
3.1 相机和指纹:HAL版本不匹配的连锁反应
开机动画过了、能进桌面了,很多人以为大功告成,结果打开相机一按就闪退。这类问题的根源几乎都在HAL版本上。
Android 9开始相机走的是android.hardware.camera.provider@2.4,Android 10升级到2.5,同时保留了一个2.4的legacy服务给老设备用。如果你的vendor里提供的是2.4,而GSI的框架只去连2.5,那就连不上,相机直接报"无法连接到相机服务"。
判断方法是看系统实际注册了哪些HAL:
adb shell lshal | grep camera adb shell lshal | grep fingerprintlshal这个命令非常好用,它会列出当前所有已注册的HAL服务、版本和进程名。如果相机那一行显示"not found"或者列出的版本和vendor里的对不上,方向就明确了。
指纹同理,标准接口是android.hardware.biometrics.fingerprint@2.1。部分厂商还有自己的一套私有接口,GSI里没有对应的实现,这种情况下指纹基本没救——除非你能拿到厂商闭源的那部分blob并保证它能跑起来,而这在实际操作里往往不现实。
提示:看到相机或指纹挂掉,先跑
lshal确认HAL是否注册,再决定是改配置还是放弃。不要一上来就去翻相机驱动的so文件。
3.2 蓝牙、WiFi、音频:配置文件和blob必须成对出现
这三个模块有个共同特点:它们不只是缺一个HAL,还缺一堆配置文件和权限配置。
蓝牙的典型组合是vendor里提供libbt-vendor.so加一个bt_vendor.conf,再加上/vendor/etc/bluetooth/下的若干配置文件。移植时如果只把so拷过去了,配置文件没带,蓝牙会"能开但搜不到设备"或者"开了立刻关"。
WiFi的问题多数出在驱动和固件上。高通的机器需要wlan.ko模块和/vendor/etc/wifi/、/vendor/firmware/wlan/下的固件文件。这些文件版本必须和内核匹配,跨版本拷贝基本必挂。
音频是最容易"看起来能用但实际有毛病"的。audio_policy_configuration.xml定义了声卡、输出通路、音量曲线。移植时如果直接从别的机器拷一份过来,可能扬声器有声但耳机没声,或者通话音量异常。正确做法是优先用原厂ROM里的那一份,只做最小改动。
我的习惯是:这三个模块的文件整理成一个清单文件,每次移植照着清单逐项核对,比出事之后再翻日志快得多。
3.3 传感器、灯光、振动这些小件该排在第几位
说实话,这类小件不影响日常使用,但影响体验。传感器挂了会导致自动旋转失效、屏幕亮度不自动调节;灯光挂了通知灯不亮;振动挂了打游戏没反馈。
从投入产出比看,我的建议顺序是:先解决影响核心功能的(信号、通话、相机、WiFi),再处理影响手感的(指纹、传感器),最后才管振动、通知灯这种。有些机器的传感器HAL是android.hardware.sensors@2.0,GSI默认按2.1去找,找不到就完全没有传感器数据——这种可以在vendor的manifest里改声明版本,或者在system侧加一份兼容矩阵,属于"改配置就能解决"的类型,值得花时间。
4. 信号、通话和基带:移植过程中最容易翻车的一段
4.1 RIL与radio HAL的版本对应
打电话这件事看着简单,实际是整条链路:应用层 -> Telephony框架 -> RILJ -> rild -> vendor RIL库 -> 基带。中间任何一环版本对不上,结果就是"有信号但打不出去"或者"完全无服务"。
Android 8之后radio HAL标准化为android.hardware.radio@1.0/1.1/1.2,Android 10之后改成了@1.4/1.5,Android 11之后是AIDL版本。老机器的vendor里给的是1.0或1.1,新GSI只认1.5,这就断了。
排查命令:
adb shell lshal | grep radio adb shell dumpsys telephony.registry | head -50 adb shell getprop | grep -iE "gsm|ril|radio"lshal看HAL注册情况,dumpsys telephony.registry看注册状态和信号强度,getprop那一堆gsm.sim.state、gsm.operator.alpha能直观反映SIM有没有被识别。
4.2 双卡、VoLTE和运营商配置
双卡机器上常见的情况是卡1能用卡2不能用,或者双卡都能识别但只能上一张的数据。这通常不是HAL问题,而是属性配置和运营商配置的问题。
需要关注的几个位置:
ro.telephony.default_network决定默认网络模式,改成和原厂一致。/vendor/etc/下的运营商配置文件(比如apns-conf.xml),丢了会导致移动数据连不上。- VoLTE开关依赖
persist.dbg.ims_volte_enable这类属性和IMS相关的HAL支持,GSI对IMS的支持普遍偏弱,这条要有心理准备。
我的经验是:GSI场景下,先保证能打电话、能收发短信、能上网,VoLTE和双卡双待可以往后放。把主卡通话打通,一台机器就算具备日用的基础了。
4.3 实测排查顺序
给一个我实际用的顺序,照着走能少绕弯路:
- 确认
lshal能看到radio相关的HAL服务。 - 用
dumpsys telephony.registry确认SIM卡状态是READY还是ABSENT。 - 检查
apns-conf.xml是否存在,运营商名称能不能正确显示。 - 拨号测试,同时抓
adb logcat -b radio看RIL层的往返消息。 - 如果第4步里能看到请求发出但没有响应,基本确定是vendor RIL库和框架不匹配,需要考虑换回原厂ROM的vendor或者放弃通话功能。
第4步那个-b radio缓冲区很多人不知道,RIL层所有交互都在里面,比在main缓冲区里大海捞针有效得多。
5. SELinux拒绝、AVB校验和动态分区造成的启动失败
5.1 avc denied的阅读方式和快速验证
avc: denied这行日志信息量很大,值得单独讲。完整格式大概是这样:
avc: denied { read } for pid=1234 name="xxx" dev="sda1" ino=5678 scontext=u:r:hal_camera_default:s0 tcontext=u:object_r:vendor_file:s0 tclass=file permissive=0关键看三段:scontext是谁在请求(源上下文)、tcontext是请求谁(目标上下文)、{ read }是要什么权限。搞明白这三段,就知道该往哪个策略文件里加规则。
快速验证最简单的方法是把SELinux临时设成宽容模式:
adb shell setenforce 0 adb shell getenforce如果改成permissive之后功能正常了,那就确定是SELinux策略缺失,接下来就是补规则的事。注意setenforce 0重启就失效,只能用来验证不能当解决方案。
5.2 从permissive收敛到enforcing
补规则这件事,标准流程是用audit2allow:
adb shell dmesg | grep avc > avc.log audit2allow -i avc.log -o patch.te但这里有个坑:audit2allow会把所有denial都转成allow规则,包括一些本来就不该放行的。如果一次性全放行,等于把SELinux关了还自我感觉良好。我的做法是分批处理,每次只放行当前调试模块相关的规则,加完之后重启验证别的功能没退化,再处理下一批。
还有一个更稳妥的替代方案:优先改文件的安全上下文而不是加allow规则。如果是你自己拷进去的文件权限上下文不对,用chcon或者restorecon修正比加规则更干净。
5.3 AVB和dm-verity:刷完起不来的另一个常见原因
如果你刷了非官方镜像之后开机直接黄字警告然后进不去系统,八成是AVB校验的问题。解决办法是刷一个关闭校验的vbmeta:
fastboot --disable-verity --disable-verification flash vbmeta vbmeta.img这个vbmeta.img空白的就行,关键是那两个参数。很多教程会让你直接fastboot flashing unlock然后随便刷,但漏了vbmeta这一步,结果就是无限重启。
动态分区(super分区)的机器还要注意刷写方式。Android 10以后很多机器把system、vendor、product、odm塞进一个super分区,你不能单独刷system,得用fastboot flash system配合动态分区工具,或者干脆整包刷super.img。刷错方式的表现是能进fastboot但系统起不来,日志里会看到找不到逻辑分区。
6. 把排查从"感觉"变成"证据":工具链和操作节奏
6.1 logcat该怎么抓才有用
我见过太多人抓日志就是adb logcat然后往上翻,翻两分钟就放弃了。正确姿势是按场景抓:
| 场景 | 命令 | 关注重点 |
|---|---|---|
| 开机过程 | adb logcat -b all -v threadtime | init、zygote、system_server启动顺序 |
| 单个App崩溃 | adb logcat --pid=$(adb shell pidof 包名) | 崩溃堆栈和so库名 |
| 通话问题 | adb logcat -b radio | RIL请求和响应配对 |
| 内核崩溃 | adb shell dmesg | 内核oops和driver加载失败 |
| Native崩溃 | adb shell ls /data/tombstones/ | tombstone文件里的调用栈 |
-v threadtime这个参数一定要加,它会带上线程ID和时间戳,看多线程并发问题时比默认格式清楚太多。另外抓日志时建议直接重定向到文件,用编辑器搜索比在终端里滚动效率高十倍。
6.2 perfetto和dumpsys:进阶排查的两个利器
Android 9之后系统内置了perfetto,可以抓系统级的trace,看CPU调度、binder调用、锁等待。开机卡顿这类"知道有问题但不知道卡在哪"的场景,trace比日志有用:
adb shell perfetto -o /data/misc/perfetto-traces/trace -t 10s sched freq idle am wm gfx view binder_driver adb pull /data/misc/perfetto-traces/tracedumpsys则是查状态的神器,几乎每个系统服务都能dump。常用的几个:
dumpsys activity看Activity栈和进程状态dumpsys package看包安装和权限dumpsys battery看充电和电量dumpsys display看屏幕状态dumpsys sensorservice看传感器注册情况
这些命令的输出都很长,配合grep用。
6.3 版本管理和可回滚的刷机节奏
这一点是很多人忽略但极其重要的:每改一个东西就单独刷一次,绝不批量改。看着慢,实际快得多。因为一旦批量改了五处然后出问题,你要花五倍时间做二分定位。
我自己的节奏是:
- 刷入基线包,确认能开机,抓一份"已知良好"的日志存档。
- 只改一处,刷入,验证。
- 有问题就回退到上一步,日志对比两份文件的差异。
- 每通过一步就在笔记里记一行"改了什么、结果如何"。
配合git管理你的补丁文件,每次提交写清楚改了什么,几个月后回头看的时候你会感谢自己。
7. 从一次性修复到可维护的适配分支
7.1 补丁怎么整理才不烂尾
做移植做久了你会发现,真正麻烦的不是第一次修好,而是下次系统升级之后要重新修一遍。解决办法是把所有改动整理成结构化的补丁集。
我的目录结构大致是:
device_port/ ├── props/ # 所有属性覆盖文件 ├── overlay/ # RRO资源覆盖 ├── selinux/ # 策略补丁 ├── vendor_blobs/ # 必要的闭源库 ├── configs/ # 音频、WiFi等配置文件 └── notes/ # 每处改动的原因说明notes/这个目录看着多余,但它是最有价值的。半年后你只会记得"这里改过",不会记得"为什么改"。写清楚原因的笔记能救命。
7.2 overlay和属性覆盖的取舍
同一个问题往往有好几种改法,选哪种要看影响面。
属性覆盖最简单,改一个prop就行,但它是全局的,可能影响到别的模块。适合那种"只影响单一行为"的调整。
RRO overlay是官方推荐的资源覆盖机制,粒度细、可回滚,适合改资源配置类的值。缺点是写起来麻烦,要建完整的overlay包。
直接替换文件最粗暴也最有效,适合配置文件这类没有更好的覆盖机制的场景。但要注意版本升级时文件路径可能变化,需要维护。
三条原则:能用overlay就别改属性,能改配置就别动二进制,能动二进制就别改内核。
7.3 系统大版本升级时的适配成本
从Android 9的GSI换到Android 11的GSI,适配成本大概在哪?我的实测感受是:
| 变更项 | 影响程度 | 主要工作 |
|---|---|---|
| VNDK版本提升 | 高 | 检查vendor二进制能否加载 |
| HAL接口升级 | 高 | 补兼容服务或降级使用 |
| 属性分区拆分 | 中 | 重新定位属性文件位置 |
| SELinux策略收紧 | 中 | 补新的denial规则 |
| 权限模型变化 | 低到中 | 检查应用权限声明 |
大体上,一台Android 9时代的机器跨到Android 11系统,如果能接受放弃VoLTE和指纹,主要工作量集中在HAL兼容和SELinux上,两三天的调试能出一个可日用的版本。跨到Android 12以上就要看具体机型了,有些机器的老vendor确实撑不住。
最后分享一个我踩过好几次的教训:调试的时候一定先备份一份能正常工作的完整镜像,包括vendor、boot、dtbo、super。我早期有一次在调WiFi的时候把vendor刷坏了,又找不到原厂包,硬是花了三天从论坛里翻出别人备份的固件才救回来。现在我的习惯是拿到任何一台机器,第一件事就是全分区备份,哪怕占几个G的空间也值得。