引言
在Android手机上,我们通常说的桌面其实就是launcher应用,更狭义一点就是workspace,workspace是桌面在实现时的抽象定义。桌面上显示的应用图标、文件夹和小部件都是显示在workspace中的,我们可以增删应用快捷图标,增删文件夹,增删小部件,但是这些桌面上显示的元素状态是怎么保存下来的呢?答案是,launcher使用了一个专门的数据库保存了这些状态,以便下次重启后依然能按照最新的变动显示。
相关实现类介绍
LauncherProvider:虽然launcher.db存储的数据,基本都是供launcher应用本身访问,但是原生launcher中基本所有的workspace相关数据库操作,都是通过这个provider统一访问的。
DatabaseHelper:LauncherProvider的静态内部类,该类继承自SQLiteOpenHelper,封装了数据库相关操作。
workspace数据存储基本围绕这两个类进行,学习launcher是怎么也绕不开的。
正文
1、launcher.db创建过程分析
先看下LauncherProvider onCreate生命周期方法:
@Override
public boolean onCreate() {
if (FeatureFlags.IS_DOGFOOD_BUILD) {
Log.d(TAG, "Launcher process started");
}
mListenerHandler = new Handler(mListenerWrapper);
// The content provider exists for the entire duration of the launcher main process and
// is the first component to get created.
MainProcessInitializer.initialize(getContext().getApplicationContext());
return true;
}
从打印的log内容和注释可以知道,LauncherProvider是launcher中第一个被初始化的组件,可以看出,workspace的显示应该是需要基于数据存储的,不然没必要优先加载,尤其在launcher这样用户体验很重要的应用当中。
LauncherProvider在onCreate时并没有立即创建数据库和执行建表操作,那是在什么地方呢?
我们看LauncherProvider另一个方法:
protected synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
if (RestoreDbTask.isPending(getContext())) {
if (!RestoreDbTask.performRestore(mOpenHelper)) {
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
}
// Set is pending to false irrespective of the result, so that it doesn't get
// executed again.
RestoreDbTask.setPending(getContext(), false);
}
}
}
通观launcher,就这一个地方实例化了DatabaseHelper,其他需要操作数据库的地方,都必须调用此方法先创建数据库。如LauncherProvider中query方法中的使用:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
createDbIfNotExists();
......
......
return result;
}
继续跟进去看下上面实例化DatabaseHelper的构造函数:
DatabaseHelper(Context context, Handler widgetHostResetHandler) {
this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
// Table creation sometimes fails silently, which leads to a crash loop.
// This way, we will try to create a table every time after crash, so the device
// would eventually be able to recover.
if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
// This operation is a no-op if the table already exists.
addFavoritesTable(getWritableDatabase(), true);
addWorkspacesTable(getWritableDatabase(), true);
}
initIds();
}
/**
* Constructor used in tests and for restore.
*/
public DatabaseHelper(
Context context, Handler widgetHostResetHandler, String tableName) {
super(context, tableName, SCHEMA_VERSION);
mContext = context;
mWidgetHostResetHandler = widgetHostResetHandler;
}
咋一看有两个构造函数,上面实例化时只用到了第一个,但是通过调用第一个会间接调用到第二个构造函数,进而调用到SQLiteOpenHelper中的父构造函数。
到这里,在DatabaseHelper构造函数被执行完后,launcher.db其实已经创建出来了,下面接着看数据中的建表过程。
2、workspaceScreens和favorites表创建过程分析
launcher.db数据库中包含两张表,分别是:
- workspaceScreens:对应桌面的每一页,桌面可以左右滑动几页,就有几条记录。
- favorites:用于存储桌面上显示的元素,包含应用快捷图标、文件夹、小部件,每一个元素对应一条记录。
上面DatabaseHelper的第一个构造函数中的建表的操作,看起来有点费解,为什么放在这个地方,不是应该放在onCreate中做吗?我们先放一放,先看看DatabaseHelper onCreate生命周期方法的实现:
@Override
public void onCreate(SQLiteDatabase db) {
if (LOGD) Log.d(TAG, "creating new launcher database");
mMaxItemId = 1;
mMaxScreenId = 0;
addFavoritesTable(db, false);
addWorkspacesTable(db, false);
// Fresh and clean launcher DB.
mMaxItemId = initializeMaxItemId(db);
onEmptyDbCreated();
}
果然,onCreate方法中也有建表操作。如果是首次创建数据库,DatabaseHelper在被实例化后,触发SQLiteDatabase的创建,同时会回调onCreate生命周期方法。
在看上面代码时,我产生了一个疑问?DatabaseHelper构造和onCreate方法中都包含了建表操作,不是重复了吗?
通过打印日志跟踪,发现实际建表操作是在onCreate方法中进行的,DatabaseHelper在调用父类构造函数后其实并不会触发数据库创建,真正触发的是getWritableDatabase方法。因此,DatabaseHelper构造函数中建表操作并不会执行,不会发生重复建表。
再次单独贴下上面DatabaseHelper构造函数中的代码,以示强调:
if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
// This operation is a no-op if the table already exists.
addFavoritesTable(getWritableDatabase(), true);
addWorkspacesTable(getWritableDatabase(), true);
}
看下addWorkspacesTable建表方法实现:
private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
");");
}
构造函数中,在addWorkspacesTable方法调用时传递的optional参数为true,避免了重复建表。
上面onCreate方法最后一行代码,也值得分析一下,方法实现如下:
protected void onEmptyDbCreated() {
......
......
// Set the flag for empty DB
Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
}
这里需要关注的就是最后一行代码,这里记录了一个EMPTY_DATABASE_CREATED标记,表示空数据库创建了,加这个标记有什么用呢?肯定有其他地方用到这个判断。
继续追踪代码,发现在loadWorkspace时,loadDefaultFavoritesIfNecessary方法用到了此标记:
synchronized private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
......
......
clearFlagEmptyDbCreated();
}
}
private void clearFlagEmptyDbCreated() {
Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
}
这里使用这个标记判断是否需要加载默认的workspace配置数据到数据库,最后一行代码clearFlagEmptyDbCreated方法调用,用于清空了这个标记,下次就不需要再次加载了。
从中得出一个结论,launcher正常在首次加载时,才会加载默认配置到数据库,其他情况是不会加载的。
3、DatabaseHelper onCreate生命周期之外创建表
/**
* Clears all the data for a fresh start.
*/
public void createEmptyDB(SQLiteDatabase db) {
try (SQLiteTransaction t = new SQLiteTransaction(db)) {
db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
onCreate(db);
t.commit();
}
}
DatabaseHelper中还单独封装了上面的方法,用于在一些数据异常情况下,删除表并重新创建空表,重新载入数据,避免launcher应用发生异常。
总结
到这里,大体分析完了launcher.db和表的创建过程,此篇暂时分析到这,具体loadDefaultFavoritesIfNecessary是如何加载默认配置到数据库的,下篇我们再详细分析。