上面一篇文章說了Launcher是如何被啟動的,Launcher啟動的過程主要是加載界面數據然后顯示出來,
界面數據都是系統APP有關的數據,都是從Launcher的數據庫讀取,下面我們詳細分析Launcher如何加載數據。
在Launcher.java的onCreate()方法里面,調用了開始加載數據接口:
//Edited by mythou
//http://www.cnblogs.com/mythou/
//加載啟動數據
if (!mRestoring)
{ mModel.startLoader(this, true); }
mModel是LauncherModel的對象,由此可見,數據加載主要是在LauncherModel類里面實現的。
1、Callbacks接口
LauncherModel里面,需要先分析一個Callbacks接口。
//Edited by mythou
//http://www.cnblogs.com/mythou/
public interface Callbacks { public boolean setLoadOnResume(); public int getCurrentWorkspaceScreen(); public void startBinding(); public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end); public void bindFolders(HashMap<Long,FolderInfo> folders); public void finishBindingItems(); public void bindAppWidget(LauncherAppWidgetInfo info); public void bindAllApplications(ArrayList<ApplicationInfo> apps); public void bindAppsAdded(ArrayList<ApplicationInfo> apps); public void bindAppsUpdated(ArrayList<ApplicationInfo> apps); public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent); public void bindPackagesUpdated(); public boolean isAllAppsVisible(); public void bindSearchablesChanged(); }
Callbacks接口提供了很多接口,用於返回相關的數據給Launcher模塊,下面我們對每個接口作用做個闡釋。
setLoadOnResume() :當Launcher.java類的Activity處於onPause的時候,如果重新恢復,需要調用onResume,此時需要在onResume調用這個接口,恢復Launcher數據。
getCurrentWorkspace():獲取屏幕序號(0~4)
startBinding():通知Launcher開始加載數據。清空容器數據,重新加載
bindItems(ArrayList<ItemInfo> shortcuts, int start, int end):加載App shortcut、Live Folder、widget到Launcher相關容器。
bindFolders(HashMap<Long, FolderInfo> folders):加載folder的內容
finishBindingItems():數據加載完成。
bindAppWidget(LauncherAppWidgetInfo item):workspace加載APP 快捷方式
bindAllApplications(final ArrayList<ApplicationInfo> apps):所有應用列表接着APP圖標數據
bindAppsAdded(ArrayList<ApplicationInfo> apps):通知Launcher新安裝了一個APP,更新數據。
bindAppsUpdated(ArrayList<ApplicationInfo> apps):通知Launcher一個APP更新了。(覆蓋安裝)
bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent):通知Launcher,應用被刪除
bindPackagesUpdated():多個應用更新。
isAllAppsVisible():返回所有應用列表是否可見狀態。
bindSearchablesChanged():Google搜索欄或者刪除區域發生變化時通知Launcher
2、數據加載流程
Launcher.java類繼承了Callbacks接口,並實現了該接口。LauncherModel里面會調用這些接口,反饋數據和狀態給Launcher。數據加載總體分為兩部分,一部分是加載workspace的數據,另一部分是加載All APP界面的數據。
下面是一個加載數據流程圖:
3、startLoader()
下面我們先分析startLoader()接口,startLoader主要是啟動了一個線程,用於加載數據。
//Edited by mythou
//http://www.cnblogs.com/mythou/
public void startLoader(Context context, boolean isLaunching) { synchronized (mLock) { //...............
if (mCallbacks != null && mCallbacks.get() != null) {
isLaunching = isLaunching || stopLoaderLocked(); mLoaderTask = new LoaderTask(context, isLaunching); sWorkerThread.setPriority(Thread.NORM_PRIORITY); sWorker.post(mLoaderTask); } } }
startLoader主要是啟動LoaderTask線程里面的run方法。sWorker是一個Handle對象,用於啟動線程的run方法。
4、LoaderTask的run()方法
//Edited by mythou
//http://www.cnblogs.com/mythou/
public void run() { //............ keep_running: {
//...............
//加載當前頁面的數據,先把一頁的數據加載完成,
//主要是為了增加程序流暢性,提高用戶體驗 if (loadWorkspaceFirst) { if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); loadAndBindWorkspace(); } else { if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps"); loadAndBindAllApps(); } if (mStopped) { break keep_running; } // THREAD_PRIORITY_BACKGROUND設置線程優先級為后台,
//這樣當多個線程並發后很多無關緊要的線程分配的CPU時間將會減少,有利於主線程的處理 synchronized (mLock) { if (mIsLaunching) { if (DEBUG_LOADERS) Log.d(TAG, "Setting thread priority to BACKGROUND"); android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); } }
//等待線程空閑的時候,繼續加載其他頁面數據 waitForIdle(); //加載剩余頁面的數據,包含workspace和all app頁面 if (loadWorkspaceFirst) { if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); loadAndBindAllApps(); } else { if (DEBUG_LOADERS) Log.d(TAG, "step 2: special: loading workspace"); loadAndBindWorkspace(); } // Restore the default thread priority after we are done loading items synchronized (mLock) { android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); } } }
上面是經過簡化的LoaderTask的run方法代碼,其實主要就兩部分操作,第一部分操作,加載當前頁面的數據
(當前workspace頁面或者當前All APP頁面的數據)然后等待線程空閑的時候,再加載剩余的頁面數據。
代碼上面加了關鍵注釋,可以結合代碼分析。這樣做主要目的是增加Launcher啟動的速度,讓用戶覺得系統初始化速度
較快,有較好的用戶體驗。先把用戶看見的界面初始化完畢,然后再開一個后台線程慢慢加載其他的數據。
下面我們分別分析workspace和All APP加載和綁定。
5、workspace加載數據
loadAndBindWorkspace()方法主要就是執行loadWorkspace()和 bindWorkspace()方法。
下面分別對這兩個方法進行分析。
//Edited by mythou
//http://www.cnblogs.com/mythou/
private void loadWorkspace() { //..........
//清空容器,存放界面不同的元素,App快捷方式、widget、folder synchronized (sBgLock) { sBgWorkspaceItems.clear(); sBgAppWidgets.clear(); sBgFolders.clear(); sBgItemsIdMap.clear(); sBgDbIconCache.clear(); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final Cursor c = contentResolver.query( LauncherSettings.Favorites.CONTENT_URI, 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)
//表示屏幕上的位置,
//第一維表示分屏的序號,其中最后一個代表Hotseat
//第二維表示x方向方格的序號
//第三維表示y方向方格的序號
final ItemInfo occupied[][][] = new ItemInfo[Launcher.SCREEN_COUNT + 1][mCellCountX + 1][mCellCountY + 1]; //讀取數據庫響應鍵值列序號 try { final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); final int intentIndex = c.getColumnIndexOrThrow (LauncherSettings.Favorites.INTENT); final int titleIndex = c.getColumnIndexOrThrow (LauncherSettings.Favorites.TITLE); final int iconTypeIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.ICON_TYPE);
//...........
while (!mStopped && c.moveToNext()) { try { int itemType = c.getInt(itemTypeIndex); switch (itemType) {
//item類型為ITEM_TYPE_APPLICATION或者ITEM_TYPE_SHORTCUT
//container為CONTAINER_DESKTOP或者CONTAINER_HOTSEAT
//把當前的item添加到sWorkspaceItems中
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: emType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { info = getShortcutInfo(manager, intent, context, c, iconIndex, titleIndex, mLabelCache); } else { info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex); switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
//添加數據 sBgWorkspaceItems.add(info); break; default: //如果item的屬性是folder,添加到folder,創建forder FolderInfo folderInfo = findOrMakeFolder(sBgFolders, container); folderInfo.add(info); break; } sBgItemsIdMap.put(info.id, info); } else { } break; //item類型為文件夾,添加 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: id = c.getLong(idIndex); FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id);
//.........
sBgItemsIdMap.put(folderInfo.id, folderInfo); sBgFolders.put(folderInfo.id, folderInfo); break; //Widget添加 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: // Read all Launcher-specific widget details int appWidgetId = c.getInt(appWidgetIdIndex); id = c.getLong(idIndex); //........... sBgItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); sBgAppWidgets.add(appWidgetInfo); break; } } catch (Exception e) { Log.w(TAG, "Desktop items loading interrupted:", e); } } } finally { c.close(); } } }
workspace的數據加載總的來說也是按照元素屬性來區分加載,分為App快捷方式、Widget、Folder元素。
這幾個元素分別加載到不同的容器里面。其中sItemsIdMap保存所有元素的id和ItemInfo組成的映射。其他
元素分別加載到3個不同的容器里面,用於后面綁定數據用。這里只給出了loadWorkspace的流程代碼,詳細代碼,
需要看源碼,還有很多細節。不過剛開始分析Launcher,我的原則是先把握整體流程和知道改動代碼,需要在哪里查找。
6、workspace綁定數據
Launcher的內容綁定分為五步:分別對應着startBinding()、bindItems()、bindFolders()、 bindAppWidgets()、
finishBindingItems()的調用。下面針對bindWorkspace做個簡單的流程分析。
//Edited by mythou
//http://www.cnblogs.com/mythou/
private void bindWorkspace() {
//通知Launcher開始綁定數據 mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //綁定數據到launcher,Launcher回調,清空相關容器 OWL callbacks.startBinding(); } } }); //添加元素到workspace,主要是添加APP快捷方式 N = workspaceItems.size(); for (int i=0; i<N; i+=ITEMS_CHUNK) { final int start = i; final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindItems(workspaceItems, start, start+chunkSize); } } }); } //文件夾綁定 final HashMap<Long, FolderInfo> folders = new HashMap<Long, FolderInfo>(sFolders); mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindFolders(folders); } } }); //分兩次加載widget ,當前界面和其他界面,增強用戶體驗OWL
//其他頁面widget會在后台線程再次加載 final int currentScreen = oldCallbacks.getCurrentWorkspaceScreen(); N = sAppWidgets.size(); // once for the current screen for (int i=0; i<N; i++) { final LauncherAppWidgetInfo widget = sAppWidgets.get(i); if (widget.screen == currentScreen) { mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAppWidget(widget); } } }); } } //加載其他看不見的屏幕widget for (int i=0; i<N; i++) { final LauncherAppWidgetInfo widget = sAppWidgets.get(i); if (widget.screen != currentScreen) { mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.bindAppWidget(widget); } } }); } } //加載完成,通知Launcher,已經完成數據加載 mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { callbacks.finishBindingItems(); } } }); }
上面就是Launcher的workspace綁定數據的過程,跟加載數據過程很相似,也是區分3中類型的元素進行加載。
下面我們總結一下,workspace的加載和綁定數據的過程。我們現在回頭看,可以發現,其實workspace里面就是
存放了3中數據ItemInfo、FolderInfo、LauncherAppWidgetInfo。分別對應我們的APP快捷方式、文件夾、Widget
數據。其中FolderInfo、LauncherAppWidgetInfo都是繼承了ItemInfo。數據加載過程,就是從Launcher的數據庫
讀取數據然后按元素屬性分別放到3個ArrayList里面。綁定數據過程就是把3個ArrayList的隊列關聯到Launcher界面里面。
7、ALL APP數據加載綁定
//Edited by mythou
//http://www.cnblogs.com/mythou/
private void loadAllAppsByBatch() {//只有這兩個標記才需要顯示在所有程序列表 OWL final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); final PackageManager packageManager = mContext.getPackageManager(); List<ResolveInfo> apps = null; int N = Integer.MAX_VALUE; int startIndex; int i=0; int batchSize = -1; while (i < N && !mStopped) { if (i == 0) { mAllAppsList.clear(); final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; //過濾需要顯示的app apps = packageManager.queryIntentActivities(mainIntent, 0); if (DEBUG_LOADERS) { Log.d(TAG, "queryIntentActivities took " + (SystemClock.uptimeMillis()-qiaTime) + "ms"); } if (apps == null) { return; } N = apps.size(); if (DEBUG_LOADERS) { Log.d(TAG, "queryIntentActivities got " + N + " apps"); } if (N == 0) { // There are no apps?!? return; } //mBatchSize==0表示一次性加載所有的應用 if (mBatchSize == 0) { batchSize = N; } else { batchSize = mBatchSize; } } final boolean first = i <= batchSize; final Callbacks callbacks = tryGetCallbacks(oldCallbacks); final ArrayList<ApplicationInfo> added = mAllAppsList.added; mAllAppsList.added = new ArrayList<ApplicationInfo>(); //綁定加載所有的APP數據 mHandler.post(new Runnable() { public void run() { final long t = SystemClock.uptimeMillis(); if (callbacks != null) { if (first) { //一次性加載所以app,返回數據到launcher callbacks.bindAllApplications(added); } else { callbacks.bindAppsAdded(added); } if (DEBUG_LOADERS) { Log.d(TAG, "bound " + added.size() + " apps in " + (SystemClock.uptimeMillis() - t) + "ms"); } } else { Log.i(TAG, "not binding apps: no Launcher activity"); } } }); }
AllAPP的數據加載和綁定跟workspace的差不多,也是先加載數據然后綁定數據,通知Launcher。加載數據的時候
從PackageManager獲取所有已經安裝的APK包信息,然后過濾只包含需要顯示在所有應用列表的應用,需要包含
ACTION_MAIN和CATEGORY_LAUNCHER兩個屬性。這個我們在編寫應用程序的時候都應該知道。
AllAPP加載跟workspace不同的地方是加載的同時,完成數據綁定的操作,也就是說第一次加載AllAPP頁面的數據,
會同時綁定數據到Launcher。第二次需要加載的時候,只會把數據直接綁定到Launcher,而不會重新搜索加載數據。
Launcher啟動加載和綁定數據就是這樣完成。綁定完數據,Launcher就可以運行。
系列文章:
Android Launcher分析和修改1——Launcher默認界面配置(default_workspace)
Android Launcher分析和修改2——Icon修改、界面布局調整、壁紙設置
Android Launcher分析和修改3——Launcher啟動和初始化
Edited by mythou
原創博文,轉載請標明出處:http://www.cnblogs.com/mythou/p/3165259.html