AOSP Android14 Launcher3——RecentsView最近任务数据加载
最近任务是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
。