1. 程式人生 > >android M Launcher之LauncherModel (三)

android M Launcher之LauncherModel (三)

通過前兩篇的分析,我們已經知道了LauncherModel的初始化及工作流程,如果您還不熟悉的話請看前兩篇博文
android M Launcher之LauncherModel (一)

android M Launcher之LauncherModel (二)

瞭解了LauncherModel的工作過程後,我們繼續來學習LauncherModel中提供的一些工具,從而瞭解Google工程師在自家系上怎麼開發的。

1、執行緒等待waitForIdle

我們在這個系列第二篇開始講LoaderTask的run方法時,在載入繫結桌面資料之後,載入與繫結應用程式之前,有一個等待動作即呼叫了waitForIdle方法,為什麼要去調這個方法呢,大家都知道在繫結桌面資料時我們是去UI執行緒中處理的,接觸過應用開發的都知道UI執行緒正常情況下是不能阻塞的,否則有可能產生ANR,這將嚴重影響使用者體驗。所有這裡LoaderTask在將結果傳送給UI執行緒之後,為了保證介面繫結任務可以高效的完成,往往會將自己的任務暫停下來,等待UI執行緒處理完成。不知道大家有沒有這樣設計過,反正我是沒有。從這兒也可以看出Google工程師是比較嚴謹的。

那我們就來分析下waitForIdle函式是怎麼實現的。

首先,建立一個UI執行緒閒時執行的任務,這個任務負責設定某些關鍵的控制標誌,並將其通過PostIdle方法加入處理器的訊息佇列中。

 mHandler.postIdle(new Runnable() {
                    public void run() {
                        synchronized (LoaderTask.this) {
                            mLoadAndBindStepFinished = true;
                            if
(DEBUG_LOADERS) { Log.d(TAG, "done with previous binding step"); } LoaderTask.this.notify(); } } });

這樣一來,只有UI執行緒閒置下來的時候,這裡定義的任務才會得到執行,這也就說明了介面已經重新整理完成。而一旦這個任務得到執行,就會將mLoadAndBindStepFinished 置為true,以控制即將來臨的有條件的無限等待。
最後 設定一個有條件的無限等待,等待來自UI執行緒的指示。

  while (!mStopped && !mLoadAndBindStepFinished) {
                    try {
                        // Just in case mFlushingWorkerThread changes but we aren't woken up,
                        // wait no longer than 1sec at a time
                        this.wait(1000);
                    } catch (InterruptedException ex) {
                        // Ignore
                    }
                }

只要沒有中斷且載入沒有完成這裡將一直等待,知道完成,這樣看還是很簡單的邏輯呀,你有想到麼。

2、停止載入工作stopLocked

對於一項任務,有時候我們需要停止它的工作,以保證資料或者流程的正確性。我們看下LoaderTask是怎麼做的:

 public void stopLocked() {
            synchronized (LoaderTask.this) {
                mStopped = true;
                this.notify();
            }
        }

可以看出要停止工作只需將mStopped 置為true, 而LoaderTask在執行過程中會頻繁的查詢mStopped 標誌,所有在開始設定之前需對LoaderTask上鎖,以保證mStopped 得到正確的設定,有了上鎖就必須有解鎖,這就是this.notify();的作用。

3、獲取通道 tryGetCallbacks

LoaderTask在執行一次載入任務的時候,都毫無意外的需要檢驗通往Launcher的通道是否存在,或者當前的通道是否經歷過重建,如果這樣的情況存在,那麼LoaderTask久沒有繼續執行的必要了。
tryGetCallbacks工具的作用就是幫助完成通往Launcher通道的驗證。

  Callbacks tryGetCallbacks(Callbacks oldCallbacks) {
            synchronized (mLock) {
                if (mStopped) {
                    return null;
                }

                if (mCallbacks == null) {
                    return null;
                }

                final Callbacks callbacks = mCallbacks.get();
                if (callbacks != oldCallbacks) {
                    return null;
                }
                if (callbacks == null) {
                    Log.w(TAG, "no mCallbacks");
                    return null;
                }

                return callbacks;
            }
        }

這裡程式碼應該比較好明白吧。

4、桌面空間判斷工具 checkItemPlacement

桌面上的每一個元件想要載入到桌面或者HotSeat,都需要確定當前的桌面或者HotSeat中是否還有足夠的空間,checkItemPlacement方法用於完成這個任務。

這個方法分為幾步,

  • 獲取Launcher屬性及item屬性

    要檢查需要處理的項是否有足夠的空間,首先要知道當前Launcher有多少空間可以被佔用,以及當前的項所處的空間情況,

    LauncherAppState app = LauncherAppState.getInstance();
            InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
            final int countX = profile.numColumns;
            final int countY = profile.numRows;
            //被處理項的桌面索引
            long containerIndex = item.screenId;
  • 處理被處理項在所處容器的佔用情況。
    主要處理兩種情況,一種是HotSeat容器,一種是桌面容器。
    如果需要處理的是HotSeat容器 ,它需要一些判斷,如下:
  if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                // Return early if we detect that an item is under the hotseat button
                if (mCallbacks == null ||
                        mCallbacks.get().isAllAppsButtonRank((int) item.screenId)) {
                    Log.e(TAG, "Error loading shortcut into hotseat " + item
                            + " into position (" + item.screenId + ":" + item.cellX + ","
                            + item.cellY + ") occupied by all apps");
                    return false;
                }
                //獲取HotSeat容器中空間的佔用情況,
                final ItemInfo[][] hotseatItems =
                        occupied.get((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT);
                //如果被處理項要求的位置超過了HotSeat的容量,返回false
                if (item.screenId >= profile.numHotseatIcons) {
                    Log.e(TAG, "Error loading shortcut " + item
                            + " into hotseat position " + item.screenId
                            + ", position out of bounds: (0 to " + (profile.numHotseatIcons - 1)
                            + ")");
                    return false;
                }
                //如果HotSeat空間已經被Launcher分配
                if (hotseatItems != null) {
                    //如果被處理的項所要求的位置已經被佔用,返回分配失敗
                    if (hotseatItems[(int) item.screenId][0] != null) {
                        Log.e(TAG, "Error loading shortcut into hotseat " + item
                                + " into position (" + item.screenId + ":" + item.cellX + ","
                                + item.cellY + ") occupied by "
                                + occupied.get(LauncherSettings.Favorites.CONTAINER_HOTSEAT)
                                [(int) item.screenId][0]);
                        return false;
                        //如果該空間空閒,則將此空間分配給此項,返回分配成功
                    } else {
                        hotseatItems[(int) item.screenId][0] = item;
                        return true;
                    }
                    //如果HotSeat空間還沒有被分配,分配空間後返回分配成功
                } else {
                    //開闢HotSeat空間
                    final ItemInfo[][] items = new ItemInfo[(int) profile.numHotseatIcons][1];
                    items[(int) item.screenId][0] = item;
                    //將分配的空間加入佔用列表中維護
                    occupied.put((long) LauncherSettings.Favorites.CONTAINER_HOTSEAT, items);
                    return true;
                }
            } 

如果是桌面容器

if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
                if (!workspaceScreens.contains((Long) item.screenId)) {
                    // The item has an invalid screen id.
                    return false;
                }
            } 

其他情況一律返回分配成功。

  • 新增佔用列表

如果對應的桌面索引存在,但Launcher並沒有在這個桌面頁上分配空間,那麼Launcher需要為此分配足夠的空間:

if (!occupied.containsKey(item.screenId)) {
                ItemInfo[][] items = new ItemInfo[countX + 1][countY + 1];
                occupied.put(item.screenId, items);
            }
  • 判斷快捷方式是否有足夠的空間

在這裡需要判斷當前處理的項是否有足夠的空間容納該項,因為該項有可能是一個桌面小部件,可能佔用比較大的空間。

 //獲取螢幕上被佔用情況
            final ItemInfo[][] screens = occupied.get(item.screenId);
            //如果被處理的項超過範圍,則返回分配失敗
            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP &&
                    item.cellX < 0 || item.cellY < 0 ||
                    item.cellX + item.spanX > countX || item.cellY + item.spanY > countY) {
                Log.e(TAG, "Error loading shortcut " + item
                        + " into cell (" + containerIndex + "-" + item.screenId + ":"
                        + item.cellX + "," + item.cellY
                        + ") out of screen bounds ( " + countX + "x" + countY + ")");
                return false;
            }
  • 檢查空間及螢幕佔用

有了足夠的空間,還需要判斷該項需要佔用的空間內是否還被其他桌面項(快捷方式 桌面小部件) 佔用,如果該項需要的空間範圍內只有一塊被佔用也屬於分配失敗。

 // Check if any workspace icons overlap with each other
            for (int x = item.cellX; x < (item.cellX + item.spanX); x++) {
                for (int y = item.cellY; y < (item.cellY + item.spanY); y++) {
                    if (screens[x][y] != null) {
                        Log.e(TAG, "Error loading shortcut " + item
                                + " into cell (" + containerIndex + "-" + item.screenId + ":"
                                + x + "," + y
                                + ") occupied by "
                                + screens[x][y]);
                        return false;
                    }
                }
            }
  • 讓被處理的項佔用它需要的空間
for (int x = item.cellX; x < (item.cellX + item.spanX); x++) {
                for (int y = item.cellY; y < (item.cellY + item.spanY); y++) {
                    screens[x][y] = item;
                }
            }

6、桌面元件的排序工具sortWorkspaceItemsSpatially

在Launcher中,在繫結桌面元件之前都需要對桌面元件進行一次排序,原則上是Y軸方向從上到下,X軸從左到右

 final LauncherAppState app = LauncherAppState.getInstance();
            final InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
            // XXX: review this
            Collections.sort(workspaceItems, new Comparator<ItemInfo>() {
                @Override
                public int compare(ItemInfo lhs, ItemInfo rhs) {
                    int cellCountX = (int) profile.numColumns;
                    int cellCountY = (int) profile.numRows;
                    int screenOffset = cellCountX * cellCountY;
                    int containerOffset = screenOffset * (Launcher.SCREEN_COUNT + 1); // +1 hotseat
                    long lr = (lhs.container * containerOffset + lhs.screenId * screenOffset +
                            lhs.cellY * cellCountX + lhs.cellX);
                    long rr = (rhs.container * containerOffset + rhs.screenId * screenOffset +
                            rhs.cellY * cellCountX + rhs.cellX);
                    return (int) (lr - rr);
                }
            });

這個是java對一個數組的排序方式。

以上就是LoaderTask提供的一些重要工具,通過學習Google的原始碼我們也可以把它應用到實際的開發中去,提高自己程式碼的健壯性。

好了,從第一篇開始到這篇結束,我們終於把LauncherModel的整個工作流程走了一遍。以下是整個LauncherModel系列的索引

一 、LauncherModel的的例項化
二 、 LauncherModel資料的載入流程
三 、LauncherModel中一些工具的介紹