Launcher3 Workspace載入流程淺析(基於Aosp P版本)
launcher3 Workspace載入流程淺析
前言
Aosp launcher3原始碼隨著Android大版本的迭代,也在不斷的更新,大體功能變化不大,但卻在不斷的重構和優化程式碼,程式碼的封裝和擴充套件性變得越來越好,作為launcher開發者,也要緊跟步伐去學習,把好的實現及時運用到實際開發中去。本文將基於最新的Android P版本的launcher3,分析下這個流程。
幾個重要的類
這幾個類可以算是launcher主要的框架類,熟悉了它們,對launcher就基本有個大概瞭解了:
- Launcher:launcher主介面,是一個Activity,開機後由系統自動啟動。
- LauncherAppState:這是一個單例類,其中初始化了基本所有涉及的重要物件,包括LauncherModel
- LauncherModel:這是workspace資料載入最核心的一個類,負責workspace資料的載入和更新,並維護了所有快取,還負責監聽package的各種變化。
- LoaderResults:該類是Launcher3 P版本新增的,把之前繫結view的操作都封裝了起來。
- LauncherProvider:封裝了workspace相關的資料庫操作,外部通過該provider呼叫操作
- BgDataModel:該類是Launcher3 O版本新增的,對memory快取作了封裝,快取了所有app、shortcut、folder、widget、screen等資料
- Workspace:這就是桌面上網格顯示應用圖示的View,可以左右滑動,顯示多頁。
原始碼淺析
1. Launcher.java onCreate方法開始
private LauncherModel mModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
......
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.setLauncher(this);
mModel.startLoader(currentScreen);//開始載入資料
......
}
onCreate中呼叫LauncherAppState.getInstance會建立LauncherAppState單例,在LauncherAppState建構函式中,做了很多初始化操作,其中一個就是例項化LauncherModel,並且會持有它的引用。然後使用mModel物件呼叫startLoader開始載入workspace和all apps資料,本文主要討論workspace的資料載入。
LauncherAppState建構函式如下:
private final LauncherModel mModel;
private LauncherAppState(Context context) {
......
mModel = new LauncherModel(this, mIconCache, AppFilter.newInstance(mContext));
}
LauncherModel只在此處例項化了一次,它實際上也是一個單例項的存在。
2. LauncherModel中startLoader方法被吊起
public boolean startLoader(int synchronousBindPage) {
......
LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
mBgAllAppsList, synchronousBindPage, mCallbacks);
startLoaderForResults(loaderResults);
}
public void startLoaderForResults(LoaderResults results) {
synchronized (mLock) {
stopLoader();
mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results);
runOnWorkerThread(mLoaderTask);//在子執行緒載入資料
}
}
LoaderTask實現了Runnable介面,封裝了載入資料的操作,作為一個任務被加入執行緒佇列。
3. LoaderTask執行run方法
run方法實現如下:
public void run() {
......
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
TraceHelper.partitionSection(TAG, "step 1.1: loading workspace");
loadWorkspace();//載入workspace
verifyNotStopped();
TraceHelper.partitionSection(TAG, "step 1.2: bind workspace workspace");
mResults.bindWorkspace();//繫結workspace
// Notify the installer packages of packages with active installs on the first screen.
TraceHelper.partitionSection(TAG, "step 1.3: send first screen broadcast");
sendFirstScreenActiveInstallsBroadcast();
// Take a break
TraceHelper.partitionSection(TAG, "step 1 completed, wait for idle");
waitForIdle();
verifyNotStopped();
// second step
TraceHelper.partitionSection(TAG, "step 2.1: loading all apps");
loadAllApps();//載入all apps
TraceHelper.partitionSection(TAG, "step 2.2: Binding all apps");
verifyNotStopped();
mResults.bindAllApps();//繫結app apps資料到介面
verifyNotStopped();
TraceHelper.partitionSection(TAG, "step 2.3: Update icon cache");
updateIconCache();
// Take a break
TraceHelper.partitionSection(TAG, "step 2 completed, wait for idle");
waitForIdle();
verifyNotStopped();
// third step
TraceHelper.partitionSection(TAG, "step 3.1: loading deep shortcuts");
loadDeepShortcuts();
verifyNotStopped();
TraceHelper.partitionSection(TAG, "step 3.2: bind deep shortcuts");
mResults.bindDeepShortcuts();
// Take a break
TraceHelper.partitionSection(TAG, "step 3 completed, wait for idle");
waitForIdle();
verifyNotStopped();
// fourth step
TraceHelper.partitionSection(TAG, "step 4.1: loading widgets");
mBgDataModel.widgetsModel.update(mApp, null);
verifyNotStopped();
TraceHelper.partitionSection(TAG, "step 4.2: Binding widgets");
mResults.bindWidgets();
transaction.commit();
} catch (CancellationException e) {
// Loader stopped, ignore
TraceHelper.partitionSection(TAG, "Cancelled");
}
......
}
run方法中依次loadWorkspace、bindWorkspace、loadAllApps、bindAllApps等直至所有資料載入完畢。
4. loadWorkspace
這裡主要分析下Workspace的載入過程:
private void loadWorkspace() {
......
Log.d(TAG, "loadWorkspace: loading default favorites");
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES);
synchronized (mBgDataModel) {
mBgDataModel.clear();
......
// 這邊LoaderCursor對Cursor操作做了封裝
final LoaderCursor c = new LoaderCursor(contentResolver.query(
LauncherSettings.Favorites.CONTENT_URI, null, null, null, null), mApp);
}
}
loadWorkspace方法主要做了兩件事情,一件是載入預設配置xml檔案中的items,並把預設資料存入資料庫,另一件是載入資料庫中的資料到快取,當然還會做各種有效性校驗,這裡不做具體分析。
此處載入預設配置到資料庫是呼叫LauncherProvider中call方法實現的,程式碼如下:
@Override
public Bundle call(String method, final String arg, final Bundle extras) {
......
switch (method) {
......
case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
loadDefaultFavoritesIfNecessary();
return null;
}
}
}
LauncherProvide的call方法,又呼叫了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 private void loadDefaultFavoritesIfNecessary() {
SharedPreferences sp = Utilities.getPrefs(getContext());
if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
Log.d(TAG, "loading default workspace");
AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
if (loader == null) {
loader = AutoInstallsLayout.get(getContext(),widgetHost, 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(), widgetHost,
mOpenHelper, partnerRes, workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHost);
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
// 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.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHost));
}
clearFlagEmptyDbCreated();
}
}
如上面方法註釋,此處會按照優先順序順序,檢查4種配置是否存在。預設配置xml檔案,如果是定製ROM,可以配置在系統裡面,以apk的形式,或者固定路徑的形式提供配置;如果是第三方Launcher,需要放在在apk裡面。
5. LoaderResults封裝繫結
android P上新增了LoaderResults類,封裝了資料繫結到介面的操作,如下面的bindWorkspace,使程式碼結構更加合理和清晰。
public void bindWorkspace() {
Runnable r;
......
// Tell the workspace that we're about to start binding items
r = new Runnable() {
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.clearPendingBinds();
callbacks.startBinding();
}
}
};
mUiExecutor.execute(r);
// Bind workspace screens
mUiExecutor.execute(new Runnable() {
@Override
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.bindScreens(orderedScreenIds);
}
}
});
Executor mainExecutor = mUiExecutor;
// Load items on the current page.
bindWorkspaceItems(currentWorkspaceItems, currentAppWidgets, mainExecutor);
// In case of validFirstPage, only bind the first screen, and defer binding the
// remaining screens after first onDraw (and an optional the fade animation whichever
// happens later).
// This ensures that the first screen is immediately visible (eg. during rotation)
// In case of !validFirstPage, bind all pages one after other.
final Executor deferredExecutor =
validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
mainExecutor.execute(new Runnable() {
@Override
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.finishFirstPageBind(
validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null);
}
}
});
bindWorkspaceItems(otherWorkspaceItems, otherAppWidgets, deferredExecutor);
// Tell the workspace that we're done binding items
r = new Runnable() {
public void run() {
Callbacks callbacks = mCallbacks.get();
if (callbacks != null) {
callbacks.finishBindingItems();
}
}
};
deferredExecutor.execute(r);
}
6. LauncherModel.Callbacks介面
Launcher實現了LauncherModel.Callbacks介面,用於回撥介面顯示更新,程式碼如下:
public interface Callbacks {
......
//這邊只列出了這邊分析涉及的介面,便於檢視
public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);//繫結items
public void bindScreens(ArrayList<Long> orderedScreenIds);//繫結screen
public void bindAllApplications(ArrayList<AppInfo> apps);//繫結所有應用
}
Launcher中的實現,繫結螢幕的程式碼如下:
@Override
public void bindScreens(ArrayList<Long> orderedScreenIds) {
// Make sure the first screen is always at the start.
if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
orderedScreenIds.remove(Workspace.FIRST_SCREEN_ID);
orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
LauncherModel.updateWorkspaceScreenOrder(this, orderedScreenIds);
} else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
// If there are no screens, we need to have an empty screen
mWorkspace.addExtraEmptyScreen();
}
bindAddScreens(orderedScreenIds);
// After we have added all the screens, if the wallpaper was locked to the default state,
// then notify to indicate that it can be released and a proper wallpaper offset can be
// computed before the next layout
mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
}
bindAddScreens會根據實際計算所得的螢幕個數,建立螢幕的View,launcher中每一屏對應的View是一個CellLayout。
繫結Item的程式碼如下:
@Override
public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
......
int end = items.size();
for (int i = 0; i < end; i++) {
final ItemInfo item = items.get(i);
// Short circuit if we are loading dock items for a configuration which has no dock
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
mHotseat == null) {
continue;
}
final View view;
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: {
ShortcutInfo info = (ShortcutInfo) item;
view = createShortcut(info);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: {
view = FolderIcon.fromXml(R.layout.folder_icon, this,
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
(FolderInfo) item);
break;
}
case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: {
view = inflateAppWidget((LauncherAppWidgetInfo) item);
if (view == null) {
continue;
}
break;
}
default:
throw new RuntimeException("Invalid Item Type");
}
/*
* Remove colliding items.
*/
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
View v = cl.getChildAt(item.cellX, item.cellY);
Object tag = v.getTag();
String desc = "Collision while binding workspace item: " + item
+ ". Collides with " + tag;
if (FeatureFlags.IS_DOGFOOD_BUILD) {
throw (new RuntimeException(desc));
} else {
Log.d(TAG, desc);
getModelWriter().deleteItemFromDatabase(item);
continue;
}
}
}
workspace.addInScreenFromBind(view, item);
}
.......
workspace.requestLayout();
}
bindItems會迴圈建立對於型別的View,重新整理顯示到桌面上,這邊建立的View就是我們最終看到的應用圖示、資料夾、小部件。
總結
以上即本次分析的所有內容,主要討論了androd P版本launcher3 workspace的載入和繫結過程,談不上深度解析,歡迎指正。後續會繼續深入,具體到一些關鍵細節的實現,以及開發過程中常涉及的需求修改點。