Android U WMS : Activity 冷启动(2) 添加启动窗口

AMS&WMS: Activity启动之生命周期 只分析了 Activity 启动时的生命周期流程,而忽略了窗口相关的流程,例如,Activity 窗口是如何显示,窗口动画是如何执行的。

本文来探讨窗口中一个比较有意思的话题 -- 启动窗口,AOSP 称之为 starting window。当 app 启动时,会先显示一个启动窗口,再显示 Activity 的真实窗口。

那么,为何需要先显示一个"多余"的启动窗口,而不是直接显示 Activity 的真窗呢?因为,当启动 app 时, Activity 窗口的绘制需要一点时间,尤其当 app 冷启动的时候,创建进程还会需要更多时间。因此,在这段真空期内,如果不显示启动窗口,通常就会显示一个白屏界面,那么给用户的感觉就是,系统好像不怎么流畅。

启动窗口大致分为两类

  1. Splash Screen : app 冷启动的时候,会显示 splash screen 类型的启动窗口。app 开发人员对这个应该不陌生,可以为 app 配置 splash screen,例如,配置显示的图标,甚至还可以自定义 splash screen 的退出动画。
  2. Snapshot:app 热启动时,会先显示一个 snapshot 类型的启动窗口,它是 Task 的一个快照。一般来说,当发生 Task 切换时,例如,从 app 返回桌面,系统会对 app Task 拍一个快照。

Splash Screen 官方文档: developer.android.google.cn/develop/ui/...

AMS&WMS: Activity启动之生命周期 分析的是 app 冷启动,那么会显示一个 splash screen 类型的启动窗口,本文就来分析它是如何添加的。

WMCore 添加启动窗口

根据 AMS&WMS: Activity启动之生命周期 # startActivityInner() 的分析,当调用 Activity 的 root task 的 startActivityLocked() 时,会添加启动窗口

java 复制代码
// Task.java

// r 是 app 要启动的 activity
// topTask 是 Launcher task
// newTask 是 true,表示新创建了 Task
// isTaskSwitch 为 true,表示是 task 切换
// options 是启动 app 的 activity 时传入的,不为 null
// sourceRecord 指 Launcher
void startActivityLocked(ActivityRecord r, @Nullable Task topTask, boolean newTask,
        boolean isTaskSwitch, ActivityOptions options, @Nullable ActivityRecord sourceRecord) {
    // 找到 topTask 下,可以支持 PIP 的 ActivityRecord,此时为 null
    final ActivityRecord pipCandidate = findEnterPipOnTaskSwitchCandidate(topTask);
    
    Task rTask = r.getTask();
    // ture
    final boolean allowMoveToFront = options == null || !options.getAvoidMoveToFront();
    // true
    final boolean isOrhasTask = rTask == this || hasChild(rTask);
    if (!r.mLaunchTaskBehind && allowMoveToFront && (!isOrhasTask || newTask)) {
        // 在创建 root task 时,已经把 root task 作为 top child 保存到 TDA 下
        positionChildAtTop(rTask);
    }
    Task task = null;
    if (!newTask && isOrhasTask && !r.shouldBeVisible()) {
        // ...
    }
    final Task activityTask = r.getTask();
    if (task == activityTask && mChildren.indexOf(task) != (getChildCount() - 1)) {
        // ...
    }
    
    // task 代表 activity 直属的 Task
    task = activityTask;
    
    ProtoLog.i(WM_DEBUG_ADD_REMOVE, "Adding activity %s to task %s "
                    + "callers: %s", r, task, new RuntimeException("here").fillInStackTrace());
    
    if (mActivityPluginDelegate != null) {
        // ...
    }
    if (isActivityTypeHomeOrRecents() && getActivityBelow(r) == null) {
        // ...
        return;
    }
    if (!allowMoveToFront) {
        // ...
        return;
    }
    
    final DisplayContent dc = mDisplayContent;
    
    if (DEBUG_TRANSITION) Slog.v(TAG_TRANSITION,
            "Prepare open transition: starting " + r);
    
    // FLAG_ACTIVITY_NO_ANIMATION 表示不需要为启动的 activity 应用动画
    if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_ANIMATION) != 0) {
        // ...
    } else {
        // 在 shell transition 打开的情况下,app transition 已经废弃
        dc.prepareAppTransition(TRANSIT_OPEN);
        mTaskSupervisor.mNoAnimActivities.remove(r);
    }
    
    if (newTask && !r.mLaunchTaskBehind) {
        // ...与PIP相关...
    }
    
    
    // 1. 检测是否要显示启动窗口
    boolean doShow = true;
    if (newTask) {
        // 从 start u0 的信息可知,是有这个标志位的
        if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
            resetTaskIfNeeded(r, r);
            // 如果 Task top activity 就是要启动的 activity,那么需要 show starting window
            doShow = topRunningNonDelayedActivityLocked(null) == r;
        }
    } else if (options != null && options.getAnimationType()
            == ActivityOptions.ANIM_SCENE_TRANSITION) {
        // ...
    }
    
    // 启动 activity 的 Bundle 参数,也可以指定不显示启动窗口
    if (options != null && options.getDisableStartingWindow()) {
        // ...
    }
    
    if (r.mLaunchTaskBehind) {
        // ...
    } 
    // SHOW_APP_STARTING_PREVIEW 是启动窗口功能的开关,默认为 true
    else if (SHOW_APP_STARTING_PREVIEW && doShow) {
        // Figure out if we are transitioning from another activity that is
        // "has the same starting icon" as the next one.  This allows the
        // window manager to keep the previous window it had previously
        // created, if it still had one.
        Task baseTask = r.getTask();
        // 获取 Task 下,是否有 ActivityRecord 携带启动窗口的数据
        // 目前为 null
        final ActivityRecord prev = baseTask.getActivity(
                a -> a.mStartingData != null && a.showToCurrentUser());
        // 2. 添加启动窗口
        mWmService.mStartingSurfaceController.showStartingWindow(r, prev, newTask,
                isTaskSwitch, sourceRecord);
    }
}

Task 的作用就是检测是否满足添加启动窗口的条件,如果满足,就交给 StartingSurfaceController 开始添加启动窗口。

java 复制代码
// StartingSurfaceController.java

void showStartingWindow(ActivityRecord target, ActivityRecord prev,
        boolean newTask, boolean isTaskSwitch, ActivityRecord source) {
    if (mDeferringAddStartingWindow) {
        // ...
    } else {
        target.showStartingWindow(prev, newTask, isTaskSwitch, true /* startActivity */,
                source);
    }
}

StartingSurfaceController 是一个控制器,它负责去创建和释放启动窗口。而创建启动窗口的任务,是交给 ActivityRecord 去执行的

java 复制代码
// ActivityRecord.java

void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
        boolean startActivity, ActivityRecord sourceRecord) {
    showStartingWindow(prev, newTask, taskSwitch, isProcessRunning(), startActivity,
            sourceRecord, null /* candidateOptions */);
}

// prev 是前一个拥有 starting window 数据的 ActivityRecord,此时为 null
// newTask 为 true
// taskSwitch 为 true
// processRunning 为 false,此时 app 进程还没有创建
// startActivity 为 true
// sourceRecord 指 Launcher activity
// candidateOptions 为 null
/**
 * @param prev Previous activity which contains a starting window.
 * @param processRunning Whether the client process is running.
 * @param startActivity Whether this activity is just created from starter.
 * @param sourceRecord The source activity which start this activity.
 * @param candidateOptions The options for the style of starting window.
 */
void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
        boolean processRunning, boolean startActivity, ActivityRecord sourceRecord,
        ActivityOptions candidateOptions) {
    if (mTaskOverlay) {
        return;
    }

    // 参数 candidateOptions 为 null
    // mPendingOptions 是启动 activity 传入的 Bundle 参数转化而来
    final ActivityOptions startOptions = candidateOptions != null
            ? candidateOptions : mPendingOptions;
    
    if (startOptions != null
            && startOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) {
        // Don't show starting window when using shared element transition.
        return;
    }

    // 获取并解析启动窗口 theme
    // startActivity 此时为 true
    final int splashScreenTheme = startActivity ? getSplashscreenTheme(startOptions) : 0;
    final int resolvedTheme = evaluateStartingWindowTheme(prev, packageName, theme,
            splashScreenTheme);

    // 检测启动窗口是否显示纯色启动窗口
    // false
    mSplashScreenStyleSolidColor = shouldUseSolidColorSplashScreen(sourceRecord, startActivity,
            startOptions, resolvedTheme);

    // false
    // 当前 ActivityRecord 的状态是 INITIALIZING
    // 这个应该是代表 app 端是否创建了 Activity
    final boolean activityCreated =
            mState.ordinal() >= STARTED.ordinal() && mState.ordinal() <= STOPPED.ordinal();
    
    // If this activity is just created and all activities below are finish, treat this
    // scenario as warm launch.
    // false
    // 这里处理的情况,如注释所说,这种情况下也需要显示启动窗口
    final boolean newSingleActivity = !newTask && !activityCreated
            && task.getActivity((r) -> !r.finishing && r != this) == null;

    
    // 执行下一步的添加启动窗口
    final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
            prev, newTask || newSingleActivity, taskSwitch, processRunning,
            allowTaskSnapshot(), activityCreated, mSplashScreenStyleSolidColor, allDrawn);
    
    // 这个 log 代表 WMCore 已经成功调度添加了启动窗口
    if (DEBUG_STARTING_WINDOW_VERBOSE && scheduled) {
        Slog.d(TAG, "Scheduled starting window for " + this);
    }
}    


// from 为 null,表示前一个拥有启动窗口的 ActvityRecord
// newTask 为 true
// taskSwitch 为 true
// processRunning 为 false
// allowTaskSnapshot 为 true,表示允许为 task 拍快照
// activityCreated 为 false,表示客户端的 Activity 还没有创建
// isSimple 为 false,表示是否使用纯色的启动窗口
// activityAllDrawn 为 false,它代表 ActivityRecord 下的窗口是否全部绘制完成
boolean addStartingWindow(String pkg, int resolvedTheme, ActivityRecord from, boolean newTask,
        boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot,
        boolean activityCreated, boolean isSimple,
        boolean activityAllDrawn) {
    if (!okToDisplay()) {
        return false;
    }

    // 如果已经有 starting window 的数据,那么就不需要再添加 starting window
    if (mStartingData != null) {
        return false;
    }

    // 如果 ActivityRecord 下已经有窗口显示,那么就不需要显示 starting window
    final WindowState mainWin = findMainWindow();
    if (mainWin != null && mainWin.mWinAnimator.getShown()) {
        // App already has a visible window...why would you want a starting window?
        return false;
    }

    // 获取 task 的快照(snapshot),由于是冷启动 app,因此,系统此时还没有 task 的快照
    final TaskSnapshot snapshot =
            mWmService.mTaskSnapshotController.getSnapshot(task.mTaskId, task.mUserId,
                    false /* restoreFromDisk */, false /* isLowResolution */);
    
    // 1.获取 starting window type
    // 此时获取的 type 为 STARTING_WINDOW_TYPE_SPLASH_SCREEN
    final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
            allowTaskSnapshot, activityCreated, activityAllDrawn, snapshot);

    // false
    final boolean useLegacy = type == STARTING_WINDOW_TYPE_SPLASH_SCREEN
            && mWmService.mStartingSurfaceController.isExceptionApp(packageName, mTargetSdk,
                () -> {
                    ActivityInfo activityInfo = intent.resolveActivityInfo(
                            mAtmService.mContext.getPackageManager(),
                            PackageManager.GET_META_DATA);
                    return activityInfo != null ? activityInfo.applicationInfo : null;
                });

    // 2. 把 starting window 的数据,转换成 bit 位,包装成一个 int 类型
    final int typeParameter = StartingSurfaceController
            .makeStartingWindowTypeParameter(newTask, taskSwitch, processRunning,
                    allowTaskSnapshot, activityCreated, isSimple, useLegacy, activityAllDrawn,
                    type, packageName, mUserId);

    if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
        // ...
        return createSnapshot(snapshot, typeParameter);
    }

    // Original theme can be 0 if developer doesn't request any theme. So if resolved theme is 0
    // but original theme is not 0, means this package doesn't want a starting window.
    if (resolvedTheme == 0 && theme != 0) {
        return false;
    }

    // from 此时为 null
    if (from != null && transferStartingWindow(from)) {
        return true;
    }

    // There is no existing starting window, and we don't want to create a splash screen, so
    // that's it!
    if (type != STARTING_WINDOW_TYPE_SPLASH_SCREEN) {
        return false;
    }

    // 开始创建 splash screen 启动窗口的数据的log
    ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SplashScreenStartingData");

    // 3. 创建 SplashScreenStartingData ,保存 splash screen 启动窗口的数据
    mStartingData = new SplashScreenStartingData(mWmService, resolvedTheme, typeParameter);

    // 4. 执行下一步的添加启动窗口
    scheduleAddStartingWindow();
    return true;
}    

ActivityRecord 检测了一些条件,并且解析了一些数据,这些数据被转换为 int 变量的 bit 位。然后,创建 SplashScreenStartingData 保存所有启动窗口的数据。最后,继续执行添加启动窗口的下一步

这里来看下一些数据是如何计算出来的。首先看下 starting window type 是如何计算的

java 复制代码
// ActivityRecord.java

private int getStartingWindowType(boolean newTask, boolean taskSwitch, boolean processRunning,
        boolean allowTaskSnapshot, boolean activityCreated, boolean activityAllDrawn,
        TaskSnapshot snapshot) {
    // mewTask 此时为 true
    if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null
            && mActivityComponent.equals(task.intent.getComponent())) {
        // ...
    }
    
    // false
    final boolean isActivityHome = isActivityTypeHome();
    if ((newTask || !processRunning || (taskSwitch && !activityCreated))
            && !isActivityHome) {
        // app 冷启动的 starting window type
        return STARTING_WINDOW_TYPE_SPLASH_SCREEN;
    }
    
    if (taskSwitch) {
        // ...
    }
    
    return STARTING_WINDOW_TYPE_NONE;
}

可以看到,对于冷启动 app 来说,启动窗口类型计算出来为 STARTING_WINDOW_TYPE_SPLASH_SCREEN

再来看下,如何把数据转化为一个 int 值

java 复制代码
// StartingSurfaceController.java

static int makeStartingWindowTypeParameter(boolean newTask, boolean taskSwitch,
        boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated,
        boolean isSolidColor, boolean useLegacy, boolean activityDrawn, int startingWindowType,
        String packageName, int userId) {
    int parameter = 0;
    if (newTask) { // true
        parameter |= TYPE_PARAMETER_NEW_TASK;
    }
    if (taskSwitch) {// true
        parameter |= TYPE_PARAMETER_TASK_SWITCH;
    }
    if (processRunning) {//false
        parameter |= TYPE_PARAMETER_PROCESS_RUNNING;
    }
    if (allowTaskSnapshot) {//true
        parameter |= TYPE_PARAMETER_ALLOW_TASK_SNAPSHOT;
    }
    if (activityCreated || startingWindowType == STARTING_WINDOW_TYPE_SNAPSHOT) { // false
        parameter |= TYPE_PARAMETER_ACTIVITY_CREATED;
    }
    if (isSolidColor) {//false
        parameter |= TYPE_PARAMETER_USE_SOLID_COLOR_SPLASH_SCREEN;
    }
    if (useLegacy) {//false
        parameter |= TYPE_PARAMETER_LEGACY_SPLASH_SCREEN;
    }
    if (activityDrawn) {//false
        parameter |= TYPE_PARAMETER_ACTIVITY_DRAWN;
    }

    // 这个表示,如果启动窗口是纯色View,app 是否可以收到 
    // SplashScreen.OnExitAnimationListener#onSplashScreenExit(SplashScreenView) 回调
    // TODO: 这个应该默认是 enabled 状态的
    if (startingWindowType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
            && CompatChanges.isChangeEnabled(ALLOW_COPY_SOLID_COLOR_VIEW, packageName,
            UserHandle.of(userId))) {
        parameter |= TYPE_PARAMETER_ALLOW_HANDLE_SOLID_COLOR_SCREEN;
    }
    return parameter;
}

继续看下一步的添加启动窗口

java 复制代码
// ActivityRecord.java

void scheduleAddStartingWindow() {
    mAddStartingWindow.run();
}

private class AddStartingWindow implements Runnable {
    @Override
    public void run() {
        // Can be accessed without holding the global lock
        final StartingData startingData;
        synchronized (mWmService.mGlobalLock) {
            // 前面刚刚给 mStartingData 赋值了
            startingData = mStartingData;
            if (mStartingData == null) {
                // ...
            }
        }

        ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Add starting %s: startingData=%s",
                this, startingData);
        
        StartingSurfaceController.StartingSurface surface = null;
        try {
            // 1. 由 StartingData 创建启动窗口 'surface'
            surface = startingData.createStartingSurface(ActivityRecord.this);
        } catch (Exception e) {
            Slog.w(TAG, "Exception when adding starting window", e);
        }
        
        if (surface != null) {
            boolean abort = false;
            synchronized (mWmService.mGlobalLock) {
                // If the window was successfully added, then we need to remove it.
                if (mStartingData == null) {
                    // ...
                } else {
                    // 2. ActivityRecord#mStartingSurface 保存创建的 starting window surface
                    mStartingSurface = surface;
                }
                if (!abort) {
                    // 这个log代表发起创建 starting window surface 成功了
                    ProtoLog.v(WM_DEBUG_STARTING_WINDOW,
                            "Added starting %s: startingWindow=%s startingView=%s",
                            ActivityRecord.this, mStartingWindow, mStartingSurface);
                }
            }
            if (abort) {
                // ...
            }
        } else {
            ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Surface returned was null: %s",
                    ActivityRecord.this);
        }
    }
}    

在这一步中,ActivityRecord 利用 StartingData 创建启动窗口 'surface',并用 ActivityRecord#mStartingSurface 保存。

但是,这个启动窗口的 'surface',并不是真正的用于绘制的 surface,来看下 StartingData 创建的过程

java 复制代码
// SplashScreenStartingData.java

class SplashScreenStartingData extends StartingData {
    @Override
    StartingSurface createStartingSurface(ActivityRecord activity) {
        return mService.mStartingSurfaceController.createSplashScreenStartingSurface(
                activity, mTheme);
    }
}

SplashScreenStartingData 把数据传给 StartingSurfaceController ,然它来创建 splash screen 启动窗口 'surface'

从代码设计的意图看,StartingData 被设计为 Model 层,它负责与 Controller(StartingSurfaceController)通讯,例如,这里的创建启动窗口'surface'。

但是,这个设计,让我感觉非常各异。我个人认为,数据层应该提升为 ActivityRecord ,因为启动窗口所有数据,都是保存在 ActivityRecord 中,例如, ActivityRecord#mStartingData 以及 ActivityRecord#mStartingSurface 。因此,真正应该与启动窗口 controller 通讯的,应该为 ActivityRecord。

java 复制代码
// StartingSurfaceController.java
public class StartingSurfaceController {
    StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, int theme) {

        synchronized (mService.mGlobalLock) {
            final Task task = activity.getTask();
            // 1. 通过 TaskOrganizerController 添加启动窗口
            if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(
                    task, activity, theme, null /* taskSnapshot */)) {
                // 2.返回 StartingSurface,
                // StartingSurface 就只是保存了 task 而已
                return new StartingSurface(task);
            }
        }
        return null;
    }
}  

StartingSurfaceController 通过 TaskOrganizerController 添加启动窗口,然后返回一个代表启动窗口 'surface' 的 StartingSurface。

这个 StartingSurface 只是一个包装了 Task 的数据类而已,如下

java 复制代码
// StartingSurfaceController.java

final class StartingSurface {
    private final Task mTask;

    StartingSurface(Task task) {
        mTask = task;
    }

    /**
     * Removes the starting window surface. Do not hold the window manager lock when calling
     * this method!
     * @param animate Whether need to play the default exit animation for starting window.
     */
    public void remove(boolean animate) {
        synchronized (mService.mGlobalLock) {
            mService.mAtmService.mTaskOrganizerController.removeStartingWindow(mTask, animate);
        }
    }
}

从这里可以看出,启动窗口其实与 Task 绑定的,而不是 ActivityRecord。

继续看下 TaskOrganizerController 是如何添加启动窗口的

java 复制代码
// TaskOrganizerController.java

// taskSnapshot 为 null
boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
        TaskSnapshot taskSnapshot) {
    final Task rootTask = task.getRootTask();
    if (rootTask == null || activity.mStartingData == null) {
        return false;
    }

    // 获取 WMShell 注册的 ITaskOrganizer 
    final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
    if (lastOrganizer == null) {
        return false;
    }

    // 1. starting window 相关的信息填充到 StartingWindowInfo
    final StartingWindowInfo info = task.getStartingWindowInfo(activity);
    if (launchTheme != 0) {
        info.splashScreenThemeResId = launchTheme;
    }
    info.taskSnapshot = taskSnapshot; // null
    info.appToken = activity.token;
    
    
    // make this happen prior than prepare surface
    try {
        // 2.通知WMShell添加启动窗口
        lastOrganizer.addStartingWindow(info);
    } catch (RemoteException e) {
        Slog.e(TAG, "Exception sending onTaskStart callback", e);
        return false;
    }
    return true;
}

TaskOrganizerController 把启动窗口的数据包装到 StartingWindowInfo,然后传送给 WMShell,让其添加启动窗口

这里展示下 StartingWindowInfo 是如何包装启动窗口数据的,方便后面查阅

java 复制代码
// Task.java

StartingWindowInfo getStartingWindowInfo(ActivityRecord activity) {
    final StartingWindowInfo info = new StartingWindowInfo();
    info.taskInfo = getTaskInfo();
    // 这个就是要启动 Activity 的信息
    info.targetActivityInfo = info.taskInfo.topActivityInfo != null
            && activity.info != info.taskInfo.topActivityInfo
            ? activity.info : null;
    
    info.isKeyguardOccluded =
        mAtmService.mKeyguardController.isDisplayOccluded(DEFAULT_DISPLAY);

    // 取自 activity.mStartingData.mTypeParams
    info.startingWindowTypeParameter = activity.mStartingData != null
            ? activity.mStartingData.mTypeParams
            : (StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED
                    | StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS);
     
    // false
    if ((info.startingWindowTypeParameter
            & StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED) != 0) {
        final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION);
        if (topMainWin != null) {
            info.mainWindowLayoutParams = topMainWin.getAttrs();
            info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes();
        }
    }
    // If the developer has persist a different configuration, we need to override it to the
    // starting window because persisted configuration does not effect to Task.
    info.taskInfo.configuration.setTo(activity.getConfiguration());
    
    // 此时ActivityRecord下还没有全屏窗口显示,因此这里返回 null
    final ActivityRecord topFullscreenActivity = getTopFullscreenActivity();
    if (topFullscreenActivity != null) {
        final WindowState topFullscreenOpaqueWindow =
                topFullscreenActivity.getTopFullscreenOpaqueWindow();
        if (topFullscreenOpaqueWindow != null) {
            info.topOpaqueWindowInsetsState =
                    topFullscreenOpaqueWindow.getInsetsStateWithVisibilityOverride();
            info.topOpaqueWindowLayoutParams = topFullscreenOpaqueWindow.getAttrs();
        }
    }
    return info;
}

WMShell 创建启动窗口

来看下 WMShell 是如何完成添加启动窗口的

java 复制代码
// ShellTaskOrganizer.java

public void addStartingWindow(StartingWindowInfo info) {
    if (mStartingWindow != null) {
        mStartingWindow.addStartingWindow(info);
    }
}
java 复制代码
// StartingWindowController.java

public void addStartingWindow(StartingWindowInfo windowInfo) {
    // 注意,这是在 splash screen thread 中执行的
    mSplashScreenExecutor.execute(() -> {
        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addStartingWindow");
        // 1. 计算启动窗口类型
        // 此时返回的是 STARTING_WINDOW_TYPE_SPLASH_SCREEN
        final int suggestionType = mStartingWindowTypeAlgorithm.getSuggestedWindowType(
                windowInfo);
        
        final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo;
        if (suggestionType == STARTING_WINDOW_TYPE_WINDOWLESS) {
            // ...
        } else if (isSplashScreenType(suggestionType)) {
            // 2. 添加启动窗口
            mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, suggestionType);
        } else if (suggestionType == STARTING_WINDOW_TYPE_SNAPSHOT) {
            // ...
        }

        if (suggestionType != STARTING_WINDOW_TYPE_NONE
                && suggestionType != STARTING_WINDOW_TYPE_WINDOWLESS) {
            int taskId = runningTaskInfo.taskId;
            // TODO: 这一段代码有什么用
            int color = mStartingSurfaceDrawer
                    .getStartingWindowBackgroundColorForTask(taskId);
            if (color != Color.TRANSPARENT) {
                mTaskBackgroundColors.append(taskId, color);
            }

            // TODO: mTaskLaunchingCallback 是谁注册的,有什么用?
            if (mTaskLaunchingCallback != null && isSplashScreenType(suggestionType)) {
                mTaskLaunchingCallback.accept(taskId, suggestionType, color);
            }
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    });
}

StartingWindowController 解析了启动窗口类型,然后把这个类型传递给 StartingSurfaceDrawer ,让其添加启动窗口。

对于窗口开发者来说,StartingWindowController 有一个非常重要的事情,我这里得提一下。它所有的操作,都必须在 splash screen thread 下执行,也就是通过 mSplashScreenExecutor 去执行。

解析启动窗口类型,基本上就是根据 WMShell 传递过来的数据进行解析的,解析的结果为 STARTING_WINDOW_TYPE_SPLASH_SCREEN ,这里就不展示代码了。 接下来看下 StartingSurfaceDrawer 是如何添加启动窗口的

java 复制代码
// StartingSurfaceDrawer.java

/**
 * A class which able to draw splash screen or snapshot as the starting window for a task.
 */
public class StartingSurfaceDrawer {

    void addSplashScreenStartingWindow(StartingWindowInfo windowInfo,
            @StartingWindowType int suggestType) {
        mSplashscreenWindowCreator.addSplashScreenStartingWindow(windowInfo, suggestType);
    }
}

StartingWindowController 并不会真正地去创建启动窗口,而只是管理如何创建不同类型的启动窗口,以及保存创建的启动窗口的记录。 而对于 splash screen 的启动窗口,交给 SplashScreenWindowCreator 去创建

java 复制代码
// SplashScreenWindowCreator.java

void addSplashScreenStartingWindow(StartingWindowInfo windowInfo,
        @StartingWindowInfo.StartingWindowType int suggestType) {
    final ActivityManager.RunningTaskInfo taskInfo = windowInfo.taskInfo;
    final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
            ? windowInfo.targetActivityInfo
            : taskInfo.topActivityInfo;
    if (activityInfo == null || activityInfo.packageName == null) {
        return;
    }
    // replace with the default theme if the application didn't set
    final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
    final Context context = SplashscreenContentDrawer.createContext(mContext, windowInfo, theme,
            suggestType, mDisplayManager);
    if (context == null) {
        return;
    }

    // 1.构建 WindowManager.LayoutParams
    final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters(
            context, windowInfo, suggestType, activityInfo.packageName,
            suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
                    ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT, windowInfo.appToken);

    final int displayId = taskInfo.displayId;
    final int taskId = taskInfo.taskId;
    final Display display = getDisplay(displayId);

    // TODO(b/173975965) tracking performance
    // Prepare the splash screen content view on splash screen worker thread in parallel, so the
    // content view won't be blocked by binder call like addWindow and relayout.
    // 1. Trigger splash screen worker thread to create SplashScreenView before/while
    // Session#addWindow.
    // 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
    // traversal, which will call Session#relayout on splash screen thread.
    // 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at
    // the same time the splash screen thread should be executing Session#relayout. Blocking the
    // traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.

    // Record whether create splash screen view success, notify to current thread after
    // create splash screen view finished.

    // SplashScreenViewSupplier 用来缓存并提供 SplashScreenView
    final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();

    // 2. 创建 FrameLayout,作为启动窗口的 root view
    final FrameLayout rootLayout = new FrameLayout(
            mSplashscreenContentDrawer.createViewContextWrapper(context));
    rootLayout.setPadding(0, 0, 0, 0);
    rootLayout.setFitsSystemWindows(false);

    final Runnable setViewSynchronized = () -> {
        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
        // waiting for setContentView before relayoutWindow
        // 通过 SplashScreenViewSupplier 获取 worker thread 创建的 SplashScreenView
        // 注意,这里可能会阻塞,一直等到 worker thread 创建完 SplashScreenView
        SplashScreenView contentView = viewSupplier.get();

        // add window 时,会把数据缓存到 mStartingWindowRecordManager,因此这里可以获取到
        final StartingSurfaceDrawer.StartingWindowRecord sRecord =
                mStartingWindowRecordManager.getRecord(taskId);
        // 类型确实是 SplashWindowRecord
        final SplashWindowRecord record = sRecord instanceof SplashWindowRecord
                ? (SplashWindowRecord) sRecord : null;
        // If record == null, either the starting window added fail or removed already.
        // Do not add this view if the token is mismatch.
        if (record != null && windowInfo.appToken == record.mAppToken) {
            // if view == null then creation of content view was failed.
            if (contentView != null) {
                try {
                    // 终于把 SplashScreenView 添加到 root view 下
                    rootLayout.addView(contentView);
                } catch (RuntimeException e) {
                    Slog.w(TAG, "failed set content view to starting window "
                            + "at taskId: " + taskId, e);
                    contentView = null;
                }
            }
            // SplashWindowRecord#mSplashView 保存 SplashScreenView
            record.setSplashScreenView(contentView);
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    };

    // TODO: 告诉 SystemUI 做什么
    requestTopUi(true);

    // 3. worker thread 中创建 SplashScreenView
    // 创建完成之后,通过第四个参数的回调,把 SplashScreenView 保存到 SplashScreenViewSupplier
    mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
            viewSupplier::setView, viewSupplier::setUiThreadInitTask);

    try {
        // 4. 通过 window manager 添加启动窗口的 root view
        if (addWindow(taskId, windowInfo.appToken, rootLayout, display, params, suggestType)) {
            // We use the splash screen worker thread to create SplashScreenView while adding
            // the window, as otherwise Choreographer#doFrame might be delayed on this thread.
            // And since Choreographer#doFrame won't happen immediately after adding the window,
            // if the view is not added to the PhoneWindow on the first #doFrame, the view will
            // not be rendered on the first frame. So here we need to synchronize the view on
            // the window before first round relayoutWindow, which will happen after insets
            // animation.
            // 5.监听 insets animation 下一帧,获取 worker thread 中创建的 SplashScreenView ,并且把 SplashScreenView 保存到 root view 中
            mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);

            // add window 时,已经保存到了 mStartingWindowRecordManager,因此这里能获取到
            final SplashWindowRecord record =
                    (SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId);
            if (record != null) {
                record.parseAppSystemBarColor(context);
                // Block until we get the background color.
                final SplashScreenView contentView = viewSupplier.get();
                if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
                    contentView.addOnAttachStateChangeListener(
                            new View.OnAttachStateChangeListener() {
                                @Override
                                public void onViewAttachedToWindow(View v) {
                                    // 配合启动窗口背景色,设置状态栏导航栏的 appearance
                                    // 这样状态栏和导航栏的颜色,能与启动窗口背景色相符
                                    final int lightBarAppearance =
                                            ContrastColorUtil.isColorLight(
                                                    contentView.getInitBackgroundColor())
                                                    ? LIGHT_BARS_MASK : 0;
                                    contentView.getWindowInsetsController()
                                            .setSystemBarsAppearance(
                                            lightBarAppearance, LIGHT_BARS_MASK);
                                }

                                @Override
                                public void onViewDetachedFromWindow(View v) {
                                }
                            });
                }
            }
        } else {
            // ...
        }
    } catch (RuntimeException e) {
        // ....
    }
}

SplashScreenWindowCreator 才是真正添加启动窗口的地方。但是,从代码看,似乎有点小复杂。从 AOSP 注释看,为了对添加启动窗口进行加速,使用了两个线程来完成添加启动窗口

  1. 启动窗口的真身就是一个 View,它需要通过 WindowManager 添加并显示出来。因此,需要一个布局参数 WindowManager.LayoutParams。
  2. 创建 FrameLayout,作为启动窗口的根 View。注意,此时根 View 只是一个空壳而已。
  3. SplashscreenContentDrawer 在 splash screen worker thread 中创建启动窗口的 View,名为 SplashScreenView,并通过 viewSupplier::setView 回调,把它缓存到 SplashScreenViewSupplier 中。参考【创建 SplashScreenView
  4. 通过 WindowManager 添加启动窗口的根 View,注意,此时根 View 仍然是一个空壳。ViewRootImpl 会向 WMS 发起 add window 来添加窗口,并且还会向 Choreographer 添加一个类型为 CALLBACK_TRAVERSAL 的回调,当下一次 vsync 信号到来时,会向 WMS 发起 relayout window 获取绘制的 surface,然后执行 measure,layout,draw 等操作。
  5. 向 Choreographer 添加一个类型为 CALLBACK_INSETS_ANIMATION 的回调,当下一次 vsync 信号到来时,从 SplashScreenViewSupplier 中同步阻塞地获取 SplashScreenView,并添加到启动窗口的根 View 中。这个 SplashScreenView 同步获取的过程,就是 AOSP 注释所说的,把 splash screen worker thread 中缓存的 SplashscreenView ,同步到 splash screen thread 中。

布局参数具体包含了哪些数据,我这里就不一一展示出来。不过,我重点需要提几点数据

  1. type 为 为 TYPE_APPLICATION_STARTING ,而 Activity 的真窗类型为 TYPE_BASE_APPLICATION
  2. token 为 activity token,因此启动窗口能正确保存到 ActivityRecord 下。
  3. 由于此时启动窗口类型为 STARTING_WINDOW_TYPE_SPLASH_SCREEN ,因此 format 为 PixelFormat.TRANSLUCENT,它是半透明的。这个对于启动窗口的 reveal 动画,有至关重要的作用。

第4步和第5步,向 Choreographer 注册了两个类型的回调,当 vsync 到来时,会先执行 CALLBACK_INSETS_ANIMATION 回调,然后执行 CALLBACK_TRAVERSAL 回调。因此,这就导致会先把 SplashScreenView 保存到根 View 中,然后再向 WMS 发起 relayout window,最后绘制出来。

OK,到这里,已经从整体上搞清楚 WMShell 是如何添加启动窗口的。但是,这就满足了吗?不!让我们继续探究启动窗口 View,也就是 SplashScreenView,是如何一步一步创建出来的。

创建 SplashScreenView

java 复制代码
// SplashScreenContentDrawer.java

void createContentView(Context context, @StartingWindowType int suggestType,
        StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer,
        Consumer<Runnable> uiThreadInitConsumer) {
    // 1.在 splash screen worker thread 中创建启动窗口 View
    mSplashscreenWorkerHandler.post(() -> {
        SplashScreenView contentView;
        try {
            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
            // 创建SplashScreenView
            contentView = makeSplashScreenContentView(context, info, suggestType,
                    uiThreadInitConsumer);
            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
        } catch (RuntimeException e) {
            Slog.w(TAG, "failed creating starting window content at taskId: "
                    + info.taskInfo.taskId, e);
            contentView = null;
        }
        
        // 2. 把 SplashScreenView 缓存到 SplashScreenViewSupplier
        splashScreenViewConsumer.accept(contentView);
    });
}

SplashScreenContentDrawer 正如名字所说,绘制启动窗口的内容。但是,对于窗口开发者来说,我得提醒一下,它所有的操作必须在 mSplashscreenWorkerHandler 中执行。用 AOSP 的话来说,就是需要在 splash screen worker thread 中执行。

java 复制代码
// SplashScreenContentDrawer.java

private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info,
        @StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
    // 从系统中获取启动窗口需要的尺寸,例如,显示图标的大小
    updateDensity();

    // 从 theme 中获取启动窗口的数据,保存到 mTmpAttrs
    // 例如,背景色,图标
    getWindowAttrs(context, mTmpAttrs);
    
    mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode();

    // 此时类型为 TYPE_APPLICATION_STARTING
    // legacyDrawable 为 null
    final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
            ? peekLegacySplashscreenContent(context, mTmpAttrs) : null;
            
    final ActivityInfo ai = info.targetActivityInfo != null
            ? info.targetActivityInfo
            : info.taskInfo.topActivityInfo;
    
    // 获取启动窗口背景色
    final int themeBGColor = legacyDrawable != null
            ? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
            : getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));

    return new SplashViewBuilder(context, ai)
            .setWindowBGColor(themeBGColor) // mThemeColor
            .overlayDrawable(legacyDrawable) // mOverlayDrawable,此时为 null
            .chooseStyle(suggestType) // mSuggestType
            .setUiThreadInitConsumer(uiThreadInitConsumer) // mUiThreadInitTask
            // TODO:这个应该要注册splash screen 退出动画监听器
            .setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen()) // mAllowHandleSolidColor
            .build();
}

SplashScreenContentDrawer 从 theme 中解析了启动窗口数据,然后通过 SplashViewBuilder 来创建 SplashScreenView。

这里展示下获取了启动窗口数据的代码,方便后面查阅

java 复制代码
//SplashscreenContentDrawer.java

private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
    final TypedArray typedArray = context.obtainStyledAttributes(
            com.android.internal.R.styleable.Window);

    // 这两个都是窗口背景色,只不过 windowSplashScreenBackground 在 Android 12 之后有效
    attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
    // 注意,背景色默认为 Color.TRANSPARENT
    attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
            R.styleable.Window_windowSplashScreenBackground, def),
            Color.TRANSPARENT);

    // 图标
    attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
            R.styleable.Window_windowSplashScreenAnimatedIcon), null);
    // 商标
    attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
            R.styleable.Window_windowSplashScreenBrandingImage), null);
    // 图标背景色,默认为 TRANSPARENT
    attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
            R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
            Color.TRANSPARENT);
    typedArray.recycle();
    ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
            "getWindowAttrs: window attributes color: %s, replace icon: %b",
            Integer.toHexString(attrs.mWindowBgColor), attrs.mSplashScreenIcon != null);
}

接着看下 SplashScreenView 的构建

java 复制代码
//SplashscreenContentDrawer.java

SplashScreenView build() {

    // 1.获取图标 Drawable
    Drawable iconDrawable;
    if (mSuggestType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN
            || mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
        // ...
    } else if (mTmpAttrs.mSplashScreenIcon != null) { // app自定义启动窗口图标
        // Using the windowSplashScreenAnimatedIcon attribute
        iconDrawable = mTmpAttrs.mSplashScreenIcon;

        // There is no background below the icon, so scale the icon up
        if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
                || mTmpAttrs.mIconBgColor == mThemeColor) {
            mFinalIconSize *= NO_BACKGROUND_SCALE;
        }

        // 1. 创建图标 Drawable
        // 注意,这里其实创建了两个 Drawable,一个是图标 Drawable,另一个是图标背景色 Drawable
        // 这两个 Drawable 是保存到 mFinalIconDrawables 数组中
        createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */);
    } else { // 默认没有自定义窗口图标
        // ...
    }

    // 2. 利用创建的两个 Drawable,去填充 View
    return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask);
}

当前启动窗口的类型为 TYPE_APPLICATION_STARTING ,并且这里假定 app 在 theme 中自定义了启动窗口图标。因此,这里会先为图标以及图标背景色创建两个 Drawable,然后填充到 View 中。

先看下创建两个 Drawable 的过程,如下

java 复制代码
//SplashscreenContentDrawer.java

// legacy 为 false
// loadInDetail 为 false
private void createIconDrawable(Drawable iconDrawable, boolean legacy,
        boolean loadInDetail) {
    if (legacy) {
        // ...
    } else {
        // 注意,最后一个参数为 mSplashscreenWorkerHandler,它代表 splash screen worker thread
        mFinalIconDrawables = SplashscreenIconDrawableFactory.makeIconDrawable(
                mTmpAttrs.mIconBgColor, mThemeColor, iconDrawable, mDefaultIconSize,
                mFinalIconSize, loadInDetail, mSplashscreenWorkerHandler);
    }
}
java 复制代码
// SplashscreenIconDrawableFactory.java

static Drawable[] makeIconDrawable(@ColorInt int backgroundColor, @ColorInt int themeColor,
        @NonNull Drawable foregroundDrawable, int srcIconSize, int iconSize,
        boolean loadInDetail, Handler preDrawHandler) {
    Drawable foreground;
    Drawable background = null;
    
    // backgroundColor 是图标背景色
    boolean drawBackground =
            backgroundColor != Color.TRANSPARENT && backgroundColor != themeColor;
        
    // foregroundDrawable 是启动窗口要显示的图标
    if (foregroundDrawable instanceof Animatable) { // 处理 AnimatedVectorDrawable 这类情况
        // ...
    } else if (foregroundDrawable instanceof AdaptiveIconDrawable) {//处理AdaptiveIconDrawable
        // ...
    } else {// 默认情况
        // Adaptive icon don't handle transparency so we draw the background of the adaptive
        // icon with the same color as the window background color instead of using two layers
        // 1. 创建图标 Drawable
        // 最后一个参数 preDrawHandler 代表 splash screen worker thread
        foreground = new ImmobileIconDrawable(
                new AdaptiveForegroundDrawable(foregroundDrawable),
                srcIconSize, iconSize, loadInDetail, preDrawHandler);
    }

    if (drawBackground) {
        // 2. 创建一个图标背景色的 Drawable
        background = new MaskBackgroundDrawable(backgroundColor);
    }

    // 3. 返回 Drawble 数组,第一个元素是图标Drawable,第二个元素是图标背景色Drawable
    return new Drawable[]{foreground, background};
}

可以看到,这里确实创建了两个 Drawable。重点看下创建图标 Drawable 过程,非常有意思

java 复制代码
// SplashscreenIconDrawableFactory.java

private static class ImmobileIconDrawable extends Drawable {
    private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
            | Paint.FILTER_BITMAP_FLAG);
    private final Matrix mMatrix = new Matrix();
    private Bitmap mIconBitmap;
    ImmobileIconDrawable(Drawable drawable, int srcIconSize, int iconSize, boolean loadInDetail,
            Handler preDrawHandler) {
        // loadInDetail 此时为 false
        if (loadInDetail) {
            // ...
        } else {
            final float scale = (float) iconSize / srcIconSize;
            mMatrix.setScale(scale, scale);
            // preDrawHandler 代表 splash screen worker thread
            preDrawHandler.post(() -> preDrawIcon(drawable, srcIconSize));
        }
    }

    private void preDrawIcon(Drawable drawable, int size) {
        synchronized (mPaint) {
            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "preDrawIcon");
            // 把启动窗口图标,提前绘制到 Bitmap 上
            mIconBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
            final Canvas canvas = new Canvas(mIconBitmap);
            drawable.setBounds(0, 0, size, size);
            drawable.draw(canvas);
            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
        }
    }    
}

创建图标 Drawable 的精髓在于,在 splash screen worker thread 中提前把 Drawable 绘制到 Bitmap 上。我之所以说'精髓',可能有些读者看不懂,这里再次展示下创建启动窗口 View 的 AOSP 注释,来自于 SplashscreenWindowCreator#addSplashScreenStartingWindow()

vbnet 复制代码
Prepare the splash screen content view on splash screen worker thread in parallel, so the
content view won't be blocked by binder call like addWindow and relayout.
1. Trigger splash screen worker thread to create SplashScreenView before/while
Session#addWindow.
2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
traversal, which will call Session#relayout on splash screen thread.
3. Pre-draw the BitmapShader if the icon is immobile(静止的,不动的) on splash screen worker thread, at
the same time the splash screen thread should be executing Session#relayout. Blocking the
traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.

重点看下第3点,这里的意思应该是,如果进入了绘制阶段,而图标的 Bitmap 没有绘制完成,那么会阻塞绘制,直到图标 Bitmap 绘制完成。为何会阻塞,因为图标 Bitmap 绘制,和图标 Drawable 的绘制,都在一个对象上进行同步,如下

似乎,加载 Bitmap 是比较耗时。为了不让其阻塞添加启动窗口的进度,在 splash screen worker thread 创建 SplashScreenView 之时,并没有立即加载图标 Bitmap,而是在 splash screen worker thread 中,再 post 一个 Runnable 来加载。

不过,我有一个疑问,有没有一种可能,splash screen worker thread 还没有执行加载图标 Bitmap 的 Runnable,系统就执行了 Drawable 的绘制呢?那岂不是启动窗口显示不出来图标?当然,这应该是一种极端情况。

java 复制代码
    private static class ImmobileIconDrawable extends Drawable {

        private void preDrawIcon(Drawable drawable, int size) {
            // 同步 mPaint,绘制 Bitmap
            synchronized (mPaint) {
                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "preDrawIcon");
                mIconBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
                final Canvas canvas = new Canvas(mIconBitmap);
                drawable.setBounds(0, 0, size, size);
                drawable.draw(canvas);
                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
            }
        }

        @Override
        public void draw(Canvas canvas) {
            // 同步 mPaint,绘制 Drawable
            synchronized (mPaint) {
                if (mIconBitmap != null) {
                    canvas.drawBitmap(mIconBitmap, mMatrix, mPaint);
                } else {
                    // this shouldn't happen, but if it really happen, invalidate self to wait
                    // for bitmap to be ready.
                    invalidateSelf();
                }
            }
        }        

两个 Drawable 创建完成后,现在用来填充 View

java 复制代码
private SplashScreenView fillViewWithIcon(int iconSize, @Nullable Drawable[] iconDrawable,
        Consumer<Runnable> uiThreadInitTask) {
    Drawable foreground = null;
    Drawable background = null;
    if (iconDrawable != null) {
        // 启动窗口图标
        foreground = iconDrawable.length > 0 ? iconDrawable[0] : null;
        // 启动窗口图标背景色
        background = iconDrawable.length > 1 ? iconDrawable[1] : null;
    }

    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "fillViewWithIcon");
    final ContextThemeWrapper wrapper = createViewContextWrapper(mContext);
    final SplashScreenView.Builder builder = new SplashScreenView.Builder(wrapper)
            .setBackgroundColor(mThemeColor) // mBackgroundColor,整个启动窗口的背景色
            .setOverlayDrawable(mOverlayDrawable) // mOverlayDrawable,legacy drawable, 此时为 null
            .setIconSize(iconSize) // mIconSize
            .setIconBackground(background) // mIconBackground,图标背景色
            .setCenterViewDrawable(foreground) // mIconDrawable ,指图标
            // 这个是 SplashScreenViewSupplier::setUiThreadInitTask 回调
            .setUiThreadInitConsumer(uiThreadInitTask) // mUiThreadInitTask
            // TODO: 这个应该与退出动画监听器有关
            .setAllowHandleSolidColor(mAllowHandleSolidColor); // mAllowHandleSolidColor

    // mTmpAttrs.mBrandingImage 从 theme 中获取的启动窗口商标
    if (mSuggestType == STARTING_WINDOW_TYPE_SPLASH_SCREEN
            && mTmpAttrs.mBrandingImage != null) {
        // 保存商标图标以及宽高
        builder.setBrandingDrawable(mTmpAttrs.mBrandingImage, mBrandingImageWidth,
                mBrandingImageHeight);
    }
    
    // 构建 SplashScreenView
    final SplashScreenView splashScreenView = builder.build();
    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    return splashScreenView;
}

通过 Builder 模式保存数据,然后构建 SplashScreenView。来看下最终的 SplashScreenView 庐山真面目

java 复制代码
// SplashScreenView.java

public SplashScreenView build() {
    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "SplashScreenView#build");
    final LayoutInflater layoutInflater = LayoutInflater.from(mContext);

    // 加载布局
    final SplashScreenView view = (SplashScreenView)
            layoutInflater.inflate(R.layout.splash_screen_view, null, false);
    view.mInitBackgroundColor = mBackgroundColor;
    if (mOverlayDrawable != null) {
        view.setBackground(mOverlayDrawable);
    } else {
        // 设置整个背景色
        view.setBackgroundColor(mBackgroundColor);
    }
    view.mClientCallback = mClientCallback;

    view.mBrandingImageView = view.findViewById(R.id.splashscreen_branding_view);
    boolean hasIcon = false;

    // 设置启动窗口的中心图标
    if (mIconDrawable instanceof SplashScreenView.IconAnimateListener
            || mSurfacePackage != null) { 
        // 处理的是动画Drawable,或者有 SurfaceView 的情况
    } else if (mIconSize != 0) {
        // 获取图标的 ImageView
        ImageView imageView = view.findViewById(R.id.splashscreen_icon_view);
        assert imageView != null;
        final ViewGroup.LayoutParams params = imageView.getLayoutParams();
        params.width = mIconSize;
        params.height = mIconSize;
        imageView.setLayoutParams(params);
        // ImageView 设置图标以及背景色
        if (mIconDrawable != null) {
            imageView.setImageDrawable(mIconDrawable);
        }
        if (mIconBackground != null) {
            imageView.setBackground(mIconBackground);
        }
        hasIcon = true;
        view.mIconView = imageView;
    }

    // mOverlayDrawable 是 legacy drawable,此时为 null
    if (mOverlayDrawable != null || (!hasIcon && !mAllowHandleSolidColor)) {
        view.setNotCopyable();
    }
    view.mParceledIconBackgroundBitmap = mParceledIconBackgroundBitmap;
    view.mParceledIconBitmap = mParceledIconBitmap;

    // 设置商标图标
    if (mBrandingImageHeight > 0 && mBrandingImageWidth > 0 && mBrandingDrawable != null) {
        final ViewGroup.LayoutParams params = view.mBrandingImageView.getLayoutParams();
        params.width = mBrandingImageWidth;
        params.height = mBrandingImageHeight;
        view.mBrandingImageView.setLayoutParams(params);
        view.mBrandingImageView.setBackground(mBrandingDrawable);
    } else {
        view.mBrandingImageView.setVisibility(GONE);
    }

    if (mParceledBrandingBitmap != null) {
        view.mParceledBrandingBitmap = mParceledBrandingBitmap;
    }
    if (DEBUG) {
        Log.d(TAG, "Build " + view
                + "\nIcon: view: " + view.mIconView + " drawable: "
                + mIconDrawable + " size: " + mIconSize
                + "\nBranding: view: " + view.mBrandingImageView + " drawable: "
                + mBrandingDrawable + " size w: " + mBrandingImageWidth + " h: "
                + mBrandingImageHeight);
    }
    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    return view;
}

很简单,加载布局,并对布局中的控制进行设置,例如,设置图标,设置商标,等等。来看下这个布局文件 splash_screen_view.xml

xml 复制代码
<android.window.SplashScreenView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:padding="0dp"
    android:orientation="vertical">

    <ImageView android:id="@+id/splashscreen_icon_view"
          android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          android:layout_gravity="center"
          android:padding="0dp"
          android:background="@null"
          android:contentDescription="@string/splash_screen_view_icon_description"/>

    <View android:id="@+id/splashscreen_branding_view"
          android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          android:layout_gravity="center_horizontal|bottom"
          android:layout_marginBottom="60dp"
          android:padding="0dp"
          android:background="@null"
          android:forceHasOverlappingRendering="false"
          android:contentDescription="@string/splash_screen_view_branding_description"/>

</android.window.SplashScreenView>

根 View 是一个 FrameLayout,中间显示一个图标 ImageView,底下中间位置是一个商标 View。

下一步

下一篇文章,将来看看 WMS 侧,启动窗口是如何添加、绘制、以及显示。

相关推荐
约翰先森不喝酒1 小时前
Android RecyclerView 实现 GridView ,并实现点击效果及方向位置的显示
android
wk灬丨2 小时前
Android Choreographer 监控应用 FPS
android·kotlin
魏大橙3 小时前
长亭WAF绕过测试
android·运维·服务器
志尊宝3 小时前
Android 中使用高德地图实现根据经纬度信息画出轨迹、设置缩放倍数并定位到轨迹路线的方法
android
吾爱星辰3 小时前
Kotlin while 和 for 循环(九)
android·开发语言·kotlin
我命由我123453 小时前
Kotlin 极简小抄 P3(函数、函数赋值给变量)
android·开发语言·java-ee·kotlin·android studio·学习方法·android-studio
大风起兮云飞扬丶5 小时前
Android——内部/外部存储
android
niurenwo6 小时前
Android深入理解包管理--记录存储模块
android
文 丰6 小时前
【Android Studio】使用雷电模拟器调试
android·ide·android studio