AOSP Android14 Launcher3——RecentsView最近任务数据加载

最近任务是Launcher中的一个重要的功能,显示用户最近使用的应用,并可以快速切换到其中的应用;用户可以通过底部上滑停顿进入最近任务,也可以在第三方应用底部上滑进最近任务。

这两种场景之前的博客也介绍过,本文就不再赘述。

本篇就来讲讲最近任务中的这些任务卡片是如何一步步加载并显示在页面上的。

RecentsView

这个类是最近任务的核心类。

RecentsView继承自PagedView,所以,这个页面可以上下或者左右滚动,展示最近任务卡片并快速滚动。

最近任务卡片即TaskView,是最近任务RecentsView的childView。RecentsView将TaskView通过addView的方式添加到界面上。如图所示。

RecentsView是如何将TaskView添加的呢?其中显示的每个Task的数据哪里来的呢?

这里就涉及到一个重要的方法reloadIfNeed

java 复制代码
 // 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方法

java 复制代码
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 background
        mLoadingTasksInBackground = 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 callback
                    ArrayList<GroupTask> result = mResultsUi.stream().filter(filter)
                            .map(GroupTask::copy)
                            .collect(Collectors.toCollection(ArrayList<GroupTask>::new));

                    callback.accept(result);
                }
            });
        });

        return requestLoadId;
    }

其中的loadTasksInBackground是真正加载任务数据的地方,代码如下:

java 复制代码
quickstep/src/com/android/quickstep/RecentTasksList.java
  /**
     * Loads and creates a list of all the recent tasks.
     */
    @VisibleForTesting
    TaskLoadResult 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 it
        Collections.reverse(rawTasks);

        SparseBooleanArray tmpLockedUsers = new SparseBooleanArray() {
            @Override
            public boolean get(int key) {
                if (indexOfKey(key) < 0) {
                    // Fill the cached locked state as we fetch
                    put(key, mKeyguardManager.isDeviceLocked(key));
                }
                return super.get(key);
            }
        };

        TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size());

        int numVisibleTasks = 0;
 		 ...... 省略  数据加工处理

        return allTasks;
    }

接着又在SystemUiProxy的getRecentTasks方法中加载

java 复制代码
quickstep/src/com/android/quickstep/SystemUiProxy.java
    public 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<>();
        }
    }

其中的关键代码是

java 复制代码
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 加载最近任务数据的流程是一个典型的分层异步带缓存的设计:

  1. UI 层 (RecentsView): 需要数据来展示,并发起请求。
  2. 模型层 (RecentsModel): 作为中间层,管理数据缓存(图标、缩略图)和数据源(任务列表),并处理数据更新和分发。
  3. 数据源层 (RecentTasksList): 负责直接与系统服务交互,获取原始的最近任务列表,并提供简单的缓存机制。

详细步骤:

  1. 触发加载 (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 用于过滤任务。
  2. 请求传递 (Request Forwarding) - RecentsModel:

    • RecentsModelgetTasks(Consumer<ArrayList<GroupTask>> callback, Predicate<GroupTask> filter) 方法 (line 151, 165) 接收到来自 RecentsView 的请求。
    • 不会 自己直接去获取系统数据,而是将请求进一步委托RecentTasksList 实例 (mTaskList),调用 mTaskList.getTasks(false /* loadKeysOnly */, callback, filter) (line 167)。它将 RecentsView 传来的回调函数和过滤器原样传递下去。
  3. 检查缓存与异步加载 (Cache Check & Async Loading) - RecentTasksList:

    • RecentTasksList.getTasks(...) (line 121) 首先检查其UI 线程缓存 (mResultsUi) 是否包含针对当前请求 ID (mChangeId) 的有效数据(并且满足 loadKeysOnly 的要求)。mChangeId 会在系统任务列表发生变化时递增。
    • 缓存命中 (Cache Hit) : 如果 mResultsUi 有效,它会立即(通过 mMainThreadExecutor.post) 将缓存的数据(经过 filter 过滤和拷贝 map(GroupTask::copy)) 传递给 RecentsViewapplyLoadPlan 回调。加载流程快速结束。
    • 缓存未命中 (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
        • 如果 loadKeysOnlyfalse,则创建完整的 Task 对象 (Task.from(...)),包含任务的详细信息。
        • 处理单任务和分屏任务对,并将它们包装成 GroupTaskDesktopTask 对象。
        • 将处理后的 ArrayList<GroupTask> 存储在 mResultsBg 中。
      • 后台任务完成后,它会将结果 (mResultsBg) 通过 mMainThreadExecutor.execute 发送回主线程
  4. 数据返回与 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 按钮状态等。
  5. 系统更新通知 (System Update Notification):

    • RecentTasksList 在初始化时通过 mSysUiProxy.registerRecentTasksListener(...) (line 76) 注册了一个监听器。
    • 当系统(如 SystemUI)中的最近任务列表发生变化时,会通过 Binder 回调 IRecentTasksListener.onRecentTasksChanged()
    • 这个回调被调度到主线程执行 RecentTasksList.this::onRecentTasksChanged (line 80)。
    • onRecentTasksChanged (line 185) 调用 invalidateLoadedTasks() (line 189),这会递增 mChangeId 并将 mResultsUimResultsBg 标记为无效。
    • 这保证了下一次 RecentsView 调用 reloadIfNeeded() 时,会触发一次新的数据加载流程,而不是使用过期的缓存。

总结:

Launcher 的最近任务加载是一个精心设计的流程,它通过 RecentsModel 解耦了 UI 和数据获取,利用 RecentTasksList 处理与系统的交互和基本缓存。通过异步加载避免阻塞 UI 线程,并通过 mChangeId 和系统回调确保数据在需要时能得到及时更新,同时利用缓存 (mResultsUi, mResultsBg) 提高了效率。图标和缩略图的加载/缓存则由 RecentsModel 中的 TaskIconCacheTaskThumbnailCache 独立处理,并通过 TaskVisualsChangeListener 接口将更新通知给 RecentsView

相关推荐
贤泽3 天前
Android15 ContentProvider 深度源码分析(上)
android·aosp
贤泽3 天前
Android15 ContentProvider 深度源码分析(下)
android·aosp
贤泽6 天前
android 15 AOSP Broadcast 广播机制源码分析
android·aosp
奔跑吧 android8 天前
【车载Audio】【AudioHal 07】【高通音频架构】【从逻辑策略到物理执行】
音视频·audio·aosp·android15·8295·音频子系统
42nf1 个月前
Android Launcher3添加负一屏
android·launcher3·android负一屏
不会Android的潘潘1 个月前
受限系统环境下的 WebView 能力演进:车载平台 Web 渲染异常的根因分析与优化实践
android·java·前端·aosp
奔跑吧 android1 个月前
【车载audio开发】【Qualcomm PAL 详解 6】【PAL 总体架构与模块交互指南】
audio·aosp·pal·高通音频框架·8155·8295
奔跑吧 android1 个月前
【车载audio开发】【Qualcomm PAL 详解 4】【Session 模块 介绍】
audio·aosp·高通·车载音频
不会Android的潘潘1 个月前
adb指令扩展方案
android·adb·aosp
Just_Paranoid2 个月前
【AOSP】Android Dump 信息快速定位方法
android·adb·framework·service·aosp·dumpsys