前言
1. 网络优化概述
Android 网络优化方法主要讲的就是线下网络测试工具
、线上网络监控方案
、流量优化方案
和质量优化方案
。
做网络优化时,要考虑多个维度
、统计多种数据
,还要建立完善的监控体系
。
常见的网络测试工具有 Network Profiler
、Charles
和Stetho
。
常见的线上网络监控方案有OkHttp 的 EventListener
、NetworkStatsManager
和 TrafficStats
。
常见的流量优化方案有数据缓存
、数据压缩
和图片压缩
。
常见的质量优化方案有HttpDns 优化
、协议版本优化
以及资本优化
。
考虑到多个维度
指的是做网络优化时要考虑流量维度
和质量维度
,然后根据这两个维度的数据减少流量消耗
以及提升请求速度
。
流量维度
指的是 App 在一段时间内流量消耗的精准度量,流量消耗可以区分为网络类型
、前后台
以及请求和响应
等维度。客户端要记录这些数据,这样服务端就可以在需要的时候下发指令
控制客户端上传这些数据,客户端也可以在相关数据超过阈值后主动上报
。
质量维度
可以从请求时长
、请求成功率
和Top失败接口
等数据上进行区分,以便后续能快速定位和解决问题。
统计多种数据
指的是不能只统计线上流量消耗,还要统计 App 使用时长
等数据,结合多种数据来分析流量的消耗是否合理。
建立完善的监控体系
,指的是做网络优化要监控网络请求成功率
和网络请求异常
,比如流量消耗过多
、请求次数过多
以及下载文件过大
等异常。
网络优化要做线下测试
和线上监控
,在做线下测试时,要测试不同的网络类型
下客户端的使用体验,比如切换 Wifi
和流量
,还要测试不同的网络状态
,比如弱网测试
和无网测试
。在做线下测试时,要把测试周期拉长一些
,因为客户端中有的网络请求不是实时上报的,测试网络的时候测久一点才能测出问题。另外一点就是有的 App 对安全性要求较高,切换网络时可能会导致登录态失效,这时要注意网络请求取消时有没有关闭加载弹窗
。
Network Profiler
是 Android Studio 提供的一个网络分析工具,在 Network Profiler 中包含了连接视图
、线程视图
、请求概览
、响应信息
、请求信息
以及调用栈
等数据。在连接视图中可以看到请求的内容类型
、请求状态
和请求耗时
等信息。
Charles
是一个抓包工具
,除了 Charles ,还有Fiddler
、Wireshark
、TcpDump
等抓包工具。Charles 支持断点调试
、模拟数据
以及弱网模拟
功能。当服务端的接口没有开发完成,或者是我们想要验证一下不同的响应数据在客户端的处理是否正常时,就可以使用模拟数据。
Stetho
是 Facebook 开源的一个应用连接桥,使用 Stetho 可以在 Chrome 浏览器中查看 Android 设备和应用相关的信息
,包含了网络请求信息
、视图查看、数据库查看和命令行扩展等功能。使用 Stetho,要在 OkHttp 客户端添加 Stetho 拦截器,然后在 Chrome 中输入 Chrome://inspect
,就能看到客户端发起的请求信息以及接收到的响应数据
。
服务端
要监控不同纬度的请求耗时
、失败率
以及 Top 失败接口
,比如地域
和时间段
等维度,失败率包括请求失败
和业务失败
。
客户端
要监控请求的每一个步骤的信息
,比如 DNS 解析时间
、连接建立时间
、请求时间
以及网络包大小
等信息。客户端还要监控图片请求,因为一张图片消耗的流量可能比 5 个甚至更多的接口的数据还要多。而且还要建立网络容灾机制
,比如备用服务器分流
和在一定时间内请求多次失败时,不再发起请求,并且加长重试时间
,避用户量暴涨时,服务器扛不住。
另外客户端和服务端都要监控网络异常
,比如在服务端要判断有没有人在刷我们的服务器,有的话可以拒绝这些 IP 的服务
。而客户端则要加上主动预警功能,比如用户下载了一个超过设定值的文件
,下载完后可以把这个结果上报给服务器,表明遇到了异常,需要进一步确认。
OkHttp 的 EventListener
能获取到请求的各个步骤的耗时和数据,提供了DNS 解析
、建立连接
、建立 HTTPS 连接
、写入请求首部
、写入请求体
等事件的开始
、结束
和失败
的回调方法。而且 EventListener 还能和 Glide
搭配使用,以获取图片请求的各个步骤的数据。
另外就是 OkHttp 默认单个主机能并发执行的请求最多为 5 个
,如果我们做了域名收敛,请求的都是同一个主机,就可以把 OkHttpClient 的 maxRequestsPerHost
的值设大一些。
NetworkStatsManager
是一个流量统计管理器
,可以获取某个时间段
和不同网络类型
的流量消耗,但是需要用户开启查看使用情况
权限,用户体验较差。
做流量优化
时要先设定预期
以及和竞品对比
,比如预计一个新功能在用户使用后,单次消耗流量为 300kb,但是上线后超过 300kb,这时就要看下找出并解决这个问题。另外还要和竞品对比,比如同样是发布评论的主流程,相同的评论和相同的图片,和竞品对比之下的流量消耗
是多少。
TrafficiStats
也是一个流量数据统计工具,缺点是只能统计到手机上次重启后到现在的流量消耗。
在我们记录了网络相关的日志后,在上报性能日志
时,大致要做的事情有四件,分别是App 启动时执行一个后台任务
、每隔一段时间(如 30 秒)就获取一次网络数据
、给数据加上标志
,比如区分前后台的标志、在合适的时机把数据上报到 APM 后台
,合适的时机指的是用户反馈时
、达到阈值
或处于 WiFi 网络下
这样的情况下,这样性能日志消耗的流量就不会对用户造成大的影响。
数据缓存
可以分为 OkHttp 的 Cache
和 无网拦截器
两种方案,我们项目中一般会有一些对实时性要求不高的接口,比如省市区列表,这时就可以考虑缓存这些接口的数据。
Cache
是 OkHttp 的缓存拦截器要用到的一个类,不设置的话 OkHttp 默认是不会缓存响应数据的。Cache 内部使用了 DiskLruCache
,创建 Cache 只需要指定缓存目录
和缓存大小
,然后设置给 OkHttpClient 就可以了。使用 Cache 要注意的是,默认缓存拦截器只会缓存 GET 和 HEAD 等获取资源的请求方法
的响应。
无网拦截器
指的是自定义一个实现了 Interceptor 接口的类,然后在网络不可用时,把强制缓存策略 FORCE_CACHE
传到 Request 的 Builder 的 cacheControl()
方法中就可以了。做数据缓存时,数据要加上过期时间
或版本号
,每次请求的时候客户端要判断一下数据有没有过期,或者是判断一下数据的版本号有没有更新。
第二个流量优化方案是数据压缩
,具体有 开启 Gzip 压缩
、压缩请求头
以及合并请求
三个方案。
开启 Gzip 压缩
就是把内容编码 Content-Encoding
首部设为 gzip
,如果我们的网络框架用的是 OkHttp ,那么在没有设置 Content-Encoding 请求头的情况下,OkHttp 默认会我们把 Content-Encodingshe 设置为 gzip。
压缩请求头
指的是在请求头不变的情况下,让服务端缓存请求头,在需要请求头的某些信息时,根据之前的请求头的 MD5 从之前的缓存中取。
合并请求
指的是把可以合并的请求合并起来,因为每一个网络请求头会有冗余信息。
第三个流量优化方案就是图片压缩
,具体的有缩略图
、WebP
以及 Luban
。
缩略图
指的是在列表中优先使用缩略图,因为在列表中直接展示原图没有意义。
WebP
是一种提供了有损压缩和无损压缩的图片格式,能把图片体积减少 50% 甚至更多。
Luban
是一个图片压缩工具,模仿了微信朋友圈的压缩策略,能把图片体积减少 95% 甚至更多,缺点是有损的,就像朋友圈的图片多少有点模糊一样。
网络请求质量优化的两个指标是网络请求成功率
和网络请求速度
,网络请求质量优化常见的三个方案是 使用 HttpDNS
、升级 HTTP 版本
以及砸钱
。
使用 HttpDNS
可以绕过运营商域名解析过程
,HttpDNS 使用的不是传统的 DNS 协议,而是使用 HTTP 协议,使用 HttpDNS 的好处是防劫持
以及提升访问速度
。
升级 HTTP 版本
指的是把 HTTP 协议升级为 HTTP/2
,HTTP/2 是一个二进制协议,最大的改进就是客户端和服务器可以同时发送多个请求和响应
,不用像 HTTP/1.1 一样按顺序请求。
砸钱
指的是花钱做 CDN 加速
、提高带宽
以及动静资源分离
。
1. 网络优化的三个要点
网络优化的三个要点分别是多维
、精准
和监控
。
多维指的是网络优化要考虑多个维度
,一谈到网络优化,大部分人首先想到的就是流量消耗,但是实际上流量消耗多少只是网络优化的其中一个维度。
只对流量消耗一个维度进行优化是不够的,甚至有的团队即便在流量优化上也没有做好,比如对于网络流量的消耗统计不够全面和精确。
精准指的是在做网络流量统计时的度量要精准
,如果只是获取了具体消耗了多少的值,对于我们定位和解决问题是没有太大的帮助,因为这个值只能表明用户使用了多少流量。否则如果线上用户反馈 App 消耗流量较多,但是我们不知道这个用户总共使用了 App 多长时间的话,那就不好定位问题所在,如果用户使用 App 的时间比较长,那消耗流量多一些很可能是正常的。又比如用户反馈 App 在后台消耗流量比较多,但是我们只统计了整体的值,那就无法断定 App 在后台运行时到底消耗了多少流量。
监控指的是要建设全面且完善的网络监控体系
,不能只监控一个指标,假如只监控网络请求成功率,那我们就只能知道用户大概的网络使用情况,这种粗粒度的监控没办法帮助我们找出并解决问题的根源。比如线上用户使用了某个功能使用了 1000 次,然后出现了 1 次异常,而且用户点击重试后就恢复正常了。这样单从数据上来看的话,网络请求的成功率还是比较高的,但是只通过成功率一个值是无法知道这一次异常出现的原因,也就无法避免后续出现这类异常。
2. 网络优化的两个维度
网络优化的两个维度分别是流量维度
和质量维度
。
流量维度指的是 App 在一段时间内流量消耗的精准度量
,流量消耗大不仅对用户有影响,对公司的运营成本也有影响,比如带宽
、服务器数量
、CDN
等方面的开支,而且网络请求密集对手机耗电量
也有一定的影响,在流量维度上,我们要区分类型
、监控异常
并上报日志
。
-
区分类型
区分类型就是要知道用户在
不同网络类型
(流量、WiFi)下的流量消耗、区分 App 在前台
和在后台
时的流量消耗。只有积累了不同维度的数据,才能快速断定和解决问题。 -
监控异常
异常监控就是我们不仅要知道用户的
流量消耗均值
,还要知道线上用户消耗流量的异常率。这里的异常分为流量消耗过多
、请求次数过多
以及下载文件过大
三种。 -
上报日志
最理想的情况就是我们对所有的网络请求,在本地都有一个完整的监控,每一个请求的
Request
和Response
相关的所有信息都全部记录下来。服务端可以下发指令控制客户端上传这些数据,客户端也可以在相关数据超过阈值后主动上报。
2. 质量维度
网络请求的质量也非常关键,它直接对应了用户的真实体验,如果网络请求速度慢或请求成功率比较低,都会导致不好的用户体验。
对于网络请求质量的监控,可以从请求时长
、请求成功率
、失败率
、Top 失败接口
等维度进行区分,以便后续能快速定位和解决问题。
3. 网络优化的两个误区
做网络优化时不能只关注流量消耗,忽视了其他维度
,做网络监控时不能只关注均值和整体的数据,忽略了个体的数据
。
比如前面提到的请求成功率的例子,从整体上来看成功率非常高,但是这种数据无法帮助我们改善单次请求。
1. 三个线下测试工具
常用的线下网络测试工具有 Network Profiler
、Charles
、Stetho
这 3 个,Network Profiler 是 Android Studio 提供的网络分析工具,能显示实时的网络活动,比如发送网络请求、接收的数据以及连接数。Charles 是抓包工具,Stetho 是 Facebook 开源的一个网络分析工具。
1.1 线下测试注意事项
线下测试是为了把问题尽可能在上线前暴露出来
,在线下测试环节,我们要注意网络切换
、弱网/无网测试
、请求是否有误
以及放长测试周期
等 4 点。
在做线下测试时,要切换不同的网络,比如 Wifi 和流量,而且还要做弱网测试和无望测试,保证我们的 App 不同的网络类型以及不同的网络状态下都有着很好的体验。
在请求失败时,首先要看接口请求是否有误,比如传了过多的参数或传参不正确
。
在做线下流量消耗测试时,要把周期拖长一些
,因为 C 端的 App 到了稳定期后,用户可能高达几千万甚至更多,这时 App 功能一般是非常复杂的,而且包含了其他功能,比如性能监控等。这些功能的网络请求往往不是实时上报的,所以我们在做流量消耗测试的时候,周期要拉长,简单测 10 分钟很可能测不出问题。
有的 App 对安全性要求比较高,如果突然切换网络状态,会导致用户的登录态失效
,也就是要用户重新登录。这时我们要注意重新登录后 App 会不会重新回到之前的流程中,不能中断用户正在进行的流程
。
在弱网或无网状态下,要测试加载弹窗是否会停止
,如果不注意的话,在无网的情况下,应用的请求失败了,但是却没有关闭 Loading 弹窗,影响了用户的其他操作。如果对无网状态重视不足,就测不出这样的 Bug 。
1.1 Network Profiler
Network Profiler 是 AS 自带的网络分析工具,它能显示实时网络活动,比如发送网络请求、接收的数据以及连接数等。
在 Profiler 中有一个连接视图(Connection View)和线程视图(ThreadView),连接视图中包含了连接的信息,比如请求时间、响应数据、请求数据以及调用栈,线程视图中包含了不同的网络请求线程的执行时间线
。
首先点击 run 旁边的 Profile 'app'
按钮开始分析。
然后在 Profiler 下方可以看到连接视图
(Connection View),右侧则是某个连接的信息,包括请求时间、响应、请求以及调用栈。
下面是线程视图。
Request 面板中包含了 User-Agent 和 Connection 等首部信息,以及请求体。
Response 面板中包含了响应体和响应首部数据。
Call Stack(调用栈)中包含了网络请求的调用栈。
1.2 Charles
常见的抓包工具有 Charles
、Fiddler
、Wireshark
和 TcpDump
四个,其中 Charles 是用 Java 开发的,在 Mac 上使用得比较多,Fiddler 在 Windows 上使用得比较多。下面我们重点来看下 Charles 的使用, Charles 支持断点
、模拟数据
以及弱网模拟
等功能。
1.2.1 基础用法
首先在官网上下载 Charles
- 打开 Charles 的代理功能
- 查看电脑 IP 地址
vboxnet2 是虚拟机 Virtual Box 的地址。
- 设置 WiFi 代理地址
-
同意连接
设置代理后,当我们在设备上访问网络时,Charles 会弹出一个是否允许连接的弹窗,我们点击允许(Allow)即可。
-
安装证书
如果是抓取 HTTPS ,那就要安装证书,否则请求和响应信息都会是乱码。
搜索 charles 并选择始终信任。
- 安装手机证书
选择安装证书在移动设备后,Charles 会弹出一个代理提示,让我们去 chls.pro/ssl 下载并安装证书。
我已经下载过了,所以这里提示的是重新下载,下载后点击安装即可。
-
添加主机
然后在 SSL Proxying Settings 中添加我们要抓包的主机地址,这里可以使用通配符 *.* 。
- 添加网络安全配置
在 res/xml 下添加 network_security_config.xml
然后在 Manifest 的 Application 标签中添加该配置。
然后就可以看到抓包结果了。
1.2.2 断点
我们右键点击任意一个请求后,就可以看到断点(Breakpoints)选项。
如果想要查看已经添加的断点列表,可以在 Proxy->Breakpoint Settings 中查看。
如果想只对 Request 或 只对 Response 断点,可以点击断点,然后在编辑断点弹窗中设置。
下面看下进入断点后的界面,当我们再次发起打了断点的请求时,可以看到断点界面。
当我们点击 Edit Request 标签后,我们可以在这里编辑请求的信息。
当我把 version 改为 3 后,可以看到请求中的参数改成了 3 。
1.2.3 模拟数据
Charles 有一个叫 Map Local 的功能,Map Local 多用于服务端接口未开发完毕,只定义了协议时的情况。
我们可以用 Map Local 模拟自己想要的假数据,可以自由模拟各种脏数据,这样就算服务端开发进度比较慢,不会影响客户端的开发进度,除了 Map Local,也可以使用 MockK 等用于测试的模拟框架来实现功能。
下面我们来看下怎么使用 Map Local。
然后右键点击请求,弹出菜单的最下方就是 Map Local。
点击 Map Local 后,可以看到 Edit Mapping 弹窗。
原始的响应是下面这样的。
然后建立一个 maplocal 文件,把 group 里的 count 改成 20 。
当客户端再次发起相同的请求时,就可以看到 group 的 count 的值变成了 20 。
如果想关闭 Map Local,可以点击 Tools -> Map Local,然后把 Enable Map Local 的勾选取消掉。
1.2.4 弱网模拟
Charles 支持弱网模拟功能,点击 Proxy -> Start throtting。
勾选 Enable Throttling 后,就开启了限流功能,限流功能支持下面几个选项。
预设值(Throttle preset)
带宽(Bandwidth)
利用率(Utilisation)
往返延迟(Round-trip-latency)
MTU(传输 TCP 包的最大尺寸)
可靠性(Reliability,丢包)
稳定性(Stability,抖动)
-
不稳定质量范围(Unstable quality range)
主要针对 Statbility 中设置的范围;
下面是一些常用的预设值。
下面这个请求,在限流前的总处理时间是 177 毫秒。
当开启了弱网模拟后,变成了 9.3 秒。
1.3 Stetho
看完了 Charles,下面我们来学习下 Stetho,Stetho 是一个强大的应用连接桥,可以连接 Android 和 Chrome,有网络监控、视图查看、数据库查看、命令行扩展等功能,下面来看下它的用法。
首先添加依赖,这个库是 Stetho 针对 OkHttp 的实现。
// Android 应用调试工具
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
然后初始化并添加拦截器。
在 Chrome 中输入 Chrome://inspect ,然后就可以看到设备上的 Chrome 已经打开的标签,下方就是打开了 Stetho 调试的 App。
点击豆芽下面的 inspect 后,就可以看到 DevTools,点击 DevTools 中的 Network 标签,就可以看到 App 发出的请求和接收到的响应等数据。
由于 Stetho 的抓包功能没那么强大,所以一般情况下不会用 Stetho 作为抓包工具。
2. 线上监控的三个要点
在看获取网络流量前,我们先来看下线上监控的三个要点:服务端监控
、客户端监控
以及异常监控
。
2.1 服务端监控
服务端要监控不同维度
的网络请求耗时
、失败率
以及 Top 失败接口
,,比如地域
、时间段
、版本
和机型
。
而失败率要分为请求失败
和业务失败
两个维度,请求失败就是网络请求失败,业务失败指的是网络请求虽然成功了,但是用户没有拿到自己需要的数据,统计多个维度的失败率才能更敏锐地感知线上的异常波动
。
在 APM 后台,最好统计一下在一段时间内,比如 1 天或 1 周内,排行 Top 的失败接口或异常接口
,每隔一段时间就进行一次统计
,这样就能知道哪些接口不稳定,以便进行针对性的优化。
2.2 客户端监控
客户端监控比服务端监控更关键
,在客户端的监控更全面,能拿到更多的数据,客户端在线上要监控请求的每一个步骤的信息
、图片请求
以及建立网络容灾机制
,要统计请求耗时
、成功率
、错误码
以及图片加载每一步的耗时
。虽然图片加载耗时和成功率在服务端也可以统计,但是服务端拿到的数据不完整,因为有的请求压根没到服务端就失败了
。而且服务端传过来的数据加上网络通道的延迟时间,肯定比服务端统计到的时间要长,所以我们要在客户端也加上统计。
客户端能拿到请求的每一个步骤的信息,包括 DNS 解析时间
、建立连接的时间
、请求时间
以及网络包大小
等信息。同时我们可以记录用户每一次网络请求的操作
,比如具体请求了哪些接口
,请求是否成功
以及请求失败的原因
,这些信息我们都能作为监控的基础信息传给 APM 服务端
,在后面会介绍如何监控网络请求的每一步。
客户端还要监控图片请求
,一张图片消耗的流量可能比 5 个甚至更多接口的数据还要多。
客户端要建立网络容灾机制
,假如某一天我们的用户量突然暴涨,服务器可能扛不住这个压力,对于服务端来说,可以用备用服务器分流
,避免把主服务器搞垮。而客户端也可以做一个策略,在一定时间内网络请求失败多次时,就不再进行网络请求
。
2.3 异常监控
异常监控的目的,是提升我们对异常的感知灵敏度,而不是被动等待用户反馈。
1. 服务器防刷
在服务端,我们要判断是不是有人在刷我们的服务器,也就是恶意攻击,如果检测到有人在刷服务器,我们可以锁定 IP 拒绝这些 IP 访问。
2. 异常兜底
在客户端,我们可以加上主动预警能力
,比如用户下载了一个超过设定值的文件
,客户端在下载后就可以把这个结果上报给服务器,表明现在遇到了异常,需要研发同学进一步确认。
在一些场景下,服务端可能出现流量过多扛不住的情况,这时客户端可以做一个兜底策略,如果在一定时间内,比如 30 秒内,接口请求连续失败 5 次,那就不允许持续访问,同时把重试时间设长一些
。
3. 单点问题追查
假如线上用户反馈 App 消耗流量过多,或者是在后台时消耗流量较多,我们都可以具体分析用户下的网络请求日志,以及下发命令查看具体时间段的流量消耗。
3. 三个线上监控方案
有的问题只在线上出现,在线下发现不了,比如在线下测试某个版本的时候,H5 包不一定是新版本,但是到了线上后,部分用户可能会被命中,然后下发了新版本的包。
而这些包如果没有经过压缩,这样的异常就无法在线下的测试环境中全部发现,只能通过线上监控发现。
下面我们来看下三个线上监控方案:
- OkHttp 事件监听器
- NetworkStatsManager
- TrafficStats
这三个方案中 OkHttp 事件监听器能获取到的数据最细致,也最实用,而 NetworkStatsManager 和 TrafficStats 主要是用于获取流量消耗。
3.1 OkHttp 事件监听器
3.1.1 自定义事件监听器
下面我们来看下如何结合 OkHttp 获取网络请求质量数据,OkHttp 给我们留了一个事件监听器 EventListener 回调,我们可以自己实现这个监听器,监听每一次的请求。
首先定义一个 Model,用于存储请求和响应相关数据,这里为了演示只加上了部分数据,大家可以根据自己的需要制定自己的 Model。
创建 OkHttpEventListener,并重写我们想要的方法,这里为了演示只重写了部分方法,OkHttpEventListener 提供了非常多的方法,大家可以根据自己的需要重写。
然后设置 OkHttpClient 的事件监听器工厂 。
设置了工厂后,我们就能拿到每个网络请求的每一步的耗时和相关数据,包括 DNS 解析事件、请求事件、响应时间、响应字节数等数据,这些都是我们做线上监控必不可少的数据支撑。
3.1.2 自定义 GlideModule
自定义 GlideModue 是为了监控图片加载的过程,下面我们来看下怎么监控 Glide 加载图片过程的耗时和相关数据。
首先添加插件和依赖。
// Kotlin 插件
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
// ...
}
dependencies {
// ...
// 图片加载
kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "com.github.bumptech.glide:okhttp3-integration:4.11.0"
}
然后定义一个 GlideModue,替换 ModelLoader 工厂。
点击 Build -> Clean Project,再点击 Build -> Make Project。
然后把 Glide 替换为 GlideApp。
然后我们就可以在自定义的事件监听器里监听到图片加载过程的耗时和大小了。
3.1.3 OkHttp 最大并发请求数
关于请求的频率,我们可以看下 OkHttp 默认的请求池,在 OkHttp 的 Dispatcher 分发器中,有一个 executeService 线程池,它的核心池大小为 0 ,最大值是整型的最大值。
但这并不是意味着通过 OkHttp 发送的网络请求可以并发无数个,对于 IO 密集型任务我们可以多发送一些,因为这类任务对 CPU 消耗不大,但是我们还是要注意,单个 App 能创建的线程数也是有上限的。
虽然线程池大小没有限制,但是在请求执行前会在 promoteAndExecute() 方法中判断是否已超出最大请求数量,而判断的依据就是 maxRequests(最大并发请求数) 和 maxRequestsPerHost(单个主机最大并发请求数)。
之所以要设置单个主机最多能并发执行的请求数量,防止某个域名的请求过多,导致其他域名没有机会执行。
假如我们的项目中已经做了域名收敛,只有一个域名,就可以增加单个域名下的数量限制,这样同时执行的网络请求的数量就能多一些。
3.1.4 区分前后台流量
之所以要在日志中区分前后台流量,是因为很多用户会担心 App 一直在后台消耗流量,如果粗粒度地只获取流量数据,是不知道这些流量有多少是 App 在后台运行时消耗的,这样的话不要说解决用户反馈的问题,就连定位问题都定位不了。
通过 ProcessLifecycleOwner 给请求数据打上标志,这样我们就可以分别知道用户在前台和后台的流量消耗,而且用户反馈时,我们就可以自己查看该用户的流量消耗统计,判断是否存在异常。
这个方案存在的不足,就是当用户在 30 秒内切换 App 的前后台状态时,会有一定的误差,但是这个误差是在可接受范围内的。
使用这种方式,再结合 APM 后台设置的阈值,让客户端在流量消耗达到阈值后自动上报,从而实现更精准的流量消耗监控。
下面我们来看下怎么使用 ProcessLifecycleOwner 。
1. 添加依赖
有很多 AndroidX 的库自带了 process 库,大家可以在 External Libraries 中看下有没有,有就不用添加下面这个库了。
dependencies {
// AndroidX Lifecycle 进程
implementation 'androidx.lifecycle:lifecycle-process:2.2.0'
}
2. 定义应用生命周期观察者
3. 添加观察者
4. ProcessLifecycleOwner 原理
在 ProcessLifecycleOwner 出来之前,我们自己也可以通过计数的方式来判断是不是所有 Activity 都隐藏了,是的话则断定为应用隐藏了。
而 ProcessLifecycleOwner 只是帮我们把这件事情做了,在 ProcessLifecycleOwner 的初始化方法 ini 中调用了 attach 方法,而 attach 方法中实现了 Activity 生命周期回调接口。
以 onStart 为例,ProcessLifecycleOwner 中有一个 mStartdCounter,每次有一个 Activity 的 onStart 方法被调用了,mStartdCounter 就会加 1,如果 mStartCounter 等于 1 ,则表明应用启动了,然后 ProcessLifecycleOnwer 就会通过 mRegistry 调用我们自定义的 onAppForeground 方法。
3.1.5 四步上传数据
下面是上报性能日志的大概的四个步骤。
-
后台任务
在 App 启动时,执行一个后台任务;
-
间隔统计
这个任务每隔一段时间(如 30 秒内)就获取网络数据;
-
自定数据
自己维护一份数据统计,给数据加上标志,记住用户在前后台的流量消耗总量;
-
上报数据
在合适的时机(如用户反馈、达到阈值、处于 WiFi 网络下)把数据上报到 APM 后台,这样对用户的流量就不会造成影响,而且数据也能作为流量治理依据;
3.2 NetworkStatsManager
如果我们不使用 OkHttp 的 EventListener ,而是用了 NetworkStatsManager,那么当用户反馈说某个时间段内的流量消耗较多时,我们也可以在后台给客户端下发一个指令,让客户端上传特定时间段内的流量统计数据,然后结合用户使用时长判断 App 的流量消耗是否真的存在异常。
NetworkStatsManager 是 API 23 后的流量统计管理器,它可以获取某个时段的流量信息,也可以获取不同网络类型下的流量消耗,它最大的不足就是用户体验比较差,需要用户开启“查看使用情况”权限。
下面是一些使用网络或对流量的消耗比较多的场景。
API 请求
-
升级包
各种升级的资源包,比如 App 升级包、WebView 使用的 H5 Zip 包、RN 使用到的 bundle 包等;
-
配置
在 App 做大后,还要用到各种配置信息,比如做 A/B 测试时使用的配置信息、运营活动下发的配置信息;
-
图片
图片是流量消耗的大户,图片的下载和上传都非常消耗流量;
-
监控
在 App 做大后,还会做各种监控功能,比如 APM 监控的各种数据都需要网络才能上传到服务端;
3.2.1 流量优化的三个要点
在讲怎么用 NetworkStatsManager 获取流量消耗前,我们要先了解下流量优化的三个要点。
1. 不能只看绝对值
绝对值不能作为流量消耗偏高的唯一统计标准,不能说 App 消耗了 10M 的流量,那就要马上去优化。
绝对值的对比是没有意义的,比如用了 App 30 分钟,浏览了很多的商品或视频,那用了 10M 可能已经算少了。
2. 对比竞品
我们最好是对比竞品在相同的场景下的流量消耗,比如和竞品一起跑一个发布评论的主流程,这里要注意,要保证两个 App 发布的评论是相同的,而且图片也是相同的,以保证变量是唯一的。
这样对比一下,如果我们和竞品的流量消耗差距比较大,那我们对流量优化的步伐,就应该加快一些,绝对值跟对比对比可以结合使用。
3. 设定预期
我们要判断一下新上功能的流量消耗,比如我们要预期是用户使用新功能后,单次消耗流量应该为 300K 左右,但是上线后超过了 300K 时,我们就要确认下流量消耗是否偏高。
3.2.2 NetworkStatsManager 基本用法
1. readNetworkStats
2. querySummary
下面是常用的 querySummary 方法各个参数的介绍。
querySummary 的内部处理流程如下。
在这个流程中,最重要的就是在 NetworkStatsCollection 的 mStats 中读取系统已经保存好的网络统计数据。
3. 申请授权
使用 NetworkStatsManager 需要额外的权限 PACKAGE_USAGE_STATS , 这个权限是系统权限,需要主动引导用户开启应用的“有权查看使用情况的应用”权限。
首先在清单文件中权限声明。
然后在代码中主动引导用户开启权限。
3.3 TrafficStats
TrafficStats 是 API 8 后提供的一种流量数据统计方案,它统计到的数据其实是我们手机上次重启后的流量消耗,也就是重启前的流量是统计不到的。
TrafficStats 常用方法:
-
getUidRxBytes(int uid)
获取指定 Uid 的接收流量;
-
getTotalTxBytes()
获取 App 总发送流量;
下面我们是 TrafficStats 的使用方式。
一般情况下不使用 TrafficStats,因为它存在下面两个问题:
-
无法获取特定应用的流量消耗
无法获取特定应用的流量消耗,只能给我们一个总的流量消耗值,这个值对于我们解决问题来说帮助不大。
-
无法获取特定时间段的流量消耗
比如线上某个用户反馈发现昨天 App 的流量消耗过多,这时我们是无法知道用户具体消耗了多少流量的。
4. 三个流量优化方案
下面我们来看下三个流量优化方案:
- 数据缓存
- 数据压缩
- 图片压缩
4.1 数据缓存
下面我们来看下数据缓存的两个方法:
- OkHttp 缓存
- 过期时间与增量更新
4.1.1 OkHttp 缓存
如果我们仔细跟一下自己项目中的接口,就会发现很多对实时性没有那么高要求的接口,使用缓存不仅可以节约流量,而且能大幅提升数据访问速度。
我们常用的网络库,比如 OkHttp 和 Volley,都有比较好的缓存实践。
而且没做缓存对用户体验也不好,一般的 App 会在打开后显示一个无数据的界面,和展示上一次的数据相比,这个用户体验其实是比较差的。
1. 无网拦截器
下面我们重点看下 OkHttp 的缓存实践,首先定义一个无网拦截器。
然后是给 OkHttpClient 添加拦截器。
添加了无网络拦截器后,当无网络的情况下打开我们的 App 时,也能获取到上一次的数据,也能使用 App,这样就能提升用户体验。
2. OkHttp 缓存处理流程
OkHttp 的缓存拦截器对于请求的处理流程如下。
4.1.2 过期时间与增量更新
1. 过期时间
在服务端返回的数据中加上一个过期时间,这样我们每次请求的时候判断一下有没有过期,如果没有过期就不需要去重新请求。
2. 增量更新
数据增量更新的具体思路,就是在数据中加上一个版本的概念,每次接收数据都进行版本对比,只接收有变化的数据。
这样传输的数据量就会减少很多,比如省市区和配置等数据比较少更新,如果每次都要请求省市区的数据,这就是在浪费流量。
我们只需要更新发生变化的数据,因为和服务器相关比较密切,在这里就不给大家举例了。
4.2 数据压缩
1. Gzip
对于 Post 请求,Body 是用 Gzip 压缩的,也就是请求的时候带上 Gzip 请求头,服务端返回的时候也加上 Gzip 压缩,这样数据流就是被压缩过的。
2. 压缩请求头
请求头也占用一定的体积,在请求头不变的情况下,我们可以只传递一次,以后都只需要传递上一次请求头的 MD5 值,服务端做一个缓存,在需要请求头中的某些信息时,就可以直接从之前的缓存中取。
3. 合并网络请求
每一个网络请求都会有冗余信息,比如请求头,而合并网络请求就可以减少冗余信息的传递;
4.3 图片压缩
1. 缩略图
图片压缩的第一个手段,就是在列表中优先使用缩略图,因为展示原图会加大内存消耗和流量消耗,而且在列表中直接展示原图没有意义。
下面是原图和缩略图的对比大小,缩略图尺寸为原图的 50%,大小为原图的 10%。
2. WebP
图片压缩的第二个手段,就是使用 Webp 格式,下面是同一张图片在 PNG 格式和 WebP 格式下的对比,WebP 格式的大小为 PNG 格式的 51%。
3. Luban
比如我们在上传图片的时候,做一个压缩比如在本地是一个 2M 的图片,完整地上传上去意义不大,只会增加我们的流量消耗,最好是压缩后再上传。
而在图片压缩上做得比较好的就是鲁班,下面我们来看下鲁班的使用方法。
首先添加依赖。
dependencies {
// 图片压缩
implementation 'top.zibin:Luban:1.1.8'
}
然后添加对图片进行压缩。
下面这张图片的原始大小为 1.6M,压缩后变成了 213KB,体积为原始大小的 13%。
5. 三个质量优化方案
在前面我们学习了网络请求流量优化,但是实际上,对用户体验破坏最大的是网络请求质量差,我们一般在开发或测试阶段,都是在公司用 WiFi 测试,网络质量比较好,假设我们的 App 在流量消耗上问题不大,但是用户经常反馈界面打不开、打开慢、图片加载不出来等问题。
这时用户很有可能会卸载我们的 App ,转向竞品,只有在网络请求质量高,用户体验好,继续使用我们的 App 时,用户才有可能遇到流量消耗的问题,所以网络质量优化比流量优化更关键。
网络质量优化的两个指标:
- 网络请求成功率
- 网络请求速度
这两个指标都会影响用户体验,在介绍网络请求质量优化前,我们先来看下一个 Http 请求的过程。
客户端发出一个请求
,这个请求到达运营商的 DNS 服务器,然后被解析成对应的 IP 地址。
创建连接
,会走 TCP 三次握手,然后根据 IP 地址找对对应的服务器,发送一个请求。
服务器找到对应的资源,然后原路返回给客户端
.
5.1 HttpDNS
首先来看怎么在发出请求这一步上优化,网络请求成功率与速度一上来就受 DNS 服务器的影响,如果我们的 DNS 解析到 IP 地址的过程被劫持或 DNS 解析慢,都会严重影响用户体验。
DNS 被劫持的结果就是用户得到的数据并不是我们真实想要提供给用户的数据,如果 DNS 解析慢,那用户等待的请求时间就会变长。
所以 DNS 优化是网络质量优化的第一步,我们使用 HttpDNS,绕过运营商域名解析过程,HttpDNS 不是使用传统的 DNS 协议,向 DNS 服务器的 53 端口发送请求,而是使用 Http 协议,向服务器的 80 端口发送请求。
这样做的好处有两个:
-
防劫持
降低 Local DNS 劫持,绕过运营商域名解析过程;
-
提升速度
降低平均访问时长,因为节省了一次解析过程;
腾讯云和阿里云都提供了 HttpDNS 服务,具体的实现可能看他们的官方文档。
5.2 Http 协议版本优化
刚刚说到了网络请求的第二步是创建连接,当中涉及 TCP 三次握手,这个过程是比较长的,如果每次请求都要走三次握手,那这个效率是比较低的。
所以 Http 的不同版本对这点的优化也非常多,下面是 Http 协议不同版本之间的主要区别。
-
Http 1.0
较老的版本,现在已经很少见到用这个版本的服务了,它最大的缺点就是 TCP 连接不复用,每个 TCP 连接只能发送 1 个请求,如果要请求别的资源,就必须重新建立一个连接。
TCP 创建连接的成本非常高,需要三次握手,并且在开始阶段发送速度比较慢,也就是 Http 1.0 的性能非常差。
-
Http 1.1
Http 1.1 的出现只比 1.0 晚了半年,它最大的变化就是引入了持久连接,从这个版本开始,是默认不关闭的,可以被多个网络请求复用,这样效率就有了很大的提升。
它还是有些缺陷,就是它虽然允许 TCP 复用,但是同一个 TCP 连接里面的所有数据通讯必须按顺序来,也就是处理完一个请求后,再响应下一个请求。
如果前面的网络请求比较慢,那后面的请求也只能等着。
-
Http 2.0
Http 2.0 是一个二进制协议,它最大的改进就是客户端和服务端可以同时发送多个请求和响应,不需要像 Http 1.1 一样按顺序请求,是一个双向的实时通信。
这点是 Http 2.0 相对于 Http 1.1 的最大的改进,大家以后要跟服务端配合时,有得选的前提下,尽可能选择高版本的 Http 协议。
5.3 资本优化
最后一个优化方案就是砸钱,常见的手段有 CDN 加速、提高带宽、动静资源分离。
大家需要注意,使用 CDN 后,如果某个资源需要更新,更新完成后是需要清理缓存的,这些优化不涉及客户端,同时也不要忘了减少传输量,注意请求的时机和频率,这一条和我们前面讲到的流量优化相关。
参考资料
- 慕课网—Top团队大牛带你玩转Android性能分析与优化
- macOS Charles 4.x版本的安装及使用
- Setting up Charles to Proxy your Android Device
- 网络安全配置
- Charles网络设置
- Stetho
- Android应用流量统计——NetworkStatsManager使用
- AppOpsManager权限检测适配
- Android中的uid
- Glide 4.X 使用自定义okhttp 加载图片(忽略https验证)
- ProcessLifecycleOwner判断Android应用程序前后台切换
- NetworkStatsCollection