本实践围绕游戏中心在弱网环境下的性能优化展开,针对复杂网络场景下的页面加载慢、资源加载失败等问题,提出了优化方案:接入支持 QUIC 协议的 Cronet 网络库,通过更快的连接建立与传输特性提升请求响应速度。配合弱网状态精细化判定与限速测试,线上灰度实验显示页面加载失败率下降 40%,请求耗时降低 7%,图片加载速度在正常至极差网络环境均有显著提升。
作者:vivo 互联网客户端团队- Ke Jie
1分钟看图掌握核心观点👇

01. 弱网优化背景
游戏中心 APP 的核心功能依赖网络连接,如游戏下载、更新、启动、礼包领取及活动参与等。而在电梯、地下车库等弱网环境中,用户常遇到进入页面慢、图片资源加载不出来等问题,严重影响体验,导致活跃下降和用户流失。
随着移动游戏用户规模扩大,确保在复杂网络条件下的稳定访问和核心功能可用性,成为提升留存和转化的关键。通过优化传输协议、传输数据优化等,可显著改善弱网下的使用体验,保障用户使用流畅性,提升整体用户满意度。
02 如何去定义网络状态
在移动应用中,网络状态的定义通常是指当前设备所处的网络连接类型与质量。它不仅仅是“有网”或“没网”,还包括网络速度、延迟、丢包率等关键指标,特别在进行弱网优化时,需要更精细地感知和分类网络状态。如果要对优化效果进行衡量,首页要定义各种情形下归属哪种网络状态。
由于网络状态并没有一个统一的定义,游戏中心基于以下维度构建立了App内部的弱网判定标准。
弱网与疑似弱网对比

大概的现象可以总结为:
- 弱网环境:网络质量严重下降,已对用户体验造成明显影响。
- 疑似弱网环境:网络出现不稳定或退化迹象,但尚未达到严重弱网程度。
游戏中心通过判断网络状态、WIFI信号、手机信号强弱、Ping百度/Vivo域名、最近接口请求失败率、上下行带宽、最近请求平均耗时等维度,赋予不同的网络状态值,将网络状态值作为作为埋点的公参上报,作为优化前后提取数据的维度。
03 游戏中心接入QUIC协议
QUIC协议简介
QUIC 是 Google 在 2013 年推出的一种新型网络协议,全称是“快速 UDP 网络连接”(Quick UDP Internet Connections)。它和我们常用的 TCP 协议不一样,是基于 UDP 打造的。QUIC 的目标是让网站和应用加载得更快,同时也更加安全。
它能一次建立多个数据连接,而且建立连接的速度比传统方式更快,这意味着打开网页、看视频或传输数据时,等待的时间会更短。此外,QUIC 还具备自动控制网络带宽的功能,可以根据网络情况进行调节,避免网络堵塞。
Google 希望用 QUIC 来替代现有的 TCP 协议,并推动它成为互联网新的标准协议。
QUIC协议应用场景
- 轻量资源传输优化:对于图片、图标等体积较小的文件,能够快速完成传输,缩短加载时间,提升整体响应效率。
- 视频播放体验增强:在进行视频点播时,可以实现更快的内容呈现,提升首帧加载速度,减少播放中断,提高观影流畅度。
- 高频交互请求加速:针对如登录验证、支付流程等频繁交互的请求场景,可有效提升数据响应速度,改善用户的操作体验。
- 复杂网络下保持稳定:在网络条件较差,如高延迟或频繁丢包的情况下,依然能维持稳定的数据传输,减少失败和卡顿,保障服务可用性。
- 应对大规模并发访问:在面对大量用户同时访问、多资源并行加载等高并发情境时,具备更强的连接能力,提升整体访问速度与稳定性。
实现方式
Cronet和Okhttp一样都是网络库,Cronet 原生支持 QUIC,而 OkHttp 默认不支持 QUIC。
由于原来业务中对Okhttp网络库是有一定改造的,所以这里在Okhttp网络库中去接入Cronet库,做好兼容。
网络库实现的思路是自定义 Cronet 拦截器,一个完整的 Cronet 拦截器主要包含三个步骤:
- OkHttp Request 转换为 Cronet Request
- 发起 Cronet 请求并处理生命周期
- Cronet Response 转 OkHttp Response
将自定义的 Cronet 拦截器添加到 OkHttp 拦截链的末尾,保证其他拦截器(如缓存、日志、认证)正常工作后,才使用 Cronet 处理请求。
OkHttpClient辅助类中兼容Cronet:
// 1. 创建缓存路径
val cachePath = File(AppContext.getContext().cacheDir, CRONET_CACHE_PATH)
if (!cachePath.isDirectory) {
cachePath.mkdirs()
VLog.d(TAG, "no cronet cache dir, mkdirs")
}
// 2. 构建 CronetEngine
var builder = CronetEngine.Builder(AppContext.getContext())
try {
builder = builder
.setStoragePath(cachePath.absolutePath) // 设置缓存路径
.enableBrotli(false) // 是否开启 Br 压缩,暂不开启
.enableQuic(true) // 开启 QUIC
.enableHttp2(true) // 开启 HTTP/2
.enableHttpCache(
CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP,
SIZE_1_MB.toLong()
) // 1MB 磁盘缓存,需先设置 setStoragePath()
// 配置 QUIC Hint
NetworkManager.getInstance().quicHintHosts?.forEach {
builder = builder.addQuicHint(it, 443, 443)
}
// 构建 CronetEngine
cronetEngine = builder.build()
} catch (e: Throwable) {
VLog.e(
TAG,
"init cronet engine fail",
e
) // 初始化 CronetEngine 失败,则返回 null,不走 QUIC 请求
cronetEngine = null
}
// 3. 构建 OkHttpClient 并集成 Cronet
val netClientBuilder = defOkhttpClient.newBuilder()
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
.writeTimeout(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(CronetInterceptor.Builder(cronetEngine).build())
CronetInterceptor拦截器,对需要使用QUIC协议的域名进行QUIC请求,相关域名可以做成配置项,具备线上随时切换的能力。
CronetInterceptor拦截器作用主要职责是:OKHttp 的Request 转换成Cronet Request,并能接收响应。
public final class CronetInterceptor implementsInterceptor {
private static final String TAG = "CronetInterceptor";
private final RequestResponseConverter mConverter;
private CronetInterceptor(RequestResponseConverter converter){
this.mConverter = checkNotNull(converter);
}
@Override
public Response intercept(Chain chain) throws IOException {
if (chain.call().isCanceled()) {
thrownew IOException("Request call canceled");
}
Request request = chain.request();
if (OkHttpClientHelper.INSTANCE.isNeedUseCronet(request.url())) {
VLog.d(TAG, "use Cronet request:" + request.url());
return proceedWithCronet(chain); // 使用 Cronet 发起 Quic 请求
} else {
VLog.d(TAG, "don't use Cronet request:" + request.url());
return proceedDefault(chain); // 不使用 Cronet 请求
}
}
private Response proceedWithCronet(Chain chain) throws IOException {
RequestResponseConverter.CronetRequestAndOkHttpResponse requestAndOkHttpResponse =
mConverter.convert(chain.request(), chain.readTimeoutMillis(), chain.writeTimeoutMillis());
try {
requestAndOkHttpResponse.getRequest().start();
return toInterceptorResponse(requestAndOkHttpResponse.getResponse(), chain.call());
} catch (Throwable e) {
VLog.e(TAG, "proceedWithCronet exception:", e);
throw e;
}
}
private Response proceedDefault(Chain chain) throws IOException {
try {
Request request = chain.request();
VLog.d(TAG, "intercept " + request.method() + ", " + request.tag());
request = RequestHelper.handleRequest(request);
Response response = chain.proceed(request);
int retryNum = 0;
while ((response == null || !response.isSuccessful()) && retryNum < DEFAULT_RETRY_COUNT) {
retryNum++;
if (response != null && response.body() != null) {
response.body().close();
}
response = chain.proceed(request);
}
return response;
} catch (Throwable e) {
if (e instanceof IOException) {
throw e;
} else {
thrownew IOException(e);
}
}
}
private Response toInterceptorResponse(Response response, Call call){
checkNotNull(response.body());
return response
.newBuilder()
.body(new CronetInterceptorResponseBody(response.body(), call))
.build();
}
}
接收到响应后,需要将Croent Response 转成 OKHttp Response,核心的实现:
Response toResponse(Request request, OkHttpBridgeRequestCallback callback) throws IOException {
Response.Builder responseBuilder = new Response.Builder();
UrlResponseInfo urlResponseInfo = getFutureValue(callback.getUrlResponseInfo());
@Nullable String contentType = getLastHeaderValue(CONTENT_TYPE_HEADER_NAME, urlResponseInfo);
@Nullable String contentLengthString = null;
List<String> contentEncodingItems = new ArrayList<>();
for (String contentEncodingHeaderValue : getOrDefault(
urlResponseInfo.getAllHeaders(),
CONTENT_ENCODING_HEADER_NAME,
Collections.emptyList())) {
Iterables.addAll(contentEncodingItems, COMMA_SPLITTER.split(contentEncodingHeaderValue));
}
boolean keepEncodingAffectedHeaders =
contentEncodingItems.isEmpty() || !ENCODINGS_HANDLED_BY_CRONET.containsAll(contentEncodingItems);
if (keepEncodingAffectedHeaders) {
contentLengthString = getLastHeaderValue(CONTENT_LENGTH_HEADER_NAME, urlResponseInfo);
}
ResponseBody responseBody =
createResponseBody(
request,
urlResponseInfo.getHttpStatusCode(),
contentType,
contentLengthString,
getFutureValue(callback.getBodySource()));
responseBuilder
.request(request)
.code(urlResponseInfo.getHttpStatusCode())
.message(urlResponseInfo.getHttpStatusText())
.protocol(convertProtocol(urlResponseInfo.getNegotiatedProtocol()))
.body(responseBody);
for (Map.Entry<String, String> header : urlResponseInfo.getAllHeadersAsList()) {
boolean copyHeader = true;
if (!keepEncodingAffectedHeaders) {
if (Ascii.equalsIgnoreCase(header.getKey(), CONTENT_LENGTH_HEADER_NAME)
|| Ascii.equalsIgnoreCase(header.getKey(), CONTENT_ENCODING_HEADER_NAME)) {
copyHeader = false;
}
}
if (copyHeader) {
responseBuilder.addHeader(header.getKey(), header.getValue());
}
}
return responseBuilder.build();
}
这样整体在OkHttp网络库中,能够兼容使用Cronet网络库,整体的流程就通了。
测试方式及配置
① 域名支持
需要将支持的域名配置成支持QUIC,这里注意需要和运营商确认是否支持GQUIC/IQUIC。
② 限制网速参数
各个网络状态的参数可以参考这样设置:

③ 测试工具
由于QUIC抓包比较复杂,这里自定义了脚本,通过限制延迟时间、带宽、丢包率来限制网速,参数可以参考上一小节。
#!/bin/bash
# 延迟时间,以毫秒为单位进行指定。
# 带宽,以千比特或兆比特为单位进行指定。
# 丢包率,以百分比进行指定。
# 比如设置 300 毫秒的延迟时间、100 千比特的带宽和 50% 的丢包率,请运行以下命令:
# bash NetworkSimulation.sh 300ms 100kbit 50%
# 如需设置 100 毫秒的延迟时间、1 兆比特的带宽和 0% 的丢包率,请运行以下命令:
# bash NetworkSimulation.sh 100ms 1mbit 0%
# root device and set it to permissive mode
adb root
adb shell setenforce 0
# Clear the current tc control
adb shell tc qdisc del dev ifb0 root
adb shell ip link set dev ifb0 down
adb shell tc qdisc del dev wlan0 ingress
adb shell tc qdisc del dev wlan0 root
if [ $# -eq 1 ]; then
echo "setup cleared"
elif [ $# -eq 3 ]; then
latency=$1
bandwidth=$2
packetloss=$3
# Create a virtual device for ingress
adb shell ip link set dev wlan0 up
adb shell ip link set dev ifb0 up
adb shell tc qdisc del dev wlan0 clsact
adb shell tc qdisc add dev wlan0 handle ffff: ingress
adb shell tc filter add dev wlan0 parent ffff: protocol all u32 match u32 00 action mirred egress redirect dev ifb0
# Throttle upload bandwidth / latency / packet loss
adb shell tc qdisc add dev wlan0 root handle 1: htb default11
adb shell tc class add dev wlan0 parent 1: classid 1:1 htb rate "$bandwidth"
adb shell tc class add dev wlan0 parent 1:1 classid 1:11 htb rate "$bandwidth"
adb shell tc qdisc add dev wlan0 parent 1:11 handle 10: netem delay "$latency" loss "$packetloss"
# Throttle download bandwidth
adb shell tc qdisc add dev ifb0 root handle 1: htb default10
adb shell tc class add dev ifb0 parent 1: classid 1:1 htb rate "$bandwidth"
adb shell tc class add dev ifb0 parent 1:1 classid 1:10 htb rate "$bandwidth"
else
echo "Invalid parameters"
fi
通过命令行执行类似于bash NetworkSimulation.sh 100ms 1mbit 0%命令,即可以限制手机的网络状态。
04 优化效果
在本次面向核心接口与图片域名的线上 A/B 灰度实验中,经过一段时间的观测与数据对比,灰度策略取得了显著优化效果,主要体现在以下几个方面:
- 页面加载失败率显著下降:整体失败率下降 40%,显著提升页面可用性;
- 页面请求响应性能优化:平均页面请求耗时下降 7%,加载更流畅;
- 正常网络环境图片加载速度提升:加载速度提升 38%,提升用户体验;
- 弱网络环境图片加载速度提升:加载速度提升 30%,弱网下表现更优;
- 极差网络环境图片加载速度提升:加载速度提升达58%,保障极端场景下的可用性与体验。
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。