Android开发Android开发经验谈Android技术知识

Android性能优化:网络优化(三、网络优化篇)下

2020-12-28  本文已影响0人  下饭小当家

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

五、网络请求质量优化(🔥)

1、Http 请求过程

2、HTTPDNS

问题:DNS 解析慢/被劫持?

使用 HTTPDSN,HTTPDNS 不是使用 DNS 协议,向 DNS 服务器传统的 53 端口发送请求,而是使用 HTTP 协议向 DSN 服务器的 80 端口发送请求。

1)、HTTPDNS 优势

2)、HTTPDNS + OKHttp 实践

在 Awesome-WanAndroid 中已经实现了 HTTPDNS 优化,其优化代码如下所示:

// HttpModule-provideClient:httpDns 优化
builder.dns(OkHttpDns.getIns(WanAndroidApp.getAppComponent().getContext()));

/**
 * FileName: OkHttpDNS
 * Date: 2020/5/8 16:08
 * Description: HttpDns 优化
 * @author JsonChao
 */
public class OkHttpDns implements Dns {

    private HttpDnsService dnsService;
    private static OkHttpDns instance = null;

    private OkHttpDns(Context context) {
        dnsService = HttpDns.getService(context, "161133");
        // 1、设置预解析的 IP 使用 Https 请求。
        dnsService.setHTTPSRequestEnabled(true);
        // 2、预先注册要使用到的域名,以便 SDK 提前解析,减少后续解析域名时请求的时延。
        ArrayList<String> hostList = new ArrayList<>(Arrays.asList("www.wanandroid.com"));
        dnsService.setPreResolveHosts(hostList);
    }

    public static OkHttpDns getIns(Context context) {
        if (instance == null) {
            synchronized (OkHttpDns.class) {
                if (instance == null) {
                    instance = new OkHttpDns(context);
                }
            }
        }
        return instance;
    }

    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        String ip = dnsService.getIpByHostAsync(hostname);
        LogHelper.i("httpDns: " + ip);
        if(ip != null){
            List<InetAddress> inetAddresses = Arrays.asList(InetAddress.getAllByName(ip));
            return inetAddresses;
        }
        // 3、如果从阿里云 DNS 服务器获取不到 ip 地址,则走运营商域名解析的过程。
        return Dns.SYSTEM.lookup(hostname);
    }
}
复制代码

重新安装 App,通过 HTTPDNS 获取到 IP 地址 log 如下所示:

2020-05-11 10:41:55.139 4036-4184/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpDns.java | 52 | lookup] httpDns: 47.104.74.169
2020-05-11 10:41:55.142 4036-4185/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpDns.java | 52 | lookup] httpDns: 47.104.74.169
复制代码

3、网络库的连接管理

利用 HTTP 协议的 keep-alive,建立连接后,会先将连接放入连接池中,如果有另一个请求的域名和端口是一样的,就直接使用连接池中对应的连接发送和接收数据。在实现网络库的连接管理时需要注意以下4点:

4、协议版本升级

HTTP 1.0

TCP 连接不复用,也就是每发起一个网络请求都要重新建立连接,而刚开始连接都会经历一个慢启动的过程,可谓是慢上加慢,因此 HTTP 1.0 性能非常差。

HTTP 1.1

引入了持久连接,即 TCP 连接可以复用,但数据通信必须按次序来,也就是后面的请求必须等前面的请求完成才能进行。当所有请求都集中在一条连接中时,在网络拥塞时容易出现 TCP 队首阻塞问题。

HTTP 2

QUIC

Google 2013 实现,2018 基于 QUIC 协议的 HTTP 被确认为 HTTP3。

QUIC 简单理解为 HTTP/2.0 + TLS 1.3 + UDP。弱网环境下表现好与 TCP。

优势

目前的缺点

使用场景

QUIC 加密协议原理

5、网络请求质量监控

1)、接口请求耗时、成功率、错误码

在 Awesome-WanAndroid 中已经使用 OkHttpEventListener 实现了网络请求的质量监控,其代码如下所示:

// 网络请求质量监控
builder.eventListenerFactory(OkHttpEventListener.FACTORY);

/**
 * FileName: OkHttpEventListener
 * Date: 2020/5/8 16:28
 * Description: OkHttp 网络请求质量监控
 * @author quchao
 */
public 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);
        LogHelper.i("okHttp Call Start");
        okHttpEvent.callStartTime = System.currentTimeMillis();
    }

    /**
     * DNS 解析开始
     *
     * @param call
     * @param domainName
     */
    @Override
    public void dnsStart(Call call, String domainName) {
        super.dnsStart(call, domainName);
        okHttpEvent.dnsStartTime = System.currentTimeMillis();
    }

    /**
     * DNS 解析结束
     *
     * @param call
     * @param domainName
     * @param inetAddressList
     */
    @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);
        okHttpEvent.connectStartTime = System.currentTimeMillis();
    }

    @Override
    public void secureConnectStart(Call call) {
        super.secureConnectStart(call);
        okHttpEvent.secureConnectStart = System.currentTimeMillis();
    }

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

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

    @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.callEndTime = System.currentTimeMillis();
        // 记录 API 请求成功
        okHttpEvent.apiSuccess = true;
        LogHelper.i(okHttpEvent.toString());
    }

    @Override
    public void callFailed(Call call, IOException ioe) {
        LogHelper.i("callFailed ");
        super.callFailed(call, ioe);
        // 记录 API 请求失败及原因
        okHttpEvent.apiSuccess = false;
        okHttpEvent.errorReason = Log.getStackTraceString(ioe);
        LogHelper.i("reason " + okHttpEvent.errorReason);
        LogHelper.i(okHttpEvent.toString());
    }
}
复制代码

成功 log 如下所示:

2020-05-11 11:00:42.678 6682-6847/json.chao.com.wanandroid D/OkHttp: --> GET https://www.wanandroid.com/banner/json
2020-05-11 11:00:42.687 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ Thread: RxCachedThreadScheduler-3
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ OkHttpEventListener.callStart  (OkHttpEventListener.java:46)
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpEventListener.java | 46 | callStart] okHttp Call Start
2020-05-11 11:00:42.688 6682-6848/json.chao.com.wanandroid I/WanAndroid-LOG: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2020-05-11 11:00:43.485 6682-6847/json.chao.com.wanandroid D/OkHttp: <-- 200 OK https://www.wanandroid.com/banner/json (806ms, unknown-length body)
2020-05-11 11:00:43.496 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ Thread: RxCachedThreadScheduler-2
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ OkHttpEventListener.callEnd  (OkHttpEventListener.java:162)
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │    LogHelper.i  (LogHelper.java:37)
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ [OkHttpEventListener.java | 162 | callEnd] NetData: [
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ callTime: 817
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ dnsParseTime: 6
2020-05-11 11:00:43.498 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ connectTime: 721
2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ secureConnectTime: 269
2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ responseBodySize: 975
2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ apiSuccess: true
2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: │ ]
2020-05-11 11:00:43.499 6682-6847/json.chao.com.wanandroid I/WanAndroid-LOG: └────────────────────────────────────────────────────────────────────────────────────────────────────────────────
复制代码

2)、根据网络质量来动态设定网络服务的重要参数(超时、并发线程数)

6、压缩

1)、header(HTTP 2.0 头部压缩)

深入探索 Android 网络优化(二、网络优化基础篇)下 - 首部压缩

2)、URL

不变参数客户端只需上传以此,其它参数均在接入层进行扩展。

3)、body

使用 Protocol Buffers 替代 JSON 序列化。

4)、图片

5)、压缩算法

7、加密

HTTPS 通常需要多消耗 2 RTT 的协商时延。

1)、HTTPS 优化

1、提高连接复用率

2、减少握手次数(TLS 1.3 实现 0 RTT 协商)

TLS 1.2 引入了 SHA-256 哈希算法,摒弃了 SHA-1,对增强数据完整性有着显著优势。

IETF(Internet Engineering Task Froce,互联网工程任务组)制定的 TLS 1.3 是有史以来最安全、复杂的 TLS 协议。它具有如下特点:

1)、更快的访问速度

相比于 TLS 1.2 及之前的版本,TLS 1.3 的握手不再支持静态的 RSA 密钥交换,使用的是带有前向安全的 Diffie-Hellman 进行全面握手。因此 TLS 1.3 只需 1-RTT 握手时间。

2)、更强的安全性

删除了之前版本的不安全的加密算法。

此外,我们可以在 Google 浏览器设置 TLS 1.3。

3、slight-ssl

参考 TLS 1.3 协议,合并请求,优化加密算法,使用 session-ticket 等策略,力求在安全和体验间找到一个平衡点。

在 TLS 中性能开销最大的是 TLS 握手阶段的 RSA 加解密。在 slight-ssl 中又尝试如下几种解决方案:

4、微信 mmtls 原理

基于 TLS 1.3 草案标准而实现。

类似于 TLS 协议,mmtls 协议也是位于业务层与网络连接层中间。

mmtls 协议组成图
Handshake 协议

TLS 1.3 Handshake 协议有如下几类:

而 mmtls Handshake 协议有如下几种:

1-RTT ECDHE 密钥协商原理

ECDH 密钥交换协议需要使用两个算法:

但是 1-RTT ECDHE 算法容易被中间人攻击,中间人可以截获双方的公钥运行 ECDH_Generate_key 生成自己的公私钥对,然后将公钥发送给某一方。

如何解决中间人攻击?

中间人攻击产生的本质原因是没有经过端点认证,需要”带认证的密钥协商“。

数据认证的方式?

数据认证有对称与非对称两种方式:

ECDH 认证密钥协商就是 ECDH 密钥协商 + 数字签名算法 ECDSA。

双方密钥协商会对自身发出的公钥使用签名算法,由于签名算法中的公钥 ECDSA_verify_key 是公开的,中间人没有办法阻止别人获取公钥。

而 mmtls 仅对 Server 做认证,因为通信一方签名其协商数据就不会被中间人攻击。

在 TLS 中,提供了可选的双方相互认证的能力:

1-RTT PSK 密钥协商原理

在之前的 ECDH 握手下,Server 会下发加密的 PSK{key, ticket{key}},其中:

1)、首先,Client 将 ticket{key}、Client_Random 发送给 Server。

2)、然后,Server 使用 ticket_key 解密得到 key、Server_Random、Client_Random 计算 MAC 来认证。

3)、最后,Server 将 Server_Random、MAC 发送给 Client,Client 同 Server 使用 ticket_key 解密得到 key、Server_Random、Client_Random 去计算 MAC 来验证是否与收到的 MAC 匹配。

0-RTT ECDH 密钥协商原理

要想实现 0-RTT 密钥协商,就必须在协商一开始就将业务数据安全地传递到对端。

预先生成一对公私钥(static_svr_pub_key, static_svr_pri_key),并将公钥预置在 Client,私钥持久保存在 Server。

1)、首先,Client 通过 static_svr_pub_key 与 cli_pri_key 生成一个对称密钥SS(Static Secret),用 SS 衍生的密钥对业务数据加密。

2)、然后,Client cli_pub_key、Client_Random、SS 加密的 AppData 发送给 Server,Sever 通过 cli_pub_key 和 static_svr_pri_key 算出 SS,解密业务数据包。

1-RTT PSK 密钥协商原理

在进行 1-RTT PSK 握手之前,Client 已经有一个对称加密密钥 key 了,直接使用此 key 与 ticket{key} 一起传递给 Server 即可。

TLS 1.3 为什么要废除 RSA?

因此 TLS 1.3 引入了 PFS(perfect forward secrecy,前向安全性),即完全向前保密,一个密钥被破解,并不会影响其它密钥的安全性。

例如 0-RTT ECDH 密钥协商加密依赖了静态 static_svr_pri_key,不符合 PFS,我们可以使用 0-RTT ECDH-ECDHE 密钥协商,即进行 0-RTT ECDH 协商的过程中也进行 ECDHE 协商。0-RTT PSK 密钥协商的静态 ticket_key 同理也可以加入 ECDHE 协商。

verify_key 如何下发给客户端?

为避免证书链验证带来的时间消耗及传输带来的带宽消耗,直接将 verify_Key 内置客户端即可。

如何避免签名密钥 sign_key 泄露带来的影响?

因为 mmtls 内置了 verify_key 在客户端,必要时及时通过强制升级客户端的方式来撤销公钥并更新。

为什么要在上述密钥协商过程中都要引入 client_random、server_random、svr_pub_key 一起做签名?

因为 svr_pri_Key 可能会泄露,所有单独使用 svr_pub_key 时会有隐患,因为需要引入 client_random、server_random 来保证得到的签名值唯一对应一次握手。

Record 协议

1、认证加密

2、密钥扩展

双方使用相同的对称密钥进行加密通信容易被某些对称密钥算法破解,因此,需要对原始对称密钥做扩展变换得到相应的对称加密参数。

密钥变长需要使用密钥延时函数(KDF,Key Derivation Function),而 TLS 1.3 与 mmtls 都使用了 HKDF 做密钥扩展。

3、防重放

为解决防重放,我们可以为连接上的每一个业务包都添加一个递增的序列号,只要 Server 检查到新收到的数据包的序列号小于等于之前收到的数据包的序列号,就判断为重放包,mmtls 将序列号作为构造 AES-GCM 算参数 nonce 的一部分,这样就不需要对序列号单独认证。

在 0-RTT 握手下,第一个业务数据包和握手数据包无法使用上述方案,此时需要客户端在业务框架层去协调支持防重放。

小结

mmtls 的 工作过程 如下所示:

其优势具有如下4点:

3)、复用 Session Ticket 会话,节省一个 RTT 耗时。

最后,我们可以在统一接入层对传输数据二次加密,需要注意二次加密会增加客户端与服务器的处理耗时。

如果手机设置了代理,TLS 加密的数据可以被解开并被利用,如何处理?

可以在 客户端锁定根证书,可以同时兼容老版本与保证证书替换的灵活性。

8、网络容灾机制

9、资本手段优化

六、网络库设计

1、统一的网络中台

在一线互联网公司,都会有统一的网络中台:

2、如何设计一个优秀的统一网络库?

3、统一网络库的核心模块有哪些?

4、高质量网络库

1)、Chromium 网络库

2)、微信 Mars

一个跨平台的 Socket 层解决方案,不支持完整的 HTTP 协议。

Mars 的两个核心模块如下:

其中 STN 模块的组成图如下所示:

包包超时

动态超时

根据网络情况,调整其它超时的系数或绝对值。

Mars 是如何进行 连接优化 的?

复合连接

每间隔几秒启动一个新的连接,只要有连接建立成功,则关闭其它连接。=> 有效提升连接成功率。

自动重连优化

网络切换

通过感知网络的状态切换到更好的网络环境下。

Mars 是如何进行 弱网优化 的?

常规方案

1)、快速重传
2)、HARQ(Hybrid Automatic Repeat reQuest)

进阶方案

TCP 丢包的恢复方式 TLP
发图-有损下载

在弱网下尽量保证下载完整的图片轮廓显示,提高用户体验。

发图-有损上传数据

有损上传数据的流程,与有损下载流程同理:

发图-低成本重传

将分包转成流式传输。

七、其它优化方案

1、异地多活

一个多机房的整体方案,在多个地区同时存在对等的多个机房,以用户维度划分,多机房共同承担全量用户的流量。

在单个机房发送故障时,故障机房的流量可以快速地被迁引到可用机房,减少故障的恢复时间。

2、抗抖动优化

应用一种有策略的重试机制,将网络请求以是否发送到 socket 缓冲区作为分割,将网络请求生命周期划分为”请求开始到发送到 socket 缓冲区“和”已经发送到 socket 缓冲区到请求结束“两个阶段。

这样当用户进电梯因为网络抖动的原因网络链接断了,但是数据其实已经请求到了 socket 缓冲区,使用这种有策略的重试机制,我们就可以提升客户端的网络抗抖动能力。

3、SYNC 机制

同步差量数据,达到节省流量,提高通信效率与请求成功率。

客户端用户不在线时,SYNC 服务端将差量数据保持在数据库中。当客户端下次连接到服务器时,再同步差量数据给用户。

4、高并发流量处理:服务端接入层多级限流

核心思想是保障核心业务在体验可接受范围内做降级非核心功能和业务。从入口到业务接口总共分为四个层级,如下所示:

5、JobScheduler

结合 JobScheduler 来根据实际情况做网络请求. 比方说 Splash 闪屏广告图片, 我们可以在连接到 Wifi 时下载缓存到本地; 新闻类的 App 可以在充电, Wifi 状态下做离线缓存。

6、网络请求优先级排序

app应该对网络请求划分优先级尽可能快地展示最有用的信息给用户。(高优先级的服务优先使用长连接)

立刻呈现给用户一些实质的信息是一个比较好的用户体验,相对于让用户等待那些不那么必要的信息来说。这可以减少用户不得不等待的时间,增加APP在慢速网络时的实用性。(低优先级使用短连接)

7、建立长连通道

实现原理

将众多请求放入等待发送队列中,待长连通道建立完毕后再将等待队列中的请求放在长连通道上依次送出。

关键细节

HTTP 的请求头键值对中的的键是允许相同和重复的。例如 Set-Cookie/Cookie 字段可以包含多组相同的键名称数据。在长连通信中,如果对 header 中的键值对用不加处理的字典方式保存和传输,就会造成数据的丢失。

8、减少域名和避免重定向。

9、没有请求的请求,才是最快的请求。

七、网络体系化方案建设

1、线下测试

1)、正确认识

尽可能将问题在上线前暴露出来。

2)、侧重点

2、线上监控

1)、服务端监控

宏观监控维度

1)、请求耗时

区分地域、时间段、版本、机型。

2)、失败率

业务失败与请求失败。

3)、Top 失败接口、异常接口

以便进行针对性地优化。

微观监控维度

1)、吞吐量(requests per second)

RPS/TPS/QPS,每秒的请求次数,服务器最基本的性能指标,RPS 越高就说明服务器的性能越好。

2)、并发数(concurrency)

反映服务器的负载能力,即服务器能够同时支持的客户端数量,越大越好。

3)、响应时间(time per request)

反映服务器的处理能力,即快慢程度,响应时间越短越好。

4)、操作系统资源

CPU、内存、硬盘和网卡等系统资源。可以利用 top、vmstat 等工具检测相关性能。

优化方针

2)、客户端监控

要实现客户端监控,首先我们应该要统一网络库,而客户端需要监控的指标主要有如下三类:

为了运算简单我们可以抛弃 UV,只计算每一分钟部分维度的 PV。

1、Aspect 插桩 — ArgusAPM

关于 ArgusAPM 的网络监控切面源码分析可以参考我之前写的 深入探索编译插桩技术(二、AspectJ) - 使用 AspectJ 打造自己的性能监控框架

缺点

监控不全面,因为 App 可能不使用系统/OkHttp 网络库,或是直接使用 Native 网络请求。

2、Native Hook

需要 Hook 的方法有三类:

不同版本 Socket 的实现逻辑会有差异,为了兼容性考虑,我们直接 PLT Hook 内存所有的 so,但是需要排除掉 Socket 函数本身所在的 libc.so。其 PLT 的 Hook 代码如下所示:

hook_plt_method_all_lib("libc.so", "connect", (hook_func) &create_hook);
hook_plt_method_all_lib("libc.so, "send", (hook_func) &send_hook);
hook_plt_method_all_lib("libc.so", "recvfrom", (hook_func) &recvfrom_hook);
复制代码

下面,我们使用 PLT Hook 来获取网络请求信息。

项目地址

其成功 log 如下所示:

2020-05-21 15:10:37.328 27507-27507/com.dodola.socket E/HOOOOOOOOK: JNI_OnLoad
2020-05-21 15:10:37.328 27507-27507/com.dodola.socket E/HOOOOOOOOK: enableSocketHook
2020-05-21 15:10:37.415 27507-27507/com.dodola.socket E/HOOOOOOOOK: hook_plt_method
2020-05-21 15:10:58.484 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 10
2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: stack:com.dodola.socket.SocketHook.getStack(SocketHook.java:13)
libcore.io.Linux.connect(Native Method)
libcore.io.BlockGuardOs.connect(BlockGuardOs.java:126)
libcore.io.IoBridge.connectErrno(IoBridge.java:152)
libcore.io.IoBridge.connect(IoBridge.java:130)
java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:129)
java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:356)
java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:200)
java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:182)
java.net.SocksSocketImpl.connect(SocksSocketImpl.java:357)
java.net.Socket.connect(Socket.java:616)
com.android.okhttp.internal.Platform.connectSocket(Platform.java:145)
com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:141)
com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:112)
com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:184)
com.android.okhttp.internal.http.Strea
2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: AF_INET6 ipv6 IP===>14.215.177.39:443
2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 1
2020-05-21 15:10:58.495 27507-27677/com.dodola.socket E/HOOOOOOOOK: Ignore local socket connect
2020-05-21 15:10:58.523 27507-27677/com.dodola.socket E/HOOOOOOOOK: socket_connect_hook sa_family: 1
2020-05-21 15:10:58.523 27507-27677/com.dodola.socket E/HOOOOOOOOK: Ignore local socket connect
2020-05-21 15:10:58.806 27507-27677/com.dodola.socket E/HOOOOOOOOK: respond:<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a>  <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号  <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
复制代码

此外,我们也可以使用爱奇艺提供的 android_plt_hook 来实现 PLT Hook。

缺点

接管了系统的 Local Socket,需要在代码中增加过滤条件。

3)、接入层监控

为什么要做接入层监控?

监控维度

服务的入口和出口流量、服务端的处理时延、错误率等。

4)、监控报警

监控的同时如何实现准确的自动化报警呢?

通常是两种结合使用。

3、异常监控体系搭建

1)、服务器防刷

超限拒绝访问。

2)、客户端

3)、单点问题追查

如果用户反馈 App 消耗的流量过多,或后台消耗流量较多,我们都可以具体地分析网络请求日志、以及下发命令查看具体时间段的流量、客户端线上监控 + 体系化方案建设 来实现单点问题的追查。

八、网络优化常见问题

1、在网络方面你们做了哪些监控,建立了哪些指标?

注意:体现演进的过程。

网络优化及监控我们刚开始并没有去做,因此我们在 APP 的初期并没有注意到网络的问题,并且我们通常是在 WIFI 场景下进行开发,所以并没有注意到网络方面的问题。

当 APP 增大后,用户增多,逐渐由用户反馈 界面打不开或界面显示慢,也有用户反馈我们 APP 消耗的流量比较多。在我们接受到这些反馈的时候,我们没有数据支撑,无法判断用户反馈是不是正确的。同时,我们也不知道线上用户真实的体验是怎样的。所以,我们就 建立了线上的网络监控,主要分为 质量监控与流量监控

1)、质量监控

首先,最重要的是接口的请求成功率与每步的耗时,比如 DNS 的解析时间、建立连接的时间、接口失败的原因,然后在合适的时间点上报给服务器。

2)、流量监控

首先,我们获取到了精准的流量消耗情况,并且在 APM 后台,可以下发指令获取用户在具体时间段的流量消耗情况。 => 引出亮点 => 前后台流量获取方案。 关于指标 => 网络监控。

2、怎么有效地降低用户的流量消耗?

注意:结合实际案例

1)、数据:缓存、增量更新(这一步减少了非常多的流量消耗)

首先,我们处理了项目当中展示数据相关的接口,同时,对时效性没那么强的接口做了数据的缓存,也就是一段时间内的重复请求直接走缓存,而不走网络请求,从而避免流量浪费。对于一些数据的更新,例如省市区域、配置信息、离线包等信息,我们 加上版本号的概念,以实现每次更新只传递变化的数据,即实现了增量更新 => 亮点:离线包增量更新实现原理与关键细节。

2)、上传:压缩

然后,我们在上传流量这方面也做了处理,比如针对 POST 请求,我们对 Body 做了 GZip 压缩,而对于图片的发送,必须要经过压缩,它能够在保证清晰度的前提下极大地减少其体积。

3)、图片:缩略图、webp

对于图片展示,我们采用了不同场景展示不同图片的策略,比如在列表展示界面,我们只展示了缩略图,而到用户显示大图的时候,我们才去展示原图。 => 引出 webp 的使用策略。

3、用户反馈消耗流量多这种问题怎么排查?

首先,部分用户遇到流量消耗多的情况是肯定会存在的,因为线上用户非常多,每个人遇到的情况肯定是不一样的,比如有些用户他的操作路径比较诡异,可能会引发一些异常情况,因此有些用户可能会消耗比较多的流量。

1)、精准获取流量的能力

我们在客户端可以精确q地获取到流量的消耗,这样就给我们排查用户的流量消耗提供了依据,我们就知道用户的流量消耗是不是很多。

2)、所有请求大小及次数的监控

此外,通过网络请求质量的监控,我们知道了用户所有网络请求的次数与大小,通过大小和次数排查,我们就能知道用户在使用过程中遇到了哪些 bug 或者是执行了一些异常的逻辑导致重复下载,处于不断重试的过程之中。

3)、主动预警的能力

在客户端,我们发现了类似的问题之后,我们还需要配备主动预警的能力,及时地通知开发同学进行排除验证,通过以上手段,我们对待用户的反馈就能更加高效的解决,因为我们有了用户所有的网络请求数据。

4、系统如何知道当前 WiFi 有问题?

如果一个 WiFi 发送过数据包,但是没有收到任何的 ACK 回包,这个时候就可以初步判断当前的 WiFi 是有问题的。

九、总结

网络优化可以说是移动端性能优化领域中水最深的领域之一,要想做好网络优化必须具备非常扎实的技术功底与全链路思维。总所周知,对于一个工程师的技术评级往往是以他最深入的那一两个领域为基准,而不是计算其技术栈的平均值。因此,建议大家能找准一两个点,例如 网络、内存、NDK、Flutter,对其进行深入挖掘,以打造自身的技术壁垒。

作者:jsonchao
链接:https://juejin.cn/post/6844904186333642766
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上一篇 下一篇

猜你喜欢

热点阅读