Android OkHttp使用过程中的一些经验总结

2024-03-31  本文已影响0人  头秃到底

前言

在主流操作系统,网络一直是一个核心的模块,我们常用的通信软件、娱乐软件、视频软件都需要通过网络来传输信息,而我们传输各种信息就会使用到各种网络协议,如icmp、dns、http、tcp、udp等协议都已经是网络主机的标准配置。

实际上,现如今很多app都是使用Okhttp,网上也有很多相关的优化方法,本篇对其中Android中常用的部分进行汇总一下,同时也会提出一些新的思路,我们主要围绕DNS、HTTP、WebView三部分来汇总。

DNS部分

dns相关问题

在正常情况下,其实很难遇到DNS问题,但是在使用的过程中,特别是使用CDN的时候,这个问题就会更加明显,主要问题有以下几个:

常见的DNS优化方法

基于上述情况,我们也有很多应对措施

HTTP 部分

HTTP相关问题

HTTP 优化

一般来说,我们常用的网络框架就是Okhttp了,我们以Okhttp优化为例。当然,后续如果有新轮子,那么肯定要超过Okhttp才行,显然,很多部分必然是共性的,因此这部分也适合其他网络框架。

WebView部分

WebView相关问题

优化

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) {

    }
});

体系统一

实际上,我们遇到最大的问题是,网络框架无法统一、Cookie无法统一、监控无法统一。相信很多开发者也考虑过这些问题,然而,现实是目前依旧很难。

问题

一些优化

统一网络框架

在Java中,我们可以利用java.net.URL来接管Http(s)UrlConnection。

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

不过缺陷是有的

一个问题是WebView除了ajax (get、head)部分,其他请求无法被接管,还有native部分无法被接管,不过native部分大部分app可以忽略不计了。主要原因是WebView内核中的网络框架也很不错,比okhttp的历史还久远一些

Android 4.4之前的MediaPlayer是无法被接管的,Android 5.0 之后google将MediaPlayer的网络请求交给了java层的MediaHttpConnection,因此,使得我们有很大的发挥空间。不过,由于国内的厂商不按规范,在线上也会看到小部分厂商魔改MediaPlayer,导致高版本是不走MediaHTTPConnection的,因此,你不得不保留网络代理。目前来说,这种情况不是很乐观,因为oppo的一些设备就有这种问题。处在死亡边缘的类似流媒体缓存服务器,本应被淘汰,然而又被国内厂商拉了回来,说来也挺搞笑的。

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 的网络请求部分也有所突破。

上一篇 下一篇

猜你喜欢

热点阅读