
做Android系统定制的人十有八九会被客户一句“给我搞个默认静态IP”整得头皮发麻。最近我在一块基于RK3576方案、跑Android14的工控板上就接了这么个需求客户要求设备插上网线后不用进设置、不用敲adb命令开机就能拿到一个固定的局域网IP而且恢复出厂设置之后这个配置还得在。这听起来挺简单真做起来才发现链路比想象中长——既要搞清楚Android14里以太网管理模块跑在哪个进程、配置存在哪个文件还得把固件烧录这套流程跑顺否则改完代码没法验证等于白干。这篇文章就从固件烧录开始一直讲到Framework层改代码、塞RRO资源包、最后验证系统默认静态IP的全过程包括我踩过的坑和定位问题的方法给正在搞同类项目的朋友一个可直接参考的完整记录。1. 项目背景客户要的“默认静态IP”到底难点在哪1.1 需求来源与实际场景这批设备是放在工厂产线机台上的安卓工控屏板子用的RK3576系统是Android14。产线网络环境要求每台设备固定一个IP方便上位机通过网口挨个采集数据。客户提的需求很明确我插上网线开机过一会儿ip addr里就得是那个指定的IP不能是DHCP随机分配的也不能我手动在设置里配完然后一重启又没了。这需求在Windows或者普通Linux上都很容易实现但放在Android14上就有意思了。Android的上层应用是没法直接改网络接口配置的或者说能改但权限和生命周期都卡得很死。你就算在系统设置里把以太网的IP改成静态重启之后系统会去读以太网模块的配置文件如果这个文件不存在或者内容是默认的DHCP那配置就丢了。换句话说真正要解决的问题不是“怎么配静态IP”而是“怎么让Android系统一开机、一恢复出厂、每次插拔网线后都主动把静态IP作为默认行为套上去”。1.2 为什么不能简单靠App或脚本搞定可能有人会说我写个开机自启的App在onCreate里调EthernetManager.setConfiguration不就行了。这个思路方向没错但有几个现实问题。第一AOSP的EthernetManager虽然有setConfiguration这个接口但应用层能拿到的EthernetManager是系统API普通应用用反射调还算凑合一旦Android版本升级或者厂商改动API代码就崩给你看。第二靠App设IP有个致命时序问题——系统启动的时候以太网服务先起来网络链接先建立然后才轮到App启动中间会有几秒钟时间网口是DHCP状态或者没配IP状态一些急急忙忙上电就要联网的产线场景根本等不起。第三就算你做成开机自启恢复出厂设置之后App还在但App要拿到静态IP的配置来源又得做数据持久化又是一堆事。所以结论很清晰要做得干净、稳定、可量产必须把静态IP配置下沉到系统以太网服务的默认逻辑里去。这也是我后面花力气改Framework代码的原因。1.3 文章面向人群这篇文章适合这么几类人正在搞RK/Rockchip平台安卓系统定制、想给设备默认配置有线静态IP的BSP工程师做安卓盒子、工控屏、边缘网关被客户反复改网络需求的嵌入式开发以及刚接触Android系统源码想顺着一个真实需求搞明白以太网服务是怎么跑起来的同学。我会把从烧录到改码再到验证的完整链路都过一遍每个人都能找到自己需要的部分。2. 整体方案设计从固件层到系统层的改动链路2.1 Android14有线网络模块的架构变化动手之前先把Android14的以太网架构摸清楚。AOSP里跟以太网相关的代码主要在packages/modules/Ethernet这个目录下编译产物是EthernetService跑在系统进程里对外提供IEthernetManagerAIDL接口。在Android14里以太网相关的数据配置文件路径变成了/data/misc/apexdata/com.android.ethernet/ipconfig.txt这是EthernetConfigStore用来保存各网卡IpConfiguration的持久化文件。整个流程可以大概理解为系统启动时EthernetTracker会读取这个ipconfig.txt如果里面没有当前网卡的配置就按默认的DHCP来创建配置。然后以太网接口状态变化、插拔网线都会触发EthernetTracker重新更新配置。所以我们要做的就是在“读不到配置”或者“网卡信息更新”的时候给它注入一个默认的静态IP配置。这里有个很重要的细节Android14里EthernetTracker对ipconfig.txt的读取时机并不只在开机网线插拔、甚至某些系统属性变化都可能触发重新读取。如果只在启动时改一次配置后续插拔网线很容易被拉回DHCP。所以方案必须覆盖updateConfiguration这个核心路径保证任何时候配置都不被覆盖掉。2.2 三种常见方案的对比我在动手前评估了三种方案各有优劣。第一种是纯应用层方案开机自启一个App去调EthernetManager.setConfiguration好处是不用改系统代码坏处是时序不稳恢复出厂后应用启动比网络服务晚会有一段时间IP不对。第二种是init脚本方案在init.rc里加一个服务等sys.boot_completed之后用cmd ethernet之类的命令设置静态IP这个比App稍微早点但Android的cmd ethernet命令并不是对所有字段都支持完整静态配置而且依然有竞态问题。第三种是直接在Framework层修改EthernetTracker的默认配置逻辑或者通过RRO资源覆盖内置一个默认配置让以太网服务在没有其他配置的时候主动使用我们写死的静态IP。方案对比可以用下面这个表来看对比项应用层App方案init脚本方案Framework层默认配置方案启动时序靠后网络服务之后稍微靠前但仍可能竞态和网络服务同步加载可维护性需要处理AIDL隐藏API命令有限配置复杂改代码编固件一劳永逸恢复出厂后需要持久化应用数据脚本在system分区可以保留天然生效开发成本低中较高最终我选择了第三种并且用了一种“配置文件代码兜底”的组合实现把静态IP参数写在系统分区的一个配置文件里让代码启动时去读读不到再走代码默认值。这样做的好处是客户以后想改IP不用重新编译整包固件只需要用调试工具改一下/system/etc/下的文件就行对我们做项目交付的人来说少了很多扯皮。2.3 方案落地路径总览整个落地路径我分成四步第一步把板子烧录到能正常工作的基线固件确认以太网接口、adb、串口都正常这一步是为了排除硬件和原厂固件的问题第二步在源码环境里修改EthernetTracker增加默认静态IP的读取逻辑第三步编写并编译RRO资源包或者直接往系统分区塞配置文件把静态IP参数内置进固件第四步重新打包固件、烧录、恢复出厂验证。文章后面几章就按这个顺序来写。3. 固件烧录准备先把环境跑通再谈改代码3.1 RK平台固件打包与烧录基础RK平台RK3576、3588这类的固件结构和高通那边不太一样最终交付的通常是一个update.img的大包里面包含了loader、uboot、bootkernelramdisk、system、vendor、dtb等分区的镜像。烧录工具官方提供的是Windows下的RKDevTool也可以配合DriverAssistant安装驱动。我第一次搞RK平台的时候不知道loader和maskrom模式的区别卡了半天。实际上loader模式一般是USB烧录工具和板子正常通信的模式而maskrom模式是设备启动异常、引导加载器没跑起来时的救砖模式。正常烧录流程下设备用adb reboot loader命令重启进入loader模式或者按住板子上的RECOVERY按键再上电都能进loader。如果连loader模式都进不去比如uboot分区刷坏了那就得按住maskrom键进入maskrom模式用RKDevTool的“高级功能”里的“烧写Boot”或者直接点“升级固件”强制烧录。这块板子的按键位置得看原理图我手头这块是接了RECOVERY和MASKROM两个测试点对应底板丝印上有标注。3.2 烧录步骤与注意事项我实际操作时的流程是这样的先装好RK驱动用USB线连接板子的OTG口和电脑然后adb reboot loader此时RKDevTool会识别到一个LOADER设备设备列表里会显示“发现一个设备LOADER”。然后在“升级固件”页签里加载编译好的update.img勾选“擦写”选项点“升级”。等进度条跑完工具提示“升级成功”板子会自动重启或需要手动断电重启。这里有几个坑要注意烧录前最好把原来的固件备份一下尤其是parameter分区表和uboot万一新固件起不来还能刷回原厂。“按分区升级”和“升级固件”不一样。如果只改了kernel单独升级boot分区会比整包升级快很多但如果改动涉及system和vendor还是老老实实整包升级避免新旧分区不匹配。烧录时不要动USB线板子不要在烧录中途断电我遇到过一半断电把loader整没的情况后面只能进maskrom救砖折腾了大半天。3.3 烧录后基线验证拿到一块新板子或者刷完新固件不要急着改代码先做一个完整的基线验证。我是通过串口波特率一般1500000RK平台专属连上板子的调试串口然后依次检查这几项系统能否正常开机进launcheradb devices能看到设备。用网线连接路由器和板子的RJ45口然后在串口或者adb里执行ifconfig eth0确认网卡有IP。RK3576默认以太网接口名一般是eth0内核驱动用的是GMACgmac0或gmac1如果你想确认网卡有没有被驱动注册可以看dmesg | grep -i eth或者ls /sys/class/net/。执行ping 网关地址确认基础网络通不通。基线验证的目的是把“硬件网络问题”和“系统软件问题”分开。我碰到过一块板子网口灯不亮、ifconfig eth0里根本没有IP的情况排查了半天最后发现是网线座子的触点虚焊跟软件一点关系没有。所以不要嫌麻烦先跑一遍。4. Android14静态IP系统默认配置的核心实现4.1 关键代码位置与文件结构在Android14源码根目录下以太网服务相关代码在packages/modules/Ethernet/service/这个路径。主要关注这几个文件src/com/android/server/ethernet/EthernetTracker.java核心管理类负责扫描以太网接口、加载/保存配置、应用配置。src/com/android/server/ethernet/EthernetConfigStore.java负责读写/data/misc/apexdata/com.android.ethernet/ipconfig.txt。src/com/android/server/ethernet/EthernetNetworkFactory.java负责把IpConfiguration落地到内核网络栈。我建议在改代码之前先在这几个类里打上日志跑一遍正常联网流程把调用链捋清楚。Android Studio或者源码自带的IDE打开工程全局搜索updateConfiguration方法能看到整个配置更新的入口。4.2 自定义静态IP配置文件的格式为了让客户能灵活改IP我在/system/etc/下新建了一个配置文件ethernet_static.conf格式就仿照properties文件# ethernet_static.conf interfaceeth0 ip192.168.1.100 netmask255.255.255.0 gateway192.168.1.1 dns1223.5.5.5 dns28.8.8.8这里把DNS写死成了公共DNS实际项目里客户要求的是用内网DNS那就改成他们给的地址。需要说明的是这个配置文件放在system分区意味着固件打包的时候会一起打进去。如果要临时测试也可以先adb push到/data/local/tmp代码里做一个调试路径的兼容但量产固件必须用/system/etc下的正式路径。4.3 修改EthernetTracker加载默认静态IP配置接下来是重头戏修改EthernetTracker.java。我选择的切入点是它读取IpConfiguration的地方位置大概在updateConfiguration()方法里。AOSP默认逻辑是先尝试从EthernetConfigStore读保存过的配置如果读不到就创建一个DHCP默认配置。我们要改成如果读不到保存过的配置就去解析/system/etc/ethernet_static.conf解析成功就返回静态IP配置否则再走DHCP兜底。代码逻辑上我新增了一个工具方法loadStaticIpConfigurationFromFile()代码大致如下private static final String STATIC_IP_CONF /system/etc/ethernet_static.conf; private IpConfiguration loadDefaultIpConfiguration() { IpConfiguration saved mIpConfigStore.getIpConfiguration(mIface); if (saved ! null) { return saved; } IpConfiguration staticConfig loadStaticConfigFromFile(); if (staticConfig ! null) { return staticConfig; } return new IpConfiguration.Builder() .setIpAssignment(IpConfiguration.IpAssignment.DHCP) .build(); } private static IpConfiguration loadStaticConfigFromFile() { File confFile new File(STATIC_IP_CONF); if (!confFile.exists()) { return null; } Properties props new Properties(); try (InputStream in new FileInputStream(confFile)) { props.load(in); } catch (IOException e) { Log.e(TAG, Failed to read static ip config, e); return null; } try { String ip props.getProperty(ip); String netmask props.getProperty(netmask); String gateway props.getProperty(gateway); String dns1 props.getProperty(dns1); String dns2 props.getProperty(dns2); if (ip null || netmask null) { throw new IllegalArgumentException(ip or netmask missing); } StaticIpConfiguration.Builder staticBuilder new StaticIpConfiguration.Builder(); // 掩码转前缀长度例如 255.255.255.0 - 24 InetAddress maskAddr NetworkUtils.numericToInetAddress(netmask); int prefixLength NetworkUtils.maskToPrefixLength(maskAddr); staticBuilder.setIpAddress(new LinkAddress( NetworkUtils.numericToInetAddress(ip), prefixLength)); if (gateway ! null) { staticBuilder.setGateway(NetworkUtils.numericToInetAddress(gateway)); } ListInetAddress dnsList new ArrayList(); if (dns1 ! null) { dnsList.add(NetworkUtils.numericToInetAddress(dns1)); } if (dns2 ! null) { dnsList.add(NetworkUtils.numericToInetAddress(dns2)); } if (!dnsList.isEmpty()) { staticBuilder.setDnsServers(dnsList); } return new IpConfiguration.Builder() .setIpAssignment(IpConfiguration.IpAssignment.STATIC) .setStaticIpConfiguration(staticBuilder.build()) .build(); } catch (Exception e) { Log.e(TAG, parse static ip config failed, e); return null; } }这段代码有几个关键点。第一NetworkUtils.numericToInetAddress和maskToPrefixLength是Android自带的网络工具类帮我省掉了自己写掩码转换的麻烦但要注意这个类在system server进程里能不能直接引用如果提示找不到类可以用android.net.InetAddresses.parseNumericAddress()替代。第二解析出来的IpConfiguration必须设置IpAssignment.STATIC并且把StaticIpConfiguration塞进去这两个缺一不可。第三异常要全部兜住配置文件格式错了不能导致整个以太网服务崩溃否则系统起不来更麻烦。4.4 处理网线插拔导致的配置回退问题只改配置读取还不够。实际上插拔网线的时候EthernetTracker会走一套复杂的更新流程如果网卡down再up很可能会重新走DHCP。这个问题我调试的时候遇到过配置已经设成静态IP了拔掉网线再插回去ip addr一看IP变成了169.254开头的link-local或者干脆没了。查了半天发现问题的根源在于EthernetNetworkFactory在网卡状态变化时会重新执行start逻辑而这个逻辑里有一处会调用getIpConfiguration()。如果这个方法返回的是冒泡上来的“默认配置”那么插拔后就丢了我们设好的静态IP。解决办法有两种思路一种是在EthernetTracker里加一个标记一旦设置过静态IP短时间内的网卡up/down事件不再重置配置另一种是修改EthernetNetworkFactory的setDefaultConfiguration逻辑把默认配置引用成我们解析好的静态配置。我自己采用的是“配置缓存”的方式简单粗暴private IpConfiguration mManualConfigCache null; private IpConfiguration getCurrentIpConfiguration() { if (mManualConfigCache ! null isStaticConfigEnabled()) { return mManualConfigCache; } ... }只要ethernet_static.conf存在就把解析结果缓存起来网卡up/down或者以太网服务重启都不会去覆盖它。使用系统设置手动改成DHCP时我会检测到用户主动行为然后把缓存清掉。不过这个改动相对复杂实际项目里如果客户不要求“手动设置能覆盖默认静态IP”直接一直缓存即可。4.5 配置文件的覆盖和移植如果不想编译进固件还有一个办法是通过RRORuntime Resource Overlay来做但这通常适合用来覆盖系统资源值比如config_ethernet_iface_regex这类。对于自定义配置文件直接放system/etc更直接。默认的AOSP以太网接口匹配正则是在frameworks/base/core/res/res/values/config.xml里的config_ethernet_iface_regex值是eth\\d。如果设备网卡接口名是en0或者end0就得自己改这个正则。你可以在device目录下的overlay里新建frameworks/base/core/res/res/values/config.xml覆盖它这种方式是Android官方推荐的厂商定制方式编译打包时系统会自动merge。我建议在生产项目里把ethernet_static.conf路径和解析逻辑做成一个独立类方便后续客户改需求。举个例子有些客户要双网口设备每个网口一个静态IP那这个配置文件就要支持分段我后来就在项目里扩展成了类似下面这种格式[eth0] ip192.168.1.100 ... [eth1] ip192.168.2.100 ...代码里用Java的Properties工具类搭配parse一段段解析。这一个扩展花不了多少时间但能让你的方案在客户下个需求来的时候不用重写。5. 回归验证与问题排查实录5.1 验证“系统默认”是否真的默认生效代码改完、固件编译烧录之后不能光看着IP有了就算完必须做一套完整的验证流程尤其是“恢复出厂设置”这个关键场景。我的验证顺序是这样第一步正常插网线开机执行adb shell ip addr show eth0确认IP是配置的静态IPping网关通。第二步拔掉网线再插回去过十几秒再看IP确认静态IP还在没有变成DHCP或者169.254。第三步重启设备确认重启后静态IP还在。第四步进系统设置恢复出厂设置设备会自动重启重启后再看IP。这一步非常关键因为恢复出厂会清掉/data下的所有数据包括以太网服务保存的ipconfig.txt。如果我们的代码能在清空之后继续读/system/etc/ethernet_static.conf说明才是真正的系统默认。第五步同时验证一下系统设置界面里以太网的状态。Android14的设置里以太网配置界面如果显示的是“静态IP”那就说明系统读取到的就是我们预设的配置如果显示DHCP说明代码可能没生效需要回头看日志。5.2 常见问题速查表我把调试过程中遇到的和朋友遇到的典型问题整理成了一个表格方便大家快速定位现象可能原因排查与解决静态IP配置不生效一直是DHCPEthernetTracker代码修改没编译进去adb shell dumpsys ethernet查看当前实际配置来源拔插网线后配置丢失网卡up/down触发了重新初始化在EthernetNetworkFactory里加入配置缓存避免重置DNS能ping通IP但域名解析不了dns1/dns2配置没生效或netd没刷新检查adb shell dumpsys connectivity中DNS列表确认配置文件中dns地址格式正确系统设置界面显示以太网未连接网线物理链路不通或config_ethernet_iface_regex没匹配到网卡ls /sys/class/net确认接口名检查串口log里EthernetTracker是否提示“no Ethernet interfaces”恢复出厂后配置丢失恢复出厂没读到/system/etc配置文件确认配置文件在system分区执行adb shell ls -l /system/etc/ethernet_static.conf重启后网卡名称变化eth0变成eth1内核设备枚举顺序不稳定通过config_ethernet_iface_regex同时匹配多个接口或者修改内核设备树固定网卡命名5.3 调试中的几个坑与心得这轮开发我踩的坑不少挑几个有代表性的说说。第一个坑是EthernetConfigStore的缓存文件问题。我先开始只改代码调试时发现改了静态IP之后重启配置有时候生效有时候不生效后来发现是/data/misc/apexdata/com.android.ethernet/ipconfig.txt里缓存了旧配置以太网服务启动时优先读了缓存我的“从文件加载默认配置”的逻辑没走到。后面解决办法是把读取顺序改成“显式的静态配置文件优先”或者把代码改成在加载缓存之前检查配置文件存在且IP没变就不读缓存逻辑上也更符合“客户配置文件优先”这个原则。第二个坑是Android14里cmd ethernet命令和dumpsys ethernet输出跟Android 13之前不太一样字段名变了。排查问题时不要照搬老命令的老经验比如以前直接看dumpsys ethernet | grep -A5 eth0可能就能看到配置现在要先看dumpsys ethernet里的服务状态再手动触发dumpsys ethernet interface。我建议去源码里看一下EthernetManagerService的dump方法确认你用的Android版本里输出格式是什么样的。第三个坑其实跟网络配置无关但很影响体验RK3576这块板子插上HDMI线之后媒体声音会“消失”客户一度以为是我改静态IP把音频搞坏了。后来用adb shell dumpsys audio看路由发现是HDMI作为音频输出设备优先生效而媒体播放的音频焦点没有正确落到外置喇叭或耳机。这属于RK平台的音频路由策略问题修改audio_policy_configuration.xml里的availableOutputDevices把外接喇叭的优先级调到HDMI前面即可。这里分享出来是想提醒大家做一个项目涉及的模块往往不止一个遇到“改A坏B”的情况先别急着怀疑自己的改动用系统dumpsys把各个服务的状态拉出来往往很快能定位。5.4 给其他系统类似配置的旁注这篇文章虽然写的是Android14但静态IP这块的思路其实是通用的。我平时也会调试Ubuntu、OpenEuler、CentOS这些系统很多人遇到“配置完静态IP重启就没了”基本都是因为配置文件写了但没让对应的网络管理服务生效。Ubuntu现在默认用Netplan你改/etc/network/interfaces是没用的得写/etc/netplan/*.yaml再执行sudo netplan applyCentOS 7.9用/etc/sysconfig/network-scripts/ifcfg-eth0里设BOOTPROTOstatic同时要确保NetworkManager不会覆盖它OpenEuler则更习惯用nmcli命令来改连接配置。Android本质上也是一样你得找到那个真正“管理网络配置”的组件而不是在表面的配置文件上做文章。写在最后这个项目从接到需求到最终交付前后花了两周多其中一半时间其实消耗在理解Android14以太网服务的配置加载机制和反复验证各种重启/插拔场景上。我个人实际做下来的体会是如果你只是临时给一台设备设静态IP用设置界面手动配就行但如果你要的是量产方案、要恢复出厂后依然生效那就必须老老实实把代码改到系统服务层让静态IP成为以太网服务的默认行为。以后再遇到客户改IP地址我只需要改一个/system/etc下的配置文件重新打包或者直接通过系统调试接口替换该文件完全不用动代码省心很多。还有一个建议是在做这类定制时务必在代码里保留完整的日志输出后续真机调试才能快速定位问题调试日志该加的别嫌麻烦。希望这篇记录能帮你少走一些弯路。