稳定性性能系列之十四——电量与网络优化:Battery Historian与弱网处理实战

引言

你有没有遇到过这样的场景:用户投诉说「你们的App太耗电了,半天就把我手机电用完了」,或者「网络不好的时候你们的App根本用不了」。作为开发者,这两个问题可能是用户体验杀手中最致命的------电量和网络。

电量优化和网络优化看似是两个独立的主题,但它们有着密切的关系:网络请求是移动设备最大的电量消耗源之一。一个糟糕的网络策略不仅让用户体验变差,还会疯狂吃掉手机电量。

今天这篇文章,我会带你深入Android的电量管理机制,教你用Battery Historian这个神器分析电量消耗,理解Doze模式和Wakelock的优化技巧,并分享弱网环境下的实战优化方案。读完这篇文章,你将掌握:

  • Android电量管理的核心机制(Doze、App Standby)
  • Battery Historian工具的完整使用流程
  • Wakelock滥用检测和优化方法
  • 弱网环境识别和优化策略
  • 网络请求的最佳实践

准备好了吗?让我们开始这场「省电又流畅」的优化之旅!

Android电量管理机制

电量消耗的主要来源

在优化电量之前,我们先要知道电都去哪儿了。Android设备的电量消耗主要来自:

对于应用开发者来说,我们能优化的主要是网络通信、CPU运算、定位服务 这三大块。而网络优化又是重中之重,因为一次网络请求会同时消耗网络模块和CPU的电量。

PowerManager与电源管理服务

Android的电源管理由PowerManager系统服务负责。它提供了多种电源状态:

kotlin 复制代码
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager

// 检查设备是否处于省电模式
val isPowerSaveMode = powerManager.isPowerSaveMode

// 检查是否处于Doze模式
val isDeviceIdleMode = powerManager.isDeviceIdleMode

// 检查应用是否被限制后台活动
val isIgnoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(packageName)

Doze模式:深度睡眠的艺术

Doze模式是Android 6.0引入的重磅功能,目的是在设备长时间静置时进入深度睡眠,极大降低电量消耗。

Doze模式触发条件

设备必须同时满足以下条件才会进入Doze:

  • 屏幕关闭
  • 设备未在充电
  • 设备静置(没有移动)
  • 一段时间没有使用(通常是1-2小时)

Doze模式的限制

一旦进入Doze,系统会施加以下限制:

  1. 网络访问暂停 - 除了高优先级FCM消息
  2. WakeLock被忽略 - 不再能唤醒CPU
  3. Alarm延迟执行 - setExact()setWindow()推迟到维护窗口
  4. WiFi扫描停止
  5. JobScheduler和SyncAdapter暂停

但系统会定期进入维护窗口(Maintenance Window),让应用执行延迟的任务:

makefile 复制代码
初次进入Doze后的维护窗口时间间隔:
第1次: 1小时后,维护窗口5分钟
第2次: 2小时后,维护窗口5分钟
第3次: 4小时后,维护窗口5分钟
第4次及以后: 每6小时一次,维护窗口5分钟

如何适配Doze模式

错误做法 - 强行保持运行:

kotlin 复制代码
// ❌ 不要申请白名单,这只是个临时方案
val intent = Intent().apply {
    action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
    data = Uri.parse("package:$packageName")
}
startActivity(intent)

正确做法 - 使用JobScheduler:

kotlin 复制代码
// ✅ 使用JobScheduler调度任务
val jobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler

val job = JobInfo.Builder(JOB_ID, ComponentName(this, MyJobService::class.java))
    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
    .setPersisted(true)  // 设备重启后任务保留
    .build()

jobScheduler.schedule(job)
kotlin 复制代码
class MyJobService : JobService() {
    override fun onStartJob(params: JobParameters?): Boolean {
        // 执行后台任务
        doWorkAsync {
            // 任务完成后调用
            jobFinished(params, false)
        }
        return true  // 返回true表示任务还在执行
    }

    override fun onStopJob(params: JobParameters?): Boolean {
        // 任务被系统中断时调用
        return true  // 返回true表示需要重新调度
    }
}

使用WorkManager(推荐):

kotlin 复制代码
// ✅ 更推荐使用WorkManager,它会自动处理兼容性
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresBatteryNotLow(true)
    .build()

val syncWorkRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES
)
    .setConstraints(constraints)
    .build()

WorkManager.getInstance(context).enqueue(syncWorkRequest)

App Standby:待机桶管理

Android 9.0引入了App Standby Buckets(待机桶)机制,将应用分为5个优先级桶:

桶名称 说明 限制
Active 用户正在使用 无限制
Working Set 最近使用过 限制较少
Frequent 经常使用 中等限制
Rare 很少使用 严格限制
Never 从未使用或被禁用 极严格限制

系统会根据应用使用频率自动调整桶分类,分类越低,后台任务执行机会越少:

kotlin 复制代码
// 查询当前应用所在的桶
val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val bucket = usageStatsManager.appStandbyBucket

val bucketName = when (bucket) {
    UsageStatsManager.STANDBY_BUCKET_ACTIVE -> "Active"
    UsageStatsManager.STANDBY_BUCKET_WORKING_SET -> "Working Set"
    UsageStatsManager.STANDBY_BUCKET_FREQUENT -> "Frequent"
    UsageStatsManager.STANDBY_BUCKET_RARE -> "Rare"
    UsageStatsManager.STANDBY_BUCKET_NEVER -> "Never"
    else -> "Unknown"
}

Log.d("Standby", "Current bucket: $bucketName")

Battery Historian:电量分析神器

Battery Historian是Google官方的电量分析工具,能够以可视化方式展示设备电量消耗的详细信息。

环境搭建

方法一:Docker方式(推荐)

bash 复制代码
# 1. 安装Docker
# Linux: sudo apt install docker.io
# macOS: 下载Docker Desktop

# 2. 拉取Battery Historian镜像
docker pull gcr.io/android-battery-historian/stable:3.1

# 3. 运行容器
docker run -p 9999:9999 gcr.io/android-battery-historian/stable:3.1

# 4. 浏览器打开
# http://localhost:9999

方法二:源码编译(需要Go环境)

bash 复制代码
# 1. 安装Go语言环境(1.11+)
# https://golang.org/dl/

# 2. 获取源码
go get -d -u github.com/google/battery-historian/...

# 3. 进入目录并运行
cd $GOPATH/src/github.com/google/battery-historian
go run cmd/battery-historian/battery-historian.go

# 4. 浏览器打开
# http://localhost:9999

数据采集流程

1. 重置电量统计

bash 复制代码
# 重置电量统计数据
adb shell dumpsys batterystats --reset

2. 拔掉USB,进行测试操作

这一步非常重要:必须拔掉USB线,因为充电状态会影响很多行为。

执行你想分析的操作,比如:

  • 启动应用并使用一段时间
  • 让应用在后台运行
  • 触发特定功能

3. 导出电量数据

bash 复制代码
# 导出完整的电量统计数据
adb bugreport bugreport.zip

这个命令会生成一个zip文件,包含了详细的电量使用日志。

4. 上传到Battery Historian分析

打开 http://localhost:9999,上传 bugreport.zip 文件,就可以看到可视化的分析报告了。

报告解读:关键指标

Battery Historian的报告非常丰富,我们重点关注以下几个关键指标:

1. Battery Level(电量曲线)

最上方的绿色曲线显示电量变化。陡降说明耗电快,需要重点分析该时间段。

2. Screen(屏幕状态)

蓝色表示屏幕亮起,灰色表示熄屏。帮助区分前台和后台耗电。

3. Mobile Radio(移动网络状态)

显示网络状态:active > idle > off。频繁的active状态说明网络请求多。

4. GPS(定位状态)

绿色表示GPS开启。定位是耗电大户,要避免持续定位。

5. Wakelock(唤醒锁)

显示哪些应用持有Wakelock。这是重点,Wakelock滥用是耗电的主要原因之一。

6. App Usage(应用使用情况)

显示各应用的前台/后台运行时间和电量占比。

实战案例:定位耗电异常

场景: 用户反馈后台耗电严重。

分析步骤

  1. 查看电量曲线 - 发现夜间电量陡降15%
  2. 检查Screen状态 - 确认屏幕是熄灭的,排除用户操作
  3. 定位时间段 - 在凌晨2点到5点之间
  4. 检查Wakelock - 发现应用持有了大量Wakelock
  5. 查看具体Wakelock名称 - SyncService$WakeLock 持有了2小时45分钟!

问题定位: 同步服务的Wakelock没有正确释放,导致CPU无法睡眠。

修复方案:

kotlin 复制代码
// ❌ 错误:Wakelock一直持有
val wakeLock = powerManager.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK,
    "SyncService::WakeLock"
)
wakeLock.acquire()  // 获取后忘记释放

// ✅ 正确:设置超时时间
wakeLock.acquire(10 * 60 * 1000L)  // 最多持有10分钟

// ✅ 更好:使用try-finally确保释放
wakeLock.acquire()
try {
    doSync()
} finally {
    if (wakeLock.isHeld) {
        wakeLock.release()
    }
}

Wakelock深度分析与优化

Wakelock类型

Android提供了几种Wakelock类型,从完全唤醒到仅保持CPU运行:

kotlin 复制代码
// 1. PARTIAL_WAKE_LOCK - 只保持CPU运行,屏幕可以关闭(最常用)
val partialWakeLock = powerManager.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK,
    "MyApp::PartialWakeLock"
)

// 2. SCREEN_DIM_WAKE_LOCK - 保持屏幕低亮度(已废弃)
// 3. SCREEN_BRIGHT_WAKE_LOCK - 保持屏幕高亮度(已废弃)
// 4. FULL_WAKE_LOCK - 完全唤醒(已废弃)

// 现代做法:使用WindowManager.LayoutParams
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

Wakelock最佳实践

1. 始终设置超时时间

kotlin 复制代码
// ❌ 危险:没有超时时间
wakeLock.acquire()

// ✅ 安全:设置超时(毫秒)
wakeLock.acquire(10 * 60 * 1000L)  // 10分钟后自动释放

2. 使用WakefulBroadcastReceiver(旧API)

kotlin 复制代码
class MyWakefulReceiver : WakefulBroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // 自动获取Wakelock
        val componentName = startWakefulService(context, Intent(context, MyService::class.java))
    }
}

class MyService : IntentService("MyService") {
    override fun onHandleIntent(intent: Intent?) {
        try {
            // 处理任务
            doWork()
        } finally {
            // 释放Wakelock
            MyWakefulReceiver.completeWakefulIntent(intent)
        }
    }
}

3. 使用WorkManager(现代做法)

kotlin 复制代码
// WorkManager自动管理Wakelock,无需手动处理
class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        // 任务执行期间,WorkManager自动持有Wakelock
        doSyncWork()
        return Result.success()
    }
}

检测Wakelock滥用

使用adb命令

bash 复制代码
# 查看当前所有Wakelock
adb shell dumpsys power | grep "Wake Locks"

# 查看详细的Wakelock统计
adb shell dumpsys batterystats | grep -A 20 "Wake lock"

使用Battery Historian

在Battery Historian报告中,点击 Wakelock 行,可以看到:

  • 每个Wakelock的持有时长
  • 触发次数
  • 持有Wakelock的应用

如果发现某个Wakelock持有时间超过5分钟,基本可以判定为滥用。

Energy Profiler:实时电量监控

Android Studio 3.0+内置了Energy Profiler,可以实时监控应用的电量消耗。

使用步骤

  1. 打开Android Studio Profiler

    • View → Tool Windows → Profiler
    • 点击 "+" 选择设备和应用
  2. 选择Energy

    • 点击 "ENERGY" 标签
  3. 查看能耗曲线

    • 绿色:低能耗
    • 黄色:中等能耗
    • 红色:高能耗
  4. 分析能耗事件

    • CPU
    • Network
    • Location
    • Wake Lock

实战:定位网络耗电

Energy Profiler会标记出高能耗的网络请求。点击某个网络事件,可以看到:

  • 请求URL
  • 数据量
  • 持续时间
  • 调用堆栈

优化建议:

  • 合并小请求,减少网络唤醒次数
  • 使用批量接口
  • 缓存可复用数据
  • 在WiFi环境下预下载

网络性能监控

Network Profiler使用

Network Profiler是Android Studio内置的网络分析工具,可以实时查看:

  • 网络请求时间线
  • 上传/下载速度
  • 请求详情(URL、Header、Body)
  • 请求/响应耗时

使用步骤

  1. 打开Profiler - View → Tool Windows → Profiler
  2. 选择Network - 点击 "NETWORK" 标签
  3. 触发网络请求 - 使用应用,观察网络曲线
  4. 点击请求详情 - 查看完整的Request/Response信息

网络质量监控实现

手动实现一个简单的网络质量监控:

kotlin 复制代码
class NetworkQualityMonitor(private val context: Context) {

    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    // 网络质量等级
    enum class NetworkQuality {
        EXCELLENT,  // 优秀
        GOOD,       // 良好
        MODERATE,   // 中等
        POOR,       // 较差
        VERY_POOR   // 极差
    }

    // 获取当前网络类型
    fun getNetworkType(): String {
        val network = connectivityManager.activeNetwork ?: return "NONE"
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return "NONE"

        return when {
            capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "WIFI"
            capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
                // 获取具体的移动网络类型
                val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
                when (telephonyManager.dataNetworkType) {
                    TelephonyManager.NETWORK_TYPE_LTE -> "4G"
                    TelephonyManager.NETWORK_TYPE_NR -> "5G"
                    else -> "3G"
                }
            }
            else -> "OTHER"
        }
    }

    // 测量网络延迟(RTT)和带宽
    suspend fun measureNetworkQuality(): NetworkQuality = withContext(Dispatchers.IO) {
        val startTime = System.currentTimeMillis()

        try {
            // 1. 测试延迟 - ping一个小文件
            val pingUrl = URL("https://www.google.com/generate_204")
            val connection = pingUrl.openConnection() as HttpURLConnection
            connection.requestMethod = "GET"
            connection.connectTimeout = 5000
            connection.readTimeout = 5000

            val responseCode = connection.responseCode
            val rtt = System.currentTimeMillis() - startTime

            connection.disconnect()

            // 2. 根据RTT判断网络质量
            return@withContext when {
                rtt < 100 -> NetworkQuality.EXCELLENT    // <100ms 优秀
                rtt < 300 -> NetworkQuality.GOOD         // 100-300ms 良好
                rtt < 600 -> NetworkQuality.MODERATE     // 300-600ms 中等
                rtt < 1000 -> NetworkQuality.POOR        // 600-1000ms 较差
                else -> NetworkQuality.VERY_POOR         // >1000ms 极差
            }

        } catch (e: Exception) {
            Log.e("NetworkQuality", "测量失败", e)
            return@withContext NetworkQuality.VERY_POOR
        }
    }

    // 获取网络信号强度(仅WiFi和移动网络)
    @SuppressLint("MissingPermission")
    fun getSignalStrength(): Int {
        val network = connectivityManager.activeNetwork ?: return 0
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return 0

        return when {
            capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
                val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
                val wifiInfo = wifiManager.connectionInfo
                val level = WifiManager.calculateSignalLevel(wifiInfo.rssi, 5)
                (level + 1) * 20  // 转换为0-100
            }
            capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
                // 需要 ACCESS_FINE_LOCATION 权限
                val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
                // 这里简化处理,实际需要使用 PhoneStateListener
                50  // 默认返回50
            }
            else -> 0
        }
    }
}

使用示例:

kotlin 复制代码
val monitor = NetworkQualityMonitor(context)

lifecycleScope.launch {
    val networkType = monitor.getNetworkType()
    val quality = monitor.measureNetworkQuality()
    val signalStrength = monitor.getSignalStrength()

    Log.d("Network", "类型: $networkType, 质量: $quality, 信号: $signalStrength%")

    // 根据网络质量调整策略
    when (quality) {
        NetworkQuality.EXCELLENT, NetworkQuality.GOOD -> {
            // 正常加载高清图片
            loadHighQualityImages()
        }
        NetworkQuality.MODERATE -> {
            // 加载中等质量图片
            loadMediumQualityImages()
        }
        NetworkQuality.POOR, NetworkQuality.VERY_POOR -> {
            // 加载低质量图片,减少请求
            loadLowQualityImages()
            showNetworkPoorTip()
        }
    }
}

弱网优化策略

弱网环境识别

除了上面的网络质量监控,我们还可以通过以下方式识别弱网:

kotlin 复制代码
class WeakNetworkDetector {

    // 方法1: 通过网络类型判断
    fun isWeakNetwork(networkType: String): Boolean {
        return networkType in listOf("2G", "3G", "EDGE", "GPRS")
    }

    // 方法2: 通过请求超时次数判断
    private var consecutiveTimeouts = 0

    fun onRequestTimeout() {
        consecutiveTimeouts++
        if (consecutiveTimeouts >= 3) {
            // 连续3次超时,判定为弱网
            enableWeakNetworkMode()
        }
    }

    fun onRequestSuccess() {
        consecutiveTimeouts = 0
    }

    // 方法3: 通过下载速度判断
    fun isSlowDownload(bytesDownloaded: Long, durationMs: Long): Boolean {
        val speedKBps = bytesDownloaded / 1024.0 / (durationMs / 1000.0)
        return speedKBps < 50  // 低于50KB/s认为是弱网
    }
}

超时策略设置

动态超时 - 根据网络质量调整:

kotlin 复制代码
object TimeoutConfig {
    fun getConnectTimeout(networkQuality: NetworkQuality): Long {
        return when (networkQuality) {
            NetworkQuality.EXCELLENT -> 5_000L      // 5秒
            NetworkQuality.GOOD -> 8_000L           // 8秒
            NetworkQuality.MODERATE -> 15_000L      // 15秒
            NetworkQuality.POOR -> 25_000L          // 25秒
            NetworkQuality.VERY_POOR -> 40_000L     // 40秒
        }
    }

    fun getReadTimeout(networkQuality: NetworkQuality): Long {
        return getConnectTimeout(networkQuality) * 2  // 读取超时是连接超时的2倍
    }
}

// 使用OkHttp配置动态超时
val client = OkHttpClient.Builder()
    .connectTimeout(TimeoutConfig.getConnectTimeout(currentQuality), TimeUnit.MILLISECONDS)
    .readTimeout(TimeoutConfig.getReadTimeout(currentQuality), TimeUnit.MILLISECONDS)
    .build()

重试机制优化

指数退避重试:

kotlin 复制代码
class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response: Response? = null
        var retryCount = 0
        var exception: IOException? = null

        while (retryCount < maxRetries) {
            try {
                response = chain.proceed(request)

                // 成功,返回结果
                if (response.isSuccessful) {
                    return response
                }

                // 服务器错误(5xx)才重试,客户端错误(4xx)不重试
                if (response.code !in 500..599) {
                    return response
                }

            } catch (e: IOException) {
                exception = e
                Log.w("RetryInterceptor", "请求失败,准备重试: ${retryCount + 1}/$maxRetries", e)
            }

            retryCount++

            // 指数退避: 1秒 → 2秒 → 4秒
            val delayMs = (1000 * Math.pow(2.0, retryCount.toDouble() - 1)).toLong()
            Thread.sleep(delayMs)
        }

        // 所有重试都失败,抛出异常或返回最后的响应
        return response ?: throw exception ?: IOException("请求失败且无响应")
    }
}

// 使用
val client = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor(maxRetries = 3))
    .build()

降级策略

当网络极差时,启用降级策略:

kotlin 复制代码
class NetworkDegradationStrategy(private val networkMonitor: NetworkQualityMonitor) {

    suspend fun <T> executeWithDegradation(
        normalAction: suspend () -> T,
        degradedAction: suspend () -> T
    ): T {
        val quality = networkMonitor.measureNetworkQuality()

        return if (quality in listOf(NetworkQuality.POOR, NetworkQuality.VERY_POOR)) {
            Log.d("Degradation", "网络较差,使用降级策略")
            degradedAction()
        } else {
            normalAction()
        }
    }
}

// 使用示例
val strategy = NetworkDegradationStrategy(networkMonitor)

val articles = strategy.executeWithDegradation(
    normalAction = {
        // 正常情况:加载完整文章列表(包含图片)
        api.getArticleListWithImages()
    },
    degradedAction = {
        // 降级:只加载标题列表(不含图片)
        api.getArticleListTitleOnly()
    }
)

数据压缩

使用Gzip压缩请求和响应:

kotlin 复制代码
// OkHttp默认支持Gzip,但要确保服务器也支持
val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val original = chain.request()

        // 请求时添加Accept-Encoding
        val request = original.newBuilder()
            .header("Accept-Encoding", "gzip")
            .build()

        chain.proceed(request)
    }
    .build()

对于上传,也可以手动Gzip压缩:

kotlin 复制代码
fun compressRequestBody(body: RequestBody): RequestBody {
    return object : RequestBody() {
        override fun contentType() = body.contentType()

        override fun contentLength() = -1L  // 不知道压缩后的大小

        override fun writeTo(sink: BufferedSink) {
            val gzipSink = GzipSink(sink)
            val gzipBuffer = gzipSink.buffer()
            body.writeTo(gzipBuffer)
            gzipBuffer.close()
        }
    }
}

请求合并

将多个小请求合并为一个批量请求:

kotlin 复制代码
// ❌ 不好:多次请求
suspend fun loadUserProfiles(userIds: List<String>) {
    userIds.forEach { userId ->
        val profile = api.getUserProfile(userId)  // N次网络请求
        updateUI(profile)
    }
}

// ✅ 更好:批量请求
suspend fun loadUserProfiles(userIds: List<String>) {
    val profiles = api.getBatchUserProfiles(userIds)  // 1次网络请求
    profiles.forEach { profile ->
        updateUI(profile)
    }
}
kotlin 复制代码
// API定义
interface UserApi {
    @GET("user/{userId}")
    suspend fun getUserProfile(@Path("userId") userId: String): UserProfile

    @POST("users/batch")
    suspend fun getBatchUserProfiles(@Body userIds: List<String>): List<UserProfile>
}

离线缓存策略

使用OkHttp的缓存机制:

kotlin 复制代码
// 1. 配置缓存目录和大小
val cacheSize = 50L * 1024 * 1024  // 50MB
val cache = Cache(context.cacheDir, cacheSize)

val client = OkHttpClient.Builder()
    .cache(cache)
    .addNetworkInterceptor(CacheInterceptor())
    .build()

class CacheInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()

        // 弱网时优先使用缓存
        if (isWeakNetwork()) {
            request = request.newBuilder()
                .cacheControl(
                    CacheControl.Builder()
                        .maxStale(7, TimeUnit.DAYS)  // 缓存有效期7天
                        .build()
                )
                .build()
        }

        val response = chain.proceed(request)

        // 对响应设置缓存策略
        return response.newBuilder()
            .header("Cache-Control", "public, max-age=3600")  // 缓存1小时
            .removeHeader("Pragma")
            .build()
    }
}

更完整的缓存策略:

kotlin 复制代码
class SmartCacheStrategy(
    private val context: Context,
    private val networkMonitor: NetworkQualityMonitor
) {

    private val sharedPrefs = context.getSharedPreferences("cache_meta", Context.MODE_PRIVATE)

    // 判断缓存是否可用
    fun isCacheValid(key: String, maxAgeSeconds: Long): Boolean {
        val cachedTime = sharedPrefs.getLong(key, 0)
        val age = (System.currentTimeMillis() - cachedTime) / 1000
        return age < maxAgeSeconds
    }

    // 保存缓存时间戳
    fun markCached(key: String) {
        sharedPrefs.edit().putLong(key, System.currentTimeMillis()).apply()
    }

    // 根据网络质量决定缓存策略
    suspend fun getCacheMaxAge(networkQuality: NetworkQuality): Long {
        return when (networkQuality) {
            NetworkQuality.EXCELLENT, NetworkQuality.GOOD -> 300L      // 5分钟
            NetworkQuality.MODERATE -> 1800L                           // 30分钟
            NetworkQuality.POOR, NetworkQuality.VERY_POOR -> 86400L    // 1天
        }
    }
}

网络优化最佳实践

1. DNS优化

使用HTTPDNS避免DNS劫持和提升解析速度:

kotlin 复制代码
// 使用阿里云HTTPDNS(示例)
class HttpDnsInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val url = original.url

        // 使用HTTPDNS解析IP
        val ip = HttpDns.getIpByHost(url.host)

        if (ip != null) {
            val newUrl = url.newBuilder()
                .host(ip)
                .build()

            val newRequest = original.newBuilder()
                .url(newUrl)
                .header("Host", url.host)  // 保留原始Host
                .build()

            return chain.proceed(newRequest)
        }

        return chain.proceed(original)
    }
}

2. 连接复用 - HTTP/2

使用OkHttp默认支持HTTP/2,实现请求多路复用:

kotlin 复制代码
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))  // 支持HTTP/2
    .build()

HTTP/2的优势:

  • 多路复用 - 一个连接可以并发多个请求
  • 头部压缩 - 减少重复头部的传输
  • 服务器推送 - 主动推送资源

3. 图片加载优化

使用Glide/Coil进行智能加载:

kotlin 复制代码
// 根据网络质量加载不同分辨率的图片
fun loadImage(imageView: ImageView, imageUrl: String, networkQuality: NetworkQuality) {
    val url = when (networkQuality) {
        NetworkQuality.EXCELLENT, NetworkQuality.GOOD -> "${imageUrl}?quality=high"
        NetworkQuality.MODERATE -> "${imageUrl}?quality=medium"
        NetworkQuality.POOR, NetworkQuality.VERY_POOR -> "${imageUrl}?quality=low"
    }

    Glide.with(imageView.context)
        .load(url)
        .diskCacheStrategy(DiskCacheStrategy.ALL)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .into(imageView)
}

4. 预加载策略

在WiFi环境下预加载可能需要的资源:

kotlin 复制代码
class PreloadManager(private val context: Context) {

    fun preloadIfNeeded() {
        if (isWiFiConnected() && !isBatterySaveMode()) {
            lifecycleScope.launch {
                // 预加载今日推荐文章
                preloadArticles()
                // 预加载视频封面
                preloadVideoThumbnails()
            }
        }
    }

    private fun isWiFiConnected(): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = cm.activeNetwork ?: return false
        val capabilities = cm.getNetworkCapabilities(network) ?: return false
        return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
    }

    private fun isBatterySaveMode(): Boolean {
        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        return pm.isPowerSaveMode
    }
}

5. 流量控制

限制后台流量使用:

kotlin 复制代码
class TrafficController {
    private val maxDailyTraffic = 50 * 1024 * 1024L  // 50MB

    private val prefs = context.getSharedPreferences("traffic", Context.MODE_PRIVATE)

    fun canUseNetwork(): Boolean {
        val today = getToday()
        val usedTraffic = prefs.getLong("traffic_$today", 0)
        return usedTraffic < maxDailyTraffic
    }

    fun recordTraffic(bytes: Long) {
        val today = getToday()
        val used = prefs.getLong("traffic_$today", 0)
        prefs.edit().putLong("traffic_$today", used + bytes).apply()
    }

    private fun getToday(): String {
        return SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date())
    }
}

实战案例

案例1: 后台定位任务电量优化

问题: 地图应用后台持续定位导致电量消耗严重。

优化前:

kotlin 复制代码
// ❌ 持续高精度定位
locationManager.requestLocationUpdates(
    LocationManager.GPS_PROVIDER,
    1000L,  // 1秒更新一次
    0f,
    locationListener
)

电量消耗: 30%/小时

优化后:

kotlin 复制代码
// ✅ 动态调整定位策略
class SmartLocationManager(private val context: Context) {

    private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    private val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager

    fun startSmartLocationUpdates(listener: LocationListener) {
        val (interval, provider) = getOptimalLocationStrategy()

        locationManager.requestLocationUpdates(
            provider,
            interval,
            0f,
            listener
        )
    }

    private fun getOptimalLocationStrategy(): Pair<Long, String> {
        return when {
            // 省电模式:使用网络定位,30秒更新
            powerManager.isPowerSaveMode -> {
                30_000L to LocationManager.NETWORK_PROVIDER
            }
            // 前台运行:使用GPS,5秒更新
            isAppInForeground() -> {
                5_000L to LocationManager.GPS_PROVIDER
            }
            // 后台运行:使用网络定位,5分钟更新
            else -> {
                300_000L to LocationManager.NETWORK_PROVIDER
            }
        }
    }
}

优化结果:

  • 电量消耗降至 8%/小时
  • 减少了 73% 的电量消耗
  • 用户续航时间提升 3倍

案例2: 弱网环境下载优化

问题: 在地铁等弱网环境下,文件下载经常失败。

优化前:

kotlin 复制代码
// ❌ 单次下载,失败后从头开始
suspend fun downloadFile(url: String, outputFile: File) {
    URL(url).openStream().use { input ->
        FileOutputStream(outputFile).use { output ->
            input.copyTo(output)
        }
    }
}

成功率 : 弱网下只有 40%

优化后 - 实现断点续传:

kotlin 复制代码
// ✅ 支持断点续传的下载器
class ResumableDownloader {

    suspend fun downloadWithResume(
        url: String,
        outputFile: File,
        maxRetries: Int = 5
    ): Result<File> = withContext(Dispatchers.IO) {
        var retryCount = 0
        var lastException: Exception? = null

        while (retryCount < maxRetries) {
            try {
                val downloaded = if (outputFile.exists()) outputFile.length() else 0

                val connection = URL(url).openConnection() as HttpURLConnection
                connection.requestMethod = "GET"

                // 设置断点续传
                if (downloaded > 0) {
                    connection.setRequestProperty("Range", "bytes=$downloaded-")
                    Log.d("Download", "从 $downloaded 字节继续下载")
                }

                // 检查是否支持断点续传
                val responseCode = connection.responseCode
                if (responseCode != HttpURLConnection.HTTP_OK &&
                    responseCode != HttpURLConnection.HTTP_PARTIAL) {
                    return@withContext Result.failure(IOException("服务器不支持断点续传"))
                }

                // 下载数据
                connection.inputStream.use { input ->
                    FileOutputStream(outputFile, true).use { output ->  // append=true
                        val buffer = ByteArray(8192)
                        var bytesRead: Int
                        var totalBytes = downloaded

                        while (input.read(buffer).also { bytesRead = it } != -1) {
                            output.write(buffer, 0, bytesRead)
                            totalBytes += bytesRead

                            // 更新进度
                            val progress = (totalBytes * 100 / connection.contentLength).toInt()
                            Log.d("Download", "下载进度: $progress%")
                        }
                    }
                }

                connection.disconnect()
                return@withContext Result.success(outputFile)

            } catch (e: Exception) {
                lastException = e
                retryCount++
                Log.w("Download", "下载失败,重试 $retryCount/$maxRetries", e)

                // 指数退避
                delay(1000 * (1 shl retryCount))
            }
        }

        Result.failure(lastException ?: IOException("下载失败"))
    }
}

优化结果:

  • 成功率提升至 95%
  • 平均下载时间减少 60%(减少重复下载)
  • 用户体验显著改善

总结

电量优化和网络优化是Android应用性能优化中至关重要的两个方向,它们直接影响用户体验和应用口碑。

核心要点回顾

电量优化:

  • 理解Doze模式和App Standby机制,使用JobScheduler/WorkManager适配
  • 使用Battery Historian分析电量消耗,定位异常
  • 避免Wakelock滥用,始终设置超时时间
  • 优化定位策略,根据场景动态调整
  • 减少后台网络请求,合并任务

网络优化:

  • 识别弱网环境,动态调整超时和重试策略
  • 实现智能降级,保证弱网下的基本可用性
  • 使用数据压缩、请求合并减少流量
  • 启用HTTP/2连接复用
  • 实现离线缓存和断点续传

优化检查清单

  • 后台任务使用WorkManager调度
  • 为Wakelock设置超时时间
  • 定位服务根据前后台动态调整
  • 网络请求设置合理超时
  • 实现指数退避重试机制
  • 弱网下启用降级策略
  • 启用请求响应压缩
  • 使用OkHttp缓存机制
  • WiFi环境下预加载资源
  • 使用Battery Historian定期分析

参考资料

官方资料

系列文章:

作者简介: 多年Android系统开发经验,专注于系统稳定性与性能优化领域。欢迎关注本系列,一起深入Android系统的精彩世界!


🎉 感谢关注,让我们一起深入Android系统的精彩世界!

找到我 : 个人主页

相关推荐
Coffeeee2 小时前
了解一下Android16更新事项,拿捏下一波适配
android·前端·google
用户41659673693552 小时前
深入解析安卓 ELF 16KB 页对齐:原生编译与脚本修复的权衡
android
恋猫de小郭3 小时前
Compose Multiplatform 1.10 Interop views 新特性:Overlay 和 Autosizing
android·flutter·macos·kotlin·github·objective-c·cocoa
胖虎13 小时前
Android 文件下载实践:基于 OkHttp 的完整实现与思考
android·okhttp·下载文件·安卓下载·安卓中的下载
_李小白3 小时前
【Android 美颜相机】第四天:CameraLoader、Camera1Loader 与 Camera2Loader
android·数码相机
00后程序员张3 小时前
iOS APP 性能测试工具,监控CPU,实时日志输出
android·ios·小程序·https·uni-app·iphone·webview
YIN_尹3 小时前
【MySQL】数据类型(下)
android·mysql·adb
invicinble3 小时前
认识es的多个维度
android·大数据·elasticsearch
前端切图仔0014 小时前
Chrome 扩展程序上架指南
android·java·javascript·google