前言

在干流操作体系,网络一向是一个核心的模块,咱们常用的通讯软件、娱乐软件、视频软件都需求经过网络来传输信息,而咱们传输各种信息就会运用到各种网络协议,如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没有回来成果就让事务等候,假如没有回来时先运用体系的解析办法。
  • 监听网络改变,及时改写DNS: 手机用户常常在不同基站之间穿梭,或许是wifi和gprs切换网络,因而,网络改变时及时改写DNS很有必要,可是这儿要记住,在拿到新地址之后更新旧地址比较稳当。
  • 预解析DNS: 许多网页中都有dns预解析的逻辑(prefetch),假如咱们app中存在多种域名,提早解析出来显着很有必要。
  • IP挑选:假如一个域名对应多个IP,这个时分有必要经过竞速去挑选。当然,这个是独自的逻辑,不要为了挑选IP,让事务等候。
  • 域名动态别离:静态资源独自存储在特定的服务器,由于这类归于I/O类,不会耗费太多cpu,而动态资源如事务相关的,需求许多核算,因而别离动态域名很有必要。其实CDN便是担任静态的资源存储,事务是担任动态资源,别离能够前进吞吐量。
  • 域名兼并:实际上域名数量越少越好,过多的域名解析时刻很长,因而,在动态别离的状况下,尽量削减域名数量。
  • 避免运用特别字符的域名:比方下划线、其他国家的文字等,尽量以ascii文本为主,特别字符和特别文字要避免。
  • 避免运用灵敏国家或区域的域名。
  • 避免运用在特定国家或区域无法拜访的域名。

HTTP 部分

HTTP相关问题

  • 弱网问题:在任何网络恳求中,弱网影响很大,其实弱网不仅仅影响HTTP,比方DNS、ICMP都会收到影响,可是网络的快慢也很难经过检测手段去优化,其间一个原因是一些状况下网络具有波动性,或许时好时坏。其次Traffics计算也存在误差。别的,这儿简单出现的反常是Connection Timeout和Read Timeout相关。
  • 无网问题:无网络引发的问题是直接、清晰的,这个时分建议网络拜访必定失利,出现的过错Socket相关的反常。
  • UnknownHostException:引发此类问题首要是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逻辑,证书问题,其次,必要时开启制作缓冲
mWebView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
    @Override
    public void onViewAttachedToWindow(View v) {
        mWebView.removeOnAttachStateChangeListener(this);
        if(v.isHardwareAccelerated()) {
            mWebView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
        }else{
            mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
        }
    }
    @Override
    public void onViewDetachedFromWindow(View v) {
    }
});
  • WebView#reload白屏问题:这类问题首要原因是运用loadUrl(url,Map)去加载资源,这个办法有个坏处,便是reload时Map参数会一向保持存在,比方一些开发者会将未登录的token传入,可是在html中登录之后reload,就会改写到未登录状态。
  • WebView url#hash问题:实际上Html5有比较标准的pushState机制,可是一些开发喜爱运用url#hash办法,这就会和WebView的HistoryBack形成必定的冲突,有或许引发白屏。
  • 加载时长时刻黑屏或许白屏:首要原因是加载太慢,实际上,处理这种问题主需求把WebView的透明度设置为0或许View设置为INVISBLE即可(不会影响WebView的渲染),一起展示loading,加载完成再展示WebView。

体系一致

实际上,咱们遇到最大的问题是,网络结构无法一致、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官方也有 《ObsoleteUrlFactory》来修复了一些问题。

try {
    OkUrlFactory okUrlFactory = new OkUrlFactory(client);
    URL.setURLStreamHandlerFactory(okUrlFactory);
} catch(Exception e) {
}

不过缺点是有的

一个问题是WebView除了ajax (get、head)部分,其他恳求无法被接管,还有native部分无法被接管,不过native部分大部分app能够忽略不计了。首要原因是WebView内核中的网络结构也很不错,比okhttp的前史还长远一些 《webview 底层网络库

Android 4.4之前的MediaPlayer是无法被接管的,Android 5.0 之后google将MediaPlayer的网络恳求交给了java层的MediaHttpConnection,因而,使得咱们有很大的发挥空间。不过,由于国内的厂商不按标准,在线上也会看到小部分厂商魔改MediaPlayer,导致高版本是不走MediaHTTPConnection的,因而,你不得不保留网络署理。现在来说,这种状况不是很乐观,由于oppo的一些设备就有这种问题。处在逝世边缘的相似《AndroidVideoCache 》流媒体缓存服务器,本应被筛选,然而又被国内厂商拉了回来,说来也挺搞笑的。

native无法接管:其实能够忽略,不过,建议依照android的标准,完成android.media.MediaHTTPConnection。

一致Cookie存储

这儿的一致并不是接口一致,而是存储一致。

在过往的项目中,做过混合开发的开发者或许深有体会,token和cookie老打架,前面咱们提到过,token在loadUrl(url,map)后调用reload,改写到服务器的是旧的token。因而,开发过程中,假如能运用Cookie就不要运用token,就算咱们不开发WebView,那么也建议运用Cookie,究竟一致网络结构之后,实际运用Cookie会更简单。

别的咱们知道,android.webkit.CookieManager能读能取,因而,咱们运用CookieManager作为底层存储接口,在Okhttp CookieJar中对接CookieManager是一种不错的挑选。

public class CookieJarImpl implements CookieJar {
    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        synchronized (cookiesMap) {
            log("call saveCookies start "+url);
            try {
                saveCookiesToWebKit(host,url.toString(),cookies);
            } catch (Throwable throwable) {
                log("save CookieToWebKit error "+throwable);
            }
            log("call saveCookies finish "+url);
        }
    }
    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        log("call loadCookies start "+url);
        synchronized (cookiesMap) {
            String host = url.host();
            try{
                return readCookiesFromeWebKit(host);
            } catch (Throwable throwable) {
                log("read CookieToWebKit error "+throwable);
            }
            return Collections.EMPTY_LIST;
        }
      }
}

一致链接池与线程池

开发中咱们会以到,过多的创立OkhttpClient的现象,有的是模块之间无依赖,有的是需求特别,实际上这种状况并不是不合理,比方有些OkhttpClient自身就要求5秒的超时,有的要30s,理论上加阻拦器是能够调整,可是你得保护url map之类的机制。

实际上,咱们能够将ConnectionPool作为单例,传入到每个OkHttpClient.Builder中。

ConnectionPool pool = OkhttpManager.get().getConnectionPool();
Dispatcher dispatcher = OkhttpManager.get().getDispatcher();

OkHttpClient.Builder builder1 = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.connectionPool(pool);
.....
OkHttpClient.Builder builder2 = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.connectionPool(pool);

当然,Cache-Control也可做相似的操作。

接口拜访计算

咱们一致网络结构之后,就能监控每个恳求的数量,这点咱们就不深入了。

public class HttpEventListenerFactory implements EventListener.Factory {
    static volatile HttpEventListenerFactory eventListenerFactory = null;
    private volatile EventListener mHttpEventListener;
    public static HttpEventListenerFactory getFactory() {
        if (eventListenerFactory == null) {
            synchronized (HttpEventListenerFactory.class) {
                if (eventListenerFactory == null) {
                    eventListenerFactory = new HttpEventListenerFactory();
                }
            }
        }
        return eventListenerFactory;
    }
    @Override
    public EventListener create(Call call) {
        if (mHttpEventListener == null) {
            synchronized (HttpEventListenerFactory.class) {
                if (mHttpEventListener == null) {
                    mHttpEventListener = new HttpEventListener();
                }
            }
        }
        return mHttpEventListener;
    }
}

其他监控手段

SocketFactory + Socket Wrapper 监控办法

可挑选性监控每个Socket,一般的Socket经过SocketFactory创立,至于SSLSocket有些特别,有的是SocketFactory,有的是经过SPI办法引进,因而,在SSLTrustManager中能够挑选Socket Wrapper。

public class TrafficStatsSocketFactory extends SocketFactory {
    SocketFactory defaultSocketFactory;
    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        Socket socket = null;
        if (defaultSocketFactory == null) {
            socket = new Socket(host, port);
        }else {
            socket = defaultSocketFactory.createSocket(host, port);
        }
        return TrafficStatsSocket.wrap(socket);
    }
    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        Socket socket = null;
        if (defaultSocketFactory == null) {
            socket =  new Socket(host, port, localHost, localPort);
        }else {
            socket = defaultSocketFactory.createSocket(host, port, localHost, localPort);
        }
        return TrafficStatsSocket.wrap(socket);
    }
    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        Socket socket = null;
        if (defaultSocketFactory == null) {
            socket =  new Socket(host, port);
        }else{
            socket = defaultSocketFactory.createSocket(host, port);
        }
        return TrafficStatsSocket.wrap(socket);
    }
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        Socket socket = null;
        if (defaultSocketFactory == null) {
            socket =  new Socket(address, port, localAddress, localPort);
        }else {
            socket = defaultSocketFactory.createSocket(address, port, localAddress, localPort);
        }
        return TrafficStatsSocket.wrap(socket);
    }
}

实际上,SSLSocket也是能够被wrap

SSLTrafficSocket.wrap(sslSocket);

SocketImpFactory 监控办法

这种比较全面,确认很难识别详细哪个Socket

public static synchronized void setSocketImplFactory(SocketImplFactory fac)
    throws IOException
{
    if (factory != null) {
        throw new SocketException("factory already defined");
    }
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkSetFactory();
    }
    factory = fac;
}

Hook BlockGuardOs 监控办法

比较便利,能根据adress地址和fd确认Socket,总体上还不错,缺点是Android 4.4 之前的版本不支持。当然,你或许会说为啥不用动态署理完成,其实这个问题的难点是,Linux C Posix 函数的回来值他自身的含义,0、1,-1你无法确认哪个是正常值,一起不同的体系对反常处理也有差异,用过之后发现稳定性不足,最终仍是经过CompileOnly办法进行了继承式替换。

public class AppBlockGuardOs extends BlockGuardOs {
    private  Os os;
    public AppBlockGuardOs(Os os, Os rawOs) {
        super(rawOs); //有必要,Linux类
        this.os = os; //有必要,体系封装的BlockGaurdOs的子类
    }
    @Override
    public int pread(FileDescriptor fd, ByteBuffer buffer, long offset) throws ErrnoException, InterruptedIOException {
        return this.os.pread(fd, buffer, offset);
    }
    @Override
    public int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount, long offset) throws ErrnoException, InterruptedIOException {
        return this.os.pread(fd, bytes, byteOffset, byteCount, offset);
    }
  //省略一些代码,太多放不下
}

接入办法,接入之后,咱们便能监控到java层的UDP、TCP、DNS等,不仅如此,咱们还能够监控完全在java层翻开的各种fd,如socket fd,file fd等。

try {
    Class<?> kClassLibcore = Class.forName("libcore.io.Libcore");
    Field[] fields = kClassLibcore.getDeclaredFields();
    if (fields == null) {
        return;
    }
    Object os = getOs(kClassLibcore);
    Object rawOs = getRawOs(kClassLibcore);
    if (os == null) {
        return;
    }
    setOs(kClassLibcore, os,rawOs);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

数据包监控

开发中,抓包也是必用的技术,当然,此类工具许多。当然还有facebook 的stetho结构,直接能完成在chrome:inspect中观察网络恳求,免去了一些不必要的证书装置和署理设置,当然,android studio也代有相关才能。

不过,咱们既然能Hook BlockGuardOs,也能hook native层接口,从最底层抓包理论上也不会有什么问题。

另类计划

咱们能够看到,本篇实际上仍是围绕java层打开,显着WebView、Native其实依然没有掩盖到,当然,最多也仅仅native hook住一些接口,可是怎么将一切的网络恳求一致为一种非常困难。

不过咱们知道,一种比较高档的网络署理协议SocksV5 是能够收敛一切网络恳求,包括TCP和UDP,当然,价值是和HTTP网络署理一样,需求在内核跑一圈,同一个进程中的数据从内核绕一圈,自身就很古怪。

现在,在Android AOSP源码中,google对webkit部分增加了http 3.0相关逻辑,并且是java完成的,看样子HTTP 3.0 部分有一致的趋势,不过怎么和chrome内核互通,现在没有详细找到相关细节。别的,就算以后完成了 webkit http 3.0 直接走java 层,可是能到哪个境地依然不好说。

总结

好了,本篇就到这儿,本篇其实写出来的早,便是不知道标题该用什么,反反复复改了好屡次标题之后才发出来,这是题外话,咱们仍是回到本篇这儿,做个总结。

其实网络这部分触及的层面许多,比较网络监控,网络结构无法一致的问题不仅仅http协议如此,其他协议也是如此,咱们能做好的也便是java层了,不过话说回来,大部分app也就HTTP相关的交互,因而,本篇的一些技巧理论上是习惯大部分app了。

至于webkit、ffmpeg以及被魔改MediaPlayer,现在来说一致网络结构任然遥遥无期,希望体系厂商也能注意到这种问题,一起咱们等待google在webkit 的网络恳求部分也有所突破。