18、升级后的首次启动优化:dex2oat预编译、权限迁移、数据库版本升级
系统升级完了,重启那一刻,用户最怕什么?
怕黑屏、怕转圈、怕卡在开机动画里出不来。说白了,就是怕“第一次启动”太慢。
我做过一个项目,升级后首次启动花了整整8分钟。用户反馈直接炸了。后来我们花了两个版本去优化,才把时间压到90秒以内。今天聊的这三个点——dex2oat预编译、权限迁移、数据库版本升级——就是那次优化中踩得最深的坑。
18.1 dex2oat预编译:把“运行时编译”提前做完
先说说dex2oat。这玩意儿是Android从ART运行时开始引入的。它的作用,就是把APK里的DEX字节码,提前编译成本地机器码。
为什么要提前编译?因为如果不做,系统会在应用首次启动时边跑边编译。那速度,你想想看,跟看幻灯片似的。
18.1.1 预编译的触发时机
升级后的首次启动,系统会扫描所有已安装的应用。对每个应用,系统会判断是否需要重新编译。判断依据是:
- 应用的DEX文件是否有变更
- 系统库是否有更新
- 编译策略是否发生变化
我在项目中遇到过一个问题:升级后所有应用都被重新编译了,导致首次启动耗时暴增。后来发现是编译策略从“speed”改成了“everything”,系统认为所有应用都需要重新编译。嗯,这里要注意,编译策略的变更要谨慎。
18.1.2 编译策略的选择
Android提供了几种编译策略,各有优劣:
| 策略 | 编译范围 | 首次启动耗时 | 运行性能 |
|---|---|---|---|
| verify | 仅验证 | 极短 | 差 |
| speed | 常用方法 | 中等 | 良好 |
| everything | 全部方法 | 极长 | 最佳 |
我个人习惯,OTA升级场景下用“speed”策略就够了。没必要为了那点性能提升,让用户在开机画面等半天。
18.1.3 预编译的代码控制
如果你想在升级脚本里控制编译行为,可以这样写:
# 在升级完成后触发预编译
pm compile --reset -a
pm compile -m speed -a
第一行是重置所有应用的编译状态,第二行是用speed策略重新编译。注意,这个操作很耗时,建议放在后台执行,不要阻塞开机流程。
18.2 权限迁移:别让应用“失忆”
升级后,应用的权限可能会发生变化。为什么?因为新系统可能新增了权限,或者调整了权限的分组。如果处理不好,应用会丢失用户之前授予的权限。
说白了,就是用户明明已经授权了,升级后应用却提示“请授予权限”。用户会怎么想?这系统有bug吧?
18.2.1 权限迁移的规则
Android系统在升级时,会尝试保留应用的权限状态。但有些情况需要特殊处理:
- 新增权限:如果新系统新增了一个权限,且该权限属于“危险”级别,系统不会自动授予。应用需要重新请求。
- 权限分组变更:如果某个权限从“普通”变成了“危险”,系统会撤销该权限,应用需要重新申请。
- 权限策略变更:比如Android 11引入了“仅限一次”的权限选项,升级后系统会重置部分权限状态。
18.2.2 如何实现权限迁移
在系统升级的PackageManagerService中,有一个方法叫grantPermissions。它负责在升级后恢复应用的权限。代码逻辑大致如下:
// 伪代码,展示权限迁移的核心逻辑
void migratePermissions(String packageName, int previousSdkVersion, int newSdkVersion) {
PackageSetting ps = getPackageSetting(packageName);
if (previousSdkVersion < 30 && newSdkVersion >= 30) {
// Android 11 引入了新权限模型
// 需要重置部分权限
ps.setPermissionFlags(Manifest.permission.ACCESS_BACKGROUND_LOCATION,
PackageManager.FLAG_PERMISSION_REVOKED);
}
// 其他版本迁移逻辑...
}
我曾经踩过一个坑:升级后所有应用的“存储”权限都丢了。查了半天,发现是系统把READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE的权限分组改了,但迁移逻辑没跟上。后来我们加了一个兼容层,才把这个问题解决。
android:preserveLegacyPermission="true"。这样升级后,系统会尽量保留你的权限状态。
18.3 数据库版本升级:数据迁移的艺术
数据库版本升级,是升级后首次启动中最容易出问题的地方。为什么?因为数据库是持久化的,升级后如果表结构变了,旧数据读不出来,应用直接崩溃。
我见过最离谱的情况:一个应用升级后,数据库从版本1直接跳到版本3,中间版本2的迁移逻辑没写。结果所有用户的数据都丢了。嗯,这种问题一旦发生,基本无法挽回。
18.3.1 数据库升级的标准流程
Android的SQLiteOpenHelper提供了onUpgrade方法,用于处理数据库版本升级。标准做法是:
public class MyDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 3;
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 逐版本升级,不要直接跳
if (oldVersion < 2) {
upgradeToVersion2(db);
}
if (oldVersion < 3) {
upgradeToVersion3(db);
}
}
private void upgradeToVersion2(SQLiteDatabase db) {
db.execSQL("ALTER TABLE user ADD COLUMN age INTEGER DEFAULT 0");
}
private void upgradeToVersion3(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS address (" +
"id INTEGER PRIMARY KEY," +
"user_id INTEGER," +
"city TEXT)");
}
}
注意,这里的关键是“逐版本升级”。不要写if (oldVersion == 1 && newVersion == 3)这种逻辑。为什么?因为如果用户从版本2升级到版本3,这段代码就不会执行。你想想看,是不是这个理?
18.3.2 数据迁移的注意事项
数据库升级不仅仅是改表结构,还要考虑数据迁移。我总结了几条经验:
- 备份旧数据:在修改表结构前,先把旧数据备份到临时表。万一迁移失败,还能回滚。
- 使用事务:整个升级过程放在一个事务里。如果中途出错,数据库不会处于“半升级”状态。
- 测试所有升级路径:从版本1到版本N,每个路径都要测试。不要只测最新版本。
18.4 知识体系总览
下面这张图,把升级后首次启动的三个核心优化点串起来了。你可以看到,它们之间是有依赖关系的:
从图上可以看到,这三个优化是并行执行的。但要注意,它们之间也有依赖关系。比如,数据库升级必须在应用启动前完成,否则应用读取数据时会出错。权限迁移则可以在后台慢慢做,只要保证应用请求权限时能拿到正确的状态就行。
好了,关于升级后首次启动的优化,就聊到这里。这三个点——dex2oat预编译、权限迁移、数据库版本升级——是决定用户体验的关键。做得好,用户感觉不到升级的存在;做得不好,用户会记住你的系统“很卡”。