7、Launcher的数据库(LauncherProvider)
Launcher 的数据存储,说白了就是解决一个问题:用户辛辛苦苦摆好的桌面,重启后怎么还在?
嗯,这背后就是 LauncherProvider 在干活。它是一个 ContentProvider,负责管理桌面所有的图标、小部件、文件夹、快捷方式。我刚开始接触 Launcher 源码时,第一反应是:不就存个图标位置吗,能有多复杂?后来才发现,这里面的坑,一个比一个深。
7.1 LauncherProvider 的 ContentProvider 实现
LauncherProvider 继承自 ContentProvider,但它不是普通的 Provider。它内部维护了一个 SQLite 数据库,叫 launcher.db。这个数据库存的是用户桌面的「快照」——也就是你当前看到的每一个图标、每一个文件夹、每一个小部件的位置和配置。
它的核心职责有三点:
- 提供数据访问接口:通过 URI 对外暴露增删改查能力
- 监听包变化:当用户安装/卸载应用时,自动更新数据库
- 处理数据迁移:系统升级时,保证旧数据能平滑过渡
我个人习惯把 LauncherProvider 看作一个「中间人」。它不直接操作数据库,而是通过 LauncherDatabaseHelper 这个 SQLiteOpenHelper 来管理表结构和版本。这样设计的好处是,数据库升级的逻辑可以集中管理,不会散落在各处。
核心 URI 示例:
// 操作所有收藏项
content://com.android.launcher3.settings/favorites
// 操作某个具体项
content://com.android.launcher3.settings/favorites/#id
你想想看,桌面上的每一个图标,在数据库里都是一条记录。Launcher 启动时,会通过 Loader 异步查询这些记录,然后渲染到屏幕上。这个过程如果慢了,用户就会看到「白屏」——嗯,我在项目里就踩过这个坑。
7.2 Favorites 表结构解析
Favorites 表是 Launcher 数据库里最重要的一张表。它存储了桌面上所有「可放置」的元素。我把它拆成几个关键字段来讲:
| 字段名 | 类型 | 说明 |
|---|---|---|
| _id | INTEGER | 主键,自增 |
| title | TEXT | 应用名称或快捷方式名称 |
| intent | TEXT | 点击时启动的 Intent,序列化为字符串 |
| container | INTEGER | 所在容器:-100 表示桌面,-101 表示抽屉,其他值为文件夹 ID |
| screen | INTEGER | 所在屏幕的页码(从 0 开始) |
| cellX / cellY | INTEGER | 在网格中的列和行位置 |
| spanX / spanY | INTEGER | 占用的网格宽度和高度(小部件专用) |
| itemType | INTEGER | 类型:0=应用图标,1=文件夹,2=小部件,3=快捷方式 |
| profileId | INTEGER | 用户 ID,用于多用户场景 |
这里有个容易忽略的点:container 字段。它决定了这个图标是在桌面上,还是在文件夹里。我曾经遇到过一个 bug,用户把图标拖进文件夹后,重启手机图标消失了。查了半天,发现是 container 字段没有正确更新,导致 Launcher 认为这个图标「无处可去」。
避坑指南: 如果你要手动修改数据库,一定要保证 container、screen、cellX、cellY 这四个字段同时正确。缺一个,图标就可能「飘」到屏幕外面去。
7.3 数据库升级与数据迁移
数据库升级,是 LauncherProvider 里最容易被忽视、但出事最严重的地方。为什么?因为用户手机里可能存着几百个图标,一旦升级脚本写错了,桌面直接变空白。
Launcher 的数据库版本号定义在 LauncherDatabaseHelper 里。每次版本升级,都会走 onUpgrade() 方法。我见过最经典的写法是这样的:
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 逐版本升级,而不是直接跳转
if (oldVersion < 8) {
db.execSQL("ALTER TABLE favorites ADD COLUMN profileId INTEGER DEFAULT 0");
}
if (oldVersion < 9) {
db.execSQL("ALTER TABLE favorites ADD COLUMN spanX INTEGER DEFAULT 1");
db.execSQL("ALTER TABLE favorites ADD COLUMN spanY INTEGER DEFAULT 1");
}
if (oldVersion < 10) {
// 复杂迁移:重建表
db.execSQL("CREATE TABLE favorites_new (...)");
db.execSQL("INSERT INTO favorites_new SELECT ... FROM favorites");
db.execSQL("DROP TABLE favorites");
db.execSQL("ALTER TABLE favorites_new RENAME TO favorites");
}
}
为什么要逐版本升级?因为用户可能从 Android 8 直接升级到 Android 12,中间跳过了好几个版本。如果只写一个 if (oldVersion < 12) 的完整迁移逻辑,一旦漏掉某个字段,数据就丢了。
注意: 千万不要在 onUpgrade() 里直接 DROP 表重建。除非你想让用户骂娘。我曾经接手过一个项目,前同事就是这么干的,结果每次系统升级,用户桌面都恢复出厂设置——嗯,那段时间客服电话被打爆了。
数据迁移还有一个隐藏场景:跨版本恢复备份。比如用户从旧手机备份数据,恢复到新手机。这时候数据库版本可能比当前系统低很多。LauncherProvider 需要能处理这种「跳级」情况。我的做法是:在 onDowngrade() 里抛异常,禁止降级;在 onUpgrade() 里用循环逐版本处理,保证每个版本都有对应的迁移逻辑。
7.4 知识体系总览
下面这张图,是我梳理的 LauncherProvider 核心逻辑。你可以把它当作一个「地图」,后面讲到具体实现时,随时回来对照。
这张图里,我把 LauncherProvider 拆成了三个模块。左边是 ContentProvider 的接口层,中间是 Favorites 表的数据层,右边是数据库升级的维护层。三者缺一不可。
说实话,LauncherProvider 的代码量并不大,但它是整个桌面的「地基」。地基不稳,上面盖的楼再漂亮也没用。我见过太多第三方 ROM 在数据库迁移上翻车,导致用户数据丢失。所以,每次写 onUpgrade() 的时候,我都会多问自己一句:这个脚本,能在所有旧版本上正确执行吗?
公众号:蓝海资料掘金营,微信deep3321