背景
最近我们产品 FinClip 需要针对网络请求模块的API(request/downloadFile)增加 网络性能相关(domainLookUpStart、domainLookUpEnd、connectStart、connectEnd等)的统计。
调研
我们先去调研了微信,而微信的实现情况如下:
Android
- request 接口从客户端 7.0.10 版本开始使用 Chromium 内网络相关部分封装的底层组件 (cronet), 之前版本使用 HttpURLConnection 系统组件 (系统组件依赖系统实现会有平台兼容性问题, 我们建议用新版本微信来进行调试)
- download 接口从客户端 7.0.12 版本开始使用 cronet 组件, 之前版本使用 HttpURLConnection 组件
- upload 接口目前仍在使用 HttpURLConnection 组件
- websocket 接口从客户端 7.0.4 版本开始使用微信底层组件 wcwss, 并在 7.0.10 版本优化了调用性能
iOS
- request/download 接口从客户端 8.0.3 版本开始使用 cronet 组件, 之前版本使用 NSURLSession 系统组件
- upload 接口目前仍在使用 NSURLSession 组件
- websocket 接口从客户端 7.0.20 版本开始使用微信底层组件 wcwss, 之前版本使用 SRWebSocket 组件
cronet 是什么?
Cronet是Chromium网络引擎对不同操作系统做的封装,实现了移动端应用层、表示层、会话层协议,支持HTTP1/2、SPDY、QUIC、WebSocket、FTP、DNS、TLS等协议标准。支持Android、IOS、Chrome OS、Fuchsia,部分支持Linux、MacOS、Windows桌面操作系统。实现了Brotli数据压缩、预连接、DNS缓存、session复用等策略优化以及TCP fast open等系统优化。
更多cronet相关知识可查看如下文章:
Cronet网络库系列(一):用例与原理实现详解 zhuanlan.zhihu.com/p/93362473
可以直接使用google编译好的库:console.cloud.google.com/storage/bro…
cronet 编译成库之后,体积是多大呢?
2024-07-22 版本体积
ios_108.0.5325.0版本:纯真机的动态库是5.9M,静态库28M; 纯模拟器动态库6.4M
android_128.0.6610.0版本:25.4M
而我们SDK体积大小,对于客户来说非常敏感,这个体积大小完全无法集成。
那么有没有可能使用其他方式来实现呢?
AI 给出的方案
随后,我向Cursor 抛出问题?
1. 请帮我整理出 (https://developers.weixin.qq.com/miniprogram/dev/api/network/request/wx.request.html)
返回profile中所有的属性名。
2. iOS端如何统计这些属性?
3. Android端如何统计这些属性?
AI给出的回复:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {
NSURLSessionTaskTransactionMetrics *transactionMetrics = metrics.transactionMetrics.lastObject;
NSMutableDictionary *profile = [NSMutableDictionary dictionary];
// 重定向相关【iOS不支持】
// profile[@"redirectStart"] = @([self timeIntervalSince1970:transactionMetrics.redirectStartDate]);
// profile[@"redirectEnd"] = @([self timeIntervalSince1970:transactionMetrics.redirectEndDate]);
// 获取资源相关
profile[@"fetchStart"] = @([self timeIntervalSince1970:transactionMetrics.fetchStartDate]);
// DNS查询相关
profile[@"domainLookUpStart"] = @([self timeIntervalSince1970:transactionMetrics.domainLookupStartDate]);
profile[@"domainLookUpEnd"] = @([self timeIntervalSince1970:transactionMetrics.domainLookupEndDate]);
// 连接建立相关
profile[@"connectStart"] = @([self timeIntervalSince1970:transactionMetrics.connectStartDate]);
profile[@"connectEnd"] = @([self timeIntervalSince1970:transactionMetrics.connectEndDate]);
// SSL连接相关
profile[@"SSLconnectionStart"] = @([self timeIntervalSince1970:transactionMetrics.secureConnectionStartDate]);
profile[@"SSLconnectionEnd"] = @([self timeIntervalSince1970:transactionMetrics.secureConnectionEndDate]);
// 请求响应相关
profile[@"requestStart"] = @([self timeIntervalSince1970:transactionMetrics.requestStartDate]);
profile[@"requestEnd"] = @([self timeIntervalSince1970:transactionMetrics.requestEndDate]);
profile[@"responseStart"] = @([self timeIntervalSince1970:transactionMetrics.responseStartDate]);
profile[@"responseEnd"] = @([self timeIntervalSince1970:transactionMetrics.responseEndDate]);
// RTT (Round Trip Time)
NSTimeInterval rtt = [transactionMetrics.responseStartDate timeIntervalSinceDate:transactionMetrics.requestStartDate];
profile[@"rtt"] = @(rtt * 1000); // 转换为毫秒
// 网络类型估计 (这需要额外的逻辑或使用Reachability库)
profile[@"estimate_nettype"] = @([self estimateNetworkType:rtt]);
// HTTP RTT 估计
profile[@"httpRttEstimate"] = @(rtt * 1000);
// 传输层 RTT 估计 (这个值在iOS中难以准确获取,这里使用HTTP RTT作为近似)
profile[@"transportRttEstimate"] = @(rtt * 1000);
// 下行吞吐量估计 (这个值在iOS中难以准确获取,需要额外计算)
profile[@"downstreamThroughputKbpsEstimate"] = @([self estimateThroughputWithTask:task metrics:metrics]);
// 实际吞吐量 (与估计值相同,因为我们没有更精确的方法)
profile[@"throughputKbps"] = profile[@"downstreamThroughputKbpsEstimate"];
// 对等IP (在iOS中无法直接获取)
profile[@"peerIP"] = transactionMetrics.remoteAddress ?: @"";
// 端口 (在iOS中无法直接获取)
profile[@"port"] = @(0);
// 是否复用连接
profile[@"socketReused"] = @(transactionMetrics.isReusedConnection);
// 发送和接收的字节数
profile[@"sendBytesCount"] = @(task.countOfBytesSent);
profile[@"receivedBytedCount"] = @(task.countOfBytesReceived);
// 协议
profile[@"protocol"] = transactionMetrics.networkProtocolName ?: @"unknown";
// 是否使用高性能模式 (iOS中没有直接对应的概念)
profile[@"usingHighPerformanceMode"] = @NO;
NSLog(@"Request Profile: %@", profile);
}
- (NSTimeInterval)timeIntervalSince1970:(NSDate *)date {
return date ? [date timeIntervalSince1970] * 1000 : 0;
}
- (NSInteger)estimateNetworkType:(NSTimeInterval)rtt {
if (rtt < 0.05) return 5; // 4G
if (rtt < 0.1) return 4; // 3G
if (rtt < 0.3) return 3; // 2G
return 2; // Slow 2G
}
- (double)estimateThroughputWithTask:(NSURLSessionTask *)task metrics:(NSURLSessionTaskMetrics *)metrics {
NSTimeInterval duration = [metrics.taskInterval duration];
if (duration > 0) {
return (task.countOfBytesReceived * 8.0) / (1024.0 * duration);
}
return 0;
}
这个实现尽可能地模拟了微信小程序 wx.request 的 profile 对象中的字段。但是有一些限制和注意事项:
- 某些字段(如 peerIP 和 port)在 iOS 中无法直接获取。
- 网络类型估计(estimate_nettype)是基于 RTT 的简化估计,实际上需要更复杂的逻辑或使用 Reachability 库。
- 吞吐量估计是基于接收到的数据量和时间计算的,可能不如设备 API 提供的信息准确。
- usingHighPerformanceMode 在 iOS 中没有直接对应的概念。
- 所有时间戳都转换为毫秒级别以匹配微信 API 的格式。
- 某些字段(如 transportRttEstimate)在 iOS 中难以准确获取,所以使用了近似值。
这样功能已经实现了七八成,再对一些属性的取值做一些优化就差不多了。
而Android 下实现,Cursor 同样给出的方案:
import okhttp3.*
import okhttp3.EventListener
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
class DetailedPerformanceEventListener : EventListener() {
private val timeStamps = mutableMapOf<String, Long>()
private var startNanos: Long = 0
private var dnsStartNanos: Long = 0
private var connectStartNanos: Long = 0
private var secureConnectStartNanos: Long = 0
private var requestStartNanos: Long = 0
private var responseStartNanos: Long = 0
private var responseEndNanos: Long = 0
private var peerAddress: InetSocketAddress? = null
private var protocol: Protocol? = null
private val sendBytesCount = AtomicLong(0)
private val receivedBytesCount = AtomicLong(0)
private var socketReused = false
override fun callStart(call: Call) {
startNanos = System.nanoTime()
timeStamps["fetchStart"] = startNanos
}
override fun dnsStart(call: Call, domainName: String) {
dnsStartNanos = System.nanoTime()
timeStamps["domainLookUpStart"] = dnsStartNanos
}
override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
timeStamps["domainLookUpEnd"] = System.nanoTime()
}
override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
connectStartNanos = System.nanoTime()
timeStamps["connectStart"] = connectStartNanos
peerAddress = inetSocketAddress
}
override fun secureConnectStart(call: Call) {
secureConnectStartNanos = System.nanoTime()
timeStamps["SSLconnectionStart"] = secureConnectStartNanos
}
override fun secureConnectEnd(call: Call, handshake: Handshake?) {
timeStamps["SSLconnectionEnd"] = System.nanoTime()
}
override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
timeStamps["connectEnd"] = System.nanoTime()
this.protocol = protocol
}
override fun requestHeadersStart(call: Call) {
requestStartNanos = System.nanoTime()
timeStamps["requestStart"] = requestStartNanos
}
override fun requestHeadersEnd(call: Call, request: Request) {
// 请求头结束
}
override fun requestBodyStart(call: Call) {
// 请求体开始
}
override fun requestBodyEnd(call: Call, byteCount: Long) {
sendBytesCount.addAndGet(byteCount)
timeStamps["requestEnd"] = System.nanoTime()
}
override fun responseHeadersStart(call: Call) {
responseStartNanos = System.nanoTime()
timeStamps["responseStart"] = responseStartNanos
}
override fun responseHeadersEnd(call: Call, response: Response) {
// 响应头结束
}
override fun responseBodyStart(call: Call) {
// 响应体开始
}
override fun responseBodyEnd(call: Call, byteCount: Long) {
receivedBytesCount.addAndGet(byteCount)
responseEndNanos = System.nanoTime()
timeStamps["responseEnd"] = responseEndNanos
}
override fun callEnd(call: Call) {
val profile = generateProfile()
println("Request Profile: $profile")
}
private fun generateProfile(): Map<String, Any> {
val rtt = calculateRTT()
return mapOf(
"redirectStart" to 0, // OkHttp自动处理重定向,无法直接获取
"redirectEnd" to 0,
"fetchStart" to (timeStamps["fetchStart"] ?: 0) / 1_000_000,
"domainLookUpStart" to (timeStamps["domainLookUpStart"] ?: timeStamps["fetchStart"] ?: 0) / 1_000_000,
"domainLookUpEnd" to (timeStamps["domainLookUpEnd"] ?: timeStamps["domainLookUpStart"] ?: 0) / 1_000_000,
"connectStart" to (timeStamps["connectStart"] ?: 0) / 1_000_000,
"connectEnd" to (timeStamps["connectEnd"] ?: 0) / 1_000_000,
"SSLconnectionStart" to (timeStamps["SSLconnectionStart"] ?: 0) / 1_000_000,
"SSLconnectionEnd" to (timeStamps["SSLconnectionEnd"] ?: 0) / 1_000_000,
"requestStart" to (timeStamps["requestStart"] ?: 0) / 1_000_000,
"requestEnd" to (timeStamps["requestEnd"] ?: 0) / 1_000_000,
"responseStart" to (timeStamps["responseStart"] ?: 0) / 1_000_000,
"responseEnd" to (timeStamps["responseEnd"] ?: 0) / 1_000_000,
"rtt" to rtt,
"estimate_nettype" to estimateNetworkType(rtt),
"httpRttEstimate" to rtt, // 简化估计
"transportRttEstimate" to rtt, // 简化估计
"downstreamThroughputKbpsEstimate" to estimateThroughput(),
"throughputKbps" to calculateActualThroughput(),
"peerIP" to (peerAddress?.address?.hostAddress ?: ""),
"port" to (peerAddress?.port ?: 0),
"socketReused" to socketReused,
"sendBytesCount" to sendBytesCount.get(),
"receivedBytedCount" to receivedBytesCount.get(),
"protocol" to (protocol?.toString() ?: "unknown"),
"usingHighPerformanceMode" to false // OkHttp没有直接对应的概念
)
}
private fun calculateRTT(): Long {
return (responseStartNanos - requestStartNanos) / 1_000_000
}
private fun estimateNetworkType(rtt: Long): Int {
// 简化的网络类型估计
return when {
rtt < 50 -> 5 // 4G
rtt < 100 -> 4 // 3G
rtt < 300 -> 3 // 2G
else -> 2 // Slow 2G
}
}
private fun estimateThroughput(): Long {
val duration = (responseEndNanos - responseStartNanos) / 1_000_000_000.0
return (receivedBytesCount.get() * 8 / 1024 / duration).toLong()
}
private fun calculateActualThroughput(): Long {
val duration = (responseEndNanos - requestStartNanos) / 1_000_000_000.0
return ((sendBytesCount.get() + receivedBytesCount.get()) * 8 / 1024 / duration).toLong()
}
}
class PerformanceInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// 这里可以访问一些OkHttp内部信息
val connection = chain.connection()
if (connection != null) {
println("Socket reused: ${connection.socket().isReused}")
}
return response
}
}
// 使用示例
fun main() {
val client = OkHttpClient.Builder()
.addInterceptor(PerformanceInterceptor())
.eventListener(DetailedPerformanceEventListener())
.build()
val request = Request.Builder()
.url("https://api.github.com/users/octocat")
.build()
client.newCall(request).execute().use { response ->
println("Response code: ${response.code}")
}
}
这个示例尽可能模拟了微信小程序 wx.request 的 profile 对象中的字段。但是有一些限制和注意事项:
- 某些字段(如 redirectStart 和 redirectEnd)在 OkHttp 中难以直接获取,因为 OkHttp 自动处理重定向。
- 网络类型估计(estimate_nettype)是基于 RTT 的简化估计,实际上需要更复杂的逻辑或依赖设备 API。
- 吞吐量估计是基于接收到的数据量和时间计算的,可能不如设备 API 提供的信息准确。
- socketReused 信息需要通过 Interceptor 获取,因为 EventListener 没有直接提供这个信息。
- 某些字段(如 usingHighPerformanceMode)在 OkHttp 中没有直接对应的概念。
- 时间戳都转换为毫秒级别以匹配微信 API 的格式。
- 要使用这个性能监控系统,你需要在创建 OkHttpClient 时添加 DetailedPerformanceEventListener 和 PerformanceInterceptor。这将为每个请求生成详细的性能报告。
- 请注意,这种详细的性能监控可能会对应用程序的性能产生轻微影响,建议仅在调试或性能分析阶段使用。
我们优化一下部分属性的取值、存储、以及根据request的参数控制是否开启,做一些优化即可。
后续
我们基于AI给出的方案,做了调整比如网络类型的取值、一些阶段的起止时间,kps的计算等等。实际上花费了大概两天时间就把功能实现了,后续做了详细的测试,解决了一些特殊场景问题、参数配置因素,也在一个迭代内就完成了。
总结
其实,该功能在23年我们预估基于cronet实现可能要花好几周,而且体积还很大,基本是得放弃。 但是今年的公司提倡使用AI提升效率后,使用AI做了一次咨询,最终只花了两天就把主题功能实现了。
FinClip 是一个小程序容器SDK,我们已经推出了iOS、Android、鸿蒙、MacOS、Windows、Flutter、React Native等多个版本的SDK,是让移动 App 或跨端设备运行小程序最快,最简单的方法。