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在市场上就具备了竞争力。记住,优化不是一次性的工作,而是持续迭代的过程。每次发版前,都跑一遍性能检测,养成习惯。