面试:Android网络优化

优化点1、DNS优化,使用HttpDNS

由于进行网络请求,可能会遇到DNS被劫持和DNS解析缓慢,造成网络请求过于耗时。 这里就可以采用HTTPDNS来绕过运营商服务器解析过程,从而降低平均访问时长,提高连接率,进而提高网络访问的速度。 这里的HTTPDNS 不是使用传统的DNS协议向服务器的53端口发送请求,而是使用Http协议向服务器的80端口发送请求。

DNS解析的失败率占联网失败中很大一种,而且首次域名解析一般需要几百毫秒。针对此,我们可以不用域名,才用IP直连省去 DNS 解析过程,节省这部分时间。

另外熟悉阿里云的小伙伴肯定知道HttpDns:HttpDNS基于Http协议的域名解析,替代了基于DNS协议向运营商Local DNS发起解析请求的传统方式,可以避免Local DNS造成的域名劫持和跨网访问问题,解决域名解析异常带来的困扰。

Android 网络优化,使用 HTTPDNS 优化 DNS,从原理到 OkHttp 集成_Android开发 - UCloud云社区

DNS的问题:

1. 不稳定

DNS 劫持或者故障,导致服务不可用。

2. 不准确

LocalDNS 调度,并不一定是就近原则,某些小运营商没有 DNS 服务器,直接调用其他运营商的 DNS 服务器,最终直接跨网传输。例如:用户侧是移动运营商,调度到了电信的 IP,造成访问慢,甚至访问受限等问题。

3. 不及时

运营商可能会修改 DNS 的 TTL(Time-To-Live,DNS 缓存时间),导致 DNS 的修改,延迟生效。还有运营商为了保证网内用户的访问质量,同时减少跨网结算,运营商会在网内搭建内容缓存服务器,通过把域名强行指向内容缓存服务器的地址,来实现本地本网流量完全留在本地的目的。

HTTPDNS 的解决方案

DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。

HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。

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

使用也非常的简单,在 OkHttp.build() 时,通过 dns() 方法配置。

mOkHttpClient = httpBuilder
        .dns(HttpDns())
        .build();

优化点2: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);
    }

优化点3:GZIP压缩

HTTP协议上的Gzip编码是一种用来改进WEB应用程序性能的技术,用来减少传输数据量大小,减少传输数据量大小有两个明显的好处:

  • 可以减少流量消耗;
  • 可以减少传输的时间。

优化点4:压缩请求头

请求头也占用一定的体积,在请求头不变的情况下,我们可以只传递一次,以后都只需要传递上一次请求头的 MD5 值,服务端做一个缓存,在需要请求头中的某些信息时,就可以直接从之前的缓存中取。

优化:5:图片处理

5.1 图片下载

  • 使用WebP格式;同样的照片,采用WebP格式可大幅节省流量,相对于JPG格式的图片,流量能节省将近 25% 到 35 %;相对于 PNG 格式的图片,流量可以节省将近80%。最重要的是使用WebP之后图片质量也没有改变。
  • 使用缩略图;App中需要加载的图片按需加载,列表中的图片根据需要的尺寸加载合适的缩略图即可,只有用户查看大图的时候才去加载原图。不仅节省流量,同时也能节省内存!之前使用某公司的图片存储服务在原图链接之后拼接宽高参数,根据参数的不同返回相应的图片。

5.2 图片上传

图片(文件)的上传失败率比较高,不仅仅因为大文件,同时带宽、时延、稳定性等因素在此场景下的影响也更加明显;

  • 避免整文件传输,采用分片传输;
  • 根据网络类型以及传输过程中的变化动态的修改分片大小;
  • 每个分片失败重传的机会。

优化点6:合并网络请求

合并网络请求,减少请求次数。对于一些接口类如统计,无需实时上报,将统计信息保存在本地,然后根据策略统一上传。这样头信息仅需上传一次,减少了流量也节省了资源。

优化点7:协议层的优化

使用最新的协议,Http协议有多个版本:0.9、1.0、1.1、2等。新版本的协议经过再次的优化,例如:

所以 Http 的不同版本对这点的优化也非常多,下面是 Http 协议不同版本之间的主要区别。

  • Http 1.0

    较老的版本,现在已经很少见到用这个版本的服务了,它最大的缺点就是 TCP 连接不复用,每个 TCP 连接只能发送 1 个请求,如果要请求别的资源,就必须重新建立一个连接。

    TCP 创建连接的成本非常高,需要三次握手,并且在开始阶段发送速度比较慢,也就是 Http 1.0 的性能非常差。

  • Http 1.1

    Http 1.1 的出现只比 1.0 晚了半年,它最大的变化就是引入了持久连接,从这个版本开始,是默认不关闭的,多个请求被复用,无需重建TCP连接,这样效率就有了很大的提升。

    它还是有些缺陷,就是它虽然允许 TCP 复用,但是同一个 TCP 连接里面的所有数据通讯必须按顺序来,也就是处理完一个请求后,再响应下一个请求。

    如果前面的网络请求比较慢,那后面的请求也只能等着。

  • Http 2.0

    Http 2.0 是一个二进制协议,它最大的改进就是客户端和服务端可以同时发送多个请求和响应,不需要像 Http 1.1 一样按顺序请求,是一个双向的实时通信。

    这点是 Http 2.0 相对于 Http 1.1 的最大的改进,大家以后要跟服务端配合时,有得选的前提下,尽可能选择高版本的 Http 协议。

  • Http1.1 版本引入了“持久连接”,多个请求被复用,无需重建TCP连接;
  • Http2 引入了“多工”、头信息压缩、服务器推送等特性。

其它优化点

  • 断点续传,文件、图片等的下载,采用断点续传,不浪费用户之前消耗过的流量;
  • 重试策略,一次网络请求的失败,需要多次的重试来断定最终的失败,可以参考Volley的重试机制实现。
  • Protocol Buffer Protocol Buffer是Google的一种数据交换的格式,它独立于语言,独立于平台。相较于目前常用的Json,数据量更小,意味着传输速度也更快。 具体的对比可以参见:《Protobuffer和json深度对比》。
  • 尽量避免客户端的轮询,而使用服务器推送的方式;
  • 数据更新采用增量,而不是全量,仅将变化的数据返回,客户端进行合并,减少流量消耗;

资本优化

最后一个优化方案就是砸钱,常见的手段有 CDN 加速、提高带宽、动静资源分离。

大家需要注意,使用 CDN 后,如果某个资源需要更新,更新完成后是需要清理缓存的,这些优化不涉及客户端,同时也不要忘了减少传输量,注意请求的时机和频率,这一条和我们前面讲到的流量优化相关。

参考:探索 Android 网络优化方法 - 简书

线上监控方案:

  • OkHttp 事件监听器
  • NetworkStatsManager
  • TrafficStats

方案一:OkHttp 事件监听器

Android 性能优化之网络优化 - 知乎

OkHttp 给我们留了一个事件监听器 EventListener 回调,我们可以自己实现这个监听器,监听每一次的请求。

对所有的网络请求,在本地都有一个完整的监控,每一个请求的 Request 和 Response 相关的所有信息都全部记录下来。

服务端可以下发指令控制客户端上传这些数据,客户端也可以在相关数据超过阈值后主动上报。

public class OkHttpEvent {
    public long dnsStartTime;
    public long dnsEndTime;
    public long responseBodySize;
    public boolean apiSuccess;
    public String errorReason;
}
ublic class OkHttpEventListener extends EventListener {

    public static final Factory FACTORY = new Factory() {
        @Override
        public EventListener create(Call call) {
            return new OkHttpEventListener();
        }
    };

    OkHttpEvent okHttpEvent;
    public OkHttpEventListener() {
        super();
        okHttpEvent = new OkHttpEvent();
    }

    @Override
    public void callStart(Call call) {
        super.callStart(call);
        Log.i("lz","callStart");
    }

    @Override
    public void dnsStart(Call call, String domainName) {
        super.dnsStart(call, domainName);
        okHttpEvent.dnsStartTime = System.currentTimeMillis();
    }

    @Override
    public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
        super.dnsEnd(call, domainName, inetAddressList);
        okHttpEvent.dnsEndTime = System.currentTimeMillis();
    }

    @Override
    public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
        super.connectStart(call, inetSocketAddress, proxy);
    }

    @Override
    public void secureConnectStart(Call call) {
        super.secureConnectStart(call);
    }

    @Override
    public void secureConnectEnd(Call call, @Nullable Handshake handshake) {
        super.secureConnectEnd(call, handshake);
    }

    @Override
    public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol) {
        super.connectEnd(call, inetSocketAddress, proxy, protocol);
    }

    @Override
    public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, @Nullable Protocol protocol, IOException ioe) {
        super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe);
    }

    @Override
    public void connectionAcquired(Call call, Connection connection) {
        super.connectionAcquired(call, connection);
    }

    @Override
    public void connectionReleased(Call call, Connection connection) {
        super.connectionReleased(call, connection);
    }

    @Override
    public void requestHeadersStart(Call call) {
        super.requestHeadersStart(call);
    }

    @Override
    public void requestHeadersEnd(Call call, Request request) {
        super.requestHeadersEnd(call, request);
    }

    @Override
    public void requestBodyStart(Call call) {
        super.requestBodyStart(call);
    }

    @Override
    public void requestBodyEnd(Call call, long byteCount) {
        super.requestBodyEnd(call, byteCount);
    }

    @Override
    public void responseHeadersStart(Call call) {
        super.responseHeadersStart(call);
    }

    @Override
    public void responseHeadersEnd(Call call, Response response) {
        super.responseHeadersEnd(call, response);
    }

    @Override
    public void responseBodyStart(Call call) {
        super.responseBodyStart(call);
    }

    @Override
    public void responseBodyEnd(Call call, long byteCount) {
        super.responseBodyEnd(call, byteCount);
        //传输的字节大小
        okHttpEvent.responseBodySize = byteCount;
    }

    @Override
    public void callEnd(Call call) {
        super.callEnd(call);
        okHttpEvent.apiSuccess = true;
    }

    @Override
    public void callFailed(Call call, IOException ioe) {
        Log.i("lz","callFailed ");
        super.callFailed(call, ioe);
        okHttpEvent.apiSuccess = false;
        okHttpEvent.errorReason = Log.getStackTraceString(ioe);
        Log.i("lz","reason "+okHttpEvent.errorReason);
    }
}

方案二:NetworkStatsManager

如果我们不使用 OkHttp 的 EventListener ,而是用了 NetworkStatsManager,那么当用户反馈说某个时间段内的流量消耗较多时,我们也可以在后台给客户端下发一个指令,让客户端上传特定时间段内的流量统计数据,然后结合用户使用时长判断 App 的流量消耗是否真的存在异常。

NetworkStatsManager 是 API 23 后的流量统计管理器,它可以获取某个时段的流量信息,也可以获取不同网络类型下的流量消耗,它最大的不足就是用户体验比较差,需要用户开启“查看使用情况”权限。

方案三:TrafficStats

TrafficStats 是 API 8 后提供的一种流量数据统计方案,它统计到的数据其实是我们手机上次重启后的流量消耗,也就是重启前的流量是统计不到的。

TrafficStats 常用方法:

  • getUidRxBytes(int uid)

    获取指定 Uid 的接收流量;

  • getTotalTxBytes()

    获取 App 总发送流量;

下面我们是 TrafficStats 的使用方式。

一般情况下不使用 TrafficStats,因为它存在下面两个问题:

  • 无法获取特定应用的流量消耗

    无法获取特定应用的流量消耗,只能给我们一个总的流量消耗值,这个值对于我们解决问题来说帮助不大。

  • 无法获取特定时间段的流量消耗

    比如线上某个用户反馈发现昨天 App 的流量消耗过多,这时我们是无法知道用户具体消耗了多少流量的。

Guess you like

Origin blog.csdn.net/cpcpcp123/article/details/121861908