You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Kotlin中持续测量网络下载/上传速率(如VPN场景)?

在Kotlin中持续测量网络上下行速率

核心思路

要在VPN连接期间实时监测速率,核心是定期采样系统累计的收发字节数,通过两次采样的差值除以时间间隔计算实时速率,同时监听VPN连接状态,在连接时启动采样,断开时停止。

实现步骤

  1. 获取系统流量统计:利用Android系统的TrafficStats类获取设备累计接收(下行)和发送(上行)的字节数。
  2. 定时采样计算速率:通过Kotlin协程实现每秒一次的采样,计算两次采样的字节差,转换为易读的速率单位(B/s、KB/s、MB/s)。
  3. 监听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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 09:40:56