前言
在Launcher项目中,WorkSpace及HotSeat的所有图标如ShortcutInfo、Folder、Widget等都需要用到持久化技术以根据用户喜好排列这些图标,并能在下次打开时很方便的找到目标应用或功能。Android持久化技术分为文件存储、SharedPreferences、数据库存储,这几种数据存储方式也各有优劣,文件储存一般用于存储图片、网络请求数据等文本数据或二进制数据,SharedPreferences则是用于存放简单的键值数据,而数据库则适于存放复杂的关系型数据。而对于Launcher应用则使用的是SQLite数据库存储方式,其运算速度快,占用内存小,方便增删改查。
与Launcher数据库有关的类如下:
- LauncherProvider,Launcher应用图标的数据库内容提供者(为了方便其它应用访问,谷歌使用了四大组件之一的ContentProvider对外共享数据);在此提供者内部即是DatabaseHelper(继承自SQLiteOpenHelper),负责数据库的创建和版本升级。
- LauncherSettings,由内部类Favorites负责提供一些Uri去操作Provider,以及数据库中对应字段的字段名称。
- LauncherModel,模型层,负责通过Provider从数据库中存取数据。
数据库的创建
我们先由LauncherProvider内部的数据库的构建开始看起:
/** * 数据库 */ private static class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG_FAVORITES = "favorites"; private static final String TAG_FAVORITE = "favorite"; private static final String TAG_CLOCK = "clock"; private static final String TAG_SEARCH = "search"; private static final String TAG_APPWIDGET = "appwidget"; private static final String TAG_SHORTCUT = "shortcut"; private static final String TAG_FOLDER = "folder"; private static final String TAG_EXTRA = "extra"; private final Context mContext; private final AppWidgetHost mAppWidgetHost; private long mMaxId = -1; DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); mContext = context; mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from // the DB here if (mMaxId == -1) { mMaxId = initializeMaxId(getWritableDatabase()); } } ... }
其中mMaxId是用于标识一个自增长字段_id的当前的最大值,并且在每次添加新数据时都会调用DatabaseHelper的generateNewId方法使mMaxId+1,在DatabaseHelper中initializeMaxId即是在使用数据库Helper之前保证获取到当前数据库中的最大值,并作为成员变量存在使添加新数据时保证能正确自增。
当我们使用getWritableDatabase或getReadableDatabase获取数据库实例时,这两方法其实都会调用getDatabaseLocked方法,在此方法中如果实例不存在,则会调用mContext的openOrCreateDatabase创建DataBase,然后根据version==0(即是否第一次创建)来执行onCreate、onDowndrade、onUpdate方法。
db.beginTransaction(); try { if (version == 0) { onCreate(db); } else { if (version > mNewVersion) { onDowngrade(db, version, mNewVersion); } else { onUpgrade(db, version, mNewVersion); } } b.setVersion(mNewVersion); db.setTransactionSuccessful(); } finally { db.endTransaction(); }
由此可知道数据库第一次执行时即version==0时会调用onCreate方法。而其他version则根据版本升降分别调用onDowngrade或onUpgrade方法。
所以我们进入Launcher数据库DatabaseHelper的onCreate中:
@Override public void onCreate(SQLiteDatabase db) { if (LOGD) Log.d(TAG, "creating new launcher database"); mMaxId = 1; final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); // Default profileId to the serial number of this user. long userSerialNumber = um.getSerialNumberForUser( android.os.Process.myUserHandle()); //表名favorites db.execSQL("CREATE TABLE favorites (" + "_id INTEGER PRIMARY KEY," + "title TEXT," + "intent TEXT," + "container INTEGER," + "screen INTEGER," + "cellX INTEGER," + "cellY INTEGER," + "spanX INTEGER," + "spanY INTEGER," + "itemType INTEGER," + "appWidgetId INTEGER NOT NULL DEFAULT -1," + "isShortcut INTEGER," + "iconType INTEGER," + "iconPackage TEXT," + "iconResource TEXT," + "icon BLOB," + "uri TEXT," + "displayMode INTEGER," + "profileId INTEGER DEFAULT " + userSerialNumber + ");"); // Database was just created, so wipe any previous widgets if (mAppWidgetHost != null) { mAppWidgetHost.deleteHost(); sendAppWidgetResetNotify(); } if (!convertDatabase(db)) { // Set a shared pref so that we know we need to load the default workspace later setFlagToLoadDefaultWorkspaceLater(); } }
可以看到第一次获取数据库时会创建表favorites,此表正是用于持久化存储WorkSpace和HotSeat中各应用排列信息。下面了解下下各字段含义:
_id:用于标识区分各个应用图标,是表favorites的主键,当添加数据时通过generateNewId使_id值增加。
title:在WorkSpace(HotSeat中一般会隐藏掉)中展示的应用快捷图标的标题。
intent:当点击桌面图标时的负责启动应用的intent,它通过Intent.toUri()转换为String存储,在使用时通过Intent.parseUri()转换为intent。
container:指的是当前数据所在的容器类型,在Launcher中有两种container类型:1.CONTAINER_DESKTOP(-100),2.CONTAINER_HOTSEAT(-101)。
screen:用于标识当前数据所在的页。当container为-100时,则screen的值表现为我们桌面的页数的值,当container为-101即当前快捷图标处于HotSeat时,则为默认值-1。
cellX:当前快捷图标所在页(CellLayout)的X位置,即快捷图标在当前页横向的第X个位置。
cellY:当前快捷图标所在页(CellLayout)的Y位置,即快捷图标在当前页纵向的第Y个位置。
spanX:当前快捷图标的在所在页(CellLayout)的横向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标横向上占据一个cell的位置范围。如果当前图标为Widget,则横向占据范围可能为多个cell。
spanY:当前快捷图标的在所在页(CellLayout)的纵向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标纵向上占据一个cell的位置范围。如果当前图标为Widget,则纵向占据范围可能为多个cell。
itemType:当前快捷图标的类型,分为以下几种。
1.ITEM_TYPE_APPLICATION:用于标识应用程序,为默认入口。当我们创建应用时需指定<category android:name="android.intent.category.LAUNCHER"/>,反映到Launcher中就是此type类型。
2.ITEM_TYPE_SHORTCUT:应用程序针对的单个页面发送的创建Shortcut快捷方式的intent,即为此类型。
3.ITEM_TYPE_LIVE_FOLDER:Launcher中没有用到,谷歌已抛弃此type。
4.ITEM_TYPE_APPWIDGET:用于标识此快捷图标为Widget组件。
5.ITEM_TYPE_WIDGET_CLOCK:用于标识此快捷图标为时钟组件。
6.ITEM_TYPE_WIDGET_SEARCH:用于标识此快捷图标为搜索组件。
7.ITEM_TYPE_WIDGET_PHOTO_FRAME:用于标识此快捷图标为相册组件。
appWidgetId:在表中定义为默认值为-1的非空整型字段,如果appWidgetId不为-1,说明此快捷图标是桌面小组件Widget。用于标识唯一的Widget控件,在AppWidgetHost(在Launcher中表现为LauncherAppWidgetHost)内部作为键(key)区分SpareArray中的各个AppWidgetHostView(在Launcher中表现为LauncherAppWidgetHostView),且在widget系统管理服务AppWidgetServiceImpl中appWidgetId同packageName一道作为区分各个Widget的标识。
isShortcut:用于区分是否应用程序通过intent创建的快捷图标。如果值为0,则表示当前数据非应用程序创建的快捷图标,值1则反之。
iconType:用于标识当前快捷图标的图标类型是资源类型ICON_TYPE_RESOURCE(值为0)还是bitmap类型ICON_TYPE_BITMAP(值为1)。如果是资源类型,则需要通过PackageManager的getResourcesForApplication方法获取Resources,再通过Resources获取bitmap。
iconPackage:如果iconType标识为资源类型,则此字段才有用,用于标识资源所在包的包名。
iconResource:如果iconType标识为资源类型,则此字段才有用,用于标识资源图片的id。
icon:如果iconType为bitmap类型,此字段才有用,用于存放二进制图片数据。
uri:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。
displayMode:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。
数据库的使用
在Launcher中数据库DatabaseHelper并没有被直接使用到,而是以内容提供者即LauncherProvider的方式供外界访问,完成代码隔离。
<provider android:name="com.android.launcher2.LauncherProvider" android:authorities="com.android.launcher2.settings" android:exported="true" android:writePermission="com.android.launcher.permission.WRITE_SETTINGS" android:readPermission="com.android.launcher.permission.READ_SETTINGS" />
可以看见要访问此Launcher快捷图标信息,需要使用固定字符串的authorities值,且需要读写Settings的权限。
LauncherProvider作为数据库操作的包装类对外提供了经过包装的增删改查功能。在其构造方法中创建了DatabaseHelper实例,并且把当前Provider设置给LauncherApplication,这样就可以通过LauncherApplication.getLauncherProvider()获取内容提供者。以下是LauncherProvider的增删改查实现。
- 查询
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SqlArguments args = new SqlArguments(uri, selection, selectionArgs); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(args.table); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); result.setNotificationUri(getContext().getContentResolver(), uri); return result; }
- 增加
@Override public Uri insert(Uri uri, ContentValues initialValues) { SqlArguments args = new SqlArguments(uri); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); if (rowId <= 0) return null; uri = ContentUris.withAppendedId(uri, rowId); sendNotify(uri); return uri; }
- 删除
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { SqlArguments args = new SqlArguments(uri, selection, selectionArgs); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.delete(args.table, args.where, args.args); if (count > 0) sendNotify(uri); return count; }
- 修改
@Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SqlArguments args = new SqlArguments(uri, selection, selectionArgs); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); int count = db.update(args.table, values, args.where, args.args); if (count > 0) sendNotify(uri); return count; }
当然,在LauncherSetttings内部也为使用者提供了几种Uri。外界可通过这些Uri来访问LauncherProvider所管理的数据库。
/** * favorites表的uri,带通知内容观察者功能 */ static final Uri CONTENT_URI = Uri.parse("content://" + LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + "?" + LauncherProvider.PARAMETER_NOTIFY + "=true"); /** * favorites表的uri,不带通知内容观察者的功能 */ static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" + LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + "?" + LauncherProvider.PARAMETER_NOTIFY + "=false"); /** * 为CRUD某行数据而准备的uri可通过此方法获取 * @param id 具体的行id,即表中的_id值 * @param notify 是否带通知内容观察者功能 * @return 返回某一行的uri */ static Uri getContentUri(long id, boolean notify) { return Uri.parse("content://" + LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" + LauncherProvider.PARAMETER_NOTIFY + "=" + notify); }
我们知道,Launcher应用是通过LauncherModel的loadAndBindWorkspace方法来加载WorkSpace的应用图标信息,并绑定到桌面上的。而其中loadWorkspace即是通过内容提供者LauncherProvider从数据库中获取数据。
private void loadWorkspace() { final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; final Context context = mContext; final ContentResolver contentResolver = context.getContentResolver(); final PackageManager manager = context.getPackageManager(); final AppWidgetManager widgets = AppWidgetManager.getInstance(context); final boolean isSafeMode = manager.isSafeMode(); // Make sure the default workspace is loaded, if needed mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0, false); synchronized (sBgLock) { sBgWorkspaceItems.clear();//管理workspace或者hotseat sBgAppWidgets.clear();//管理widget sBgFolders.clear();//管理文件夹 sBgItemsIdMap.clear();//所有info管理 sBgDbIconCache.clear(); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); /*查询本地数据库信息*/ final Cursor c = contentResolver.query( LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); ...省略 }
从上可以看到,
1.调用了LauncherProvider的loadDefaultFavoritesIfNecessary方法。其内部通过一个存放在SharedPreferences中的布尔值来判断当前是否是Launcher的第一次加载,如果是,则解析R.xml.default_workspace文件(此文件中包含默认的快捷图标)到Workspace中。从以下可以看见,默认会加载几个widget和快捷图标到WorkSpace中,且指定了HotSeat默认图标为电话、联系人、短信、浏览器。如果我们自己开发Launcher,可以通过修改Launcher的R.xml.default_workspace文件来达到显示何种默认桌面的问题。
<favorites xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher"> <!-- Far-left screen [0] --> <!-- Left screen [1] --> <appwidget launcher:packageName="com.android.settings" launcher:className="com.android.settings.widget.SettingsAppWidgetProvider" launcher:screen="1" launcher:x="0" launcher:y="3" launcher:spanX="4" launcher:spanY="1" /> <!-- Middle screen [2] --> <appwidget launcher:packageName="com.android.deskclock" launcher:className="com.android.alarmclock.AnalogAppWidgetProvider" launcher:screen="2" launcher:x="1" launcher:y="0" launcher:spanX="2" launcher:spanY="2" /> <favorite launcher:packageName="com.android.camera2" launcher:className="com.android.camera.CameraLauncher" launcher:screen="2" launcher:x="0" launcher:y="3" /> <!-- Right screen [3] --> <favorite launcher:packageName="com.android.gallery3d" launcher:className="com.android.gallery3d.app.Gallery" launcher:screen="3" launcher:x="1" launcher:y="3" /> <favorite launcher:packageName="com.android.settings" launcher:className="com.android.settings.Settings" launcher:screen="3" launcher:x="2" launcher:y="3" /> <!-- Far-right screen [4] --> <!-- Hotseat (We use the screen as the position of the item in the hotseat) --> <favorite launcher:packageName="com.android.dialer" launcher:className="com.android.dialer.DialtactsActivity" launcher:container="-101" launcher:screen="0" launcher:x="0" launcher:y="0" /> <favorite launcher:packageName="com.android.contacts" launcher:className="com.android.contacts.activities.PeopleActivity" launcher:container="-101" launcher:screen="1" launcher:x="1" launcher:y="0" /> <favorite launcher:packageName="com.android.mms" launcher:className="com.android.mms.ui.ConversationList" launcher:container="-101" launcher:screen="3" launcher:x="3" launcher:y="0" /> <favorite launcher:packageName="com.android.browser" launcher:className="com.android.browser.BrowserActivity" launcher:container="-101" launcher:screen="4" launcher:x="4" launcher:y="0" /> </favorites>
2.隐式的使用了LauncherProvder进行本地数据库快捷图标信息的查询功能,查询出数据库中的数据显示到Workspace中。此隐式使用在LauncherModel中很常用,使用方式大同小异。
当然,LauncherModel不止在进入Launcher调用startLoader时才会通过LauncherProvider使用到数据库,当用户通过拖拽调整或删除图标的onDrop中也会使用到。LauncherModel中提供了很多详尽的方法来满足数据库的CRUD的操作。此处仅列举LauncherModel中使用频率较高的几个方法供读者参考。
- additemToDatabase方法
/** * * 加到数据库同时同步到集合中 * Add an item to the database in a specified container. Sets the container, screen, cellX and * cellY fields of the item. Also assigns an ID to the item. */ static void addItemToDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY, final boolean notify) { item.container = container; item.cellX = cellX; item.cellY = cellY; // We store hotseat items in canonical form which is this orientation invariant position // in the hotseat if (context instanceof Launcher && screen < 0 && container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { //如果在HotSeat中那么screen的值就是cellX。 item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); } else { item.screen = screen; } final ContentValues values = new ContentValues(); final ContentResolver cr = context.getContentResolver(); //在container,cellX,cellY,screen赋值后,把itemInfo的各成员添加到values中 item.onAddToDatabase(context, values); LauncherApplication app = (LauncherApplication) context.getApplicationContext(); //其内部其实就是maxId+1 item.id = app.getLauncherProvider().generateNewId(); values.put(LauncherSettings.Favorites._ID, item.id); //更新cellX,cellY。 item.updateValuesWithCoordinates(values, item.cellX, item.cellY); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { String transaction = "DbDebug Add item (" + item.title + ") to db, id: " + item.id + " (" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; Launcher.sDumpLogs.add(transaction); Log.d(TAG, transaction); //具体执行数据库的CURD操作 cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { checkItemInfoLocked(item.id, item, stackTrace); sBgItemsIdMap.put(item.id, item);//sBgItemsIdMap存放所有的info appwidget,folder,shortcut... switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: sBgFolders.put(item.id, (FolderInfo) item);//sBgFolders存放所有FolderInfo // Fall through case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { sBgWorkspaceItems.add(item);//存放所有ShortCutInfo } else { if (!sBgFolders.containsKey(item.container)) { // Adding an item to a folder that doesn't exist. String msg = "adding item: " + item + " to a folder that " + " doesn't exist"; Log.e(TAG, msg); Launcher.dumpDebugLogsToConsole(); } } break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.add((LauncherAppWidgetInfo) item);//存放所有AppWidgetInfo break; } } } }; runOnWorkerThread(r); }
- moveItemInDatabase方法
/** * Move an item in the DB to a new <container, screen, cellX, cellY> */ static void moveItemInDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY) { String transaction = "DbDebug Modify item (" + item.title + ") in db, id: " + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")"; Launcher.sDumpLogs.add(transaction); Log.d(TAG, transaction); item.container = container; item.cellX = cellX; item.cellY = cellY; // We store hotseat items in canonical form which is this orientation invariant position // in the hotseat if (context instanceof Launcher && screen < 0 && container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY); } else { item.screen = screen; } final ContentValues values = new ContentValues(); values.put(LauncherSettings.Favorites.CONTAINER, item.container); values.put(LauncherSettings.Favorites.CELLX, item.cellX); values.put(LauncherSettings.Favorites.CELLY, item.cellY); values.put(LauncherSettings.Favorites.SCREEN, item.screen); updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase"); } /** * 把itemInfo更新到db中,并且重新判断itemType来选择当前itemInfo该存放的List * @param context * @param values * @param item * @param callingFunction */ static void updateItemInDatabaseHelper(Context context, final ContentValues values, final ItemInfo item, final String callingFunction) { final long itemId = item.id; final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false); final ContentResolver cr = context.getContentResolver(); final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); Runnable r = new Runnable() { public void run() { cr.update(uri, values, null, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { checkItemInfoLocked(itemId, item, stackTrace); if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP && item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) { // Item is in a folder, make sure this folder exists if (!sBgFolders.containsKey(item.container)) { // An items container is being set to a that of an item which is not in // the list of Folders. String msg = "item: " + item + " container being set to: " + item.container + ", not in the list of folders"; Log.e(TAG, msg); Launcher.dumpDebugLogsToConsole(); } } // Items are added/removed from the corresponding FolderInfo elsewhere, such // as in Workspace.onDrop. Here, we just add/remove them from the list of items // that are on the desktop, as appropriate ItemInfo modelItem = sBgItemsIdMap.get(itemId); if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP || modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { switch (modelItem.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: if (!sBgWorkspaceItems.contains(modelItem)) { sBgWorkspaceItems.add(modelItem); } break; default: break; } } else { sBgWorkspaceItems.remove(modelItem); } } } }; runOnWorkerThread(r); }
- deleteItemFromDatabase方法
/** * Removes the specified item from the database 和list * @param context * @param item */ static void deleteItemFromDatabase(Context context, final ItemInfo item) { final ContentResolver cr = context.getContentResolver(); final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false); Runnable r = new Runnable() { public void run() { String transaction = "DbDebug Delete item (" + item.title + ") from db, id: " + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY + ")"; Launcher.sDumpLogs.add(transaction); Log.d(TAG, transaction); cr.delete(uriToDelete, null, null); // Lock on mBgLock *after* the db operation synchronized (sBgLock) { switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: sBgFolders.remove(item.id); for (ItemInfo info: sBgItemsIdMap.values()) { if (info.container == item.id) { // We are deleting a folder which still contains items that // think they are contained by that folder. String msg = "deleting a folder (" + item + ") which still " + "contains items (" + info + ")"; Log.e(TAG, msg); Launcher.dumpDebugLogsToConsole(); } } sBgWorkspaceItems.remove(item); break; case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: sBgWorkspaceItems.remove(item); break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: sBgAppWidgets.remove((LauncherAppWidgetInfo) item); break; } sBgItemsIdMap.remove(item.id); sBgDbIconCache.remove(item); } } }; runOnWorkerThread(r); }
结语
本文主要介绍了Launcher快捷图标的排列信息的数据库存储方式及字段含义,以及LauncherModel如何获取的这些排列信息,希望能对读者有帮助。