刷完一台自编译的 user 版本机器,插上 USB 线敲adb devices,列表空空如也,接着就得捏着鼻子进设置里翻到"开发者选项",把"USB 调试"手动拨开,再回头敲一遍命令——这个动作如果你一天要重复二十次,人是要疯的。Android Framework 常见解决方案里关于"默认开启 adb"的这一条,本质就是为了干掉这个重复动作:让系统在第一次开机、还没进过任何设置页的时候,adbd 就已经跑起来、persist.sys.usb.config里已经带上了adb、Settings.Global.ADB_ENABLED已经是 1。
这事听起来像是改一行配置就完事,但我踩过的坑告诉我没那么简单。它横跨了构建变体(build variant)、属性系统(property service)、SettingsProvider 默认库、init 对 USB gadget 的配置、以及 adbd 自身的鉴权这几层,任何一层没对齐,你看到的现象都是"adb 连不上"这同一句话,但根因完全不是一回事。下面我按"为什么这么设计 → 每一层改哪里 → 怎么排查 → 版本差异"的顺序,把这套东西完整拆一遍,代码和路径都是 AOSP 里能直接对上的。
1. user 版凭什么默认把 adb 关掉:三种构建变体的行为差异
在动手改之前,得先接受一个前提:默认关掉 adb 不是 bug,是刻意设计。AOSP 把adb当作一个调试通道,而调试通道意味着可以拿到一个 shell、可以run-as进应用私有目录、可以读写/data/local/tmp。让这个通道在零售机器上一开机就敞开,安全性上是说不过去的。所以官方给出的逻辑是:开发态默认开,量产态默认关。这个"开发态/量产态"的分界,就是构建变体。
1.1 eng、userdebug、user 三者的默认差异
AOSP 的构建变体由TARGET_BUILD_VARIANT决定,常见三个值:eng、userdebug、user。它们对应的默认行为大致是这样的:
| 变体 | ro.debuggable | ro.secure | adb 默认状态 | 是否需 RSA 授权 |
|---|---|---|---|---|
| eng | 1 | 0 | 默认开启 | 不需要 |
| userdebug | 1 | 1 | 默认开启 | 需要(弹窗确认) |
| user | 0 | 1 | 默认关闭 | 需要(弹窗确认) |
ro.debuggable这个属性非常关键。它是很多系统组件判断"我现在是不是运行在可调试环境"的依据:adbd会看它决定要不要接受无认证连接、logd会看它决定日志级别、很多#ifdef之外的运行时判断也依赖它。ro.secure则决定adbd以什么身份运行——ro.secure=0时adbd直接以 root 身份跑,ro.secure=1时降权到 shell。
所以你会看到一种很常见的做法:为了省事,把user版本硬改成ro.debuggable=1。我不太推荐这么干,原因后面第 6 节会展开,因为它会连带把一堆安全和性能相关的行为一起打开,副作用面远超"让 adb 能连上"这个目标。
1.2 adb 使能其实由两套并行的机制在管
这是最容易混淆的地方。很多人以为"adb 开没开"是一个布尔值,实际上在系统视角里有两条并行的链路:
- 属性链路:
persist.sys.usb.config里有没有adb这个 function。它直接决定 USB gadget 的 configfs 里挂不挂adb这个功能节点,是"硬件层面"的开关。 - 设置链路:
Settings.Global.ADB_ENABLED这个数据库字段是 0 还是 1。它是 UI 上那个"USB 调试"开关的状态来源,同时UsbDeviceManager会监听它的变化,再反过来去写属性链路。
正常的人机交互是:你在设置里拨开关 → 写ADB_ENABLED→UsbDeviceManager收到 ContentObserver 回调 → 改persist.sys.usb.config→ init 收到 property trigger → 重新配置 USB gadget。这是一条完整的因果链。
问题来了:如果你只在system.prop里塞了persist.sys.usb.config=adb,但ADB_ENABLED还是 0,开机时UsbDeviceManager初始化会读到 0,然后它会把属性又改回去,把 adb 从 function 列表里摘掉。这就是"我明明改了属性,开机后getprop一看又变回 mtp 了"这类诡异现象的来源。反过来,如果你只改了ADB_ENABLED默认值,某些平台上因为属性没提前写好、启动时序赶不上,也一样连不上。
结论很直白:两条链路都要改,而且要保证它们开机后不会互相打架。下一节先讲属性这一侧。
2. 属性链路:ro.adb.secure与persist.sys.usb.config到底谁在管谁
属性这一侧要改的东西不多,但每一个都有明确的职责,改之前必须搞清楚它影响的是哪一环。
2.1ro.adb.secure=0与那条adb unauthorized弹窗
从 Android 4.2.2 开始,adb 引入了基于 RSA 密钥对的授权机制。第一次连接时,adbd会检查$ADB_VENDOR_KEYS和/data/misc/adb/adb_keys里有没有你这台主机的公钥,没有就返回unauthorized,同时在设备上弹出"是否允许 USB 调试"的对话框,点允许之后公钥才会被写进adb_keys。
这个机制受ro.adb.secure控制:
ro.adb.secure=1(user/userdebug 默认):启用授权,需要弹窗确认。ro.adb.secure=0:adbd跳过授权检查,连上就是设备。
如果你做的是一台自己用的调试机,或者产线上的测试机,ro.adb.secure=0基本是标配。否则每次换一台电脑、每次恢复出厂设置,都得去屏幕上点一下那个弹窗,机器要是没屏幕(比如某些工控板、车机),这就直接卡死了。
改的位置在设备目录下的system.prop,比如device/<vendor>/<device>/system.prop:
ro.adb.secure=0注意ro.前缀的属性是只读且只读一次的,由 init 在很早的阶段从build.prop(其实是/system/build.prop、/vendor/build.prop等合并而来)加载,所以它必须走构建系统进build.prop,在init.rc里用setprop是改不动的。
2.2persist.sys.usb.config与UsbDeviceManager的联动
persist.sys.usb.config描述的是"当前 USB 口对外暴露哪些功能",取值是逗号分隔的 function 列表,常见的有adb、mtp、ptp、midi、rndis等。比如手机插上电脑默认是mtp,adb,纯充电模式可能是空的。
要让开机就带 adb,在同一个system.prop里加上:
persist.sys.usb.config=adb或者你想保留 MTP:
persist.sys.usb.config=mtp,adb这里有个细节值得说清楚:persist.前缀的属性会被 property service 持久化到/data/property/目录下。这意味着它一旦被写过,就会一直生效,重启也不会丢。所以你第一次刷机改完是好的,后面无论怎么改system.prop,只要用户没清 data,/data/property/persist.sys.usb.config里的旧值就会覆盖你的新值。这个坑我在第 4 节还会再提一次,因为它是最容易让人怀疑人生的地方。
另外一个容易忽略的点:persist.sys.usb.config最终是被UsbDeviceManager读写的。你在system.prop里设的值只是给"第一次开机、属性还没被持久化过"时兜底用的。真正的权威状态是运行时由UsbDeviceManager依据ADB_ENABLED计算出来的。所以它俩必须一致,否则就打架。
在frameworks/base/services/usb/java/com/android/server/usb/UsbDeviceManager.java里,你能看到这个类的构造函数里会去读一次初始值:
mAdbEnabled = (Settings.Global.getInt(mContentResolver, Settings.Global.ADB_ENABLED, 0) > 0);同时它注册了一个AdbSettingsObserver:
class AdbSettingsObserver extends ContentObserver { @Override public void onChange(boolean selfChange) { boolean enable = Settings.Global.getInt(mContentResolver, Settings.Global.ADB_ENABLED, 0) > 0; mHandler.sendMessage(MSG_ENABLE_ADB, enable); } }这两段代码就是"两条链路会互相打架"的源代码级证据。mAdbEnabled初值取自设置数据库,observer 又会在数据库变化时反过来改属性。想让属性不被覆盖,就必须让数据库那边的默认值也是"开"。这就引出了第三节。
3. 让"USB 调试"开关一开机就是亮的:改 SettingsProvider 默认值
如果你只改了属性、没改设置数据库,会出现一个很微妙的状态:adb 可能能用,但设置里那个开关是灰的(关闭状态)。用户一旦手贱去拨一下再拨回来,UsbDeviceManager就会把你辛苦设置的属性覆盖掉。所以正确做法是把默认值也改掉,让 UI 状态和属性状态一致。
3.1def_adb_enabled在 defaults.xml 里的位置
打开frameworks/base/packages/SettingsProvider/res/values/defaults.xml,你能找到这么一行:
<bool name="def_adb_enabled">false</bool>改成true即可。这个文件是SettingsProvider首次创建数据库时用的默认值来源,系统里各种"出厂默认"都是从这里取的。改成true之后,设备第一次开机、settings.db第一次被创建时,ADB_ENABLED就被填成 1。
顺手建议把开发者选项的总开关也打开,否则用户在设置界面里根本看不到"USB 调试"这一项(虽然功能上 adb 已经能连了,但为了调试方便还是开了好):
<bool name="def_development_settings_enabled">true</bool>3.2DatabaseHelper里那行loadBooleanSetting
光改 XML 不够,得确认它真的被加载了。进frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java,在loadGlobalSettings(老版本是loadSecureSettings)里能找到类似这样的一行:
loadBooleanSetting(stmt, Settings.Global.ADB_ENABLED, R.bool.def_adb_enabled);这说明def_adb_enabled这个资源确实会被写进Settings.Global.ADB_ENABLED。在 Android L 之前,ADB_ENABLED一度属于Settings.Secure,对应的加载函数是loadSecureSettings。如果你在改一个老平台(比如 Android 4.4),发现loadGlobalSettings里找不到这一行,就去loadSecureSettings里翻。
注意:这里改的是数据库的初始值,不是"每次开机强制置位"。如果用户在运行期间手动关掉了 USB 调试,关掉的状态会被持久化进
settings.db,重启后依然是关的。这是符合预期的行为,不要试图用这行配置去实现"永远开"。
3.3 改完为什么必须清 data 或重新编译
SettingsProvider的默认值只在数据库不存在的时候生效。如果你是在一台已经开机过的机器上 adb push 替换了SettingsProvider.apk,那数据库早就存在了,def_adb_enabled根本不会被执行到,你会以为改动没用。
所以验证时的正确姿势有三种:
- 恢复出厂设置,或者手动删掉
/data/system/users/0/settings_global.xml(不同版本路径略有差异),让它重建; - 直接
adb shell settings put global adb_enabled 1手动写一下应急验证; - 整包重新编译烧写一枚干净镜像。
我在产线做测试固件时,习惯把这三步写成一个脚本:烧写 → 清 data → 自动settings get global adb_enabled回读确认。省得每次靠肉眼翻设置页。
4. 只改属性不改设置会怎样:一次真实的时序踩坑记录
这一段我想完整复现一次我遇到的排查过程,因为这个现象特别容易把人带偏。
4.1 现象:开机后adb devices是空的,通知栏开关却是灰的
当时的情况是:我在system.prop里加了persist.sys.usb.config=adb和ro.adb.secure=0,烧完机开机,adb devices什么都没有。第一反应是属性没生效,于是adb shell进不去,只能串口里敲getprop persist.sys.usb.config——结果打印出来是mtp,我设的adb不见了。
4.2 排查链路:从getprop到dmesg到 configfs
我按这个顺序往下查:
getprop | grep adb:确认ro.adb.secure是 0,ro.debuggable是 0(当时是 user 版)。ro.adb.secure=0生效了,说明build.prop加载正常。getprop persist.sys.usb.config:看到是mtp,说明属性被别的进程覆盖过。dmesg | grep -i usb:看到 USB gadget 初始化时只挂了 mtp 相关节点,没有 adb 的 function。ls /config/usb_gadget/g1/functions/:只有ffs.adb没被 link 进去,印证了第 3 步。settings get global adb_enabled:返回 0。到这里就基本锁定了。
4.3 根因:UsbDeviceManager的初始值读取时机
结合第 2.2 节那段源码就说得通了。UsbDeviceManager启动时先读ADB_ENABLED,读到 0,于是mAdbEnabled=false;接着它走getDefaultFunctions()计算默认 function 列表,因为mAdbEnabled是 false,adb 被排除,然后它把这个结果写回persist.sys.usb.config,我原来设的adb就被覆盖成mtp了。整个过程里我改的属性只在"UsbDeviceManager还没起来"的短暂窗口里存在过。
修法就是第 3 节说的,把def_adb_enabled也改成 true,让ADB_ENABLED初值就是 1。改完之后UsbDeviceManager启动时读到 1,mAdbEnabled=true,它算出来的默认 function 列表里自然就带 adb,属性也是它自己写进去的,不存在被覆盖的问题。
这个坑给我的教训是:改属性只是"喂"给系统一个初始输入,真正决定结果的是UsbDeviceManager的状态机。要改的是状态机的输入源(设置数据库),而不是它输出的结果(属性)。很多人卡在这就是因为把因果搞反了。
5. 版本与平台差异:从 Android 4.4 到新版,改法差在哪
这套逻辑不是一成不变的,Android 每个大版本都在动 USB 和 adb 这块的实现。如果你手上平台跨度比较大,下面这张对照表能帮你少走弯路。
5.1 Android 4.x:persist.service.adb.enable时代
在 Android 4.x 上,adb 的持久化开关是persist.service.adb.enable,不是persist.sys.usb.config。当时UsbDeviceManager里还留着对它的兼容读取,代码里能看到一个LEGACY_PERSIST_ENABLE_ADB之类的常量。这个时期还有个persist.service.adb.enable=1配上persist.service.usb.setting的组合写法。如果你在维护一个 4.4 的老车机项目,翻资料时看到persist.service.adb.enable,不要以为它是错的,那是那个时代的正确写法。
5.2 Android 5.0 到 9.0:persist.sys.usb.config成为主流
从 L 开始,persist.sys.usb.config成为标准,Settings.Global.ADB_ENABLED也从 Secure 迁到了 Global。这个区间的改法就是我上面第 2、3 节讲的,也是流传最广的版本,网上大部分教程都基于这个阶段。
5.3 Android 10 以后:configfs 与 ffs 的时序变化
Android 10 之后,USB gadget 全面转向 configfs + FunctionFS,init.usb.rc里的 trigger 从原来简单的on property:sys.usb.config=adb变成了要等sys.usb.ffs.ready和sys.usb.configfs一起就绪。这带来一个新的现象:即使属性都对,如果ffs.ready迟迟不来(比如 daemon 启动慢),开机瞬间 adb 也会短暂连不上,要等几秒。
| 版本区间 | 持久化属性 | 设置字段归属 | 备注 |
|---|---|---|---|
| 4.x | persist.service.adb.enable | Settings.Secure | 老平台专用 |
| 5.0-9.0 | persist.sys.usb.config | Settings.Global | 主流改法 |
| 10 以后 | persist.sys.usb.config | Settings.Global | 需关注 configfs 时序 |
在这个阶段,如果你对"开机后两三秒内必须能连上 adb"有硬性要求(产线自动化测试经常有这种需求),光靠默认值是不够的,得结合init.rc里的on boot阶段做兜底,比如在一个on property:sys.boot_completed=1的 trigger 里再确认一次功能列表。但要注意别写成死循环式的反复 setprop,会拖慢开机。
6. 默认开 adb 的代价,以及几个更稳妥的替代方案
把上面所有改动都做上去,adb确实一开机就是通的。但作为一个要在各种项目里反复交付的人,我必须把这事儿的代价讲清楚,不然很容易在量产阶段翻车。
6.1 安全与认证上的现实权衡
ro.adb.secure=0意味着任何物理接触设备的人,插上线就能拿到一个 shell。对内部测试机没问题,对出货机器就是明摆着的风险。所以在真机上我通常只做两件事:把def_adb_enabled设成 true(这样开发者选项里开关是亮的,但ro.adb.secure保持 1,仍然需要 RSA 授权),同时把授权公钥预置进去。
预置公钥的做法是把测试机的~/.android/adbkey.pub内容写进设备的/data/misc/adb/adb_keys,这样adbd启动时就能直接匹配上,不用弹窗。这个文件的 SELinux 上下文是adb_keys_file,写的时候要注意权限,一般用 init 的on post-fs-data阶段从/vendor/etc/adb_keys拷过去比较稳:
# init.<device>.rc on post-fs-data copy /vendor/etc/adb_keys /data/misc/adb/adb_keys chmod 0640 /data/misc/adb/adb_keys chown system shell /data/misc/adb/adb_keys这样既省了弹窗,又没把认证彻底关掉——公钥对不上,照样连不进来。
6.2 只在 userdebug 上开,或加隐藏入口
如果你能控制构建流程,最干净的办法是只在 userdebug 变体上默认开 adb,user 变体保持关闭。可以在设备目录的Android.mk或者.mk里用条件判断:
ifeq ($(TARGET_BUILD_VARIANT),userdebug) PRODUCT_PROPERTY_OVERRIDES += \ persist.sys.usb.config=mtp,adb endif量产走 user 变体,调试走 userdebug,物理隔离,谁也不用担心谁。这是我在正式项目里最推荐的方案。
如果确实必须在 user 版上开(比如某些没有开发者模式的定制设备),退一步的做法是加一个隐藏入口:比如连续点击某个版本号 N 次之后,才把ADB_ENABLED打开,同时预置公钥。这样既保留了可调试性,又不会让随便谁插上线就能进去。
6.3 一个交付前的自检清单
每次改完这套东西,我都会跑一遍下面这几条,确认没有漏项:
getprop ro.adb.secure是否符合预期;getprop persist.sys.usb.config是否包含adb;settings get global adb_enabled是否为 1;settings get global development_settings_enabled是否为 1;- 拔插一次 USB 线,看功能列表是否稳定(排查 configfs 时序问题);
- 清一次 data 再开机,验证默认值路径确实生效,而不是靠上一次遗留的持久化属性在撑着。
第 6 条是最容易被忽略的。我自己就吃过一次亏:本地测得好好的,一上产线全新机器就全空——因为我之前那台机器上的/data/property/persist.sys.usb.config早就被写成了正确的值,我根本没验证到默认值那条路。做默认值这类改动,必须用干净镜像验证,这一条我踩过不止一次。