深入探索 Android 网络优化(三、网络优化篇)上

11,584 阅读19分钟

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

本文思维导图

一、网络优化维度

1、网络优化分析

基础网络的效率就像一辆列车,时延是火车的速度 (启动时间),而带宽就像火车的车厢装载量,整个传输的物理链路就像火车的铁轨。从网络的通信过程来看,共涉及到 三个模块

  • 1)、网络库 SDK 内部的设计与策略:I/O 并发模型,针对网络问题的优化
  • 2)、服务器性能:并发、带宽能力
  • 3)、网络相关:用户网络(弱网/强网)、运营商、网络链路等

而对于网络的优化,我们可以从以下五个维度来进行。

1)、流量优化

精确获取网络流量的消耗量,解决整体均值掩盖单点异常流量的问题。

2)、网络监控

建设全面的网络监控,因为粗粒度的监控不能够帮助我们发现和解决问题。

3)、流量消耗

  • 1、精准获取一段时间的流量消耗、网络类型、前后台。
  • 2、用户流量消耗均值、异常率(消耗多、次数多)。
  • 3、完整链路全监控(Request、Response)、主动上报。

4)、网络请求质量

  • 1、请求时长、业务成功率、失败率、TOP 失败接口,导致请求失败的原因通常有两种情况:
    • 1)、弱信号:可以简单看成手机信号只有一两格的时候,这是不仅仅是信令(无线网络通信的都是一个个的信令)发出去困难,还可能导致不断切换网络、基站。App 只能在应用层做重试,因为弱信号一般都是一时的。
    • 2)、拥塞网络:可以类比为堵车、排队的场景,数据包排队,信令也在排队。这时 App 不断重试,只会使得拥塞网络更为严重。我们只能让自己的非核心业务不要去排队,并让核心业务的数据量更少,协议来回更少。
  • 2、用户体验
  • 3、请求速度、成功率:网络正常时如何更好地利用带宽提升网络请求速度?
  • 4、弱网:网络不稳定是如何最大程度上保证网络的连通性?
  • 5、安全:如何防止被第三方劫持、窃听甚至篡改?

5)、其它

  • 1、公司成本
  • 2、带宽、服务器数量、CDN
  • 3、耗电

2、网络优化误区

  • 1)、仅仅关注流量消耗,忽视其它维度。
  • 2)、仅仅关注均值、整体、忽视个体。

二、网络优化工具

1、Network Profiler

特点

  • 1)、显示实时网络活动:发送、接收数据及连接数。
  • 2)、需启动高级分析。
  • 3)、仅支持 HttpURLConnection 与 OkHttp

打开高级分析

Run => Edit Cofigurations => 界面最右边 Profiling => 打开 Enable advanced profiling (required for API level < 26 only)

使用 Network Profiler 调试 WanAndroid 网络请求

选中目标网络请求,可以看到在下方的 Connection View 一栏看到对应的网络数据,如下所示:

  • Size
  • Type
  • Status
  • Time
  • Timeline

选中 Connection View 特定的一条数据即可在右边看到该请求对应的网络数据。

Overview

该网络请求的预览信息

普通 Json 数据请求

图片加载请求

Response

Response Header 与 Body 信息

Request

Request Header 与 Body 信息

CallStack

网络请求的调用堆栈信息, 下图就是 Awesome-WanAndroid 发起一个网络请求所经历的调用堆栈:

Awesome-WanAndroid 使用 Glide 发起一个图片加载请求所经历的调用堆栈:

关键细节

高级配置中的 required for API level < 26 only 不是限定调试的手机版本小于26,我使用 API 27 的手机也可以调试。

实践中获得的新知识及感悟

如果想快速搞懂接手项目中的网络/图片加载等框架的w网络请求流程,可以使用 Profiler NETWORK 的 CallStack 功能,并且双击其中任意的一行调用链方法都可以 jump to 指定源码。

选中网络请求无法显示数据?

打开高级分析即可。

2、Charles

使用 Java 开发的,MAC 上使用较多。

特点

  • 1)、断点功能
  • 2)、Map Local
  • 3)、弱网环境模拟

安装配置

1、下载 Charles

2、截获手机端的网络包。

需要配置手机与电脑连接同一 WIFI。

1)、电脑端设置 Charles — 打开 HTTP 代理并设置代理端口
  • Charles 菜单栏 => Proxy => Proxy Settings => 填代理端口 8888 并勾选 Enable transparent HTTP proxying。
2)、手机端设置 WIFI 代理及端口
  • 获取电脑 IP 地址
    • 点击 Charles 的 help => local address。
  • 设置 WIFI 代理及端口号
    • 手机设置 => WLAN => 查看当前连接的 WIFI 详情 => 最底部代理项设置为手动 => 配置 电脑 IP 地址与端口号8888。
3)、设置完成,运行任意联网程序,Charles 会弹出请求连接的确认框,点击 allow 即可。

3、截取 HTTPS

需要信任 Charles 的 CA 证书。

1)、打开 SSL 代理,并配置 Host 与 Port
  • 电脑端 Proxy => SSL Proxying Setting => 选中 Enable SSL Proxying 并点击 Add 配置 Host 与 Port 分半为 * 与 443。
2)、信任 Charles Proxy CA 机构
  • 电脑端 Help => SSL Proxying => Install Charles Root Certificate => 选中并双击 Charles Proxy CA 根证书颁发机构 => 点击信任 => 使用此证书时选择始终信任。
3)、手机端安装 Charles 颁发的 SSL 证书
  • 电脑端 Help => SSL Proxying => Install Charles Root Certificate on a Mobile Device,此时会弹出提示框让手机端访问 chls.pro/ssl 去下载证书。
4)、手机端安装证书
  • 从文件管理器中找到下载文件 => 如果是 .pem 结尾i,将后缀名改为 .crt 并点击该文件 => 输入锁屏密码 => 等待证书导入后配置证书名(我填的是 Charles)即可。

实践过程

选中目标网络请求

从 Overview 中可以看到很全面抓包信息 。

使用断点功能

1)、右键点击要断点的 URL,选中 BreakPoints 开启断点功能。
2)、点击顶部 Proxy => Breadkpoint Settings。
3)、双击 Breakpoints Settings 面板中的目标

URL,在弹出的 Edit Breakpoint 面板中进行编辑。

4)、这里默认选择断点 Request 与 Response,我们可以选择仅断点 Response 或 Request。点击确认即断点设置完成。
5)、然后,我们就可以点击主面板右侧的 Edit Response 编辑 Response,修改完成后点击最下方的 Execute 即可。

使用 Map Local

1)、自由模拟服务端的返回数据,以提前进行接口测试。
1)、右键点击要使用 Map Local 的 URL,选中Map Local 开启断点功能。
1)、然后,我们在 Edit Mapping 面板中选择 Map To 的 Local path,选择本地设定的 maplocal 本地数据(例如 JsonString)

弱网模拟功能

  • 1)、注意开启前需将 Map Local 关闭。

  • 2)、点击 Proxy => Throttle Setting => 选中 Enable Throttling

  • 3)、这里预设了很多模拟设置,我们只需将 网络包传输的速率 Throttle preset 设置为较低的速率(一般设为 256/512)。

碰到的问题

  • 1)、注意手机与笔记本电脑需要同一 WIFI 下,不能自己开热点或使用公司内网,否则无法在 电脑端 无法弹出手机连接 Charles 的提示确认框,并且也无法下载 Charles 提供的 SSL 证书。
  • 2)、手机端下载 Charles 提供的 SSL 证书时最好不使用系统浏览器访问。

3、Wireshark

强烈推荐 geektime-webprotocol

WireShark 主要可以用来对四种流进行跟踪,如下所示:

  • TCP
  • UDP
  • SSL
  • HTTP

1)、WireShark 基本使用

如何捕获报文

  • 1)、点击捕获->选项,打开捕获窗口
    • 网卡设备/流量/捕获过滤器,点击“开始”按钮开始抓包
    • 输出(指定缓存文件)/选项(显示、名称解析、自动停止抓包条件) 面板
  • 2)、点击捕获->停止,停止抓包

Wireshark 面板

快捷方式工具栏

数据包的颜色(视图->着色规则)

设定时间显示格式

数据包列表面板的标记符号

文件操作

  • 1)、标记报文 Ctrl+M。
  • 2)、导出标记报文(文件->导出特定分组),亦可按过滤器导出报文 ,
  • 3)、合并读入多个报文(文件->合并)。

如何快速抓取移动设备的报文?

  • 1、打开手机的 wifi 热点。
  • 2、电脑连接手机的 wifi 热点。
  • 3、用 Wireshark 打开捕获->选项面板,选择 wifi 热点对应的接口设备抓包即可。

2)、Wireshark 过滤器

如果表达式的背景为绿色,则说明过滤器的语法是正确的,红色则说明有错误。

捕获过滤器

它用于减少抓取的报文体积,使用 BPF(Berkeley Packet Filter) 语法,功能相对有限。

BPF 可以在设备驱动级别提供抓包过滤接口,多数抓包工具都支持此语法。而 BPF 的 Expression 表达式由多个 primitives 原语组成。而每一个 primitives 原语则由名称或数字,以及描述它的多个 qualifiers 限定词组成。

qualifiers 限定词
  • 1、Type:设置数字或者名称所指示类型
    • host、port。
    • net ,设定子网,net 192.168.0.0 mask 255.255.255.0 等价于 net 192.168.0.0/24。
    • portrange,设置端口范围,例如 portrange 6000-8000。
  • 2、Dir:设置网络出入方向
    • src、dst、src or dst、src and dst。
    • ra、ta、addr1、addr2、addr3、addr4(仅对 IEEE 802.11 Wireless LAN 有效)。
  • 3、Proto:指定协议类型
    • ether、fddi、tr、 wlan、
    • ip、 ip6、 arp、 rarp、
    • decnet、 tcp、udp、icmp、igmp、icmp
    • igrp、pim、ah、esp、vrrp
  • 4、其他
    • gateway:指明网关 IP 地址,等价于 ether host ehost and not host host。
    • broadcast:广播报文,例如 ether broadcast 或者 ip broadcast。
    • multicast:多播报文,例如 ip multicast 或者 ip6 multicast。
    • less, greater:小于或者大于。

简单示例

src or dst portrange 6000-8000 && tcp or ip6

显示过滤器

它对已经抓取到的报文进行过滤显示,功能强大。

基于协议域过滤
  • 捕获所有 TCP 中的 RST 报文:tcp[13]&4==4。
  • 抓取 HTTP GET 报文:port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420。(47455420 是 ASCII 码的 16 进制,表示”GET ”)
  • 抓取 HTTP POST 报文:port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504F5354
显示过滤器的过滤属性

任何在报文细节面板中解析出的字段名,都可以作为过滤属性。在视图->内部->支持的协议面板里,可以看到各字段名对应的属性名。例如,在报文细节面板中 TCP 协议头中的 Source Port,对应着过滤属性为 tcp.srcport。

常用操作符
  • 1)、and(&&):AND 逻辑与,ip.src==10.0.0.5 and tcp.flags.fin。
  • 2)、or(||):OR 逻辑或,ip.scr==10.0.0.5 or ip.src==192.1.1.1。
  • 3)、xor(^^):XOR 逻辑异或,tr.dst[0:3] == 0.6.29 xor tr.src[0:3] == 0.6.29。
  • 4)、not(!):NOT 逻辑非,not llc。
  • 5)、[...​]:中括号[]Slice 切片操作符
    • [n:m]表示 n 是起始偏移量,m 是切片长度,例如:eth.src[0:3] == 00:00:83
    • [n-m]表示 n 是起始偏移量,m 是截止偏移量,例如:eth.src[1-2] == 00:83
    • [:m]表示从开始处至 m 截止偏移量,例如:eth.src[:4] == 00:00:83:00
    • [m:]表示 m 是起始偏移量,至字段结尾,例如:eth.src[4:] == 20:20
    • [m]表示取偏移量 m 处的字节,例如:eth.src[2] == 83
    • [,]使用逗号分隔时,允许以上方式同时出现,例如:eth.src[0:3,1-2,:4,4:,2] ==00:00:83:00:83:00:00:83:00:20:20:83
  • 6)、in:大括号{}集合操作符,例如 tcp.port in {443 4430..4434} ,实际等价于 tcp.port == 443 || (tcp.port >= 4430 && tcp.port ⇐ 4434)。
可用函数
  • upper:将字符串字段转换为大写。
  • lower:将字符串字段转换为小写。
  • len:返回字符串或字节数组的字节长度。
  • count:返回在一帧中字段出现的数量。
  • string:将非字符串字段转换为字符串。
显示过滤器的可视化对话框

环形缓冲器

例如使用 3 个文件的环形缓存器,从 **.1 => **.2 => **.3 然后又从 **.1 文件开始记录,形成环形。

4、TcpDump(网络数据包嗅探器)

1)、抓包步骤

1、获取 ROOT 权限的手机一部

2、下载 tcpdump

3、将 tcpdump 安装到手机上

adb push tcpdump /data/local/tmp

4、修改 tcpdump 的权限,使其具有可执行的权限

chmod 777 /data/local/tmp/tcpdump

5、执行 tcpdump 命令进行抓包,按组合键 Ctrl + C 可以停止抓包

6、将抓到的数据包的信息保存为 Pcap 文件,这里仅需在执行 tcpdump 后加上 -w 参数

tcpdump-w /data/local/tmp/tcp.pcap

7、把 Pcap 复制到 电脑上,使用 Wireshark 分析数据包的流量。

2)、捕获及停止条件

  • -D:列举所有网卡设备。
  • -i:选择网卡设备。
  • -c:抓取多少条报文。
  • --time-stamp-precision:指定捕获时的时间精度,默认毫秒 micro,可选纳秒 nano。
  • -s:指定每条报文的最大字节数,默认 262144 字节。

3)、文件操作

  • -w:输出结果至文件(可被Wireshark读取分析)。
  • -C:限制输入文件的大小,超出后以后缀加 1 等数字的形式递增。 注意单位是 1,000,000 字节。
  • -W:指定输出文件的最大数量,到达后会重新覆写第 1 个文件。
  • -G:指定每隔N秒就重新输出至新文件,注意-w 参数应基于 strftime 参数指定文件名。
  • -r:读取一个抓包文件。
  • -V:将待读取的多个文件名写入一个文件中,通过读取该文件同时 读取多个文件。

4)、输出时间戳格式

  • -t:不显示时间戳。
  • -tt:自1970年1月1日0点至今的秒数。
  • -ttt:显示邻近两行报文间经过的秒数。
  • -tttt:带日期的完整时间。
  • -ttttt:自第一个抓取的报文起经历的秒数。

5)、分析信息详情

  • -e:显示数据链路层头部。
  • -q:不显示传输层信息。
  • -v:显示网络层头部更多的信息,如 TTL、id 等。
  • -n:显示 IP 地址、数字端口代替 hostname 等。
  • -S:TCP 信息以绝对序列号替代相对序列号。
  • -A:以 ASCII 方式显示报文内容,适用 HTTP 分析。
  • -x:以 16 进制方式显示报文内容,不显示数据链路层。
  • -xx:以 16 进制方式显示报文内容,显示数据链路层。
  • -X:同时以 16 进制及 ACII 方式显示报文内容,不显示数据链路层 • -XX 同时以 16 进制及 ACII 方式显示报文内容,显示数据链路层。

5、Stetho

  • 1)、在 build.gradle 中,除了 Stetho 依赖外,还需添加 'com.facebook.stetho:stetho-okhttp3:1.5.0'。
  • 2)、在 Application 的 onCreate 方法中初始化 'Stetho.initializeWithDefaults(this)'。
  • 3)、调用 OkHttp 的 'addNeworkInterceptor' 方法添加 Stetho 用于收集网络信息而提供的网络拦截器。
  • 4)、访问 Chrome 调试页面 'chrome://inspect'。

6、其它的性能检测工具

  • strace:跟踪 Socket 相关的系统调用。
  • netstat:记录多种网络栈和接口统计信息。
  • ifconfig:记录接口配置。
  • ip:记录网络接口统计信息。
  • ping:测试网络连通性。
  • traceroute:测试网络路由。
  • /proc/net 命令:查看网络统计信息,Android TrafficState 使用了 /proc/net/xt_qtaguid/stats 和 /proc/net/xt_qtaguid/iface_stat_fmt 文件来统计 App 的流量信息。

三、精准获取流量消耗

1、如何判断 App 流量消耗偏高?

  • 1)、绝对值看不出高低。
  • 2)、对比竞品,相同 Case 对比流浪消耗。
  • 3)、异常监控超过正常指标。

2、测试方案

  • 1)、打开手机设置 => 流量管理 => 仅允许目标 App 联网
  • 2)、可以查找出大多数的问题,但是线上场景线下可能遇不到。

3、线上流量获取方案

1)、TrafficStats

特点

  • API 18 以上。
  • 记录手机重启以来的数据流量。

API

  • getMobileRxBytes():通过蜂窝流量接收到的信息。
  • getUidRxBytes(int uid):获取指定 uid 的接收流量。
  • getTotalRxBytes():总发送流量。

缺点

无法获取某个时间段内的流量消耗。

2)、NetworkStatsManager

API 23 之后。

特点

  • 1)、获取指定时间间隔内的流量信息。
  • 2)、获取不同网络类型下的消耗。

NetUtils.getStats

获取指定时间间隔的 蜂窝 + WIFI 流量总信息

4、前后台流量获取方案

问题:线上反馈 App 后天流量消耗大?

只获取一个时间段的流量不够全面。

实现原理

后台定时任务 => 获取时间间隔内流量 => 记录前后台 => 分别计算 => 上报 APM 后台 => 流量治理依据

小结

  • 1)、该方案无法获取应用在前后台切换时的流量,因此有一定的误差,但这个误差是可以接受的。
  • 2)、结合精细化的流量异常报警针对性的解决后台跑流量的问题。

成功 log 如下所示:

2020-05-11 10:47:55.633 4036-4181/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 193 | lambda$initEventAndData$1$MainActivity] backNetUseData: 4 MB
2020-05-11 10:47:55.646 4036-4181/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 194 | lambda$initEventAndData$1$MainActivity] foreNetUseData: 4 MB
2020-05-11 10:47:55.652 4036-4181/json.chao.com.wanandroid I/WanAndroid-LOG: │ [MainActivity.java | 197 | lambda$initEventAndData$1$MainActivity] totalNetUseData: 8 MB

四、网络请求流量优化

1、常见使用网络的场景

1)、数据压缩

POST 请求 Body 使用 GZip 压缩,同时服务端返回 Body 也使用 GZip 压缩。

2)、图片

  • 图片上传前压缩。
  • 图片使用策略细化:让 服务端/CDN 云服务器 优先使用缩略图/WebP格式图片。

3)、性能日志上报:批量 + 特定场景上报

APM 相关、单点问题相关。例如埋点数据可以等到某一时机点(例如 开启了 WIFI、数据量过大必须上传一部分时)再上传。

4)、数据缓存

服务端返回加上过期时间,避免每次重新获取。 节约流量且大幅提高数据访问速度,更好的用户体验。

Request 缓存设置

  • 1、Pragma:no-cache:去服务器拉取最新的资源,不使用缓存。
  • 2、If-Modified-Since:datetime:如果资源在客户端提供的时间后发生改变,服务器会返回新的资源,否则使用缓存。
  • 3、If-None-Match:etagvalue:如果资源的标识和服务器的不同,返回新的资源。

当 Request 的头部是 2 和 3 时,如果服务器的资源没有修改,则服务器会返回 HTTP/304 Not Modified,客户端会使用缓存的 Response。

Response 缓存设置

HTTP Response 是否可以缓存是由 Response 的头部控制的,服务器可以通过 Expires 和 Cache-Control 控制 Response 如何在客户端缓存。

Expires

Expires 头部会包含一个日期,即该资源缓存的有效期,客户端有新的相同请求时,如果资源缓存没有过期,则使用缓存资源,服务器不会返回任何东西。

Cache-Control

Cache-Control 可以标明 Response 如何存储及其如何使用,其选项如下所示:

  • 1)、public:Response 可以存储在任何 Cache 中,包括共享的 Cache。
  • 2)、private:Response 存储在私有 Cache 中,只能被一个用户使用。
  • 3)、no-cache:Response 将来不会被使用。
  • 4)、no-store:Response 将来不会被使用,也不会写到磁盘上。
  • 5)、max-age=#seconds:Response 在设定的时间内可以被重复使用。
  • 6)、must-revalidate:和原始服务器确认 Response 是最新后,可以使用缓存。

OKHttp 无网数据缓存实现

POST 在 OKHttp 中默认不会缓存,因为 POST 一般是用来修改数据的。在 Awesome-WanAndroid 中的 HttpModule—cacheInterceptor 中就已经实现了 OKHttp 的无网数据缓存,代码如下所示:

File cacheFile = new File(Constants.PATH_CACHE);
Cache cache = new Cache(cacheFile, 1024 * 1024 * 50);
Interceptor cacheInterceptor = chain -> {
    Request request = chain.request();
    if (!CommonUtils.isNetworkConnected()) {
        // 无网时强制使用数据缓存,以提升用户体验。
        request = request.newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .build();
    }
    Response response = chain.proceed(request);
    if (CommonUtils.isNetworkConnected()) {
        int maxAge = 0;
        // 有网络时, 不缓存, 最大保存时长为0
        response.newBuilder()
                .header("Cache-Control", "public, max-age=" + maxAge)
                .removeHeader("Pragma")
                .build();
    } else {
        // 无网络时,设置超时为4周
        int maxStale = 60 * 60 * 24 * 28;
        response.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .removeHeader("Pragma")
                .build();
    }
    return response;
};
// 缓存优化
builder.addNetworkInterceptor(cacheInterceptor);
builder.addInterceptor(cacheInterceptor);
builder.cache(cache);

5)、离线包、增量数据更新

加上版本的概念,仅传输有变化的数据。

6)、请求头压缩

如果请求头不变,服务端可以使用映射缓存 请求头 MD5 : 请求头,之后请求头都使用 MD5 即可。

7)、优化发送频率和时机

8)、合并网络请求、减少请求次数。

9)、流量兜底能力

如果发现流量异常,我们可以通过后台服务器终止协议交互,以避免问题恶化。

2、流量统计

我们可以利用 network-connection-class 进行流量统计,它内部使用的是 API 8 的 TrafficStats 类,用于获取整个手机或者某个 UID 从开机算起的网络流量。

1)、四个核心 API

// 从开机开始Mobile网络接收的字节总数,不包括Wifi
getMobileRxBytes()        
// 从开机开始所有网络接收的字节总数,包括Wifi
getTotalRxBytes()     
// 从开机开始Mobile网络发送的字节总数,不包括Wifi
getMobileTxBytes()        
// 从开机开始所有网络发送的字节总数,包括Wifi
getTotalTxBytes()         

2)、对应的Linux 内核 proc 统计接口

// stats接口提供各个uid在各个网络接口(wlan0, ppp0等)的流量信息
/proc/net/xt_qtaguid/stats
// iface_stat_fmt接口提供各个接口的汇总流量信息
proc/net/xt_qtaguid/iface_stat_fmt

3)、工作原理

  • 1)、读取 proc,并将目标 UID 下面所有网络接口的流量相加。
  • 2)、Android 7.0 之后只能通过 TrafficStats 拿到自己应用的流量信息。

公钟号同名,欢迎关注,关注后回复 Framework,我将分享给你一份我这两年持续总结、细化、沉淀出来的 Framework 体系化精品面试题,里面很多的核心题答案在面试的压力下,经过了反复的校正与升华,含金量极高~

参考链接: