Launcher3 桌面加载流程分析(下)
LauncherModel 创建LoaderTask加载数据,我们继续往下看
LoaderTask
创建LoaderTask,flags为 PagedView.INVALID_RESTORE_PAGE值-1001, 我们看它的run方法是如何执行的。
private class LoaderTask implements Runnable {
LoaderTask(Context context, int flags) {
mContext = context;
mFlags = flags;
}
public void run() {
...
keep_running: {
if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
loadAndBindWorkspace();
if (mStopped) {
break keep_running;
}
waitForIdle();
// second step
if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
loadAndBindAllApps();
}
...
}
}
根据mStopped的状态做一些预先的判断外,最先执行的是 loadAndBindWorkspace()方法,加载和绑定Workspace的数据,包括屏幕数,应用数据,widget组件信息等等,然后调用waitForIdle() 等待loadAndBindWorkspace()里创建的一些子线程执行完,修改mStopped和mLoadAndBindStepFinished的状态后执行loadAndBindAllApps(),加载所有应用,完成整个加载应用的流程。
整体流程就是上面的run方法,具体的细节我们一步步来看。
Workspace
Workspace是什么呢?大家自己看下Launcher3的主布局文件launcher.xml布局就很明了,workspace是Launcher的工作台,承载应用数据,widget组件数据,文件夹数据以及其他的功能。
加载workspace的流程分两步,
- 加载数据,loadWorkspace()
- 绑定workspace, bindWorkspace()
流程如下
private void loadAndBindWorkspace() {
mIsLoadingAndBindingWorkspace = true;
// Load the workspace
if (DEBUG_LOADERS) {
Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
}
if (!mWorkspaceLoaded) {
loadWorkspace();
synchronized (LoaderTask.this) {
if (mStopped) {
return;
}
mWorkspaceLoaded = true;
}
}
// Bind the workspace
bindWorkspace(-1);
}
加载WorkSpace数据
加载WorkSpace数据的方法都在loadWorkSpace()里, 这个步骤是整个流程最核心的,虽然只有loadWorkSpace()这个方法,但是我目前的这个版本该方法的源码就达到600多行,所以我们截取核心的代码来分析,很多细节还是要大家自己去琢磨。
final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
final Context context = mContext;
final ContentResolver contentResolver = context.getContentResolver();
final PackageManager manager = context.getPackageManager();
final boolean isSafeMode = manager.isSafeMode();
final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
final boolean isSdCardReady = context.registerReceiver(null,
new IntentFilter(StartupReceiver.SYSTEM_READY)) != null;
LauncherAppState app = LauncherAppState.getInstance();
InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
int countX = profile.numColumns;
int countY = profile.numRows;
if (GridSizeMigrationTask.ENABLED &&
!GridSizeMigrationTask.migrateGridIfNeeded(mContext)) {
// Migration failed. Clear workspace.
mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE;
}
if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) {
Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true);
LauncherAppState.getLauncherProvider().deleteDatabase();
}
if ((mFlags & LOADER_FLAG_MIGRATE_SHORTCUTS) != 0) {
// append the user's Launcher2 shortcuts
Launcher.addDumpLog(TAG, "loadWorkspace: migrating from launcher2", true);
LauncherAppState.getLauncherProvider().migrateLauncher2Shortcuts();
} else {
// Make sure the default workspace is loaded
Launcher.addDumpLog(TAG, "loadWorkspace: loading default favorites", false);
LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary();
}
首先声明了一些核心对象,ContentResolver,LauncherAppsCompat, LauncherAppState,InvariantDeviceProfile这些上一篇已经有介绍过就不再赘述。
前几个if条件,是关于数据库移植的,比如Lacuncher2,升级到Launcher3,桌面图标大小发生变化的特殊场合处理,不是我们需要特别留意的。
加载WorkSpace资源文件
关键的代码是 LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary(); 在首次打开Launcher时,会加载默认的数据,比如桌面首页显示什么内容,hotseat配置等等。看LauncherAppState代码发现调用的是 LauncherProvider的loadDefaultFavoritesIfNecessary方法
/**
* Loads the default workspace based on the following priority scheme:
* 1) From the app restrictions
* 2) From a package provided by play store
* 3) From a partner configuration APK, already in the system image
* 4) The default configuration for the particular device
*/
synchronized public void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction();
if (loader == null) {
loader = AutoInstallsLayout.get(getContext(),
mOpenHelper.mAppWidgetHost, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(getContext().getPackageManager());
if (partner != null && partner.hasDefaultLayout()) {
final Resources partnerRes = partner.getResources();
int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
"xml", partner.getPackageName());
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser();
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
createEmptyDB();
// Populate favorites table with initial favorites
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
createEmptyDB();
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser());
}
clearFlagEmptyDbCreated();
}
}
正常流程不会都执行,故简单介绍一下,该方法跟注释一样,会从以下几种方式中的一种加载默认布局
- 应用约束,调用createWorkspaceLoaderFromAppRestriction,获取用户设置的一组用于限制应用功能的Bundle串,获取Bundle里workspace.configuration.package.name具体的应用包名,获取WorkSpace默认配置资源。
- 从带有 android.autoinstalls.config.action.PLAY_AUTO_INSTALL Action的应用里获取workspace默认配置资源
- 从系统内置的partner应用里获取workspace默认配置
- 调用getDefaultLayoutParser() 获取我们Launcher里的默认资源
默认流程,会执行第四步, 然后创建数据库,建表favorites和workspaceScreens,加载数据mOpenHelper.loadFavorites
private DefaultLayoutParser getDefaultLayoutParser() {
int defaultLayout = LauncherAppState.getInstance()
.getInvariantDeviceProfile().defaultLayoutId;
return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
mOpenHelper, getContext().getResources(), defaultLayout);
}
而默认的资源就是我们配置 InvariantDeviceProfile的资源如R.xml.default_workspace_5x6,详细见上一篇文章。故我们可以在res/xml/里修改我们的默认显示应用的配置。
@Thunk
int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
ArrayList<Long> screenIds = new ArrayList<Long>();
// TODO: Use multiple loaders with fall-back and transaction.
int count = loader.loadLayout(db, screenIds);
// Add the screens specified by the items above
Collections.sort(screenIds);
int rank = 0;
ContentValues values = new ContentValues();
for (Long id : screenIds) {
values.clear();
values.put(LauncherSettings.WorkspaceScreens._ID, id);
values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS,
null, values) < 0) {
throw new RuntimeException("Failed initialize screen table"
+ "from default layout");
}
rank++;
}
// Ensure that the max ids are initialized
mMaxItemId = initializeMaxItemId(db);
mMaxScreenId = initializeMaxScreenId(db);
return count;
}
loadFavorites方法里调用DefaultLayoutParser.loadLayout(db, screenIds) 解析布局xml里的文件夹信息,应用信息,widget信息等等保存到数据库, 并获取到屏幕id集合,保存到workspaceScreens表中。至于怎么解析的,我们直接看关键的代码
AutoInstallsLayout.java
/**
* Parses the layout and returns the number of elements added on the homescreen.
*/
protected int parseLayout(int layoutId, ArrayList<Long> screenIds)
throws XmlPullParserException, IOException {
XmlResourceParser parser = mSourceRes.getXml(layoutId);
beginDocument(parser, mRootTag);
final int depth = parser.getDepth();
int type;
HashMap<String, TagParser> tagParserMap = getLayoutElementsMap();
int count = 0;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
count += parseAndAddNode(parser, tagParserMap, screenIds);
}
return count;
}
通过XmlResourceParser 解析xml文件,获取我们需要的配置。不同的标签通过不同的解析对象处理,我们使用的是AutoInstallsLayout的子类DefaultLayoutParser,在getLayoutElementsMap()方法里,我们可以看到使用的处理对象,如应用解析器ResolveParser,文件夹解析器MyFolderParser等等,解析到信息后会保存到对应的数据库中。这就是加载默认workspace的原理,解析的细节就不一一介绍了,请大家自己找到需要的解析器琢磨代码了哦
DefaultLayoutParser.java
@Override
protected HashMap<String, TagParser> getLayoutElementsMap() {
HashMap<String, TagParser> parsers = new HashMap<String, TagParser>();
parsers.put(TAG_FAVORITE, new AppShortcutWithUriParser());
parsers.put(TAG_APPWIDGET, new AppWidgetParser());
parsers.put(TAG_SHORTCUT, new UriShortcutParser(mSourceRes));
parsers.put(TAG_RESOLVE, new ResolveParser());
parsers.put(TAG_FOLDER, new MyFolderParser());
parsers.put(TAG_PARTNER_FOLDER, new PartnerFolderParser());
return parsers;
}
我们简单的介绍一个默认配置,
default_workspace_4x4.xml
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto">
<!-- Hotseat -->
<include launcher:workspace="@xml/dw_phone_hotseat" />
<!-- Bottom row -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
<favorite launcher:uri="#Intent;type=images/*;end" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
<favorite launcher:uri="market://details?id=com.android.launcher" />
</resolve>
</favorites>
- @xml/dw_phone_hotseat, hotseat配置文件,配置规则一样
- resolve标签, 通过ResolveParser解析,包含内嵌标签
- favorite, 一个app的信息,可以指定uri,或具体的包名,类名来识别app
- 可以有自定义的标签,自己实现解析即可
至此,loadDefaultFavoritesIfNecessary()就执行完成,我们回到LauncherModel继续看loadWorkspace()
读取数据
接下来就是从数据库读取从配置文件读到的信息,根据itemType, 走switch的不同case
final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
final Cursor c = contentResolver.query(contentUri, null, null, null, null);
// +1 for the hotseat (it can be larger than the workspace)
// Load workspace in reverse order to ensure that latest items are loaded first (and
// before any earlier duplicates)
final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>();
HashMap<ComponentKey, AppWidgetProviderInfo> widgetProvidersMap = null;
...
while (!mStopped && c.moveToNext()) {
try {
int itemType = c.getInt(itemTypeIndex);
boolean restored = 0 != c.getInt(restoredIndex);
boolean allowMissingTarget = false;
container = c.getInt(containerIndex);
switch (itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
...
sBgItemsIdMap.put(info.id, info);
...
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
...
sBgItemsIdMap.put(folderInfo.id, folderInfo);
sBgFolders.put(folderInfo.id, folderInfo);
...
break;
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
...
比如 ITEM_TYPE_APPLICATION和ITEM_TYPE_SHORTCUT,执行到最后会保存到LongArrayMap sBgItemsIdMap里。而ITEM_TYPE_FOLDER,保存到LongArrayMap sBgFolders,同时也保存到sBgItemsIdMap,因为FolderInfo和应用的AppInfo,ShortcutInfo都是继承ItemInfo,后续可以转型处理。ITEM_TYPE_APPWIDGET,ITEM_TYPE_CUSTOM_APPWIDGET同理。
这里的细节比较多,通常我们也不用特地修正这里的查询逻辑,故不做详细阐述。
绑定WorkSpace数据
加载完workspace数据后,往下就是讲数据绑定到workspace,调用bindWorkspace(-1)方法。上一步我, 知道数据都保存在集合sBgWorkspaceItems,sBgAppWidgets,sBgWorkspaceScreens里,这里Google不是直接遍历里面的数据,绑定到View上。而是做了一个copy的操作,避免后续的某个线程修改全局变量影响到其他的工作线程。
private void bindWorkspace(int synchronizeBindPage) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>();
ArrayList<LauncherAppWidgetInfo> appWidgets =
new ArrayList<LauncherAppWidgetInfo>();
ArrayList<Long> orderedScreenIds = new ArrayList<Long>();
final LongArrayMap<FolderInfo> folders;
final LongArrayMap<ItemInfo> itemsIdMap;
synchronized (sBgLock) {
workspaceItems.addAll(sBgWorkspaceItems);
appWidgets.addAll(sBgAppWidgets);
orderedScreenIds.addAll(sBgWorkspaceScreens);
folders = sBgFolders.clone();
itemsIdMap = sBgItemsIdMap.clone();
}
final boolean isLoadingSynchronously =
synchronizeBindPage != PagedView.INVALID_RESTORE_PAGE;
int currScreen = isLoadingSynchronously ? synchronizeBindPage :
oldCallbacks.getCurrentWorkspaceScreen();
if (currScreen >= orderedScreenIds.size()) {
// There may be no workspace screens (just hotseat items and an empty page).
currScreen = PagedView.INVALID_RESTORE_PAGE;
}
final int currentScreen = currScreen;
final long currentScreenId = currentScreen < 0
? INVALID_SCREEN_ID : orderedScreenIds.get(currentScreen);
// Load all the items that are on the current page first (and in the process, unbind
// all the existing workspace items before we call startBinding() below.
unbindWorkspaceItemsOnMainThread();
// Separate the items that are on the current screen, and all the other remaining items
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>();
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets =
new ArrayList<LauncherAppWidgetInfo>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets =
new ArrayList<LauncherAppWidgetInfo>();
LongArrayMap<FolderInfo> currentFolders = new LongArrayMap<>();
LongArrayMap<FolderInfo> otherFolders = new LongArrayMap<>();
filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
otherWorkspaceItems