如何在Kotlin中持续测量网络下载/上传速率(如VPN场景)?
在Kotlin中持续测量网络上下行速率
核心思路
要在VPN连接期间实时监测速率,核心是定期采样系统累计的收发字节数,通过两次采样的差值除以时间间隔计算实时速率,同时监听VPN连接状态,在连接时启动采样,断开时停止。
实现步骤
- 获取系统流量统计:利用Android系统的
TrafficStats类获取设备累计接收(下行)和发送(上行)的字节数。 - 定时采样计算速率:通过Kotlin协程实现每秒一次的采样,计算两次采样的字节差,转换为易读的速率单位(B/s、KB/s、MB/s)。
- 监听VPN状态:通过广播接收器和网络回调监听VPN的连接/断开事件,触发采样的启动和停止。
完整代码实现
1. 添加权限到AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
2. 速率监测类实现
import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.VpnService import android.os.Build import android.os.TrafficStats import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.text.DecimalFormat class NetworkSpeedMonitor( private val context: Context, private val lifecycleOwner: LifecycleOwner ) { private var samplingJob: Job? = null private var lastRxBytes: Long = 0 private var lastTxBytes: Long = 0 private val decimalFormat = DecimalFormat("#.##") private var isVpnConnected = false interface SpeedCallback { fun onSpeedUpdated(downloadSpeed: String, uploadSpeed: String) } private var speedCallback: SpeedCallback? = null fun setSpeedCallback(callback: SpeedCallback) { this.speedCallback = callback } init { registerVpnReceiver() setupNetworkCallback() } private fun registerVpnReceiver() { val filter = IntentFilter().apply { addAction(VpnService.ACTION_VPN_CONNECTED) addAction(VpnService.ACTION_VPN_DISCONNECTED) } context.registerReceiver(vpnReceiver, filter) } private val vpnReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { VpnService.ACTION_VPN_CONNECTED -> { isVpnConnected = true startSampling() } VpnService.ACTION_VPN_DISCONNECTED -> { isVpnConnected = false stopSampling() } } } } private fun setupNetworkCallback() { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val capabilities = cm.getNetworkCapabilities(network) if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true) { isVpnConnected = true startSampling() } } override fun onLost(network: Network) { val capabilities = cm.getNetworkCapabilities(network) if (capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true) { isVpnConnected = false stopSampling() } } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { cm.registerDefaultNetworkCallback(callback) } else { cm.activeNetwork?.let { cm.registerNetworkCallback(it, callback) } } } private fun startSampling() { if (samplingJob?.isActive == true) return lastRxBytes = TrafficStats.getTotalRxBytes() lastTxBytes = TrafficStats.getTotalTxBytes() samplingJob = CoroutineScope(Dispatchers.IO).launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { while (isVpnConnected) { delay(1000) val currentRx = TrafficStats.getTotalRxBytes() val currentTx = TrafficStats.getTotalTxBytes() val rxDiff = currentRx - lastRxBytes val txDiff = currentTx - lastTxBytes val downloadSpeed = formatSpeed(rxDiff) val uploadSpeed = formatSpeed(txDiff) withContext(Dispatchers.Main) { speedCallback?.onSpeedUpdated(downloadSpeed, uploadSpeed) } lastRxBytes = currentRx lastTxBytes = currentTx } } } } private fun stopSampling() { samplingJob?.cancel() samplingJob = null } private fun formatSpeed(bytes: Long): String { return when { bytes < 1024 -> "$bytes B/s" bytes < 1024 * 1024 -> "${decimalFormat.format(bytes / 1024.0)} KB/s" else -> "${decimalFormat.format(bytes / (1024.0 * 1024))} MB/s" } } fun release() { context.unregisterReceiver(vpnReceiver) stopSampling() } }
3. 在Activity中使用
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.widget.TextView class MainActivity : AppCompatActivity(), NetworkSpeedMonitor.SpeedCallback { private lateinit var speedMonitor: NetworkSpeedMonitor private lateinit var tvDownload: TextView private lateinit var tvUpload: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvDownload = findViewById(R.id.tv_download_speed) tvUpload = findViewById(R.id.tv_upload_speed) speedMonitor = NetworkSpeedMonitor(this, this) speedMonitor.setSpeedCallback(this) } override fun onSpeedUpdated(downloadSpeed: String, uploadSpeed: String) { tvDownload.text = "下载: $downloadSpeed" tvUpload.text = "上传: $uploadSpeed" } override fun onDestroy() { super.onDestroy() speedMonitor.release() } }
注意事项
- 权限说明:
ACCESS_NETWORK_STATE是必需权限,若需单独统计VPN接口流量而非总流量,需额外申请PACKAGE_USAGE_STATS权限,并使用NetworkStatsManager查询特定网络的流量数据(实现更复杂)。 - TrafficStats局限性:该类返回的是设备启动以来的累计值,重启设备会重置;若设备同时连接多个网络,统计的是所有接口的总流量。
- 生命周期管理:使用
repeatOnLifecycle确保采样仅在Activity处于活跃状态时运行,避免内存泄漏。 - VPN状态兼容性:部分厂商可能自定义VPN广播,同时使用
NetworkCallback检测VPN网络可用性可提升兼容性。
内容的提问来源于stack exchange,提问作者Navid Shokoufeh
相关产品推荐
相关产品推荐

