拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Framework 默认开启 adb:user 版全链路配置

Android Framework 默认开启 adb:user 版全链路配置

刷完一台自编译的 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.debuggablero.secureadb 默认状态是否需 RSA 授权
eng10默认开启不需要
userdebug11默认开启需要(弹窗确认)
user01默认关闭需要(弹窗确认)

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

我按这个顺序往下查:

  1. getprop | grep adb:确认ro.adb.secure是 0,ro.debuggable是 0(当时是 user 版)。ro.adb.secure=0生效了,说明build.prop加载正常。
  2. getprop persist.sys.usb.config:看到是mtp,说明属性被别的进程覆盖过。
  3. dmesg | grep -i usb:看到 USB gadget 初始化时只挂了 mtp 相关节点,没有 adb 的 function。
  4. ls /config/usb_gadget/g1/functions/:只有ffs.adb没被 link 进去,印证了第 3 步。
  5. 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.xpersist.service.adb.enableSettings.Secure老平台专用
5.0-9.0persist.sys.usb.configSettings.Global主流改法
10 以后persist.sys.usb.configSettings.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 一个交付前的自检清单

每次改完这套东西,我都会跑一遍下面这几条,确认没有漏项:

  1. getprop ro.adb.secure是否符合预期;
  2. getprop persist.sys.usb.config是否包含adb;
  3. settings get global adb_enabled是否为 1;
  4. settings get global development_settings_enabled是否为 1;
  5. 拔插一次 USB 线,看功能列表是否稳定(排查 configfs 时序问题);
  6. 清一次 data 再开机,验证默认值路径确实生效,而不是靠上一次遗留的持久化属性在撑着。

第 6 条是最容易被忽略的。我自己就吃过一次亏:本地测得好好的,一上产线全新机器就全空——因为我之前那台机器上的/data/property/persist.sys.usb.config早就被写成了正确的值,我根本没验证到默认值那条路。做默认值这类改动,必须用干净镜像验证,这一条我踩过不止一次。

返回列表