此次优化的目标主要是减少网页渲染的首屏时延,也就是从用户点击到页面完全加载的耗时。
加载一个页面主要分为几个步骤(以输入URL加载网页的方式为例):
1、首先,在浏览器地址栏中输入url
2、初始化webview client
3、网页缓存查询(浏览器缓存-系统缓存-路由器缓存),未命中则发起网页加载。
4、DNS解析(也分为浏览器缓存->java缓存->native缓存)。
5、发起TCP/HTTP/TLS握手。
6、请求基本HTML+CSS内容,加载js脚本。
7、生成Dom树,解析css样式,js交互,js动态请求网页内容。
8、渲染引擎工作,blink引擎+v8引擎
作为平台侧,可以优化的是webview和网络请求实现。
这里首先优化平台侧DNS的逻辑,以提高网络请求的稳定性(防劫持/崩溃),提高网络体验下限。
那么要提高dns效率,有几个方面可以进行:
1、增加/延长dns缓存
提升:增加缓存命中率,减少某些重复的dns查询
风险:dns映射失效时缓存命中,会无法联通
2、超时优化(已实现)
提升:在有nameserver失效,但未全部失效的情况下,降低整体dns时延
风险:弱网场景可能出现解析失败
3、httpDns
提升:防劫持,稳定快速
风险:成本高,需自行搭建
4、串行改并行(已实现)
提升:类似超时优化,搭配多个name server,可实现min(ns1, ns2, ...)延迟
风险:逻辑改动较大
围绕串行改并行讲下:
首先通过流程图再回顾下当前的dns主要流程
可以看出,从APP进程(java层->native层->dnsproxyd写命令)到netd进程(监听命令→执行res_nsend→send_dg),
整个流程对于单个name server来说,都是串行逻辑,虽然经历一次跨进程,但是对于java层来说,都是单线程阻塞的逻辑。
如上述,并行的逻辑粒度要细化到每个name server,就需要看哪里逻辑牵涉到了对应iface的name server数组。
java层流程图

native层流程图

netd流程图

纵观流程对应的函数,上层一直都不关心name server,直到libc中res_state的初始化才出现对当前netid对应nameserver的查询。
也就是res_init.c中,存放在nsaddr_list[MAXNS]中
if (MATCH(buf, "nameserver") && nserv < MAXNS) {
struct addrinfo hints, *ai;
...
if (getaddrinfo(cp, sbuf, &hints, &ai) == 0 &&
ai->ai_addrlen <= minsiz) {
if (statp->_u._ext.ext != NULL) {
memcpy(&statp->_u._ext.ext->nsaddrs[nserv],
ai->ai_addr, ai->ai_addrlen);
}
if (ai->ai_addrlen <=
sizeof(statp->nsaddr_list[nserv])) {
memcpy(&statp->nsaddr_list[nserv],
ai->ai_addr, ai->ai_addrlen);
} else
statp->nsaddr_list[nserv].sin_family = 0;
freeaddrinfo(ai);
nserv++;
}
}
continue;
}
|
继续向下到res_nsend中,就可以看到aosp默认的串行逻辑实现,根据res_state中设置的retry和name server,轮询阻塞发包。
循环内单次逻辑分为socket random_bind,connect,sendto和retrying_select。
如果将串行改并行,需要找到一个异步操作逻辑才可以实现。
而random_bind,connect,sendto都是阻塞逻辑,是没有机会做异步的,只有select操作是异步的。
但是回到代码,select操作是封装在send_dg中的,而retrying_select的参数只有一个name server建立的socket的对应fd
static int
retrying_select(const int sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
{
struct timespec now, timeout;
int n, error;
socklen_t len;
...
}
|
那么就需要:
1、把nameserver数组下放到select
2、循环建立socket并把socket fd数组下放
3、在select处增加循环
4、一旦有可读且有效数据,停止其他socket的select
把name server数组传到select逻辑处:
+retrying_select_array(const int socks[], const bool usable_servers[], int available_sock, fd_set *readset, fd_set *writeset, const struct timespec *finish)
+{
+ struct timespec now, timeout;
+ int n, error;
+ socklen_t len;
+ int ns;
+
+retry:
+ for (ns = 0; ns < MAXNS; ns++) {
+ if (!usable_servers[ns]) continue;
+ int sock = socks[ns];
...
+ n = pselect(sock + 1, readset, writeset, NULL, &timeout, NULL);
+ if (n == 0) {
+ syslog(LOG_WARNING, " %d retrying_select_array timeout. Abort all sockets query.\n", sock);
+ errno = ETIMEDOUT;
+ return 0;
+ }
...
+ return n;
+}
|
然后将send_dg中建立socket的循环建立,并将fd数组下发
+ int ns;
+ for (ns = 0; ns < statp->nscount; ns++) {
+ nsap = get_nsaddr(statp, (size_t)ns);
+ nsaplen = get_salen(nsap);
+ char abuf[NI_MAXHOST];
+ getnameinfo(nsap, (socklen_t)nsaplen, abuf, sizeof(abuf),
+ if (statp->_mark != MARK_UNSET) {
+ if (setsockopt(EXT(statp).nssocks[ns], SOL_SOCKET,
+ SO_MARK, &(statp->_mark), sizeof(statp->_mark)) < 0) {
+ res_nclose(statp);
+ return -1;
+ }
+ }
+ if (random_bind(EXT(statp).nssocks[ns], nsap->sa_family) < 0) {
+ Aerror(statp, stderr, "bind(dg)", errno, nsap,
+ nsaplen);
res_nclose(statp);
- return -1;
+ return (0);
}
+ if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
+ Aerror(statp, stderr, "connect(dg)", errno, nsap,
+ nsaplen);
+ res_nclose(statp);
+ return (0);
+ }
+ if (send(s, (const char*)buf, (size_t)buflen, 0) != buflen) {
+ Perror(statp, stderr, "send", errno);
res_nclose(statp);
return (0);
}
- if (__connect(EXT(statp).nssocks[ns], nsap, (socklen_t)nsaplen) < 0) {
- Aerror(statp, stderr, "connect(dg)", errno, nsap,
- nsaplen);
+ if (sendto(s, (const char*)buf, buflen, 0, nsap, nsaplen) != buflen)
+ {
+ Aerror(statp, stderr, "sendto", errno, nsap, nsaplen);
n = retrying_select_array(EXT(statp).nssocks, usable_servers, s, &dsmask, NULL, &finish);
...
}
|
还有部分边缘逻辑,比如控制整体超时,以及判断数据有效的逻辑暂时不讲述。
下图是修改后的dns行为,三个ns会一起发包,为避免某些网关不支持同时处理多个包,还需要加入大概2ms的延时间隔(此处例子未加入延时)
只要上层收到任何一个ns的有效包即可停止select