• Android项目网络请求支持Brotli压缩记录


    什么是 Brotli 压缩

    Brotli Google 官方库

    Brotli 是 Google 推出的一种无损压缩算法,通过变种的LZ77算法、Huffman编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比(如zip,gzip等),无论是压缩时间,还是压缩体积上看,它都有着更高的效率。

    Brotli 如此高的压缩比率,得益于其使用一个预定义的字典,该字典包含超过 13000 个来自文本和 HTML 文档的大型语料库的常用字符串,预定义的算法可以提升较小文件的压缩密度,而压缩与解压缩速度则大致不变。

    Brotli 特点和注意点:

    • 对比 gzip 算法,压缩性能更好,性能提升约15%~25%
    • 对 1kb 以内的文件不需要压缩,因为压缩前后差距不大
    • 几乎所有主流浏览器都已支持 br 算法,可以通过这里查询支持情况
    • 对于图片类型文件(PNG、JPG、JPEG等)和视频类型文件(MP4、AVI、WMV等)已经做了内容的压缩处理,无需 gzip 或 br 压缩,因为压缩后文件反而可能会更大
    • Brotli压缩支持的文件类型有 text/xml、text/plain、text/css、application/javascript、application/x-javascript、application/rss+xml、text/javascript、image/tiff、image/svg+xml、application/json、application/xml

    Android 项目如何使用 Brotli

    对于 web 和 ios 端,在浏览器和系统层已经开始支持 br 的自动解压了,但 Android 系统不会自动解析 br 数据,需要在应用层自己去实现。

    Brotli 大部分由 C/C++ 写成,虽然 Brotli 库包含了 Java 模块,但它只是一个描述定义,还没有真正的 Java Brotli 编码器实现(具体可以查看 issure 405),那么如何在 Android Java 项目中使用,就需要我们自己思考了。

    虽然我在 Okhttp 中发现了 BrotliInterceptor 的拦截器,但它使用的依然是是 Brotli 库的 Java 描述,同样没有真正的 Java 编码器实现,所以无法真正的解压 br 压缩后的数据:
    在这里插入图片描述
    👆BrotliInterceptor 也只有 Java 层的使用描述,而没有 C/C++ 的底层实现。

    那么要在 Java 层使用 Brotli ,只能通过 JNI 方式来调用 C/C++ 的实现了,步骤如下:

    • 首先需要将 Brotli 库通过 CMake 工具(其他构建方式在Brotli首页有列出,如Bazel)构建出对应的 so 文件(Android 平台通常只需要包括 armeabi-v7a、arm64-v8a、x86、x86-64)
    • 新建 JNI 项目,定义 Java 层编解码调用逻辑,测试与 C/C++ 层交互
    • 最后将 Java 定义封装成 Jar ,与 so 库一同依赖到 Android 工程即可(也可以封装成 AAR)
    • 同时 Android 项目中的 Okhttp 的拦截器也需要自己去实现。

    CMark 构建比较麻烦,最后还是在这个库: Brotli:https://github.com/chfdeng/Brotli 中找到已经封装好的 Brotli 压缩实现,不过它是把整个 Brotli 封装成 aar 库提供使用,我抽取了其中用到的CPU平台 so 库和 Jar 包,直接用到项目中。

    真正应用到 Android 项目

    我们项目网络层使用 Okhttp + Retrofit 实现,在没有任何处理情况下,首先通过抓包查看一下项目中的网络请求情况:

    在这里插入图片描述

    可以看到目前使用的是 gzip 压缩算法,其实这是 Okhttp 为我们默认添加的,这部分代码在 BridgeInterceptor 拦截器中:
    在这里插入图片描述
    如果没有指定 Accept-Encoding(传输编码) 和 Range(传输范围) 请求头,Okhttp 的 BrideInterceptor 拦截器就会默认添加 gzip 作为传输格式,而且在得到服务器响应时,如果响应体使用了 gzip 压缩( Accept-Encoding=gzip),就会默认解压,这也算 Okhttp 的一种优化。
    在这里插入图片描述
    由于我们服务器暂不支持gzip压缩,所以对于项目中使用 gzip 和 br 的对比也就暂时不做了,不过已经有人做了这样的对比,感兴趣可以去看这里

    添加 br 压缩请求头:Accept-Encoding:br

    如果想让服务端返回 br 压缩后的数据,那么可以通过添加请求头的方式告知服务端。但如果只对项目中请求头增加了 Accept-Encoding:br 而不做其他处理,那么会得到下面的报错(前提是服务端做了 br 压缩,也就是响应头中有:Content-Encoding:br):

    报错1:Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $

    报错2:com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $

    上面两个错误,其实都是因为没对 br 压缩后的数据做解压,导致 JSON 去解析乱码导致的,不信可以把 response body 打印出来看:
    在这里插入图片描述
    这些乱码就是压缩后的数据。

    解压 br 压缩后的数据

    在得到响应时,判断服务端是否做过压缩应该以 Content-Encoding 响应头为准,有这个响应头,且值为 br 说明需要进行 br 解压;值为 gzip 说明需要 gzip 解压。

    现在把 Brotli 压缩相关的 so 库和 jar 文件添加到依赖项目,并通过 BrotliInputStream 对上面的 Response 进行解压:
    在这里插入图片描述

    这样就得到正常的响应了。

    我们可以把这层逻辑进一步封装成 Okhttp 的一个拦截器:

    object BrotliHeadInterceptor : Interceptor {
    
      override fun intercept(chain: Interceptor.Chain): Response =
        if (chain.request().header("Accept-Encoding") == null) {
          //客户端支持br和gzip
          val request = chain.request().newBuilder().header("Accept-Encoding", "br,gzip").build()
    
          val response = chain.proceed(request)
    
          uncompress(response)
        } else {
          chain.proceed(chain.request())
        }
    
      internal fun uncompress(response: Response): Response {
        if (!response.promisesBody()) {
          return response
        }
    
        val body = response.body ?: return response
        val encoding = response.header("Content-Encoding") ?: return response
    
        val decompressedSource = when {
          //br需要额外so库
          encoding.equals("br", ignoreCase = true) -> BrotliInputStream(body.source().inputStream())
            .source().buffer()
          //okhttp自动支持gzip
          encoding.equals("gzip", ignoreCase = true) -> GzipSource(body.source()).buffer()
          else -> return response
        }
    
        return response.newBuilder().removeHeader("Content-Encoding").removeHeader("Content-Length")
          .body(decompressedSource.asResponseBody(body.contentType(), -1)).build()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在 Okhttp 构建时,将这个拦截器加入到拦截器链,即可支持 gzip 和 br。

    启用br压缩后对比

    这里只通过抓包工具进行直观对比:

    启用前:
    启用后:

    总结

    压缩的时间是固定的,数据在网络中传输的路径和时间是不定的,在算法中常量在一定情况下可以忽略(映射过来,就比如数据小于1kb或网络是直连的,这时 br 压缩收益很小)。所以对网络数据的压缩可谓一举多得:不仅能加快数据传输时间,减少带宽占用,优化了用户流量消耗,甚至能降低设备电池使用量。

    Android中用到的Brotli所有支持(jar,so文件以及封装好的Okhttp拦截器),已经上传到CSDN,可以通过下面链接下载:https://download.csdn.net/download/weixin_39397471/86273500

    参考

    Brotli Github 地址

    Brotli 算法支持情况:http://caniuse.com/#feat=brotli

    阿里Brotli压缩:https://help.aliyun.com/document_detail/120511.html

    Brotli —— 下一代的 HTTP 服务器压缩

    https://toutiao.io/posts/ei3sk4/preview

  • 相关阅读:
    记录一个错误:cannot schedule the futures after interprete shutdown
    CentOS7 安装 ElasticSearch7.10
    【Java 基础篇】Java UDP通信详解
    二维码的前世今生 与 六大测试点梳理
    【深度学习】DNN房价预测
    [管理与领导-96]:IT基层管理者 - 扩展技能 - 5 - 职场丛林法则 -10- 七分做,三分讲,完整汇报工作的艺术
    想要精通算法和SQL的成长之路 - 分发糖果
    MySQL性能调优关注点和思路
    SuperMap iServer 备份恢复与迁移
    Java核心编程(17)
  • 原文地址:https://blog.csdn.net/weixin_39397471/article/details/126135907