ARTICLE DETAIL

资讯详情

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

Android整机定制:默认开启ADB的Framework配置与排查

Android整机定制:默认开启ADB的Framework配置与排查 做 Android 整机定制的朋友多少都遇到过这么一个需求机器出厂或者烧录完一开机就希望 adb 是能用的插上电脑就能直接 adb 调试不用先去开发者选项里点那个USB 调试开关。这个需求听起来只是一行配置的事但真动手改起来很多人在 Android Framework 里翻半天改完发现要么开关亮了插上不认要么插上认了但每次都要点授权框要么重启一次又回到原样。这篇就把默认开启 adb这件事从头到尾拆开讲一遍包括它其实分几个层次、每一层对应 Framework 里哪个模块、改动点具体落在哪个文件以及我自己踩过的那几个坑。1. 先分清默认开启 adb到底指哪几件事很多文章把这件事讲成改一个属性就行这是最大的误导来源。Android 里 adb 能不能用其实是由三个互相独立又彼此联动的状态共同决定的任何一层没打通你看到的现象都会不一样。先把这三层弄清楚后面改代码才不会瞎试。1.1 第一层开关位 Settings.Global.ADB_ENABLED开发者选项里那个USB 调试开关落到数据层其实是Settings.Global.ADB_ENABLED这个整型值1 表示打开0 表示关闭。它的默认值在 AOSP 里通常被写成 0也就是说头一次开机、数据库刚建好的时候这个开关是关的。这个值本身并不直接让 USB 进入 adb 模式它更像是一个许可证。真正决定 USB 走路由谁走的是下一层。但在UsbDeviceManager里这个值会被读取到成员变量mAdbEnabled然后参与到 USB 功能组合的计算里。所以如果这一层是 0即使你把属性写成mtp,adb系统在装配 USB functions 的时候也可能把 adb 摘掉。还有一个容易被忽略的点这个开关在 UI 上属于开发者选项而开发者选项本身还有一道门就是Settings.Global.DEVELOPMENT_SETTINGS_ENABLED。如果你只把ADB_ENABLED置 1 而没管开发者选项的可见性会出现开关实际是开的但用户在设置里看不到任何痕迹这种诡异状态。对于需要交付给产线或者客户的机器这不算问题但对于需要人工二次确认的项目就会造成排查困难。1.2 第二层功能组合 persist.sys.usb.config 与 sys.usb.config这两个属性名字只差一个前缀作用却完全不同是新手最容易混淆的地方。persist.sys.usb.config是带 persist 前缀的持久化属性存在 property 分区里重启不丢。它描述的是我希望 USB 默认以什么功能组合出现常见取值有adb、mtp、ptp、mtp,adb、rndis,adb等等。开机时UsbDeviceManager会读它作为初始配置。sys.usb.config是不带 persist 的运行时属性它反映的是当前实际生效的功能组合。这个值会被 init 通过on property:sys.usb.config...触发器监听用来决定启动哪些守护进程、配置哪个 configfs 节点。也就是说persist.sys.usb.config是意图sys.usb.config是结果。adb 的设备端守护进程adbd就是被 init 拉起来的触发条件写在 rootdir 下的init.usb.rc里大意是当sys.usb.config包含 adb 时启动 adbd。所以哪怕ADB_ENABLED是 1只要sys.usb.config里没进 adbadbd 就不会被拉起插线自然没反应。1.3 第三层信任关系 ro.adb.secure 与授权密钥前两层打通之后你插上电脑会发现 adb 设备能识别但状态是unauthorized设备上弹一个允许 USB 调试吗的对话框。这就是第三层在起作用。ro.adb.secure这个只读属性控制 adbd 是否要求 RSA 密钥认证。设为 1AOSP user 和 userdebug 的默认值时PC 的公钥必须被设备端接受过也就是要用户点一次允许密钥才会被写进设备的授权列表里。设为 0 时adbd 不做认证任何 PC 插上都能直接拿到 shell。这就是为什么很多产线机器或者测试机要求把这个值设成 0——省掉每条线体、每个工位都要点一次授权的人工成本。但代价也很直接任何能物理接触设备的人插一根线就能拿到 shell。所以这个取舍必须想清楚不要因为图省事就在对外的量产版本上关掉。1.4 补充三层之间的联动顺序把这三层串起来看一次完整的插线流程大概是这样的UsbDeviceManager开机时读取ADB_ENABLED和persist.sys.usb.config算出应该启用哪些功能然后去设置sys.usb.configinit 监听到属性变化拉起 adbd 并配置 USB 控制器adbd 启动后读取ro.adb.secure决定是否走认证流程PC 端 adb server 检测到设备后根据认证结果把状态标成 device 或者 unauthorized。理解了这条链路你就能反推每一个现象对应哪一层出了问题。开关不亮是第一层插上只有充电没有USB 调试已连接通知是第二层通知有但adb devices显示 unauthorized 是第三层。排查的时候按这个顺序走比盲目试属性快得多。2. 编译期属性法改 system.prop 最快见效如果你的目标只是让这台机器开机就是 adb 模式最省事的路子是在编译期把属性写死。改动量小、见效快、不用动 Java 代码缺点是灵活性差改完必须重新编译刷机。2.1 关键属性清单与取值含义先给一张表把这一层会用到的属性、取值和实际效果列清楚。这张表建议存下来以后遇到类似需求直接查。属性名常见取值作用是否持久化persist.sys.usb.configadb/mtp,adb开机后 USB 默认功能组合是sys.usb.config同上当前生效的功能组合由系统写入否ro.adb.secure0/1是否要求 RSA 授权只读重启不变persist.adb.tcp.port5555网络调试端口持久化是service.adb.tcp.port5555网络调试端口本次运行有效否ro.debuggable0/1是否允许 adb root 等调试能力只读重启不变ro.secure0/1adbd 默认是否以 root 运行只读重启不变需要说明的是ro.adb.secure、ro.debuggable、ro.secure这几个通常由构建系统根据 build variant 自动写入你在 device 的 prop 文件里手动覆盖有些版本会被后面的赋值盖掉。稳妥的做法是在build/make/core的相关逻辑里找它们的赋值点或者用PRODUCT_PROPERTY_OVERRIDES之外的方式强制。这一点后面在改了没生效那节还会展开。2.2 属性放哪个文件build.prop、system.prop 与分区差异Android 的属性文件不是只有一个。设备侧常见的有/system/build.prop、/vendor/build.prop、/odm/build.prop、/product/build.prop还有/system/etc/prop.default之类。它们在不同的 Android 版本和分区方案下有不同的加载优先级和只读性。在 AOSP 源码里你通常会在device/vendor/product/目录下看到若干.prop或.mk文件。以常见的做法为例device/vendor/product/system.prop会被合并进system分区的build.propdevice/vendor/product/vendor.prop进vendor分区device/vendor/product/product.prop进product分区以及各种BoardConfig.mk、device.mk里的PRODUCT_PROPERTY_OVERRIDES对于persist.sys.usb.config这种 persist 属性写进任意一个分区的 build.prop 都能生效因为 persist 属性在首次读取后会被写入 property 分区并保持。但要注意如果多个分区都写了同一个 persist 属性且值不同最终以加载顺序靠后的为准这种冲突排查起来很痛苦所以建议只在一个地方写。另外提一句从 Android 9 之后属性访问开始分上下文property contexts普通应用和系统进程能读的属性范围不一样。persist.sys.usb.config属于 system 上下文可读的一般不影响你的需求但如果你在 SELinux 严格模式下调 adb 相关的东西可能会碰到avc: denied的日志这时候需要补 property_contexts 或者对应的 sepolicy不要一味觉得是属性没写进去。2.3 实操一个完整的编译期改动示例假设我手上有一台基于某厂商参考板做的定制机需求是开机默认 mtpadb且不弹授权框。我会这样改。第一步找到 device 目录下对应的 prop 文件追加# device/vendor/product/system.prop persist.sys.usb.configmtp,adb ro.adb.secure0第二步如果ro.adb.secure被构建系统覆盖就去检查build/make/core/main.mk或者你产品继承的BoardConfig里有没有针对它的赋值。更稳的方式是直接在产品的BoardConfig.mk或者 device 的init.board.rc的早期阶段用setprop# init.board.rc放在 on early-init 或 on init 段 on early-init setprop ro.adb.secure 0不过要注意ro.开头的属性理论上只允许写一次写第二次会失败并在 log 里报Ignoring setprop of read-only property。所以要在它被首次赋值之前设置或者干脆改成在源码里改默认值。第三步重新编译并刷system和vendor分区重启后验证adb shell getprop persist.sys.usb.config adb shell getprop sys.usb.config adb shell getprop ro.adb.secure正常应该看到mtp,adb、mtp,adb、0。如果sys.usb.config里没有 adb说明第二层的联动没走通这时候要去看UsbDeviceManager的逻辑也就是第 4 节的内容。提示改ro.adb.secure0会让设备失去 USB 调试的授权保护任何拿到设备的人插线就能进 shell。请不要在对外销售的量产版本上这么配工程机、展示机、内部测试设备上使用是合理的。2.4 这一层的操作心得属性法的最大好处是思路直白坏处是调试周期长。我自己的习惯是先在设备上用setprop快速验证假设确认某个属性确实能解决问题再把它固化到源码里。比如想验证persist.sys.usb.configmtp,adb有没有用可以先在已经有 root 或者 adb 的设备上adb shell setprop persist.sys.usb.config mtp,adb然后拔插一次 USB 线看行为变化。注意 persist 属性的生效往往需要触发一次 USB 状态刷新光 setprop 不一定立即生效拔插线缆或者在设置里切一下 USB 用途是最快的触发方式。3. SettingsProvider 默认值法让 USB 调试开关开机即亮如果你不想动属性或者需求里明确要求开发者选项里的 USB 调试开关要显示为已打开那就得从 SettingsProvider 下手。这条路改的是数据层的默认值属于 Android Framework 里比较正统的做法。3.1 def_adb_enabled 的读取链路Settings 数据库的初始默认值集中在frameworks/base/packages/SettingsProvider/res/values/defaults.xml里。这个文件里定义了大量的def_xxx资源比如屏幕超时、亮度、是否启用自动旋转等等。理想情况下ADB_ENABLED的默认值也应该有一个类似的def_adb_enabled。但实际情况是不同 Android 版本对ADB_ENABLED的处理并不统一。有的版本在DatabaseHelper.java的loadGlobalSettings()方法里直接写死了一个 0 或者根据构建类型判断有的版本确实从资源里读。所以第一步不要背文件路径直接在源码根目录用 grep 定位grep -rn ADB_ENABLED frameworks/base/packages/SettingsProvider/ grep -rn def_adb_enabled frameworks/base/先把写入点找出来再决定怎么改。这一步看起来笨但省时间。3.2 改 defaults.xml 和 DatabaseHelper 的两处假设你 grep 出来的结果显示loadGlobalSettings()里有一段把ADB_ENABLED写成 0 的代码。标准改法是两处配合。第一处在defaults.xml里加一个布尔资源!-- frameworks/base/packages/SettingsProvider/res/values/defaults.xml -- bool namedef_adb_enabledtrue/bool第二处在DatabaseHelper.java的loadGlobalSettings()里把原来的写死值改成读资源// frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java int adbEnabled mContext.getResources().getBoolean(R.bool.def_adb_enabled) ? 1 : 0; Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, adbEnabled);注意loadGlobalSettings()是在数据库首次创建时被调用的也就是说这个默认值只在全新开机时写入一次。后面用户自己在设置里关掉就会覆盖掉这个默认值这是符合预期的。你不可能指望每次开机都强制把它掰回来那样用户体验会很怪。3.3 首次开机、恢复出厂、OTA 三种情况的行为差异这是很关键的一点很多人改完发现没用其实是因为数据库早就建好了。loadGlobalSettings()只在 Settings 数据库不存在或者版本升级需要重建时执行。所以全新烧录、首次开机数据库是空的会走loadGlobalSettings()你的默认值生效。恢复出厂设置Settings 数据库会被清掉重建同样会走一遍默认值生效。OTA 升级数据库已存在版本号变了会走onUpgrade()。这时候loadGlobalSettings()不一定再执行你的默认值可能不会写进去。所以如果你是在已经跑起来的机器上做 OTA 测试发现开关没变先别急着怀疑代码改错了执行一次恢复出厂设置再看。如果确认要覆盖 OTA 场景需要在onUpgrade()里补一段逻辑或者用数据库版本号追加的方式触发一次写入。注意直接删除/data/system/users/0/settings_global.xml之类的文件来测试也可以但要注意权限和 SELinux 上下文手动删完文件的 owner 不对会导致 SettingsProvider 起不来反而更难排查。稳妥的做法还是走恢复出厂。3.4 这一层的一个隐藏坑有个现象我遇到过好几次开关确实被改成 1 了settings get global adb_enabled也能读到 1但 USB 插上去依然不认 adb。原因就是第 1 节说的第一层只是许可证第二层没打通。UsbDeviceManager在读取ADB_ENABLED之后需要它是一个从 0 变 1 的变化时才会去更新 USB 功能组合。如果开机时读到的就是 1而 USB 又还没插上某些实现里可能不会立即把 adb 加进当前 functions。等你插上线它走的是persist.sys.usb.config那条初始路径如果那个属性里没有 adb结果还是没有。这就是为什么我个人的经验是第一层和属性层要一起改。只改其中一层总会有边角场景漏掉。4. UsbDeviceManager 层插上电脑默认就是复合功能前面两层都是从配置的角度入手UsbDeviceManager则是真正决定 USB 功能组合的服务端逻辑。如果你遇到的是不管怎么改属性插上就是不进 adb这种顽固问题答案往往在这一层。4.1 getDefaultFunctions 与 setEnabledFunctions 的关系UsbDeviceManager在frameworks/base/services/usb/java/com/android/server/usb/UsbDeviceManager.java里有几个关键方法值得记住。getDefaultFunctions()用来返回默认应该启用哪些 USB 功能。它会参考persist.sys.usb.config的值结合mAdbEnabled等状态拼出一个功能列表。这个方法的返回值会通过setEnabledFunctions()写进sys.usb.config进而触发 init 的响应。setEnabledFunctions()负责实际设置属性、处理 configfs 或者 legacy gadget 的切换、以及处理从一种组合切到另一种组合时需要先断开再重连的逻辑。还有一个容易被忽略的状态是mAdbEnabled它在UsbDeviceManager里会通过 ContentObserver 监听ADB_ENABLED的变化。用户手动点开关时就是通过这条链路影响到 USB 功能的。4.2 属性、代码、Settings 三者的优先级当这三个来源给出不同答案时实际以谁为准这是我踩过最深的坑之一。大致规律是UsbDeviceManager启动时会优先读persist.sys.usb.config作为初始功能但同时会用mAdbEnabled做一次修正。如果你属性里写的是mtp,adb但ADB_ENABLED是 0那 adb 可能会被摘掉反过来属性里只写了mtp但ADB_ENABLED是 1某些实现下 adb 也不会自动被加上因为初始功能是从属性来的。而在运行过程中用户在 UI 上的操作或者 USB 用途切换比如下拉通知栏选传输文件、仅充电会走另外的路径覆盖当前功能这时候上面的初始逻辑就不起作用了。这个优先级不是三层固定不变的跟 Android 版本、厂商定制都有关系。判断方法很简单改完之后看sys.usb.config的实际值再用dumpsys usb看服务端是怎么描述的两者对照就能推出当前实现走的是哪条路。4.3 实操默认 mtpadb 的改法如果确认问题是出在这一层有两种改法。第一种是只改属性让它和ADB_ENABLED的默认值保持一致。前面已经说过了这里不重复。第二种是在代码里给默认功能加 adb。可以修改getDefaultFunctions()的逻辑在结果里补上UsbManager.FUNCTION_ADB。伪代码大概是这样// 示意具体方法名以你手上的分支为准 private String getDefaultFunctions() { String functions SystemProperties.get(persist.sys.usb.config, ); // 业务要求默认带 adb if (!functions.contains(UsbManager.FUNCTION_ADB)) { functions functions.isEmpty() ? UsbManager.FUNCTION_ADB : functions , UsbManager.FUNCTION_ADB; } return functions; }改这类代码要注意两点。一是修改后要保证persist.sys.usb.config也同步更新否则下次开机读到的还是旧值你会看到第一次好使重启又没了的现象。二是要处理功能组合的合法性校验有些组合比如ptp和mtp同时出现会被底层拒绝导致 USB 直接不工作。4.4 这一层的调试技巧调UsbDeviceManager最有效的工具是日志和dumpsys。打开与 USB 相关的日志后可以在 logcat 里看到功能组合的计算过程和写入sys.usb.config的动作adb logcat -s UsbDeviceManager UsbHostManager UsbService或者直接看状态adb shell dumpsys usbdumpsys usb会打印当前的功能组合、连接的设备信息、以及服务端记录的一些状态。对照你预期的功能组合就能判断是代码没走到还是走到了但底层拒绝了。另外getprop sys.usb.state也值得看一眼它反映的是 USB 控制器当前实际处于的状态有时候和sys.usb.config不一致这个差异本身就是线索。5. 免授权与网络调试把 adb 体验做顺前面四节的改动能让你插上就是 adb但如果你的场景是产线、批量测试或者远程维护还会遇到两个具体问题每次都要点授权框以及必须插线才能调。这一节专门处理这两个。5.1 ro.adb.secure 与首次授权弹窗授权弹窗的判定逻辑在 adbd 一侧。当ro.adb.secure为 1 时adbd 会对每个未知的 PC 公钥发起认证请求设备端由 Framework 弹出确认对话框用户点允许之后公钥被写入/data/misc/adb/adb_keys。如果你希望免掉这一步把ro.adb.secure设为 0 是最直接的方式。但还有另一种更温和的做法预置授权密钥。把产线 PC 的公钥提前写进设备的/data/misc/adb/adb_keys文件这样第一次连接就不需要弹窗了。这个文件属于system用户下的adb_data_fileSELinux 上下文是adb_keys_file直接打包进镜像要注意权限设置正确否则 adbd 读不到。预置密钥的好处是保留了授权机制安全性比直接关掉高得多适合有一定安全要求的批量设备。代价是你需要管理好这批公钥别到处乱发。5.2 persist.adb.tcp.port 与 service.adb.tcp.port 的区别这两个属性的区别用过一次就忘不了但还是值得正式写一遍。service.adb.tcp.port只在当前这次运行中有效设备重启后就没了。它的用法是在需要时临时打开网络调试adb shell setprop service.adb.tcp.port 5555 adb shell stop adbd adb shell start adbd重启 adbd 之后adbd 会监听 TCP 5555 端口PC 端就可以adb connect 设备IP:5555。persist.adb.tcp.port是持久化的写进去之后每次开机 adbd 都会监听这个端口不用再手动操作。批量测试场景下这个很方便但同样要评估安全风险——设备只要接入网络同网段的人就能尝试连接。需要强调的是用这两个属性开启的网络调试和所谓任何形式的网络代理工具没有任何关系它只是 adb 协议在 TCP 上的实现请务必按合规要求使用只在受控的内网和授权设备上启用。5.3 ro.debuggable、ro.secure 与 adb root有几个场景需要adb root比如直接读写系统分区、抓取某些内核日志。这两个属性和这个能力相关。ro.debuggable为 1 时系统被认为是可调试构建adbd 允许切换到 root。ro.secure为 1 时 adbd 默认以 shell 用户运行为 0 时默认以 root 运行。在 AOSP 的构建变体里user版两个值通常是ro.debuggable0、ro.secure1userdebug和eng版本ro.debuggable1。所以如果你需要adb root通常最省事的做法是把产品构建类型设成userdebug而不是在user版里硬改这两个属性——后者可能在 SELinux、Verified Boot 等环节引出更多问题得不偿失。顺带说一句adb root之后 adbd 会以 root 身份重启这会短暂断开连接脚本里要留出重连等待时间别写成立刻就发下一条命令否则必然失败。6. 常见问题与排查速查前面把原理和改法都过了一遍最后这一节把实战里最常遇到的几种现象集中列出来。我基本是靠这张表活下来的遇到问题先查表九成能定位到方向。6.1 现象、原因与处理对照表现象可能原因处理方向插上电脑只有充电无调试通知sys.usb.config里没有 adb检查persist.sys.usb.config与mAdbEnabledadb devices显示 unauthorizedro.adb.secure1且未授权点设备授权或预置adb_keys或设 secure 为 0adb devices里完全没有设备adbd 未启动 或 驱动问题查init.usb.rc触发条件换数据线检查 PC 端驱动改了属性重启后失效属性没持久化 或被后加载的 prop 覆盖确认用 persist 前缀检查多分区 prop 冲突开关亮了但设置里看不到开发者选项未启用检查DEVELOPMENT_SETTINGS_ENABLEDadb root提示 not allowedro.debuggable0使用 userdebug 构建或调整构建类型网络调试端口连不上只设了service.属性且已重启改用persist.adb.tcp.port或重设后重启 adbd6.2 改了没生效的五个典型原因排在第一的是数据库已存在。前面讲过loadGlobalSettings()只在首次创建时执行你在跑着的机器上改默认值然后重启当然看不到变化。这个坑我中过至少三次。第二是属性被后续加载的 prop 覆盖。system、vendor、product多个分区的 prop 会按顺序合并同一个 key 后面的会盖掉前面的。用getprop看最终值用grep在源码里搜所有写这个 key 的地方就能确认。第三是只读属性写不进去。ro.开头的属性只允许赋值一次你在 init 的后半段再 setprop 会静默失败只在 log 里留一行记录。要么改源码里的默认值要么在early-init阶段就设置。第四是SELinux 拦截。属性访问有上下文限制写属性时如果进程的域没有对应权限会报avc: denied。查dmesg或者logcat里的 avc 日志补上 sepolicy 规则。第五是改动没进镜像。这个听起来很低级但真的高频。比如你改了frameworks/base但只刷了system而没重新编译或者产品配置继承关系导致你的 prop 文件根本没被包含进去。判断方法是把镜像里的 prop 文件 dump 出来对比adb shell cat /system/build.prop | grep usb6.3 一套固定的验证流程最后把我自己用的一套验证流程写出来照着走基本能覆盖所有情况。第一步看属性adb shell getprop | grep -iE usb|adb重点看persist.sys.usb.config、sys.usb.config、sys.usb.state、ro.adb.secure、ro.debuggable、service.adb.tcp.port。第二步看设置数据库adb shell settings get global adb_enabled adb shell settings get global development_settings_enabled第三步看服务状态adb shell dumpsys usb第四步看日志adb logcat -d | grep -iE UsbDeviceManager|adbd|usb四步下来问题落在哪一层基本就清楚了。我实际维护过的几个定制项目绝大多数故障都集中在属性层和设置层没对齐这一个点上把这两层的默认值统一起来剩下的事情就顺了。这套方案后续还能往几个方向扩展比如按机型差异化配置默认 USB 功能、把授权密钥的注入做成产线工具的一个步骤、以及在 OTA 包里带上属性迁移逻辑。这些都属于同一个体系里的活儿把前面的链路吃透之后加哪一环都不会迷路。
返回列表