前言
在主流操作系统,网络一直是一个核心的模块,我们常用的通信软件、娱乐软件、视频软件都需要通过网络来传输信息,而我们传输各种信息就会使用到各种网络协议,如icmp、dns、http、tcp、udp等协议都已经是网络主机的标准配置。
实际上,现如今很多app都是使用Okhttp,网上也有很多相关的优化方法,本篇对其中Android中常用的部分进行汇总一下,同时也会提出一些新的思路,我们主要围绕DNS、HTTP、WebView三部分来汇总。
DNS部分
dns相关问题
在正常情况下,其实很难遇到DNS问题,但是在使用的过程中,特别是使用CDN的时候,这个问题就会更加明显,主要问题有以下几个:
- 运营商劫持:本地网络服务商为了省带宽,会对数据进行缓存,强制将域名解析到特定的地址
- DNS解析失败: 受限于区域位置、网卡设备的问题,解析不到的情况也是有的
- 网关对特殊域名拦截:近年来出现了很多顶级域名,但是这些域名特殊,一些旧的网关会主动拦截,另外一些情况比如域名中有下划线等特殊字符的,也会被拦截。
- 解析的地址访问速度过慢:一部份原因是机房部署的位置离用户太远或者解析出的地址属于不同的运营商,还有一部分原因是某一个CDN IP挂载太多高访问量域名,当然后者我们无能为力,理论上这是CDN厂商的问题了。
- 首次或者切换网络后访问过慢:这个主要原因是,未能提前解析和刷新域名。
- 多个IP访问速度无法确定: IP和域名是多对多的关系,如何确定最快的域名,这也是比较困难的
常见的DNS优化方法
基于上述情况,我们也有很多应对措施
- 引入HttpDNS: 在实际情况中,特别是使用CDN的情况,使用httpDNS当然是首选,另外一种就是iOT设备,很多网络模块兼容性较差,因此使用HTTPDNS很有必要。注意细节操作,不要因为HTTP DNS没有返回结果就让业务等待,如果没有返回时先使用系统的解析方式。
- 监听网络变化,及时刷新HttpDNS: 手机用户经常会出现网络掉线、不同网络运营商之间切换,或者是wifi和gprs切换网络,因此,网络变化时及时刷新HttpDNS很有必要。另外,由于在不同基站之间穿梭,手机ip也会变化,理论上也需要刷新,不过这个频率要稍微降低一些,因为同一地区DNS变化不会太大。至于系统的DNS,其本身就会在2s内自动刷新,我们需要优化的HTTP DNS。但是这里要记住,在拿到新地址之后更新旧地址比较妥当。
- 预解析HttpDNS: 很多网页中都有dns预解析的逻辑(prefetch),如果我们app中存在多种域名,提前解析出来显然很有必要。
- IP筛选:如果一个域名对应多个IP,这个时候有必要通过竞速去筛选。当然,这个是单独的逻辑,不要为了筛选IP,让业务等待。
- 域名动静分离:静态资源单独存储在特定的服务器,因为这类属于I/O类,不会消耗太多cpu,而动态资源如业务相关的,需要大量计算,因此分离动静域名很有必要。其实CDN就是负责静态的资源存储,业务是负责动态资源,分离可以提高吞吐量。
- 域名合并:实际上域名数量越少越好,过多的域名解析时间很长,因此,在动静分离的情况下,尽量减少域名数量。
- 避免使用特殊字符的域名:比如下划线、其他国家的文字等,尽量以ascii文本为主,特殊字符和特殊文字要避免,防止被路由防火墙以及一些网关拦截。
- 避免使用敏感国家或地区的域名。
- 避免使用在特定国家或地区无法访问的域名。
- 避免使用将域名作为国家和地区形象的域名。
HTTP 部分
HTTP相关问题
- 弱网问题:在任何网络请求中,弱网影响很大,其实弱网不仅仅影响HTTP,诸如DNS、ICMP都会收到影响,但是网络的快慢也很难通过检测手段去优化,其中一个原因是一些情况下网络具有波动性,可能时好时坏。其次Traffics统计也存在误差。另外,这里容易出现的异常是Connection Timeout和Read Timeout相关。
- 无网问题:无网络引发的问题是直接、明确的,这个时候发起网络访问必然失败,出现的错误Socket相关的异常。
- UnknownHostException、SocketConnectException :引发此类问题主要是DNS无法解析到。
- 网络访问无法统一为单一网络框架,主要是HttpUrlConnection 、MediaPlayer、WebView,Native部分无法统一为同一种框架,或者实现难度较高。
- 数据加载过慢,主要是网络差、连接慢或者数据量太大
- 数据解析过慢,主要原因是数据量太大
- 设备时间错误引发访问失败,一些设备的时间不准确或者无法矫正。
HTTP 优化
一般来说,我们常用的网络框架就是Okhttp了,我们以Okhttp优化为例。后续如果有新轮子,那么肯定要超过Okhttp才行,显然,很多部分必然是共性的,因此这部分也适合其他网络框架。
- 接入HTTPDNS,能接入就接入,尤其是使用CDN的情况。
- 压缩数据请求头,比如静态资源访问时可以不用带Cookie,动态资源访问时可以不带Cache-Control等,同时Refer、User-Agent可以更短一些 (主要是移动端不需要那么全的数据量)
- 压缩数据:压缩数据服务器端和客户端均可以做到,可以选用的算法有Gzip、Deflater。当然,数据本身也可以压缩,比如图片压缩或者转码为webp。
- 使用HTTP2 协议,HTTP2会将请求头字段进行压缩,同时也支持多路复用,配合HTTP链接池可以实现更高吞吐量的网络请求。
- 使用Cache-Control:这点对于静态资源收益明显,当然,有些动态资源的接口数据如果不是要求实时的,也是可以使用的,不过,具体控制由服务器端实现,客户端做相应的存储和读取即可。
- 使用断点续传,断点续传在请求数据量大的资源时非常有用,但是有个盲区一定要处理,那就是请求头最好带上Last-Modified,因为一些情况下,资源更新了,但是资源的链接并没有变化,导致下载的资源是不同资源拼接的错误文件。
- 分片上传:有一些情况,需要上传较大的数据,这个时候可以将数据进行分片,服务端可以使用 RandomAccessFile对文件进行拼接,当然,id和分片id需要做一些逻辑上的控制。
- 无网优化:检测无网络之后,直接加载本地缓存资源即可,如果没有本地缓存资源,那直接不要请求。
- 弱网优化:解决弱网没有很好的办法,超时逻辑和重试逻辑当然是必须的,同时数据压缩、DNS优化也是有效的。当然,还有些特殊的手段优化,比如播放器自动降码流的逻辑,使用本地数据占位等也是可以的。
- 设备时间问题:一些业务依赖设备时间,但是设备时间容易被修改,一个较好的方法是记录第一次返回的请求结果中的时间为基准BaseTime,同时记录SystemClock#uptimeMillis的时间startSystemTime,那么当前时间为
currentTime = BaseTime + (uptimeMillis() - startSystemTime)
- 避免并发请求,一定程度上,并发占用网络链路和系统资源,但是如果存在并发业务,建议从后端合并。
- 避免单一业务多次串行请求,如果出现一个请求依赖前面好几个请求,那么还是建议后端进行合并。
- 下发资源长度,一些情况下,资源长度是未明确的chunked编码,导致解析数据相对复杂一些。
- 使用连接池,网络连接池可以有效提升网络访问效率。但是在一些业务开发中,我们会经常遇到多次创建OkhttpClient的情况,理论上,OkhttpClient应该避免创建多次,这样才能尽可能利用连接池。
- 使用Tls1.3协议,此版本的协议不仅安全性提高,连接效率(消息合并、握手次数减少)上也有长足的进步《Tls v1.3 与Tls v1.2区别》
WebView部分
WebView相关问题
- WebView内存占用过高:实际上WebView的问题一直很多,第一个原因是占内存过多,在Android 5.0之后,WebView的渲染在isloatedProcess进程中进行,但是网资源依然在用户app进程中,因此内存问题一直很多。
- WebView 内存泄露:在低版本系统中,在WebView中存在很多引用,因为很多例如事件都需要映射到Html中,因此很容易内存泄露。
- 页面加载缓慢:这类问题的主要原因是DNS、资源的加载相对难以优化
- WebView黑屏或者白屏:一般来说可能存在js报错了、ssl证书报错、渲染性能较差。
优化
- WebView多进程:防止内存占用过高,这种情况下使用多进程是一种选择
- WebView泄露优化:使用MutableContextWrapper去创建WebView,在结束使用WebView时,主动解除View树引用、WebViewProvider引用等,同时调用WebView#destroyed方法
- 预加载:页面加载缓慢有很多优化方法,比如预加载WebViewProvider
-使用本地资源代替网络资源、接管ajax请求等,此类方法
- 创建WebView缓存池:如果WebView使用很频繁,可以使用WebView缓存池回收,但不要调用destroyed
- 黑屏和白屏优化:可以先排除js逻辑异常,tls证书错误,不支持支持跨域引发的黑白屏。
当然,有些WebView 渲染不稳定也会黑屏或者白屏,如果存在这种情况,必要时开启绘制缓冲
1 | java复制代码mWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { |
- WebView#reload白屏问题:这类问题主要原因是使用loadUrl(url,Map)去加载资源,这个方法有个弊端,就是reload时Map参数会一直保持存在,比如一些开发者会将未登录的token传入,但是在html中登录之后reload,就会刷新到未登录状态。
- WebView url#hash问题:实际上Html5有比较规范的pushState机制,但是一些开发喜欢使用url#hash方式,这就会和WebView的HistoryBack造成一定的冲突,有可能引发白屏。
- 首次加载,长时间黑屏或者白屏:主要原因是加载太慢,那么肯定需要优化加载逻辑。当然解决这种问题我们也要做好兜底,兜底逻辑是,把WebView的透明度设置为0或者View设置为INVISBLE即可(不会影响WebView的渲染,这种状态WebView依然会正常工作),同时展示loading,加载完成再展示WebView。
- 缓存预加载:对于WebViewClient,我们可以通过shouldInterceptRequest对接Okhttp和Glide拦截Javascript和图片请求,将提前下载的资源注入进去,当然原理还是利用Cache-Control、Expire等响应头、资源链接等。
体系统一
实际上,我们遇到最大的问题是,网络框架无法统一、Cookie无法统一、监控无法统一。相信很多开发者也考虑过这些问题,然而,现实是目前依旧很难。
问题
- Traffic监控不准确:受限于TrafficStats无法跟踪具体的Socket或者FD,因此对App 内部代理也加入了统计,有时会使得统计结果偏大。
- 网络是否可用检测不准确:理论上这种工具实际上很成熟了,但是在高版本之后检测方法变更之后,网上很多检测方法只在无线设备上生效,如果是插网线或者无密码网络的设备,很多检测方法都不能用。
- 无法统计每个接口的数据量:目前来说,最大的问题其实WebView、Native和Android 4.4以前的MediaPlayer无法监控,当然,如果走SocksV5 Proxy统一网关也是可以的,不过这种实时成本还是比较高的,一个重要的问题是,同一个进程中的数据要经过内核之后才能到Proxy Server上,显然性能一般般了。
- Cookie接口不统一:很多业务中,有的用token、有的用cookie,实际上增加了维护难度。
一些优化
统一网络框架
在Java中,我们可以利用java.net.URL来接管Http(s)UrlConnection,当然,github-api开源项目自己实现的,具体参考 《ObsoleteUrlFactory》,当然,ObsoleteUrlFactory也修复了一些问题,不过两者总体上差距不大。
1 | java复制代码try { |
不过缺陷是有的
一个问题是WebView除了ajax 中的GET/OPTIONS/HEAD 方法,其他请求无法被接管,还有Webview native部分无法被接管,Webview Native部分使用的是webkit内核中网络库,很难去接管。为什么google不统一呢?
主要原因是WebView内核中的网络框架也很不错,比okhttp的历史还久远一些 《webview 底层网络库》。当然,另一个原因是,webview的渲染是在webview.apk代码进行的,需要hook classloader ,难度也有些大。总之,这部分一言难尽。
Android 4.4之前的MediaPlayer是无法被接管的,Android 5.0 之后google将MediaPlayer的网络请求交给了java层的MediaHttpConnection,因此,使得我们有很大的发挥空间。不过,由于国内的厂商不按规范,在线上也会看到小部分厂商魔改MediaPlayer,导致高版本是不走MediaHTTPConnection,因此,你不得不保留网络代理。目前来说,这种情况也出现在一些主流厂商设置中,比如oppo的一些设备就有这种问题。类似《AndroidVideoCache 》流媒体缓存服务器,本已处在死亡的边缘,然而又被国内厂商拉了回来,说来也挺搞笑的。
Native网络部分无法接管:其实可以忽略,因为大部分app基本都是java 层实现网络请求。不过,建议按照google的做法,实现类似android.media.MediaHTTPConnection一样的方式,这样就能通过java.net.URL实现统一了。
统一Cookie存储
这里的统一并不是接口统一,而是存储统一。
在过往的项目中,做过混合开发的开发者可能深有体会,token和cookie老打架,前面我们提到过,token在loadUrl(url,map)后调用reload,刷新到服务器的是旧的token。因此,开发过程中,如果能使用Cookie就不要使用token,就算我们不开发WebView,那么也建议使用Cookie,毕竟统一网络框架之后,实际使用Cookie会更简单。
另外我们知道,android.webkit.CookieManager能存能取,因此,我们使用CookieManager作为底层存储接口,在Okhttp CookieJar中对接CookieManager是一种不错的选择。
1 | java复制代码public class CookieJarImpl implements CookieJar { |
为什么这样做呢,主要是android.webkit.CookieManager会把Cookie存入数据库,相比磁盘i/O,其管理方式更加通用。
统一Cookie多进程问题
有同学问到,WebView是单独进程的,这种Cookie如何存储呢,如果是这种情况,多进程访问数据库其实稳定性很差的,因此,建议在webView所在的进程中暴露一个AIDL接口,或者直接使用Messenger简单封装一下就行。
统一连接池与线程池
开发中我们会遇到,过多的创建OkhttpClient的现象,有的是模块之间无依赖,有的是需求特殊,实际上这种情况并不是不合理,比如有些OkhttpClient本身就要求5秒的超时,有的要30s,理论上加拦截器是可以调整,但是你得维护url map之类的机制。
实际上,我们可以将ConnectionPool作为单例,传入到每个OkHttpClient.Builder中。
ConnectionPool pool = OkhttpManager.get().getConnectionPool();
Dispatcher dispatcher = OkhttpManager.get().getDispatcher();
1 | java复制代码OkHttpClient.Builder feedbackClientBuilder = new OkHttpClient.Builder() |
当然,Cache-Control也可做类似的操作。
接口访问统计
我们统一网络框架之后,就能监控每个请求的数量,这点我们就不深入了。
1 | java复制代码public class HttpEventListenerFactory implements EventListener.Factory { |
其他监控手段
SocketFactory + Socket Wrapper 监控方式
可选择性监控每个Socket,普通的Socket通过SocketFactory创建,至于SSLSocket有些特殊,有的是SocketFactory,有的是通过SPI方式引入,因此,在SSLTrustManager中可以选择Socket Wrapper。
1 | java复制代码public class TrafficStatsSocketFactory extends SocketFactory { |
实际上,SSLSocket也是可以被wrap
1 | ini复制代码SSLTrafficSocket.wrap(sslSocket); |
SocketImpFactory 监控方式
这种比较全面,确定很难识别具体哪个Socket
1 | java复制代码public static synchronized void setSocketImplFactory(SocketImplFactory fac) |
Hook BlockGuardOs 监控方式
这种做法比较方便,能根据adress地址和fd确定Socket,总体上还不错,缺陷是Android 4.4 之前的版本不支持。
当然,你可能会说为啥不用动态代理实现,其实这个问题的难点是,Linux C Posix 函数的返回值意义比较特殊,有的时候0表示成功,有的时候1是成功,因此,另外类型返回值类型确定也比较麻烦,同时不同的系统对异常处理也有差异,用过之后发现稳定性不足,最终还是通过CompileOnly方式进行了继承式替换。
1 | java复制代码public class AppBlockGuardOs extends BlockGuardOs { |
接入方法,接入之后,我们便能监控到java层的UDP、TCP、DNS等,不仅如此,我们还可以监控完全在java层打开的各种fd,如socket fd,file fd等。
1 | java复制代码try { |
比如DNS拦截,我们就可以进行任意方式的解析,下面实现域名指定
1 | java复制代码 Map<String,String> dnsMap = new HashMap<>(); |
解析结果
1 | java复制代码D/DnsManager: == www.baidu.com/127.0.0.1 |
实现Socket FD 监控
1 | java复制代码@Override |
数据包监控
开发中,抓包也是必用的技能,当然,此类工具很多。当然还有facebook 的stetho框架,直接能实现在chrome:inspect中观察网络请求,免去了一些不必要的证书安装和代理设置,当然,android studio也带有相关能力。
不过,我们既然能Hook BlockGuardOs,也能hook native层接口,从最底层抓包理论上也不会有什么问题。
另类方案
我们可以看到,本篇实际上还是围绕java层展开,显然WebView、Native其实仍然没有覆盖到,当然,最多也只是native hook住一些接口,但是如何将所有的网络请求统一为一种非常困难。
不过我们知道,一种比较高级的网络代理协议SocksV5 是可以收敛所有网络请求,包括TCP和UDP,当然,代价是和HTTP网络代理一样,需要在内核跑一圈,同一个进程中的数据从内核绕一圈,本身就很奇怪。
目前,在Android AOSP源码中,google对增加了基于chormium的cronet网络库,可以支持QUIC(HTTP 3.0)协议,并且是java实现的,看样子http部分有统一的趋势,不过如何和chormium内核互通,目前没有具体找到相关细节,另外,就算以后实现了 webkit http 3.0 直接走java 层,但是能到哪个地步仍然不好说。
流量优化
流量优化的核心点主要是从三个方面进行
- 资源压缩: 资源要尽可能压缩,必要时进行有损压缩,这样可以减少cpu中断次数以及i/o时长。
- 资源缓存: 如果资源是不需要更新的,就不要去更新,当然,这个得利用Cache-Control、Last-Modify等
- P2P机制: 一些情况下,为了避免CDN的使用,可以在客户端创建p2p服务,以此减少CDN访问。
总结
好了,本篇就到这里,本篇其实写出来的早,就是不知道标题该用什么,反反复复改了好多次标题之后才发出来,这是题外话,我们还是回到本篇这里,做个总结。
其实网络这部分涉及的层面很多,相比网络监控,网络框架无法统一的问题不仅仅http协议如此,其他协议也是如此,我们能做好的也就是java层了,不过话说回来,大部分app也就HTTP相关的交互,因此,本篇的一些技巧理论上是适应大部分app了。
至于webkit、ffmpeg以及被魔改MediaPlayer,目前来说统一网络框架任然遥遥无期,希望系统厂商也能注意到这种问题,同时我们期待google在webkit 的网络请求部分也有所突破。
本文转载自: 掘金