Brotli 是 Google 推出的一种无损压缩算法,通过变种的LZ77算法、Huffman编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比(如zip,gzip等),无论是压缩时间,还是压缩体积上看,它都有着更高的效率。
Brotli 如此高的压缩比率,得益于其使用一个预定义的字典,该字典包含超过 13000 个来自文本和 HTML 文档的大型语料库的常用字符串,预定义的算法可以提升较小文件的压缩密度,而压缩与解压缩速度则大致不变。
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++ 的实现了,步骤如下:
CMark 构建比较麻烦,最后还是在这个库: Brotli:https://github.com/chfdeng/Brotli 中找到已经封装好的 Brotli 压缩实现,不过它是把整个 Brotli 封装成 aar 库提供使用,我抽取了其中用到的CPU平台 so 库和 Jar 包,直接用到项目中。
我们项目网络层使用 Okhttp + Retrofit 实现,在没有任何处理情况下,首先通过抓包查看一下项目中的网络请求情况:
可以看到目前使用的是 gzip 压缩算法,其实这是 Okhttp 为我们默认添加的,这部分代码在 BridgeInterceptor 拦截器中:
如果没有指定 Accept-Encoding(传输编码) 和 Range(传输范围) 请求头,Okhttp 的 BrideInterceptor 拦截器就会默认添加 gzip 作为传输格式,而且在得到服务器响应时,如果响应体使用了 gzip 压缩( Accept-Encoding=gzip),就会默认解压,这也算 Okhttp 的一种优化。
由于我们服务器暂不支持gzip压缩,所以对于项目中使用 gzip 和 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 打印出来看:
这些乱码就是压缩后的数据。
在得到响应时,判断服务端是否做过压缩应该以 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()
}
}
在 Okhttp 构建时,将这个拦截器加入到拦截器链,即可支持 gzip 和 br。
这里只通过抓包工具进行直观对比:
启用前:
启用后:
压缩的时间是固定的,数据在网络中传输的路径和时间是不定的,在算法中常量在一定情况下可以忽略(映射过来,就比如数据小于1kb或网络是直连的,这时 br 压缩收益很小)。所以对网络数据的压缩可谓一举多得:不仅能加快数据传输时间,减少带宽占用,优化了用户流量消耗,甚至能降低设备电池使用量。
Android中用到的Brotli所有支持(jar,so文件以及封装好的Okhttp拦截器),已经上传到CSDN,可以通过下面链接下载:https://download.csdn.net/download/weixin_39397471/86273500
Brotli 算法支持情况:http://caniuse.com/#feat=brotli
阿里Brotli压缩:https://help.aliyun.com/document_detail/120511.html
https://toutiao.io/posts/ei3sk4/preview