Android V app 冷启动(3) 添加启动窗口

根据 Android V app 冷启动 (1) Activity生命周期 的分析,在第一阶段启动中,当构建完窗口层级后,添加了启动窗口,本文就来分析这个课题。

WMCore 添加启动窗口

java 复制代码
// Task.java

// r 是要启动的 activity
// topTask 指的是 launcher root task
// newTask 为 true
// isTaskSwitch 为 true
// options 启动 activity 参数,不为 null
// sourceRecord 指的是 launcher
void startActivityLocked(ActivityRecord r, @Nullable Task topTask, boolean newTask,
        boolean isTaskSwitch, ActivityOptions options, @Nullable ActivityRecord sourceRecord) {
    // ...

    final Task activityTask = r.getTask();
    
    // ...

    task = activityTask;

    // Slot the activity into the history root task and proceed
    ProtoLog.i(WM_DEBUG_ADD_REMOVE, "Adding activity %s to task %s "
                    + "callers: %s", r, task, new RuntimeException("here").fillInStackTrace());

    // ...
    

    // 检测是否需要显示启动窗口
    boolean doShow = true;
    if (newTask) {
        // 从 start u0 的 log 看,是包含这个 flag 的
        if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
            // reset task 指的是把相关的 activity 移动到当前 task,或者把不相关 actvity 移出 task
            // 本文分析的案例不涉及
            resetTaskIfNeeded(r, r);
            // reset Task 后,如果 Task 的 top-non-delayed-non-finishing activity 仍然是要启动的 activity
            // 那么,需要显示启动窗口
            doShow = topRunningNonDelayedActivityLocked(null) == r;
        }
    } else if (options != null && options.getAnimationType()
            == ActivityOptions.ANIM_SCENE_TRANSITION) {
        // ...
    }
    // 启动 activity 的参数中也可以指定不需要显示启动窗口
    if (options != null && options.getDisableStartingWindow()) {
        doShow = false;
    }
    
    if (r.mLaunchTaskBehind) {
        
    } 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
        final ActivityRecord prev = baseTask.getActivity(
                a -> a.mStartingData != null && a.showToCurrentUser());
        mWmService.mStartingSurfaceController.showStartingWindow(r, prev, newTask,
                isTaskSwitch, sourceRecord);
    }
}

Task 检测了是否需要为启动的 activity 显示启动窗口,如果是,交给启动窗口控制器 StartingSurfaceController 来显示

java 复制代码
// StartingSurfaceController.java

void showStartingWindow(ActivityRecord target, ActivityRecord prev,
        boolean newTask, boolean isTaskSwitch, ActivityRecord source) {
    if (mDeferringAddStartingWindow) {
        // ...
    } else {
        // 直接交给 ActivityRecord show start window
        target.showStartingWindow(prev, newTask, isTaskSwitch, true /* startActivity */,
                source);
    }
}
java 复制代码
// ActivityRecord.java

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

void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
        boolean processRunning, boolean startActivity, ActivityRecord sourceRecord,
        ActivityOptions candidateOptions) {
    if (mTaskOverlay) {
        // We don't show starting window for overlay activities.
        return;
    }
    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;
    }

    // 解析 splash screen theme
    final int splashScreenTheme = startActivity ? getSplashscreenTheme(startOptions) : 0;
    final int resolvedTheme = evaluateStartingWindowTheme(prev, packageName, theme,
            splashScreenTheme);

    // 启动窗口是显示图标,还是纯色
    // false
    mSplashScreenStyleSolidColor = shouldUseSolidColorSplashScreen(sourceRecord, startActivity,
            startOptions, resolvedTheme);

    // 这个表示 app 端 activity 是否已经创建
    // false
    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.
    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);
    if (DEBUG_STARTING_WINDOW_VERBOSE && scheduled) {
        Slog.d(TAG, "Scheduled starting window for " + this);
    }
}


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;
    }

    if (hasStartingWindow()) {
        return false;
    }
    
    // 目前 ActivityRecord 下还没有窗口 WindowState
    final WindowState mainWin = findMainWindow(false /* includeStartingApp */);
    if (mainWin != null && mainWin.isDrawn()) {
        // App already has a visible window...why would you want a starting window?
        return false;
    }

    // app 冷启动,系统还没有 Task 的快照
    // null
    final TaskSnapshot snapshot =
            mWmService.mTaskSnapshotController.getSnapshot(task.mTaskId, task.mUserId,
                    false /* restoreFromDisk */, false /* isLowResolution */);
                    
    // 获取启动窗口类型
    // app 冷启动,启动窗口类型为 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;
                });

    // 把所有数据转化为一个 int
    final int typeParameter = StartingSurfaceController
            .makeStartingWindowTypeParameter(newTask, taskSwitch, processRunning,
                    allowTaskSnapshot, activityCreated, isSimple, useLegacy, activityAllDrawn,
                    type, isIconStylePreferred(resolvedTheme), packageName, mUserId);

    // ...

    ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SplashScreenStartingData");
    
    // 创建启动窗口数据
    mStartingData = new SplashScreenStartingData(mWmService, resolvedTheme, typeParameter);
    
    // 执行下一步添加启动窗口
    scheduleAddStartingWindow();
    return true;
}

ActivityRecord 解析数据,并把所有数据包装到 mStartingData,然后执行下一步添加启动窗口。

java 复制代码
// ActivityRecord.java

private void scheduleAddStartingWindow() {
    ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Add starting %s: startingData=%s",
            this, mStartingData);

    // 通过 StartingData 来创建启动窗口 surface
    // 但是得到的并不是一个真正的 surface,而是一个象征 surface 的数据包装类而已
    // mStartingData 的类型为 SplashScreenStartingData
    mStartingSurface = mStartingData.createStartingSurface(ActivityRecord.this);
    
    if (mStartingSurface != null) {
        ProtoLog.v(WM_DEBUG_STARTING_WINDOW,
                "Added starting %s: startingWindow=%s startingView=%s",
                ActivityRecord.this, mStartingWindow, mStartingSurface);
    } else {
        ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Surface returned was null: %s",
                ActivityRecord.this);
    }
}
java 复制代码
// SplashScreenStartingData.java

class SplashScreenStartingData extends StartingData {
    @Override
    StartingSurface createStartingSurface(ActivityRecord activity) {
        return mService.mStartingSurfaceController.createSplashScreenStartingSurface(
                activity, mTheme);
    }
}
java 复制代码
// StartingSurfaceController.java

StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, int theme) {
    final Task task = activity.getTask();
    final TaskOrganizerController controller =
            mService.mAtmService.mTaskOrganizerController;
            
    // 最终由 TaskOrganizerController 执行添加启动窗口
    if (task != null && controller.addStartingWindow(task, activity, theme,
            null /* taskSnapshot */)) {
        // StartingSurface 只是一个数据包装类,并不是真正的 surface
        return new StartingSurface(task, controller.getTaskOrganizer());
    }
    return null;
}
java 复制代码
// TaskOrganizerController.java

boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
        TaskSnapshot taskSnapshot) {
    final Task rootTask = task.getRootTask();
    if (rootTask == null || activity.mStartingData == null) {
        return false;
    }
    final ITaskOrganizer lastOrganizer = getTaskOrganizer();
    if (lastOrganizer == null) {
        return false;
    }
    
    // 把所有启动窗口的相关数据,都保存到 StartingWindowInfo
    final StartingWindowInfo info = task.getStartingWindowInfo(activity);
    if (launchTheme != 0) {
        info.splashScreenThemeResId = launchTheme;
    }
    info.taskSnapshot = taskSnapshot;
    info.appToken = activity.token;
    
    
    try {
        // 通知 WMShell 添加启动窗口
        lastOrganizer.addStartingWindow(info);
    } catch (RemoteException e) {
        Slog.e(TAG, "Exception sending onTaskStart callback", e);
        return false;
    }
    return true;
}

兜兜转转,最终是把启动窗口数据包装到 StartingWindowInfo,然后发送给 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(() -> {
        // WMShell 添加启动窗口的 trace
        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;
            int color = mStartingSurfaceDrawer
                    .getStartingWindowBackgroundColorForTask(taskId);
            if (color != Color.TRANSPARENT) {
                mTaskBackgroundColors.append(taskId, color);
            }

            if (mTaskLaunchingCallback != null && isSplashScreenType(suggestionType)) {
                mTaskLaunchingCallback.accept(taskId, suggestionType, color);
            }
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    });
}
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);
    }
}
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;
    }
    
    // 构建布局参数,为了通过 WindowManager 添加启动窗口 View
    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);

    // 注意下面一段注释,解释了添加启动窗口 View 的流程
    // 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 是为了缓存 worker thread 中创建的启动窗口 View
    final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
    
    // 创建启动窗口的根 View
    final FrameLayout rootLayout = new FrameLayout(
            mSplashscreenContentDrawer.createViewContextWrapper(context));
    rootLayout.setPadding(0, 0, 0, 0);
    rootLayout.setFitsSystemWindows(false);
    
    // 这个 Runnable 同步地获取 worker thread 中创建的启动窗口 View,并添加到根 View 中
    final Runnable setViewSynchronized = () -> {
        // trace 记录了同步并添加 view 的过程
        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
        
        // waiting for setContentView before relayoutWindow
        // 阻塞式的同步 worker thread 创建的启动窗口 View
        SplashScreenView contentView = viewSupplier.get();
        
        final StartingSurfaceDrawer.StartingWindowRecord sRecord =
                mStartingWindowRecordManager.getRecord(taskId);
        final SplashWindowRecord record = sRecord instanceof SplashWindowRecord
                ? (SplashWindowRecord) sRecord : null;
                

        if (record != null && windowInfo.appToken == record.mAppToken) {
            if (contentView != null) {
                try {
                    // 把启动窗口 View 保存到根 View 中
                    rootLayout.addView(contentView);
                } catch (RuntimeException e) {
                    // ...
                }
            }
            record.setSplashScreenView(contentView);
        }
        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
    };
    
    requestTopUi(true);
    
    // 1. SplashScreenContentDrawer 在 worker thread 中创建启动窗口 View
    // 创建完成之后,会通过第四个参数,把 view 缓存到 ViewSupplier 中
    mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
            viewSupplier::setView, viewSupplier::setUiThreadInitTask);
            
            
    try {
        // 2. 通过 WindowManager 添加启动窗口的根 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.
            // 3. 请求 Vsync 信号,在下一帧的回调中,把 worker thread 中创建的启动窗口 View,
            // 同步到当前线程,并添加到根 View 中
            mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);
            
            // ...
        } else {
            // ...
        }
    } catch (RuntimeException e) {
        // ...
    }
}

简单来说,这里就是为启动窗口创建一个 View,然后通过 WindowManager add 进去。但是,从 AOSP 可以看出,为了性能,采用多线程来,具体流程如下

  1. SplashscreenContentDrawer 在 worker thread 中启动窗口 View。
  2. splash screen thread(当前线程)先通过 WindowManager 添加启动窗口根 View。注意,ViewRootImpl 会向 Choreographer 注册一个类型为 CALLBACK_TRAVERSAL 的回调,用于执行 traversal。 relayout window 就是在 traversal 中执行的。
  3. splash screen thread(当前线程)向 Choreographer 注册一个 CALLBACK_INSETS_ANIMATION 的回调,用于同步 worker thread 中的启动窗口 View,并添加到根 View 中。

当 vsyn 信号到来,会先执行 CALLBACK_INSETS_ANIMATION 类型的回调,再执行 CALLBACK_TRAVERSAL 的回调。因此,先执行第三步,同步启动窗口 View,并添加到根 View 中,然后执行第二步,在 traversal 中,会 relayout window 获取 surface,之后绘制启动窗口。

相关推荐
crazymaple2136 分钟前
Flutter编译运行android问题之JVM版本问题
android·jvm·flutter
新知图书1 小时前
ThinkPHP8视图赋值与渲染
android·ide·android studio
众智创新团队1 小时前
Android的Activity生命周期知识点总结,详情
android·java·开发语言
etcix1 小时前
安卓谷歌地图api一个文件例子(备忘)
android
xvch2 小时前
Kotlin 2.1.0 入门教程(十四)类、构造函数、对象、抽象类
android·kotlin
yzpyzp2 小时前
kotlin-kapt
android·kotlin
B.-2 小时前
Flutter 中的生命周期
android·前端·flutter·ios
xvch3 小时前
Kotlin 2.1.0 入门教程(十六)属性、getter、setter、幕后字段、后备属性、编译时常量、延迟初始化
android·kotlin
闲暇部落3 小时前
kotlin中expect和actual关键字修饰的函数作用
android·开发语言·kotlin
weixin_411191843 小时前
Flutter使用gen_l10n实现多语言支持
android·flutter