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策略重新编译。注意,这个操作很耗时,建议放在后台执行,不要阻塞开机流程。

警告:不要在升级脚本中直接调用pm compile -m everything。我曾经见过一个厂商这么干,结果用户开机等了20分钟,直接退货了。

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_STORAGEWRITE_EXTERNAL_STORAGE的权限分组改了,但迁移逻辑没跟上。后来我们加了一个兼容层,才把这个问题解决。

提示:如果你在开发系统应用,建议在AndroidManifest中声明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预编译 提前编译DEX到机器码 权限迁移 保留用户授权状态 数据库版本升级 逐版本迁移数据 • 选择编译策略 • 后台触发编译 • 避免阻塞开机 • 处理新增权限 • 处理分组变更 • 兼容旧版本 • 逐版本升级 • 备份旧数据 • 使用事务 首次启动流畅完成

从图上可以看到,这三个优化是并行执行的。但要注意,它们之间也有依赖关系。比如,数据库升级必须在应用启动前完成,否则应用读取数据时会出错。权限迁移则可以在后台慢慢做,只要保证应用请求权限时能拿到正确的状态就行。

我的建议:在开发阶段,把这三个优化点做成自动化测试。每次升级前,跑一遍测试,确保不会出现“升级后首次启动卡死”的情况。我在团队里就是这么做的,效果很好。

好了,关于升级后首次启动的优化,就聊到这里。这三个点——dex2oat预编译、权限迁移、数据库版本升级——是决定用户体验的关键。做得好,用户感觉不到升级的存在;做得不好,用户会记住你的系统“很卡”。

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