前言小计
在移动互联网的快速发展环境下,手机用户日益对网络的使用或体验有着更深度的诉求,因此应用中的网络体验已经显得由此重要;
网络影响:
1.最直观的就是用户交互体验;
2.流量的流失;
3.电量的消耗;
上篇文章介绍了网络请求过程中dns,本文主要针对Android上网络优化
一、网络数据缓存优化
提供一种将数据存储到本地的思想,实现减少服务器的请求负荷、加快请求速度、无网也能显示内容;
(1)对于常访问的数据或首页数据,尽量缓存在本地,加载时优先加载本地数据,然后在请求网络数据,更新页面并更新缓存;
(2)在网络丢失或者网络差需要保存数据时, 网数据保存在本地,并且把发出的请求添加到队列中,当网络恢复的时候再及时发出;
(3) 强制缓存:在缓存数据未失效的情况下,可以直接使用缓存数据,由两个字段Expires和Cache-Control用于标明失效规则;
(4)对比缓存:表示需要和服务端进行相关信息的对比,由服务器决定是使用缓存还是最新内容,如果服务器判定使用缓存,返回响应吗304,判定使用最新内容,则返回响应码200和最新数据;
(5)okhttp上的缓存设置
进行数据缓存,我们可以在返回上加上过期时间,避免重新获取。这种做法节约了流量,且大幅提高数据访问的速度,增强了用户体验。在OKHTTP与Volley等一些网络框架中都有很好的实践;
下面进行OKHTTP,在无网络的情况下使用cache进行缓存
- public class NoNetInterceptor implements Interceptor {
- @Override
- public Response intercept(Chain chain) throws IOException {
- Request request = chain.request();
- Request.Builder builder = request.newBuilder();
- if(!Utils.isNetworkConnected(PerformanceApp.getApplication())){
- builder.cacheControl(CacheControl.FORCE_CACHE);
- }
- return chain.proceed(builder.build());
- }
- }
- static {
- OkHttpClient.Builder client = new OkHttpClient.Builder();
- HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
- logging.setLevel(HttpLoggingInterceptor.Level.BODY);
- Cache cache = new Cache(PerformanceApp.getApplication().getCacheDir(),10*1024*1024);
- client.
- cache(cache).
- eventListenerFactory(OkHttpEventListener.FACTORY).
- dns(OkHttpDNS.getIns(PerformanceApp.getApplication())).
- addInterceptor(new NoNetInterceptor()).
- addInterceptor(logging);
- final Retrofit RETROFIT = new Retrofit.Builder()
- .baseUrl(HTTP_SPORTSNBA_QQ_COM)
- .addConverterFactory(FastJsonConverterFactory.create())
- .client(client.build())
- .build();
- API_SERVICE = RETROFIT.create(APIService.class);
- }
二、dns优化和httpdns
1、dns解析流程
- 在App内用域名发送请求都要经过DNS解析出ip,然后再根据ip去拿对应的资源,这个过程中,如果LocalDNS中存在这个域名对应的ip,就会直接返回这个ip,类似于App内做缓存;
- 如果不存在,才会去权威DNS查询改访问哪个ip,然后查询到的ip会在LocalDNS中做缓存。也就是说,如果我们要访问新浪http://api.weibo.cn,如果LocalDNS里面有该域名对应的ip,就直接返回了ip了。
2、问题分析
- 一个新用户使用来访问api.weibo.cn,由于localDNS缓存的存在,不会去查询新浪的权威DNS,这样返回的ip是联通这个运营商的ip,从而会使得用户出现访问变慢等状况;
- 缓存还会导致一点就是,当权威DNS将域名与ip的映射发生改变之后,由于LocalDNS缓存没有及时改变,用户就会访问到错误的服务器,或者直接访问不到资源;
- 很多三四级运营商会把运营解析指向他们的缓存服务器上,并把网页里面的广告替换成他们自己的,或者内嵌他们自己的广告(之前做的APP出现过这样的情况,投诉之后会好上一段时间,但是过段时间又会出现广告);
- 竟然DNS解析存在问题,那有没有一种调度精准、成本低廉、配置方便的基于域名的流量调度系统呢?
- HttpDNS基于Http协议和域名解析的流量调度解决方案,可以在很大程度上防止上面的问题出现;
3、HttpDNS 概念
- 客户端直接访问HttpDNS接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP;
- 客户端向获取到的IP后就向直接往此IP发送业务协议请求。以Http请求为例,通过在header中指定host字段,向HttpDNS返回的IP发送标准的Http请求即可;
- 采用HttpDNS来解析域名,就绕过了三四级运营商解析域名会出现的问题,在HttpDNS返回了正确的ip之后,我们是直接采用ip去进行http请求,只需要关注通信内容的安全即可;
4、HTTPDNS 工作模式
- 在客户端的 SDK 里动态请求服务端,获取 HTTPDNS 服务器的 IP 列表,缓存到本地。随着不断地解析域名,SDK 也会在本地缓存 DNS 域名解析的结果;
- 当手机应用要访问一个地址的时候,首先看是否有本地的缓存,如果有就直接返回。这个缓存和本地 DNS 的缓存不一样的是,这个是手机应用自己做的,而非整个运营商统一做的。如何更新、何时更新,手机应用的客户端可以和服务器协调来做这件事情;
- 如果本地没有,就需要请求 HTTPDNS 的服务器,在本地 HTTPDNS 服务器的IP 列表中,选择一个发出 HTTP 的请求,会返回一个要访问的网站的 IP 列表;
请求的方式是这样的。
- http://106.2.xxx.xxx/d?dn=c.m.163.com
- {"dns":[{"host":"c.m.163.com","ips":["223.252.199.12"],"ttl":300,"http2":0}],"client":{"ip":"106.2.81.50","line":269692944}}
手机客户端自然知道手机在哪个运营商、哪个地址。由于是直接的 HTTP 通信,HTTPDNS 服务器能够准确知道这些信息,因而可以做精准的全局负载均衡;
5、HttpDNS缓存设计
- HTTPDNS 的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端、缓存、数据源三层,分别对应 SDK 客户端、本地缓存、HTTPDNS 服务器;
- App内维护一个Serve IP List。把每次App从HttpDNS取到的ip存储进入该数组,并设置权重,理论上来说从HttpDns解析下来的ip权重是最大的。这个List可以在App启动的时候,进行更新,同时取出本地缓存的Serve IP List的权重最大的ip进行数据的初始化操作(如果第一次启动,没有该List的话,就使用LocalDNS进行解析);
- Serve IP List里面的权重设置机制,很明显的一点就是从DNS解析出来的ip具有最大的权重,每次从List里面取ip应该要取权重最大的ip。列表中的ip也是需要可以动态更新配置的,根据连接或者服务的成功失败来进行动态调整,这样即使DNS解析失败,用户在一段时间后也会取到合适的ip进行访问;
- 对ip进行数据统计。在所有app内统计每个ip进行请求所需平均时间、最长时间、最短时间、请求成功次数、失败次数,需要注意的是,要区分网络环境进行统计,Wifi、4G、3G,对在不同的网络环境下数据优秀的ip进行存储,下发到App里面使用起来。这样每次启动App时可以对收集起来的ip根据不同的网络环境进行测速,选择最好的ip进行请求。需要注意的是,在网络环境切换的时候,必须要重新进行速度测试。做到这一步,可以节约DNS解析时间,以及劫持的问题;
- SDK 中的缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析,保障记录是更新的;
6、HTTPDNS调度设计
- 在客户端,可以知道手机是哪个国家、哪个运营商、哪个省,甚至哪个市,HTTPDNS服务端可以根据这些信息,选择最佳的服务节点返回;
- 如果有多个节点,还会考虑错误率、请求时间、服务器压力、网络状况等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或者性能下降的时候,可以尽快进行切换;
- 要做到这一点,需要客户端使用 HTTPDNS 返回的 IP 访问业务应用。客户端的 SDK 会收集网络请求数据,如错误率、请求时间等网络请求质量数据,并发送到统计后台,进行分析、聚合,以此查看不同的 IP 的服务质量;
- 在服务端,应用可以通过调用 HTTPDNS 的管理接口,配置不同服务质量的优先级、权重。HTTPDNS 会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的 IP 地址;
- HTTPDNS 通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商 WIFI 的 SSID 来分维度缓存。不同的运营商或者 WIFI 解析出来的结果会不同;
7、OKHttp 接入 HTTPDNS
OkHttp 中使用 HTTPDNS,有两种方式:
通过拦截器,在发送请求之前,将域名替换为 IP 地址;
通过 OkHttp 提供的 .dns() 接口,配置 HTTPDNS;
①拦截器接入
拦截器是 OkHttp 中,非常强大的一种机制,它可以在请求和响应之间,做一些我们的定制操作;
在 OkHttp 中,可以通过实现 Interceptor 接口,来定制一个拦截器。使用时,只需要在 OkHttpClient.Builder 中,调用 addInterceptor() 方法来注册此拦截器即可;
- class HTTPDNSInterceptor : Interceptor{
- override fun intercept(chain: Interceptor.Chain): Response {
- val originRequest = chain.request()
- val httpUrl = originRequest.url()
- val url = httpUrl.toString()
- val host = httpUrl.host()
- val hostIP = HttpDNS.getIpByHost(host)
- val builder = originRequest.newBuilder()
- if(hostIP!=null){
- builder.url(HttpDNS.getIpUrl(url,host,hostIP))
- builder.header("host",hostIP)
- }
- val newRequest = builder.build()
- val newResponse = chain.proceed(newRequest)
- return newResponse
- }
在拦截器中,使用 HttpDNS 这个帮助类,通过 getIpByHost() 将 Host 转为对应的 IP;
②OKHttp 标准 API 接入
OkHttp 其实本身已经暴露了一个 Dns 接口,默认的实现是使用系统的 InetAddress 类,发送 UDP 请求进行 DNS 解析;
我们只需要实现 OkHttp 的 Dns 接口,即可获得 HTTPDNS 的支持。
在我们实现的 Dns 接口实现类中,解析 DNS 的方式,换成 HTTPDNS,将解析结果返回;
- class HttpDns : Dns {
- override fun lookup(hostname: String): List<InetAddress> {
- val ip = HttpDnsHelper.getIpByHost(hostname)
- if (TextUtils.isEmpty(ip)) {
- //返回自己解析的地址列表
- return InetAddress.getAllByName(ip).toList()
- } else {
- // 解析失败,使用系统解析
- return Dns.SYSTEM.lookup(hostname)
- }
- }
- }
- mOkHttpClient = httpBuilder
- .dns(HttpDns())
- .build();
三、数据传输优化
客户端与服务端经常进行着频繁的数据传输,而数据传输又影响着用户体验,提出合理的优化建议
1、传统的传输方案
在开始的时候,采用的是xml传输,这就要使用到Serializable/Parcelable序列化以及反序列化,其传输速度之慢,基本已经被遗弃,后来又出现了JSON序列化传输,其常用工具就是GSON和fastjson,但随着时代的进步,json也体现出了局限性json的局限性主要体现在其是基于字符串的传输,在转换的时候会生成大量JsonObject,然后转化为字符串,送进流里面,然后传输,在服务端也要从流中取出,然后反序列化,一大堆繁琐的过程,其也渐渐不适合当今传输数据的要求;
2、新的数据传输方式
- 现在有如下选择可以用Protocal Buffers:强大,灵活,但是对内存的消耗会比较大,并不是移动终端上的最佳选择;
- Nano-Proto-Buffers:基于Protocal,为移动终端做了特殊的优化,代码执行效率更高,内存使用效率更佳;
- FlatBuffers:这个开源库最开始是由Google研发的,专注于提供更优秀的性能;
如下图:
FlatBuffers几乎从空间和时间复杂度上完胜其他技术
FlatBuffers是一个开源的跨平台数据序列化库,可以应用到几乎任何语言(C++,C#,Go,Java,JavaScript,PHP,Python),最开始是Google为游戏或者其他对性能要求很高的应用开发的;
FlatBuffer的优点
FlatBuffer相对于其他序列化技术,例如XML,JSON,Protocol Buffers等,有哪些优势呢?官方文档的说法如下:
- 直接读取序列化数据,而不需要解析(Parsing)或者解包(Unpacking):FlatBuffer把数据层级结构保存在一个扁平化的二进制缓存(一维数组)中,同时能够保持直接获取里面的结构化数据,而不需要解析,并且还能保证数据结构变化的前后向兼容;
- 高效的内存使用和速度:FlatBuffer 使用过程中,不需要额外的内存,几乎接近原始数据在内存中的大小;
- 灵活:数据能够前后向兼容,并且能够灵活控制你的数据结构
- 很少的代码侵入性:使用少量的自动生成的代码即可实现;
- 强数据类性,易于使用,跨平台,几乎语言无关;
- JSON是Android中很常用的数据序列化技术,但却很消耗内存,而FlatBuffer正好解决了这个问题,性能还更好了;
3、对于 Post 请求,Body 是用 Gzip 压缩的,也就是请求的时候带上 Gzip 请求头,服务端返回的时候也加上 Gzip 压缩,这样数据流就是被压缩过的。
四、网络其他优化
1、网络分级请求
(1)将网络分成移动网络、宽带网络、强网络、弱网络,不同的网络环境对请求进行不同的处理,例如在移动网络下需要进行下载任务时,停止或提示用户,在弱网络下对图片的请求的区分等;
(2) 网络状态可以由TelephonyManager.getNetworkType()方法获取到;
(3)对下载和上传文件采用断点续传功能,不浪费用户之前耗费的时间和流量;
2、流量使用优化
(1)局部更新 、分页加载;
(2) 数据加载采用增量,有更新数据时才请求新数据,合并客户端旧数据;
(3)尽量避免客户端轮询,采用服务端推送方式;
3、请求数据优化
(1)合并请求,可以将多个请求合并成一个接口请求
(2)压缩请求数据
(3)精简数据格式,只取需要的数据字段
4、连接优化
- 启用keep-alive,okhttp中已默认打开,但需要服务器支持;
- 使用http2,使用keep-alive来缓存,通过http2来复用请求连接;
- 复用连接池可以减少多个网络请求下的连接建立的消耗,而且OkHttp已经默认帮我们实现了这些功能;
- 复用连接的前提是同ip通道,如果每个请求都发送给不同的ip,那么连接池也复用不了;
- 非Http2的使用完连接后(就是Http请求完成后),我们需要手动 关闭RealCall,否则下层代码就要通过触发GC回收来帮我们检查和关闭;
- 切换不同的域名,当一个连接失败的时候用另一个域名进行连接;
总结
1、传统的 DNS 有很多问题,例如解析慢、更新不及时。因为缓存、转发、NAT问题导致客户端误会自己所在的位置和运营商,从而影响流量的调度;
2、HTTPDNS 通过客户端 SDK 和服务端,通过 HTTP 直接调用解析 DNS 的方式,绕过了传统 DNS 的这些缺点,实现了智能的调度;
3、关于连接优化还有很多知识点,后续整理出来会发表出;
4、优化部分我们讲解了网络相关的,下次会整理出图片相关的知识点,一起学习一起进步,加油;
本文转载自微信公众号「Android开发编程」,可以通过以下二维码关注。转载本文请联系Android开发编程公众号。