26、性能优化(下):启动优化、网络优化、包体积优化

性能优化这个话题,说实话,是区分「能用」和「好用」的分水岭。前面我们聊了布局和内存,今天我把剩下的三个硬骨头啃完——启动、网络、包体积。这三个搞定了,你的App基本就具备了上线的底气。

核心观点:性能优化不是炫技,是给用户省时间、省流量、省存储。说白了,就是尊重用户。

一、启动优化:用户的第一印象

用户点开你的App,如果超过2秒还没看到内容,大概率就划走了。我见过太多App,启动时黑屏两三秒,用户直接卸载。嗯,这其实是可以避免的。

1. 启动黑白屏:别让用户干等

为什么会闪一下白屏或黑屏?因为系统在加载Application和第一个Activity之前,会先绘制一个窗口背景。默认是白色或黑色,取决于你的主题。

解决方案其实很简单——给启动窗口设置一个背景图或主题色。

// styles.xml
<style name="AppTheme.Launcher" parent="Theme.AppCompat.DayNight.NoActionBar">
    <item name="android:windowBackground">@drawable/launch_background</item>
</style>

// AndroidManifest.xml
<activity
    android:name=".MainActivity"
    android:theme="@style/AppTheme.Launcher">
    ...
</activity>

// 在Activity的onCreate中恢复主题
@Override
protected void onCreate(Bundle savedInstanceState) {
    setTheme(R.style.AppTheme);
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

我的习惯:启动背景图不要用大图,用一张简单的纯色或渐变背景,加上Logo居中即可。我曾经见过有人放了一张1080p的图片,结果启动反而更慢了……

2. App Startup:懒加载的利器

很多第三方SDK需要在Application初始化。但全部堆在onCreate里,启动时间直接爆炸。App Startup就是来解决这个问题的——它允许你按需初始化,并且自动管理依赖顺序。

// 定义一个Initializer
class SdkInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        // 在这里初始化你的SDK
        MySDK.init(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        // 如果依赖其他Initializer,在这里声明
        return listOf(OtherInitializer::class.java)
    }
}

// AndroidManifest中声明
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.example.SdkInitializer"
        android:value="androidx.startup" />
</provider>

避坑指南:我曾经把一个网络库的初始化放到了App Startup里,但没注意它依赖了SharedPreferences。结果因为SP的初始化也在App Startup里,导致死锁。嗯,依赖关系一定要理清楚。

二、网络优化:快和省是王道

网络请求是App的命脉,但也是最容易出问题的地方。你想想看,用户在地铁里刷App,信号本来就差,如果请求还慢,体验能好吗?

1. 缓存策略:别每次都重新拉

我建议把缓存分为三层:内存缓存、磁盘缓存、网络缓存。说白了,就是能本地拿就别去服务器要。

缓存层级 速度 容量 典型实现
内存缓存 纳秒级 小(几MB) LruCache
磁盘缓存 毫秒级 中(几十MB) DiskLruCache
网络缓存 秒级 无限 OkHttp Cache
// OkHttp 网络缓存配置
val cacheDir = File(context.cacheDir, "http_cache")
val cache = Cache(cacheDir, 10 * 1024 * 1024) // 10MB

val client = OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor { chain ->
        val request = chain.request()
        // 有网时缓存有效期1分钟,无网时缓存有效期1周
        val maxAge = if (isNetworkAvailable()) 60 else 60 * 24 * 7
        val response = chain.proceed(request)
        response.newBuilder()
            .header("Cache-Control", "public, max-age=$maxAge")
            .build()
    }
    .build()

2. 数据压缩:能省一点是一点

JSON传输虽然方便,但体积大。我建议用Protocol Buffers或者MessagePack。如果不想换格式,至少开启Gzip压缩。

// OkHttp 开启Gzip
val client = OkHttpClient.Builder()
    .addInterceptor { chain ->
        val request = chain.request()
        val newRequest = request.newBuilder()
            .header("Accept-Encoding", "gzip")
            .build()
        chain.proceed(newRequest)
    }
    .build()

我的经验:有一次接口返回的JSON里有个字段是用户头像的Base64编码,一张图就占了200KB。后来改成URL后,体积直接降到2KB。你想想看,这种优化比什么压缩都管用。

3. DNS优化:别让域名解析拖后腿

DNS解析有时候比请求本身还慢。我建议用HTTPDNS,直接绕过运营商DNS,减少解析时间。

// 使用阿里云HTTPDNS
val httpdns = HttpDns.getService(context, "your_account_id")
val ip = httpdns.getIpByHostAsync("api.example.com")

// 如果拿到IP,直接替换
val url = if (ip != null) {
    "http://$ip/path"
} else {
    "http://api.example.com/path"
}

避坑指南:我曾经遇到过HTTPDNS返回的IP是旧的,导致请求失败。后来加了降级策略——如果IP请求失败,自动回退到域名请求。嗯,容错一定要做好。

三、包体积优化:小就是快

包体积直接影响下载转化率。你想想看,用户看到你的App 100MB,隔壁竞品才20MB,他选谁?

1. ProGuard & R8:代码的瘦身衣

ProGuard和R8不仅能混淆代码,还能移除未使用的代码和资源。我建议直接用R8,它是ProGuard的升级版,效果更好。

// build.gradle
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

// proguard-rules.pro
# 保留一些必要的类
-keep class com.example.model.** { *; }
-keep class com.example.network.** { *; }

核心要点:shrinkResources会移除未使用的资源,但要注意——如果你通过反射或动态加载使用了某个资源,它可能会被误删。所以一定要测试。

2. 资源混淆:让资源名变短

资源混淆就是把R文件里的资源名从"icon_home_selector"变成"a"。这样能减少resources.arsc的体积。

// 使用AndResGuard
apply plugin: 'com.tencent.mm.AndResGuard'

andResGuard {
    mappingFile = null
    use7zip = true
    useSign = true
    keepRoot = false
    whiteList = [
        "R.drawable.ic_launcher",
        "R.string.app_name"
    ]
    compressFilePattern = [
        "*.png",
        "*.jpg",
        "*.jpeg",
        "*.gif"
    ]
    sevenzip {
        artifact = 'com.tencent.mm:SevenZip:1.2.21'
    }
}

我的习惯:资源混淆后一定要跑一遍全功能测试。我曾经因为混淆把某个第三方SDK的图标搞丢了,结果用户反馈按钮不见了……嗯,白名单一定要加对。

3. 图片优化:体积大头

图片往往是包体积的罪魁祸首。我建议用WebP格式,它比PNG小30%以上,而且支持透明通道。

格式 压缩率 透明通道 兼容性
PNG 支持 全兼容
JPEG 不支持 全兼容
WebP 支持 Android 4.0+
AVIF 极高 支持 Android 12+

避坑指南:WebP虽然好,但Android 4.0以下不支持。如果你的App还要兼容老版本,记得准备降级方案。我曾经就因为这个被用户骂过……

知识体系总览

下面这张图,我把今天讲的内容串起来了。你可以把它当作一个检查清单——每次优化时,对着这张图看看有没有遗漏。

性能优化(下)知识体系 启动优化 网络优化 包体积优化 黑白屏处理 App Startup 懒加载初始化 三层缓存策略 Gzip/ProtoBuf压缩 HTTPDNS优化 ProGuard / R8 资源混淆 WebP图片压缩 核心目标:启动快 → 网络省 → 体积小 用户体验 = 速度 + 稳定性 + 流畅度

好了,这一章的内容就到这里。启动优化、网络优化、包体积优化,这三个方向你只要每个都做到位,你的App在市场上就具备了竞争力。记住,优化不是一次性的工作,而是持续迭代的过程。每次发版前,都跑一遍性能检测,养成习惯。

公众号:蓝海资料掘金营,微信deep3321