开发板芯片:RK3399
Android版本: 7.1.2
我们在Android开发板上开发一款App,Android开发板有wifi, 4G和以太网接口,App通过以太网与IP摄像头连接获取数据,App处理后通过wifi/4G网络将结果上报到云端。因此,要求Android能同时连接以太网(称为内网)和外网。
熟悉Android的同学可能知道,默认情况下Android在同一时刻只能使用一种网络,比如手机同时连接4G和wifi,会优先使用wifi。因此,实现这个功能需要修改Android ROM,重新烧固件。对于主要从事后端开发的我来讲,看起来挑战挺大的。下面整理了一下解决这个问题的过程和方法。
首先,制定了下面的方案和计划:
- 下载开发板的官方编译好的镜像,熟悉固件烧录流程,因为后面会进行多次固件烧录进行测试。
- 下载开发板对应的源代码编译镜像,确保编译环境和流程的正确性,因为后面要修改Android的源代码,得首先排除掉编译过程中可能引入的错误。
- 重复“修改源代码,编译,烧录固件和测试”这个循环直到问题解决
0. 现状
Android连接上wifi,此时可以正常访问外网。插上网线后,可以访问内网,但是不能访问外网了。查询资料后发现,Android内部对每种类型的网络有个评分,如果几个不同类型的网络都能使用,会使用分数高的网络,查看源代码后发现以太网的默认评分是70(可能不同的版本不一样),高于wifi和4G网络的评分,因此插上网线后就默认使用以太网访问所有IP。很自然的,把以太网的默认评分调为比其他网络类型都低就可以保证wifi/4G不会被踢掉。相关代码在EthernetNetworkFactory.java中,里面有个静态常量定义了以太网的评分。
- private static final int NETWORK_SCORE = 70;
+ private static final int NETWORK_SCORE = 30; // changed from 70 by mjshi
重新编译后烧录固件,发现插上网线后wifi依然可用,可以访问外网,但是无法访问内网。这符合预期,说明wifi没有因为插上网线后被踢掉。
1. 第一次不太成功的尝试
通过ifconfig可以看到以太网卡和wifi都同时在线,猜测是路由表没有配置好,所有IP的访问请求都默认走wifi了,也许只需要增加一条路由记录,让访问内网192.168.1.0网络的请求走以太网就可以。
按照这个思路,先看看Android的路由表,考虑到Android是基于Linux的,因此直接使用Linux上route命令得到类似如下的路由表:
30.133.236.0/22 dev wlan0 proto kernel scope link src 30.133.237.132
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.2
咋一看,好像没有问题啊,路由表中相应的路由记录的啊。经过一番查询资料后,发现Android使用了多级路由策略,简单来讲就是Android里面有多张路由表,至于哪张路由表生效是由其优先级决定的(参考这里: )。
通过ip rule list
得到类似下面的路由策略(我删除了一些不相干的记录):
0: from all lookup local
10500: from all oif wlan0 lookup wlan0
.....
23000: from all fwmark 0x0/0xffff lookup main
32000: from all unreachable
可以看到系统中实际其作用的是第二条,最后面的wlan0就是路由表的名称。
通过ip route list table wlan0
可以看到其内容,如下:
default via 30.133.239.254 dev wlan0 proto static
30.133.236.0/22 dev wlan0 proto static scope link
可以看到里面没有到内网的路由记录,因此可以访问外网,但是不能访问内网。那怎么办呢,很自然的,在wlan0中增加一条到内网路由记录,命令如下:
ip route add 192.168.1.0/24 dev eth0 proto static scope link table wlan0
此时,添加了上面的记录后可以访问内网和外网了。不过,这样还没有解决问题,因为这样改了只是临时可以用,如果系统重启或者是通过4G上外网依然无法解决。因此,我们需要在Android系统中某个地方自动把将上面的路由记录添加进去。
很自然想到,可以在wifi和4G网络刚连上的时候,对路由表进行修改,查询了一些资料后在ConnectivityService.java中的对应地方加了一些代码,尝试后不成功,主要原因是这一层是通过Android封装的接口对路由表进行修改,有些限制。而且Android代码很大(解压缩以后有25GB),不可能导入到IDE里面进行修改,修改代码效率很低。放弃这条路子,继续查询资料。
2. 第二次尝试
查询了资料了解到,Android可以在init.rc中定义service,然后设置指定的系统property就可以触发service对应的脚本。
于是,我先写了脚本来增加一条优先级较高的路由表:
#!/system/bin/sh
if [ $# -ne 1 ];then
echo "usage: $0 [add|del]"
exit 1
fi
action=$1
if [ "$action" == "add" ];then
# avoid add the rule twice
ip rule list | grep '^9000:.*1'
if [ $? -ne 0 ];then
ip rule add from all table 1 pref 9000
fi
ifconfig eth0 192.168.1.2 netmask 255.255.255.0 up
ip route add 192.168.1.0/24 via 192.168.1.2 dev eth0 table 1
setprop eth.route ""
elif [ "$action" == "del" ];then
ip rule del pref 9000
setprop eth.route ""
else
echo "unsupported action: $action"
fi
这个脚本很简单,就是增加一个优先级为9000的路由表名称为1(好像只能命名为数字),里面定义了到内网的路由记录。这样的好处是,我们不用修改默认的路由表(wlan0如果连的是wifi,wwan0如果连的是4G)。
然后在init.rc中定义了两个services:
service add_eth_route /system/bin/eth_route.sh add
class main
disabled
oneshot
service del_eth_route /system/bin/eth_route.sh del
class main
disabled
oneshot
on property:eth.route=add
start add_eth_route
on property:eth.route=del
start del_eth_route
它的意思是说,如果系统属性eth.route的值为add,则执行命令/system/bin/eth_route.sh add
。如果为del的话,则执行/system/bin/eth_route.sh del
最后,在java代码中找到以太网卡插上网线和拔掉网线的地方,设置系统属性eth.route为对应的值,代码还是在EthernetNetworkFactory中。至于是怎么找到应该改在这里,我是在logcat的日志中发现eth0 link up
。
+ String ethRoutePropKey = "eth.route";
+ if (up) {
+ Log.d(TAG, "updateInterface: " + iface + " link up, setup route...");
+ SystemProperties.set(ethRoutePropKey, "add");
+ } else {
+ Log.d(TAG, "updateInterface: " + iface + " link down, removing route...");
+ SystemProperties.set(ethRoutePropKey, "del");
+ }
采用这个方案的好处是java中代码改动很少,这样出错的几率就小。
编译、打包、烧录固件,期待能一次成功,结果是这个脚本没有被调用起来,又排查了很久在dmesg中找到了下面的错误日志:
Service xxx does not have a SELinux domain defined
有错误日志就好办,很快找到了解决办法: 按照里面的方法修改后终于可以同时访问内网和外网了。
3. 测试
测试了以下场景
- 插上网线冷启动
- 拔掉网线不能上内网,但外网访问不受影响。插上网线后能上内网,外网也正常。
- 禁用wifi/4G,内网访问不受影响。再次开启wifi/4G,恢复正常
其中,在第三个case中,禁用wifi/4G,内网也不能访问了,原因是此时只有以太网可用,Android系统将以太网提升为默认网络,会修改路由表,导致路由表错乱(具体原因没有去查了),实际上此时以太网已经处于连接状态且配置正确,没有必要再去进行配置了。因此,改动很简单,在对应的地方增加下面代码:
+ if (mEthernetCurrentState != EthernetManager.ETHER_STATE_DISCONNECTED) {
+ Log.d(TAG, "onRequestNetwork: " + mIface + "current state = " + mEthernetCurrentState);
+ return;
+ }
编译,烧录固件,测试一切符合预期,可以把chrome中几十个tab一下全关掉了,真爽!
4. 总结
解决问题总是有一些共有的套路的:
- 工具链一定先弄好,避免犯一些低级的错误,比如代码改动没有生效
- 多看日志,一开始可能日志很多不知道从哪里开始看,那就从后面往前看
- 搜索,中英文关键字都可以用,这次很多资料就是在中文资料上找到的。
- 每次改动的内容要少一点,否则一下子改很多东西,也不知道是哪个改动在发生作用。
最后感谢很多朋友分享的文章,没有你们的经验分享,我也无法解决这个问题,因此整理成文希望能帮到更多的朋友。