最近任務是Launcher中的一個重要的功能,顯示用戶最近使用的應用,并可以快速切換到其中的應用;用戶可以通過底部上滑停頓進入最近任務,也可以在第三方應用底部上滑進最近任務。
這兩種場景之前的博客也介紹過,本文就不再贅述。
本篇就來講講最近任務中的這些任務卡片是如何一步步加載并顯示在頁面上的。
RecentsView
這個類是最近任務的核心類。
RecentsView繼承自PagedView,所以,這個頁面可以上下或者左右滾動,展示最近任務卡片并快速滾動。
最近任務卡片即TaskView,是最近任務RecentsView的childView。RecentsView將TaskView通過addView的方式添加到界面上。如圖所示。
RecentsView是如何將TaskView添加的呢?其中顯示的每個Task的數據哪里來的呢?
這里就涉及到一個重要的方法reloadIfNeed
// quickstep/src/com/android/quickstep/views/RecentsView.java/*** Reloads the view if anything in recents changed.*/public void reloadIfNeeded() {if (!mModel.isTaskListValid(mTaskListChangeId)) {mTaskListChangeId = mModel.getTasks(this::applyLoadPlan, RecentsFilterState.getFilter(mFilterState.getPackageNameToFilter()));}}
這個方法通過RecentsModel的方法getTasks()來獲取任務列表。最后調用RecentTaskList的getTask方法
quickstep/src/com/android/quickstep/RecentTasksList.java/*** Asynchronously fetches the list of recent tasks, reusing cached list if available.** @param loadKeysOnly Whether to load other associated task data, or just the key* @param callback The callback to receive the list of recent tasks* @return The change id of the current task list*/public synchronized int getTasks(boolean loadKeysOnly,Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> filter) {final int requestLoadId = mChangeId;...... 省略// Kick off task loading in the backgroundmLoadingTasksInBackground = true;UI_HELPER_EXECUTOR.execute(() -> {if (!mResultsBg.isValidForRequest(requestLoadId, loadKeysOnly)) {// 異步加載任務mResultsBg = loadTasksInBackground(Integer.MAX_VALUE, requestLoadId, loadKeysOnly);}TaskLoadResult loadResult = mResultsBg;mMainThreadExecutor.execute(() -> {mLoadingTasksInBackground = false;mResultsUi = loadResult;if (callback != null) {// filter the tasks if needed before passing them into the callbackArrayList<GroupTask> result = mResultsUi.stream().filter(filter).map(GroupTask::copy).collect(Collectors.toCollection(ArrayList<GroupTask>::new));callback.accept(result);}});});return requestLoadId;}
其中的loadTasksInBackground是真正加載任務數據的地方,代碼如下:
quickstep/src/com/android/quickstep/RecentTasksList.java/*** Loads and creates a list of all the recent tasks.*/@VisibleForTestingTaskLoadResult loadTasksInBackground(int numTasks, int requestId, boolean loadKeysOnly) {int currentUserId = Process.myUserHandle().getIdentifier();// 獲取最近任務數據ArrayList<GroupedRecentTaskInfo> rawTasks =mSysUiProxy.getRecentTasks(numTasks, currentUserId);// The raw tasks are given in most-recent to least-recent order, we need to reverse itCollections.reverse(rawTasks);SparseBooleanArray tmpLockedUsers = new SparseBooleanArray() {@Overridepublic boolean get(int key) {if (indexOfKey(key) < 0) {// Fill the cached locked state as we fetchput(key, mKeyguardManager.isDeviceLocked(key));}return super.get(key);}};TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());int numVisibleTasks = 0;...... 省略 數據加工處理return allTasks;}
接著又在SystemUiProxy的getRecentTasks方法中加載
quickstep/src/com/android/quickstep/SystemUiProxy.javapublic ArrayList<GroupedRecentTaskInfo> getRecentTasks(int numTasks, int userId) {if (mRecentTasks == null) {Log.w(TAG, "getRecentTasks() failed due to null mRecentTasks");return new ArrayList<>();}try {final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,RECENT_IGNORE_UNAVAILABLE, userId);if (rawTasks == null) {return new ArrayList<>();}return new ArrayList<>(Arrays.asList(rawTasks));} catch (RemoteException e) {Log.w(TAG, "Failed call getRecentTasks", e);return new ArrayList<>();}}
其中的關鍵代碼是
quickstep/src/com/android/quickstep/SystemUiProxy.java
final GroupedRecentTaskInfo[] rawTasks = mRecentTasks.getRecentTasks(numTasks,RECENT_IGNORE_UNAVAILABLE, userId);
這里的mRecentTasks是private IRecentTasks mRecentTasks;
IRecentTasks類型,這是wm提供的一個AIDL接口。所以,最終最近任務的數據是來自于wm。
核心流程概述:
Launcher 加載最近任務數據的流程是一個典型的分層、異步、帶緩存的設計:
- UI 層 (
RecentsView
): 需要數據來展示,并發起請求。 - 模型層 (
RecentsModel
): 作為中間層,管理數據緩存(圖標、縮略圖)和數據源(任務列表),并處理數據更新和分發。 - 數據源層 (
RecentTasksList
): 負責直接與系統服務交互,獲取原始的最近任務列表,并提供簡單的緩存機制。
詳細步驟:
-
觸發加載 (Triggering Load) -
RecentsView
:- 當
RecentsView
需要顯示或刷新最近任務列表時(例如,進入概覽狀態setOverviewStateEnabled(true)
(line 1404),或者視圖附加到窗口onAttachedToWindow()
(line 1080) 后調用reloadIfNeeded()
(line 2546)),它會檢查當前數據是否需要更新。 reloadIfNeeded()
方法會調用mModel.isTaskListValid(mTaskListChangeId)
(line 2548) 來檢查當前RecentsView
持有的任務列表 ID 是否仍然有效。- 如果 ID 失效或從未加載過,它會調用
mModel.getTasks(this::applyLoadPlan, mFilterState)
(line 2550) 來請求最新的任務數據。this::applyLoadPlan
是一個回調函數,當數據加載完成后會被執行。mFilterState
用于過濾任務。
- 當
-
請求傳遞 (Request Forwarding) -
RecentsModel
:RecentsModel
的getTasks(Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> filter)
方法 (line 151, 165) 接收到來自RecentsView
的請求。- 它不會自己直接去獲取系統數據,而是將請求進一步委托給
RecentTasksList
實例 (mTaskList
),調用mTaskList.getTasks(false /* loadKeysOnly */, callback, filter)
(line 167)。它將RecentsView
傳來的回調函數和過濾器原樣傳遞下去。
-
檢查緩存與異步加載 (Cache Check & Async Loading) -
RecentTasksList
:RecentTasksList.getTasks(...)
(line 121) 首先檢查其UI 線程緩存 (mResultsUi
) 是否包含針對當前請求 ID (mChangeId
) 的有效數據(并且滿足loadKeysOnly
的要求)。mChangeId
會在系統任務列表發生變化時遞增。- 緩存命中 (Cache Hit): 如果
mResultsUi
有效,它會立即(通過mMainThreadExecutor.post
) 將緩存的數據(經過filter
過濾和拷貝map(GroupTask::copy)
) 傳遞給RecentsView
的applyLoadPlan
回調。加載流程快速結束。 - 緩存未命中 (Cache Miss): 如果
mResultsUi
無效,說明需要從系統重新加載。- 它會將
mLoadingTasksInBackground
標記為true
。 - 它使用
UI_HELPER_EXECUTOR
(后臺線程池) 調度一個后臺任務來加載數據。 - 后臺任務首先檢查后臺線程緩存 (
mResultsBg
) 是否有效。如果無效,它會調用loadTasksInBackground(...)
(line 232)。 loadTasksInBackground(...)
調用mSysUiProxy.getRecentTasks(...)
(line 236) 通過 Binder (IPC) 從 SystemUI(或其他系統服務)獲取原始的GroupedRecentTaskInfo
列表。- 獲取到原始數據后,
loadTasksInBackground
進行處理:- 反轉列表順序(系統返回的是最新->最舊,PagedView 需要最舊->最新)。
- 遍歷
GroupedRecentTaskInfo
。 - 為每個任務創建
Task.TaskKey
。 - 如果
loadKeysOnly
為false
,則創建完整的Task
對象 (Task.from(...)
),包含任務的詳細信息。 - 處理單任務和分屏任務對,并將它們包裝成
GroupTask
或DesktopTask
對象。 - 將處理后的
ArrayList<GroupTask>
存儲在mResultsBg
中。
- 后臺任務完成后,它會將結果 (
mResultsBg
) 通過mMainThreadExecutor.execute
發送回主線程。
- 它會將
-
數據返回與 UI 更新 (Data Return & UI Update) -
RecentTasksList
->RecentsModel
->RecentsView
:- 回到主線程后,
RecentTasksList
將后臺加載的結果 (mResultsBg
) 復制到 UI 線程緩存 (mResultsUi
) (line 160)。 mLoadingTasksInBackground
標記為false
。- 調用最初由
RecentsView
傳入的回調函數(即applyLoadPlan
),并將經過filter
過濾和拷貝的數據傳遞給它 (line 166)。 RecentsView.applyLoadPlan(ArrayList<GroupTask> taskGroups)
(line 1666) 被執行:- 它清空當前的視圖。
- 遍歷接收到的
taskGroups
列表。 - 對于每個
GroupTask
,它從視圖池 (mTaskViewPool
,mGroupedTaskViewPool
, etc. line 507-509) 中獲取或創建一個TaskView
(或其子類)。 - 調用
taskView.bind(task, ...)
等方法,將Task
對象中的數據(如應用名稱、圖標、縮略圖占位符等)綁定到TaskView
上。 - 將填充好數據的
TaskView
添加到RecentsView
中 (addView(tv)
line 1788)。 - 更新滾動范圍、ClearAll 按鈕狀態等。
- 回到主線程后,
-
系統更新通知 (System Update Notification):
RecentTasksList
在初始化時通過mSysUiProxy.registerRecentTasksListener(...)
(line 76) 注冊了一個監聽器。- 當系統(如 SystemUI)中的最近任務列表發生變化時,會通過 Binder 回調
IRecentTasksListener.onRecentTasksChanged()
。 - 這個回調被調度到主線程執行
RecentTasksList.this::onRecentTasksChanged
(line 80)。 onRecentTasksChanged
(line 185) 調用invalidateLoadedTasks()
(line 189),這會遞增mChangeId
并將mResultsUi
和mResultsBg
標記為無效。- 這保證了下一次
RecentsView
調用reloadIfNeeded()
時,會觸發一次新的數據加載流程,而不是使用過期的緩存。
總結:
Launcher 的最近任務加載是一個精心設計的流程,它通過 RecentsModel
解耦了 UI 和數據獲取,利用 RecentTasksList
處理與系統的交互和基本緩存。通過異步加載避免阻塞 UI 線程,并通過 mChangeId
和系統回調確保數據在需要時能得到及時更新,同時利用緩存 (mResultsUi
, mResultsBg
) 提高了效率。圖標和縮略圖的加載/緩存則由 RecentsModel
中的 TaskIconCache
和 TaskThumbnailCache
獨立處理,并通過 TaskVisualsChangeListener
接口將更新通知給 RecentsView
。