01-04-04 网络监控与测试完全指南

问题背景

在Android开发中,网络请求的调试、监控和测试是保证应用质量的关键环节。然而,直接使用生产环境API会遇到诸多问题:后端API尚未就绪、无法模拟各种异常场景、难以追踪网络性能、多环境切换繁琐、生产环境日志不足等。

在deviceSecurity项目中,需要解决以下挑战:如何记录详细的网络日志用于问题排查?如何监控网络性能指标(耗时、成功率、流量)?如何快速切换开发/测试/生产环境?如何Mock网络请求进行UI测试?如何在不同环境下验证功能?

本文将全面深入讲解网络请求日志、性能监控、多环境配置、Mock测试、网络层最佳实践,并结合deviceSecurity项目提供18+个企业级代码示例。


核心概念

1. 网络监控维度

日志维度

  • 请求信息(URL、Method、Headers、Body)
  • 响应信息(Status Code、Headers、Body、Duration)
  • 错误信息(Exception、Stack Trace)

性能维度

  • 请求耗时(总耗时、DNS耗时、连接耗时、传输耗时)
  • 数据传输量(上传字节、下载字节)
  • 成功率(成功请求数/总请求数)

业务维度

  • API调用频率
  • 错误分布(网络错误、HTTP错误、业务错误)
  • 慢请求追踪(耗时超过阈值的请求)

2. 环境配置架构

┌─────────────────────────────────────┐
│        Build Variants               │
│   (debug/release + flavor)          │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        BuildConfig                  │
│   (BASE_URL, API_KEY, etc.)        │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        EnvironmentManager           │
│   (动态切换环境)                    │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        OkHttpClient/Retrofit        │
│   (使用当前环境配置)                │
└─────────────────────────────────────┘

3. Mock测试策略

MockWebServer:单元测试,完全控制响应
Interceptor Mock:开发调试,快速验证UI
Repository Mock:ViewModel测试,隔离网络层
Fake实现:集成测试,模拟复杂逻辑


网络日志监控

1. HttpLoggingInterceptor基础配置

// HttpLoggingInterceptor配置
class NetworkModule {
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(createLoggingInterceptor())
            .build()
    }

    private fun createLoggingInterceptor(): HttpLoggingInterceptor {
        return HttpLoggingInterceptor { message ->
            Log.d("OkHttp", message)
        }.apply {
            level = if (BuildConfig.DEBUG) {
                HttpLoggingInterceptor.Level.BODY
            } else {
                HttpLoggingInterceptor.Level.NONE
            }
        }
    }
}

日志级别说明

  • NONE:不记录任何日志
  • BASIC:记录请求和响应的基本信息(URL、状态码、耗时)
  • HEADERS:记录BASIC + Headers
  • BODY:记录HEADERS + Body(完整内容)

2. 自定义网络日志拦截器

class DetailedLoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.nanoTime()

        // 记录请求信息
        logRequest(request)

        val response = try {
            chain.proceed(request)
        } catch (e: Exception) {
            // 记录网络异常
            logError(request, e, System.nanoTime() - startTime)
            throw e
        }

        val elapsedTime = System.nanoTime() - startTime

        // 记录响应信息
        logResponse(request, response, elapsedTime)

        return response
    }

    private fun logRequest(request: Request) {
        val requestBody = request.body

        Log.d("NetworkRequest", """
            ════════════════════════════════════════
            URL: ${request.url}
            Method: ${request.method}
            Headers: ${request.headers}
            Body: ${requestBody?.let { readBody(it) } ?: "No Body"}
            ════════════════════════════════════════
        """.trimIndent())
    }

    private fun logResponse(request: Request, response: Response, elapsedTime: Long) {
        val responseBody = response.peekBody(1024 * 1024) // 最多读取1MB

        Log.d("NetworkResponse", """
            ════════════════════════════════════════
            URL: ${request.url}
            Status Code: ${response.code}
            Message: ${response.message}
            Duration: ${TimeUnit.NANOSECONDS.toMillis(elapsedTime)}ms
            Headers: ${response.headers}
            Body: ${responseBody.string()}
            ════════════════════════════════════════
        """.trimIndent())
    }

    private fun logError(request: Request, error: Exception, elapsedTime: Long) {
        Log.e("NetworkError", """
            ════════════════════════════════════════
            URL: ${request.url}
            Duration: ${TimeUnit.NANOSECONDS.toMillis(elapsedTime)}ms
            Error: ${error.javaClass.simpleName}
            Message: ${error.message}
            ════════════════════════════════════════
        """.trimIndent(), error)
    }

    private fun readBody(body: RequestBody): String {
        val buffer = Buffer()
        body.writeTo(buffer)
        return buffer.readUtf8()
    }
}

3. 网络日志持久化

// 将网络日志保存到文件
class FileLoggingInterceptor(
    private val context: Context
) : Interceptor {

    private val logFile = File(context.getExternalFilesDir(null), "network_logs.txt")
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.currentTimeMillis()

        val response = chain.proceed(request)
        val duration = System.currentTimeMillis() - startTime

        // 记录到文件
        saveLogToFile(
            timestamp = dateFormat.format(Date(startTime)),
            method = request.method,
            url = request.url.toString(),
            statusCode = response.code,
            duration = duration
        )

        return response
    }

    private fun saveLogToFile(
        timestamp: String,
        method: String,
        url: String,
        statusCode: Int,
        duration: Long
    ) {
        val logEntry = "$timestamp | $method | $url | $statusCode | ${duration}ms\n"

        try {
            logFile.appendText(logEntry)

            // 限制文件大小,超过10MB则清空
            if (logFile.length() > 10 * 1024 * 1024) {
                logFile.delete()
            }
        } catch (e: IOException) {
            Log.e("FileLogging", "Failed to write log", e)
        }
    }

    // 读取日志文件
    fun getLogs(): String {
        return try {
            logFile.readText()
        } catch (e: IOException) {
            ""
        }
    }

    // 清空日志
    fun clearLogs() {
        logFile.delete()
    }
}

性能监控

1. 网络性能监控拦截器

class NetworkPerformanceInterceptor : Interceptor {
    private val metricsCollector = NetworkMetricsCollector()

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val url = request.url.toString()

        val startTime = System.nanoTime()
        var requestBytesSent = 0L
        var responseBytesReceived = 0L

        try {
            // 计算请求大小
            request.body?.let {
                requestBytesSent = it.contentLength()
            }

            val response = chain.proceed(request)

            // 计算响应大小
            responseBytesReceived = response.body?.contentLength() ?: 0L

            val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)

            // 收集指标
            metricsCollector.recordSuccess(
                url = url,
                method = request.method,
                statusCode = response.code,
                duration = duration,
                requestBytes = requestBytesSent,
                responseBytes = responseBytesReceived
            )

            return response
        } catch (e: Exception) {
            val duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)

            metricsCollector.recordFailure(
                url = url,
                method = request.method,
                duration = duration,
                error = e
            )

            throw e
        }
    }

    fun getMetrics(): NetworkMetrics = metricsCollector.getMetrics()
}

// 网络指标收集器
class NetworkMetricsCollector {
    private val metrics = mutableMapOf<String, ApiMetrics>()

    fun recordSuccess(
        url: String,
        method: String,
        statusCode: Int,
        duration: Long,
        requestBytes: Long,
        responseBytes: Long
    ) {
        val key = "$method:$url"
        val apiMetrics = metrics.getOrPut(key) { ApiMetrics() }

        apiMetrics.totalRequests++
        apiMetrics.successRequests++
        apiMetrics.totalDuration += duration
        apiMetrics.totalRequestBytes += requestBytes
        apiMetrics.totalResponseBytes += responseBytes

        if (duration > apiMetrics.maxDuration) {
            apiMetrics.maxDuration = duration
        }

        if (duration < apiMetrics.minDuration || apiMetrics.minDuration == 0L) {
            apiMetrics.minDuration = duration
        }
    }

    fun recordFailure(
        url: String,
        method: String,
        duration: Long,
        error: Exception
    ) {
        val key = "$method:$url"
        val apiMetrics = metrics.getOrPut(key) { ApiMetrics() }

        apiMetrics.totalRequests++
        apiMetrics.failedRequests++
        apiMetrics.totalDuration += duration
        apiMetrics.errors.add(error.javaClass.simpleName)
    }

    fun getMetrics(): NetworkMetrics {
        return NetworkMetrics(
            apiMetrics = metrics.toMap(),
            totalRequests = metrics.values.sumOf { it.totalRequests },
            successRate = calculateSuccessRate()
        )
    }

    private fun calculateSuccessRate(): Double {
        val total = metrics.values.sumOf { it.totalRequests }
        val success = metrics.values.sumOf { it.successRequests }
        return if (total > 0) (success.toDouble() / total) * 100 else 0.0
    }
}

data class ApiMetrics(
    var totalRequests: Int = 0,
    var successRequests: Int = 0,
    var failedRequests: Int = 0,
    var totalDuration: Long = 0,
    var maxDuration: Long = 0,
    var minDuration: Long = 0,
    var totalRequestBytes: Long = 0,
    var totalResponseBytes: Long = 0,
    val errors: MutableList<String> = mutableListOf()
) {
    val avgDuration: Long
        get() = if (totalRequests > 0) totalDuration / totalRequests else 0

    val successRate: Double
        get() = if (totalRequests > 0) (successRequests.toDouble() / totalRequests) * 100 else 0.0
}

data class NetworkMetrics(
    val apiMetrics: Map<String, ApiMetrics>,
    val totalRequests: Int,
    val successRate: Double
)

2. 慢请求追踪

class SlowRequestTracker(
    private val thresholdMs: Long = 3000  // 慢请求阈值:3秒
) : Interceptor {

    private val slowRequests = mutableListOf<SlowRequestRecord>()

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.currentTimeMillis()

        val response = chain.proceed(request)

        val duration = System.currentTimeMillis() - startTime

        // 记录慢请求
        if (duration > thresholdMs) {
            recordSlowRequest(
                url = request.url.toString(),
                method = request.method,
                duration = duration,
                statusCode = response.code
            )
        }

        return response
    }

    private fun recordSlowRequest(
        url: String,
        method: String,
        duration: Long,
        statusCode: Int
    ) {
        synchronized(slowRequests) {
            slowRequests.add(
                SlowRequestRecord(
                    timestamp = System.currentTimeMillis(),
                    url = url,
                    method = method,
                    duration = duration,
                    statusCode = statusCode
                )
            )

            // 保留最近100条
            if (slowRequests.size > 100) {
                slowRequests.removeAt(0)
            }
        }

        Log.w("SlowRequest", "⚠️ 慢请求检测: $method $url 耗时 ${duration}ms")
    }

    fun getSlowRequests(): List<SlowRequestRecord> {
        return synchronized(slowRequests) {
            slowRequests.toList()
        }
    }

    fun clearSlowRequests() {
        synchronized(slowRequests) {
            slowRequests.clear()
        }
    }
}

data class SlowRequestRecord(
    val timestamp: Long,
    val url: String,
    val method: String,
    val duration: Long,
    val statusCode: Int
)

3. 性能监控Dashboard

// ViewModel for displaying network metrics
class NetworkMonitorViewModel : ViewModel() {
    private val performanceInterceptor: NetworkPerformanceInterceptor = // inject

    private val _metrics = MutableLiveData<NetworkMetrics>()
    val metrics: LiveData<NetworkMetrics> = _metrics

    private val _slowRequests = MutableLiveData<List<SlowRequestRecord>>()
    val slowRequests: LiveData<List<SlowRequestRecord>> = _slowRequests

    fun refreshMetrics() {
        _metrics.value = performanceInterceptor.getMetrics()
    }

    fun refreshSlowRequests() {
        _slowRequests.value = slowRequestTracker.getSlowRequests()
    }
}

// UI Fragment
class NetworkMonitorFragment : Fragment() {
    private val viewModel: NetworkMonitorViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.metrics.observe(viewLifecycleOwner) { metrics ->
            displayMetrics(metrics)
        }

        viewModel.slowRequests.observe(viewLifecycleOwner) { slowRequests ->
            displaySlowRequests(slowRequests)
        }

        viewModel.refreshMetrics()
        viewModel.refreshSlowRequests()
    }

    private fun displayMetrics(metrics: NetworkMetrics) {
        binding.apply {
            totalRequestsText.text = "总请求数: ${metrics.totalRequests}"
            successRateText.text = "成功率: ${"%.2f".format(metrics.successRate)}%"

            // 显示各API的详细指标
            val metricsText = metrics.apiMetrics.entries.joinToString("\n\n") { (api, apiMetrics) ->
                """
                $api
                - 请求次数: ${apiMetrics.totalRequests}
                - 成功率: ${"%.2f".format(apiMetrics.successRate)}%
                - 平均耗时: ${apiMetrics.avgDuration}ms
                - 最大耗时: ${apiMetrics.maxDuration}ms
                - 数据上传: ${formatBytes(apiMetrics.totalRequestBytes)}
                - 数据下载: ${formatBytes(apiMetrics.totalResponseBytes)}
                """.trimIndent()
            }

            metricsDetailText.text = metricsText
        }
    }

    private fun displaySlowRequests(slowRequests: List<SlowRequestRecord>) {
        val text = slowRequests.joinToString("\n\n") { record ->
            val date = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(record.timestamp))
            "$date | ${record.method} | ${record.url}\n耗时: ${record.duration}ms"
        }

        binding.slowRequestsText.text = text.ifEmpty { "无慢请求记录" }
    }

    private fun formatBytes(bytes: Long): String {
        return when {
            bytes < 1024 -> "$bytes B"
            bytes < 1024 * 1024 -> "${bytes / 1024} KB"
            else -> "${bytes / (1024 * 1024)} MB"
        }
    }
}

多环境配置

1. BuildConfig环境配置

// build.gradle.kts
android {
    buildTypes {
        debug {
            buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com\"")
            buildConfigField("String", "API_KEY", "\"dev_key_123\"")
            buildConfigField("boolean", "ENABLE_LOGGING", "true")
        }

        release {
            buildConfigField("String", "BASE_URL", "\"https://api.example.com\"")
            buildConfigField("String", "API_KEY", "\"prod_key_456\"")
            buildConfigField("boolean", "ENABLE_LOGGING", "false")
        }
    }

    flavorDimensions += "environment"
    productFlavors {
        create("dev") {
            dimension = "environment"
            applicationIdSuffix = ".dev"
            buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com\"")
        }

        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
            buildConfigField("String", "BASE_URL", "\"https://staging-api.example.com\"")
        }

        create("prod") {
            dimension = "environment"
            buildConfigField("String", "BASE_URL", "\"https://api.example.com\"")
        }
    }
}

// 使用BuildConfig
class NetworkModule {
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .build()
    }
}

2. 动态环境切换

// 环境管理器
object EnvironmentManager {
    private const val PREF_KEY_ENV = "current_environment"

    enum class Environment(val baseUrl: String, val displayName: String) {
        DEV("https://dev-api.example.com", "开发环境"),
        STAGING("https://staging-api.example.com", "测试环境"),
        PRODUCTION("https://api.example.com", "生产环境")
    }

    private var currentEnvironment: Environment = Environment.DEV

    fun init(context: Context) {
        val prefs = context.getSharedPreferences("app_config", Context.MODE_PRIVATE)
        val envName = prefs.getString(PREF_KEY_ENV, Environment.DEV.name)
        currentEnvironment = Environment.valueOf(envName ?: Environment.DEV.name)
    }

    fun getCurrentEnvironment(): Environment = currentEnvironment

    fun switchEnvironment(context: Context, environment: Environment) {
        currentEnvironment = environment

        val prefs = context.getSharedPreferences("app_config", Context.MODE_PRIVATE)
        prefs.edit().putString(PREF_KEY_ENV, environment.name).apply()

        // 重启应用以应用新环境
        restartApp(context)
    }

    private fun restartApp(context: Context) {
        val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
        intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
        context.startActivity(intent)
        System.exit(0)
    }
}

// Retrofit配置使用动态环境
class NetworkModule(private val context: Context) {
    fun provideRetrofit(): Retrofit {
        EnvironmentManager.init(context)

        return Retrofit.Builder()
            .baseUrl(EnvironmentManager.getCurrentEnvironment().baseUrl)
            .client(provideOkHttpClient())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

3. 环境切换UI

// 环境切换Dialog
class EnvironmentSwitchDialog : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val environments = EnvironmentManager.Environment.values()
        val currentEnv = EnvironmentManager.getCurrentEnvironment()

        val items = environments.map { it.displayName }.toTypedArray()
        val checkedItem = environments.indexOf(currentEnv)

        return AlertDialog.Builder(requireContext())
            .setTitle("选择环境")
            .setSingleChoiceItems(items, checkedItem) { dialog, which ->
                val selectedEnv = environments[which]

                AlertDialog.Builder(requireContext())
                    .setTitle("确认切换")
                    .setMessage("切换到${selectedEnv.displayName}?\n应用将重启。")
                    .setPositiveButton("确定") { _, _ ->
                        EnvironmentManager.switchEnvironment(requireContext(), selectedEnv)
                    }
                    .setNegativeButton("取消", null)
                    .show()

                dialog.dismiss()
            }
            .setNegativeButton("取消", null)
            .create()
    }
}

// 在设置页面添加入口(仅Debug模式)
class SettingsFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        if (BuildConfig.DEBUG) {
            binding.environmentSwitchButton.visibility = View.VISIBLE
            binding.environmentSwitchButton.setOnClickListener {
                EnvironmentSwitchDialog().show(childFragmentManager, "env_switch")
            }

            // 显示当前环境
            binding.currentEnvironmentText.text =
                "当前环境: ${EnvironmentManager.getCurrentEnvironment().displayName}"
        } else {
            binding.environmentSwitchButton.visibility = View.GONE
        }
    }
}

Mock测试

1. MockWebServer单元测试

class ApiServiceTest {
    private lateinit var mockWebServer: MockWebServer
    private lateinit var apiService: ApiService

    @Before
    fun setUp() {
        mockWebServer = MockWebServer()
        mockWebServer.start()

        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("/"))
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        apiService = retrofit.create(ApiService::class.java)
    }

    @After
    fun tearDown() {
        mockWebServer.shutdown()
    }

    @Test
    fun `test get devices success`() = runBlocking {
        // 准备Mock响应
        val mockResponse = """
            {
                "devices": [
                    {"id": "1", "name": "Camera 1"},
                    {"id": "2", "name": "Camera 2"}
                ]
            }
        """.trimIndent()

        mockWebServer.enqueue(
            MockResponse()
                .setResponseCode(200)
                .setBody(mockResponse)
        )

        // 执行请求
        val response = apiService.getDevices()

        // 验证结果
        assertTrue(response.isSuccessful)
        assertEquals(2, response.body()?.devices?.size)

        // 验证请求
        val request = mockWebServer.takeRequest()
        assertEquals("GET", request.method)
        assertEquals("/devices", request.path)
    }

    @Test
    fun `test get devices with network error`() = runBlocking {
        // Mock网络错误
        mockWebServer.enqueue(
            MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
        )

        // 执行并捕获异常
        val exception = assertThrows(IOException::class.java) {
            runBlocking {
                apiService.getDevices()
            }
        }

        assertNotNull(exception)
    }

    @Test
    fun `test get devices with 500 error`() = runBlocking {
        // Mock服务器错误
        mockWebServer.enqueue(
            MockResponse()
                .setResponseCode(500)
                .setBody("{\"error\": \"Internal Server Error\"}")
        )

        val response = apiService.getDevices()

        assertFalse(response.isSuccessful)
        assertEquals(500, response.code())
    }
}

2. Interceptor Mock for UI测试

class MockInterceptor : Interceptor {
    private val mockResponses = mutableMapOf<String, MockResponse>()

    fun registerMockResponse(urlPattern: String, response: MockResponse) {
        mockResponses[urlPattern] = response
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val url = request.url.toString()

        // 查找匹配的Mock响应
        val mockResponse = mockResponses.entries.firstOrNull { (pattern, _) ->
            url.contains(pattern)
        }?.value

        return if (mockResponse != null) {
            // 返回Mock响应
            Response.Builder()
                .code(mockResponse.code)
                .message(mockResponse.message)
                .body(mockResponse.body.toResponseBody("application/json".toMediaType()))
                .protocol(Protocol.HTTP_1_1)
                .request(request)
                .build()
        } else {
            // 返回真实请求
            chain.proceed(request)
        }
    }

    data class MockResponse(
        val code: Int,
        val message: String = "",
        val body: String
    )
}

// 使用示例
class NetworkModule {
    fun provideOkHttpClient(useMock: Boolean = false): OkHttpClient {
        val builder = OkHttpClient.Builder()

        if (useMock && BuildConfig.DEBUG) {
            val mockInterceptor = MockInterceptor().apply {
                // 注册Mock响应
                registerMockResponse(
                    urlPattern = "/devices",
                    response = MockInterceptor.MockResponse(
                        code = 200,
                        body = """
                            {
                                "devices": [
                                    {"id": "1", "name": "Mock Camera 1"},
                                    {"id": "2", "name": "Mock Camera 2"}
                                ]
                            }
                        """.trimIndent()
                    )
                )
            }

            builder.addInterceptor(mockInterceptor)
        }

        return builder.build()
    }
}

3. Repository Mock for ViewModel测试

// Repository接口
interface DeviceRepository {
    suspend fun getDevices(): Result<List<Device>>
    suspend fun getDeviceDetail(deviceId: String): Result<Device>
}

// Fake实现(用于测试)
class FakeDeviceRepository : DeviceRepository {
    private val devices = mutableListOf(
        Device("1", "Camera 1", true),
        Device("2", "Camera 2", false)
    )

    var shouldReturnError = false
    var networkDelay = 0L

    override suspend fun getDevices(): Result<List<Device>> {
        delay(networkDelay)

        return if (shouldReturnError) {
            Result.failure(IOException("Network error"))
        } else {
            Result.success(devices)
        }
    }

    override suspend fun getDeviceDetail(deviceId: String): Result<Device> {
        delay(networkDelay)

        return if (shouldReturnError) {
            Result.failure(IOException("Network error"))
        } else {
            val device = devices.find { it.id == deviceId }
            if (device != null) {
                Result.success(device)
            } else {
                Result.failure(NoSuchElementException("Device not found"))
            }
        }
    }

    // 测试辅助方法
    fun addDevice(device: Device) {
        devices.add(device)
    }

    fun clearDevices() {
        devices.clear()
    }
}

// ViewModel测试
class DeviceViewModelTest {
    private lateinit var viewModel: DeviceViewModel
    private lateinit var fakeRepository: FakeDeviceRepository

    @Before
    fun setUp() {
        fakeRepository = FakeDeviceRepository()
        viewModel = DeviceViewModel(fakeRepository)
    }

    @Test
    fun `load devices success`() = runTest {
        // 准备数据
        fakeRepository.addDevice(Device("3", "Camera 3", true))

        // 执行
        viewModel.loadDevices()

        // 验证
        val state = viewModel.devicesState.value
        assertTrue(state is UiState.Success)
        assertEquals(3, (state as UiState.Success).data.size)
    }

    @Test
    fun `load devices with network error`() = runTest {
        // 设置错误
        fakeRepository.shouldReturnError = true

        // 执行
        viewModel.loadDevices()

        // 验证
        val state = viewModel.devicesState.value
        assertTrue(state is UiState.Error)
        assertEquals("Network error", (state as UiState.Error).message)
    }

    @Test
    fun `load devices with loading state`() = runTest {
        // 设置延迟
        fakeRepository.networkDelay = 1000L

        // 执行
        val job = launch {
            viewModel.loadDevices()
        }

        // 验证加载状态
        advanceTimeBy(500)
        assertTrue(viewModel.devicesState.value is UiState.Loading)

        // 等待完成
        advanceTimeBy(600)
        job.join()
        assertTrue(viewModel.devicesState.value is UiState.Success)
    }
}

网络调试工具

1. Charles代理配置

步骤

  1. 安装Charles并启动
  2. 手机和电脑连接同一WiFi
  3. 手机WiFi设置中配置HTTP代理
    • 服务器:电脑IP
    • 端口:8888
  4. 安装Charles证书(抓取HTTPS)
  5. Android 7.0+需要配置network_security_config.xml
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- Debug模式信任用户证书 -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
            <certificates src="system" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

<!-- AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config">
</application>

2. Flipper集成

// build.gradle.kts
dependencies {
    debugImplementation("com.facebook.flipper:flipper:0.164.0")
    debugImplementation("com.facebook.flipper:flipper-network-plugin:0.164.0")
    debugImplementation("com.facebook.soloader:soloader:0.10.4")

    releaseImplementation("com.facebook.flipper:flipper-noop:0.164.0")
}

// Application初始化
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        if (BuildConfig.DEBUG) {
            SoLoader.init(this, false)

            val client = AndroidFlipperClient.getInstance(this)
            client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
            client.addPlugin(NetworkFlipperPlugin())
            client.start()
        }
    }
}

// OkHttp配置
fun provideOkHttpClient(): OkHttpClient {
    val builder = OkHttpClient.Builder()

    if (BuildConfig.DEBUG) {
        builder.addNetworkInterceptor(FlipperOkhttpInterceptor(NetworkFlipperPlugin()))
    }

    return builder.build()
}

3. Chucker实时网络监控

// build.gradle.kts
dependencies {
    debugImplementation("com.github.chuckerteam.chucker:library:3.5.2")
    releaseImplementation("com.github.chuckerteam.chucker:library-no-op:3.5.2")
}

// OkHttp配置
class NetworkModule(private val context: Context) {
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(
                ChuckerInterceptor.Builder(context)
                    .maxContentLength(250_000L)
                    .redactHeaders("Authorization", "Bearer")
                    .alwaysReadResponseBody(true)
                    .build()
            )
            .build()
    }
}

// 在通知栏显示网络请求
// Chucker会自动在通知栏显示请求详情,点击可查看完整信息

最佳实践

1. 日志安全

class SecureLoggingInterceptor : Interceptor {
    private val sensitiveHeaders = setOf(
        "Authorization",
        "API-Key",
        "X-Auth-Token"
    )

    private val sensitiveQueryParams = setOf(
        "password",
        "token",
        "api_key"
    )

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()

        // 脱敏后记录
        val safeUrl = redactUrl(request.url)
        val safeHeaders = redactHeaders(request.headers)

        Log.d("SecureRequest", """
            URL: $safeUrl
            Headers: $safeHeaders
        """.trimIndent())

        return chain.proceed(request)
    }

    private fun redactUrl(url: HttpUrl): String {
        val sanitizedQuery = url.queryParameterNames.joinToString("&") { param ->
            val value = if (param in sensitiveQueryParams) {
                "***"
            } else {
                url.queryParameter(param)
            }
            "$param=$value"
        }

        return "${url.scheme}://${url.host}${url.encodedPath}?$sanitizedQuery"
    }

    private fun redactHeaders(headers: Headers): String {
        return headers.names().joinToString("\n") { name ->
            val value = if (name in sensitiveHeaders) {
                "***"
            } else {
                headers[name]
            }
            "$name: $value"
        }
    }
}

2. 性能监控最佳实践

只在Debug模式开启详细日志:避免Release版本性能损耗
异步记录日志:避免阻塞主线程
限制日志大小:防止磁盘空间占用过多
脱敏敏感信息:保护用户隐私和API密钥
合理的采样率:生产环境只记录关键指标

3. Mock测试最佳实践

单元测试使用MockWebServer:完全控制响应,测试边界条件
UI测试使用Interceptor Mock:快速验证界面,无需真实后端
ViewModel测试使用Repository Mock:隔离网络层,测试业务逻辑
集成测试使用Fake实现:模拟复杂场景,保持测试稳定


总结

本文全面讲解了Android网络监控与测试的核心技术和最佳实践:

核心知识点

  1. 网络日志监控 - HttpLoggingInterceptor、自定义日志拦截器、日志持久化
  2. 性能监控 - 请求耗时、流量统计、慢请求追踪、监控Dashboard
  3. 多环境配置 - BuildConfig、动态环境切换、环境切换UI
  4. Mock测试 - MockWebServer、Interceptor Mock、Repository Mock、Fake实现
  5. 调试工具 - Charles代理、Flipper、Chucker
  6. 最佳实践 - 日志安全、性能优化、测试策略

实践要点

开发阶段 - 使用详细日志和Chucker实时监控网络请求
测试阶段 - Mock网络请求进行UI测试,使用不同环境验证功能
性能优化 - 监控慢请求、优化网络性能、追踪异常请求
生产环境 - 关闭详细日志、保留关键指标、保护敏感信息
自动化测试 - 编写单元测试、集成测试,确保网络层稳定性

延伸学习

掌握网络监控与测试技术,能够快速定位网络问题、保证应用质量、提升开发效率,为构建稳定可靠的网络层打下坚实基础。


文档更新时间:2026-03-21

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐