布局结构
抓取布局后,可以看到每个图标是一个DoubleShadowBubbleTextView,父布局是CellLayout、workspace。
我们可以在CellLayout添加子view打印出调用堆栈信息,可以整体上看页面加载显示流程。
主要类
- Launcher.java:主界面,即MainActivity
- launcher.xml:主界面布局文件
- LauncherModel.java:管理Launcher状态,包括加载任务、状态回调等
- LoaderTask.java:加载任务,是一个Runnable
- LoaderResults.java:加载结果
- BgDataModel#Callbacks:数据回调接口,Launcher实现该接口,加载任务通过该接口回调给Launcher
- LauncherProvider:桌面数据提供者,采用db保存桌面图标数据(包括排列位置、类型等)
- LauncherSettings:封装访问LauncherProvider时的uri、column等一些常量,通过ContentResolver来访问LauncherProvider,不直接操作db
- LoaderCursor:封装cursor操作
流程图
创建Activity
主界面创建的时候,常规的setContentView和findView,创建LauncherModel并开始加载数据
// Launcher.java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 1.创建LauncherAppState和LauncherModel
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.getModel();
// 2.infalte布局文件,找到view
setupViews();
// 3. 设置loader监听,开始加载数据,加载完成后回调给主界面
if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
// If we are not binding synchronously, pause drawing until initial bind complete,
// so that the system could continue to show the device loading prompt
mOnInitialBindListener = Boolean.FALSE::booleanValue;
}
}
// 4. 设置view给Activity
setContentView(getRootView());
加载数据
创建加载任务
mModel.addCallbacksAndLoad将Launcher设置给LauncherModel,然后创建了加载任务
// LauncherModel.java
/**
* Adds a callbacks to receive model updates
* @return true if workspace load was performed synchronously
*/
public boolean addCallbacksAndLoad(@NonNull final Callbacks callbacks) {
synchronized (mLock) {
addCallbacks(callbacks);
return startLoader(new Callbacks[] { callbacks });
}
}
private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
synchronized (mLock) {
// 1.取消旧的加载任务
boolean wasRunning = stopLoader(); // 之前没有loader任务,wasRunning为false
boolean bindDirectly = mModelLoaded && !mIsLoaderTaskRunning;// 之前没有加载过,bindDirectly为false
boolean bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.length == 0; // bindAllCallbacks为true, 上一步已经addCallbacks了,所以callbacksList里面包含从Launcher.java传进来的callbacks
final Callbacks[] callbacksList = bindAllCallbacks ? getCallbacks() : newCallbacks;
if (callbacksList.length > 0) {
// 2.清空PendingBind
for (Callbacks cb : callbacksList) {
MAIN_EXECUTOR.execute(cb::clearPendingBinds);
}
// 3.创建LoaderResults和LoaderTask,开始加载数据
LoaderResults loaderResults = new LoaderResults(
mApp, mBgDataModel, mBgAllAppsList, callbacksList);
if (bindDirectly) {
loaderResults.bindWorkspace(bindAllCallbacks);
loaderResults.bindAllApps();
loaderResults.bindDeepShortcuts();
loaderResults.bindWidgets();
return true;
} else {
stopLoader();
mLoaderTask = new LoaderTask(
mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, loaderResults);
MODEL_EXECUTOR.post(mLoaderTask);
}
}
}
}
整体加载步骤
LoaderTask#run中根据类型加载步骤分为了5步,图标的加载主要看第一步
// LoaderTask.java
public void run() {
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
// first step
List<ShortcutInfo> allShortcuts = new ArrayList<>();
loadWorkspace(allShortcuts, memoryLogger);
// Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
// sanitizeData should not be invoked if the workspace is loaded from a db different
// from the main db as defined in the invariant device profile.
// (e.g. both grid preview and minimal device mode uses a different db)
if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
sanitizeData();
}
mResults.bindWorkspace(true /* incrementBindId */);
mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
// Take a break
waitForIdle();
// second step
List<LauncherActivityInfo> allActivityList;
allActivityList = loadAllApps();
mResults.bindAllApps();
IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
setIgnorePackages(updateHandler);
updateHandler.updateIcons(allActivityList,
LauncherActivityCachingLogic.newInstance(mApp.getContext()),
mApp.getModel()::onPackageIconsUpdated);
updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
mApp.getModel()::onPackageIconsUpdated);
waitForIdle();
// third step
List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
mResults.bindDeepShortcuts();
updateHandler.updateIcons(allDeepShortcuts,
new ShortcutCachingLogic(), (pkgs, user) -> { });
waitForIdle();
// fourth step
List<ComponentWithLabelAndIcon> allWidgetsList =
mBgDataModel.widgetsModel.update(mApp, null);
mResults.bindWidgets();
updateHandler.updateIcons(allWidgetsList,
new ComponentWithIconCachingLogic(mApp.getContext(), true),
mApp.getModel()::onWidgetLabelsUpdated);
// fifth step
loadFolderNames();
updateHandler.finish();
mModelDelegate.modelLoadComplete();
transaction.commit();
} catch (CancellationException e) {
...
}
}
图标解析
loadWorkspace()代码比较多,主要作用是通过ContentResolver查询LauncherProvider中保存的桌面图标信息,然后遍历cursor来解析数据。
在分析代码的时候,抓到数据流向,可以结合真实的db数据进行分析,其中一些判断容错处理可以跳过
主要流程如下:
- 加载默认的数据(Provider会有判断,只有第一次才会加载)
- 通过ContentResolver查询所有数据
- 遍历cursor,根据不同图标类型进行解析,如ITEM_TYPE_APPLICATION代表普通的图标,ITEM_TYPE_FOLDER代表文件夹
- 对数据进行一些校验,校验通过后将数据添加到mBgDataModel
数据流向:
Db(LauncherProvider) --> Cursor(LoaderCursor) --> List(BgDataModel)
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
Uri contentUri,
String selection,
@Nullable LoaderMemoryLogger logger) {
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
// 1. 加载默认的数据(Provider会有判断,只有第一次才会加载)
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
synchronized (mBgDataModel) {
mBgDataModel.clear();
mPendingPackages.clear();
// 2. 查询所有数据
final LoaderCursor c = new LoaderCursor(
contentResolver.query(contentUri, null, selection, null, null), contentUri,
mApp, mUserManagerState);
final Bundle extras = c.getExtras();
mDbName = extras == null
? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME);
try {
// 2.1 遍历cursor
final int appWidgetIdIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_ID);
final int appWidgetProviderIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_PROVIDER);
final int spanXIndex = c.getColumnIndexOrThrow
(LauncherSettings.Favorites.SPANX);
final int spanYIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.SPANY);
final int rankIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.RANK);
final int optionsIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.OPTIONS);
final int sourceContainerIndex = c.getColumnIndexOrThrow(
LauncherSettings.Favorites.APPWIDGET_SOURCE);
WorkspaceItemInfo info;
LauncherAppWidgetInfo appWidgetInfo;
LauncherAppWidgetProviderInfo widgetProviderInfo;
Intent intent;
String targetPkg;
List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
while (!mStopped && c.moveToNext()) {
try {
// 2.2 根据不同图标类型进行解析,ITEM_TYPE_APPLICATION代表普通的图标,ITEM_TYPE_FOLDER代表文件夹
switch (c.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
intent = c.parseIntent();
// 2.3 解析数据,进行一些校验判断
if (info != null) {
if (info.itemType
!= LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// Skip deep shortcuts; their title and icons have already been
// loaded above.
iconRequestInfos.add(
c.createIconRequestInfo(info, useLowResIcon));
}
c.applyCommonProperties(info);
info.intent = intent;
info.rank = c.getInt(rankIndex);
info.spanX = 1;
info.spanY = 1;
info.runtimeStatusFlags |= disabledState;
if (isSafeMode && !isSystemApp(context, intent)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
}
LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
if (activityInfo != null) {
info.setProgressLevel(
PackageManagerHelper
.getLoadingProgress(activityInfo),
PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
}
if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
tempPackageKey.update(targetPkg, c.user);
SessionInfo si = installingPkgs.get(tempPackageKey);
if (si == null) {
info.runtimeStatusFlags &=
~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
} else if (activityInfo == null) {
int installProgress = (int) (si.getProgress() * 100);
info.setProgressLevel(
installProgress,
PackageInstallInfo.STATUS_INSTALLING);
}
}
// 3.将数据添加到mBgDataModel
c.checkAndAddItem(info, mBgDataModel, logger);
} else {
throw new RuntimeException("Unexpected null WorkspaceItemInfo");
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(c.titleIndex);
folderInfo.spanX = 1;
folderInfo.spanY = 1;
folderInfo.options = c.getInt(optionsIndex);
// no special handling required for restored folders
c.markRestored();
c.checkAndAddItem(folderInfo, mBgDataModel, logger);
break;
}
} catch (Exception e) {
Log.e(TAG, "Desktop items loading interrupted", e);
}
}
} finally {
IOUtils.closeSilently(c);
}
// Load delegate items
mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts);
// Load string cache
mModelDelegate.loadStringCache(mBgDataModel.stringCache);
// Remove dead items
mItemsDeleted = c.commitDeleted();
// Sort the folder items, update ranks, and make sure all preview items are high res.
FolderGridOrganizer verifier =
new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
for (FolderInfo folder : mBgDataModel.folders) {
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
verifier.setFolderInfo(folder);
int size = folder.contents.size();
// Update ranks here to ensure there are no gaps caused by removed folder items.
// Ranks are the source of truth for folder items, so cellX and cellY can be ignored
// for now. Database will be updated once user manually modifies folder.
for (int rank = 0; rank < size; ++rank) {
WorkspaceItemInfo info = folder.contents.get(rank);
info.rank = rank;
if (info.usingLowResIcon()
&& info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
&& verifier.isItemInPreview(info.rank)) {
mIconCache.getTitleAndIcon(info, false);
}
}
}
c.commitRestoredItems();
}
}
可以结合db数据进行分析:
- container: 表示显示的布局,像普通、文件夹、dock栏等
- screen: 表示现在第几屏
- itemType:表示类型,像应用图标、文件夹、快捷方式等
图标显示
到这里,我们已经获取到桌面数据了,下面就是要设置到view显示出来。在上面LoaderTask#run()中,loadWorkspace()获取到了数据,而ui显示的触发在mResults.bindWorkspace(true)
bindWorkspace()先是将数据复制一份,然后遍历callback进行bind。上一步mBgDataModel保存了数据库中数据,mBgDataModel是LauncherModel的一个成员变量,在activity#onCreate时创建的,而activity就是Callbacks,所以到这里都关联了起来。
Launcher (Callbacks)—> LoaderTask —> LoaderResults --> BgDataModel --> Callbacks
// LoaderResults.java
/**
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace(boolean incrementBindId) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
final IntArray orderedScreenIds = new IntArray();
ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
synchronized (mBgDataModel) {
workspaceItems.addAll(mBgDataModel.workspaceItems);
appWidgets.addAll(mBgDataModel.appWidgets);
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
mBgDataModel.extraItems.forEach(extraItems::add);
if (incrementBindId) {
mBgDataModel.lastBindId++;
}
mMyBindingId = mBgDataModel.lastBindId;
}
for (Callbacks cb : mCallbacksList) {
new WorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
workspaceItems, appWidgets, extraItems, orderedScreenIds).bind();
}
}
WorkspaceBinder中bind方法会执行callback回调,回调到activity后,创建BubbleTextView添加到CellLayout。