ActivityMetricsLogger 深度剖析:系统如何追踪启动耗时

0 阅读12分钟

在 WindowManager/ActivityTaskManager 体系里,ActivityMetricsLogger承担了一个很明确的角色:它把一次“用户感知的页面启动/切换”拆成多个关键时刻(开始发起、转场开始、启动窗口出现、主窗口绘制完成、应用 fully drawn 上报等),将它们在不同线程、不同模块产生的时间戳收敛到同一个 transition 语义里,然后输出到 MetricsLogger/StatsD/EventLog/Logcat,以及在必要时反馈给 WaitResult

这件事之所以需要一个专门的类,是因为启动链路天然跨域:请求从 ActivityStarter 发起,进程启动/绑定在 ActivityManager/ATMS 侧推进,转场开始由 AppTransition/Transition 控制,最终“画出来”发生在 WindowState/ActivityRecord 的绘制回调点。ActivityMetricsLogger 的价值在于它充当“时间线的粘合层”,让这些分散的信号最终描述同一个 launch。


两个核心对象:LaunchingState 与 TransitionInfo

ActivityMetricsLogger 的内部结构围绕两个对象展开:LaunchingStateTransitionInfo

LaunchingState 是最早创建的“意图发起态”,在 notifyActivityLaunching(...) 中生成。它持有两个时间基准:mStartUptimeNsuptimeNanos)用于计算耗时差值,mStartRealtimeNselapsedRealtimeNanos)用于和 wall-time 关联或做跨线程标记;同时它也负责 trace(Trace.asyncTraceBegin/End)这一类“从还不知道最终目标包名开始”的观测

static final class LaunchingState {   
        final long mStartUptimeNs = SystemClock.uptimeNanos();
    
        final long mStartRealtimeNs = SystemClock.elapsedRealtimeNanos();
        /** Non-null when a {@link TransitionInfo} is created for this state. */
        private TransitionInfo mAssociatedTransitionInfo;
        /** The sequence id for trace. It is used to map the traces before resolving intent. */
        private static int sTraceSeqId;
        /** The trace format is "launchingActivity#$seqId:$state(:$packageName)". */
        String mTraceName;
}

当系统确认“确实启动了某个 Activity,并且可以等待它画出来”之后,TransitionInfo 才会出现。它代表一次正在进行的启动/转场事件,包含启动类型(冷/温/热)、进程状态、转场延迟、starting window 延迟、bindApplication 延迟、windowsDrawn 延迟等,并持有“最后一个被认为代表该转场结果的 Activity”(mLastLaunchedActivity)。这就是后面所有回调(转场开始、starting window drawn、windows drawn)最终要找到并更新的对象

一个容易忽略但非常关键的设计点是:LaunchingState 可以被复用。notifyActivityLaunching(...) 会尝试把新的 launching 事件归并进当前活跃的 transition(通过 caller activity 或 callingUid 匹配),若命中则返回已存在 TransitionInfomLaunchingState,让“连续启动(trampoline)”在统计上看起来像一个事件

private static final class TransitionInfo {
        /**
         * The field to lookup and update an existing transition efficiently between
         * {@link #notifyActivityLaunching} and {@link #notifyActivityLaunched}.
         *
         * @see LaunchingState#mAssociatedTransitionInfo
         */
        final LaunchingState mLaunchingState;

        /** The type can be cold (new process), warm (new activity), or hot (bring to front). */
        int mTransitionType;
        /** Whether the process was already running when the transition started. */
        boolean mProcessRunning;
        /** whether the process of the launching activity didn't have any active activity. */
        final boolean mProcessSwitch;
        /** The process state of the launching activity prior to the launch */
        final int mProcessState;
        /** The oom adj score of the launching activity prior to the launch */
        final int mProcessOomAdj;
        /** Whether the activity is launched above a visible activity in the same task. */
        final boolean mIsInTaskActivityStart;
        /** Whether the last launched activity has reported drawn. */
        boolean mIsDrawn;
        /** The latest activity to have been launched. */
        @NonNull ActivityRecord mLastLaunchedActivity;

        /** The type of the source that triggers the launch event. */
        @SourceInfo.SourceType int mSourceType;
        /** The time from the source event (e.g. touch) to {@link #notifyActivityLaunching}. */
        int mSourceEventDelayMs = INVALID_DELAY;
        /** The time from {@link #notifyActivityLaunching} to {@link #notifyTransitionStarting}. */
        int mCurrentTransitionDelayMs;
        /** The time from {@link #notifyActivityLaunching} to {@link #notifyStartingWindowDrawn}. */
        int mStartingWindowDelayMs = INVALID_DELAY;
        /** The time from {@link #notifyActivityLaunching} to {@link #notifyBindApplication}. */
        int mBindApplicationDelayMs = INVALID_DELAY;
        /** Elapsed time from when we launch an activity to when its windows are drawn. */
        int mWindowsDrawnDelayMs;
        /** The reason why the transition started (see ActivityManagerInternal.APP_TRANSITION_*). */
        int mReason = APP_TRANSITION_TIMEOUT;
        /** The flag ensures that {@link #mStartingWindowDelayMs} is only set once. */
        boolean mLoggedStartingWindowDrawn;
        /** If the any app transitions have been logged as starting. */
        boolean mLoggedTransitionStarting;
        /** Whether any activity belonging to this transition has relaunched. */
        boolean mRelaunched;

        /** Non-null if the application has reported drawn but its window hasn't. */
        @Nullable Runnable mPendingFullyDrawn;
        /** Non-null if the trace is active. */
        @Nullable String mLaunchTraceName;
        /** Whether this transition info is for an activity that is a part of multi-window. */
        int mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_UNSPECIFIED;

一次典型启动的耗时统计流程:从发起到首帧,再到 fully drawn

1)最早的起点:notifyActivityLaunching

启动请求进入 ActivityStarter.execute() 之后,在拿到 caller 与 callingUid 的信息后,系统立刻调用:

  • ActivityStarter.execute()ActivityTaskSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent, caller, callingUid)
class ActivityStarter {
   int execute() {
       ...
            final LaunchingState launchingState;
            synchronized (mService.mGlobalLock) {
                final ActivityRecord caller = ActivityRecord.forTokenLocked(mRequest.resultTo);
                final int callingUid = mRequest.realCallingUid == Request.DEFAULT_REAL_CALLING_UID
                        ?  Binder.getCallingUid() : mRequest.realCallingUid;
                launchingState = mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(
                        mRequest.intent, caller, callingUid);
                callerActivityName = caller != null ? caller.info.name : null;
            }
      ...
   }
}

这一刻通常比 intent resolve、真正创建 ActivityRecord、乃至进程启动更早,因此它被设计为“launch timeline 的锚点”。notifyActivityLaunching 会决定是否要创建一个全新的 LaunchingState,或将其归并到已活跃的 transition。新建时还会触发 LaunchObserver 的 onIntentStarted(...),因为“开始发起”本身就具有观测价值

  LaunchingState notifyActivityLaunching(Intent intent, @Nullable ActivityRecord caller, int callingUid) {
            if (existingInfo == null) {
                final LaunchingState launchingState = new LaunchingState();
                // Only notify the observer for a new launching event.
                launchObserverNotifyIntentStarted(intent, launchingState.mStartUptimeNs);
                return launchingState;
             }
  }
 

从 Recents 启动任务是一条特殊路径,它不走完整 ActivityStarter.executeRequest() 的启动链路,但同样会在移动 task 到前台前调用 notifyActivityLaunching(...),并且明确说明“Recents 总是新 launching state(不与已有 transition 合并)”

final LaunchingState launchingState =
                            mActivityMetricsLogger.notifyActivityLaunching(task.intent,
                                    // Recents always has a new launching state (not combinable).
                                    null /* caller */, isCallerRecents ? INVALID_UID : callingUid);
try {
    mService.moveTaskToFrontLocked(null /* appThread */,
            null /* callingPackage */, task.mTaskId, 0, options);
    // Apply options to prevent pendingOptions be taken when scheduling
    // activity lifecycle transaction to make sure the override pending app
    // transition will be applied immediately.
    if (activityOptions != null
            && activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
        targetActivity.mPendingRemoteAnimation =
                activityOptions.getRemoteAnimationAdapter();
    }
    targetActivity.applyOptionsAnimation();
    if (activityOptions != null && activityOptions.getLaunchCookie() != null) {
        targetActivity.mLaunchCookie = activityOptions.getLaunchCookie();
    }
} finally {
    mActivityMetricsLogger.notifyActivityLaunched(launchingState,
            START_TASK_TO_FRONT, false /* newActivityCreated */,
            targetActivity, activityOptions);
}

2)启动被确认:notifyActivityLaunched 创建或复用 TransitionInfo

ActivityStarter.execute() 在 startActivity 请求执行完成后,会把结果回传给 metrics logger:

  • ActivityStarter.execute()ActivityMetricsLogger.notifyActivityLaunched(launchingState, res, newActivityCreated, launchingRecord, originalOptions)
 // The original options may have additional info about metrics. The mOptions is not
// used here because it may be cleared in setTargetRootTaskIfNeeded.
final ActivityOptions originalOptions = mRequest.activityOptions != null
        ? mRequest.activityOptions.getOriginalOptions() : null;
// Only track the launch time of activity that will be resumed.
if (mDoResume || (isStartResultSuccessful(res)
        && mLastStartActivityRecord.getTask().isVisibleRequested())) {
    launchingRecord = mLastStartActivityRecord;
}
// If the new record is the one that started, a new activity has created.
final boolean newActivityCreated = mStartActivity == launchingRecord;
// Notify ActivityMetricsLogger that the activity has launched.
// ActivityMetricsLogger will then wait for the windows to be drawn and populate
// WaitResult.
mSupervisor.getActivityMetricsLogger().notifyActivityLaunched(launchingState, res,
        newActivityCreated, launchingRecord, originalOptions);
if (mRequest.waitResult != null) {
    mRequest.waitResult.result = res;
    res = waitResultIfNeeded(mRequest.waitResult, mLastStartActivityRecord,
            launchingState);
}

此时 ActivityMetricsLogger 做了两件决定“后续能不能统计”的事情。

第一件事是判断这次启动是否具备统计窗口绘制耗时的条件。如果 Activity 已经可见且 reported drawn,那么“首帧已经发生”或“不可测”,会直接 abort 掉

if (launchedActivity.isReportedDrawn() && launchedActivity.isVisible()) {
    // Launched activity is already visible. We cannot measure windows drawn delay.
    abort(launchingState, "launched activity already visible");
    return;
}

第二件事是判定启动类型和进程状态,并创建 TransitionInfoprocessRunningprocessSwitch 在这里计算:processSwitch 的语义是“目标进程没有 started 状态 Activity(或进程不存在)”,它更像“是否需要关注缓存/冷态带来的首帧成本”,而不是“是否跨应用”

final WindowProcessController processRecord = launchedActivity.app != null
        ? launchedActivity.app
        : mSupervisor.mService.getProcessController(
                launchedActivity.processName, launchedActivity.info.applicationInfo.uid);
// Whether the process that will contains the activity is already running.
final boolean processRunning = processRecord != null;
// We consider this a "process switch" if the process of the activity that gets launched
// didn't have an activity that was in started state. In this case, we assume that lot
// of caches might be purged so the time until it produces the first frame is very
// interesting.
final boolean processSwitch = !processRunning
        || !processRecord.hasStartedActivity(launchedActivity);
final int processState;
final int processOomAdj;
if (processRunning) {
    processState = processRecord.getCurrentProcState();
    processOomAdj = processRecord.getCurrentAdj();
} else {
    processState = PROCESS_STATE_NONEXISTENT;
    processOomAdj = INVALID_ADJ;
}

final TransitionInfo info = launchingSt

满足条件后 TransitionInfo.create(...) 生成对象并加入 mTransitionInfoListmLastTransitionInfo,并启动 launch trace;只有 info.isInterestingToLoggerAndObserver() 返回 true 时(当前实现直接等价于 mProcessSwitch),LaunchObserver 才会收到 onActivityLaunched(...)

3)进程绑定点:notifyBindApplication 记录 bindApplicationDelayMs

如果是冷启动或进程发生了重启,bindApplication 前后是一个很重要的分界。系统在 preBindApplication 阶段通知 metrics logger:

  • ActivityTaskManagerService.LocalService.preBindApplication(...)notifyBindApplication(wpc.mInfo)
 public PreBindInfo preBindApplication(WindowProcessController wpc, ApplicationInfo info) {
    synchronized (mGlobalLockWithoutBoost) {
        mTaskSupervisor.getActivityMetricsLogger().notifyBindApplication(wpc.mInfo);
        wpc.onConfigurationChanged(getGlobalConfiguration());
        // Let the application initialize with consistent configuration as its activity.
        for (int i = mStartingProcessActivities.size() - 1; i >= 0; i--) {
            final ActivityRecord r = mStartingProcessActivities.get(i);
            if (wpc.mUid == r.info.applicationInfo.uid && wpc.mName.equals(r.processName)) {
                wpc.registerActivityConfigurationListener(r);
                break;
            }
        }
        ProtoLog.v(WM_DEBUG_CONFIGURATION, "Binding proc %s with config %s",
                wpc.mName, wpc.getConfiguration());
        // The "info" can be the target of instrumentation.
        return new PreBindInfo(compatibilityInfoForPackageLocked(info),
                new Configuration(wpc.getConfiguration()));
    }
}

notifyBindApplication(ApplicationInfo appInfo) 会在所有活跃 transitions 里用 applicationInfo == appInfo 匹配,填充 mBindApplicationDelayMs;并且对“原本认为是热/温启动,但进程在启动请求后死亡导致实际变为冷启动”的情况做一次纠正(把 mProcessRunning 置为 false,mTransitionType 改为 cold),这使得最终统计更接近真实发生的事情

void notifyBindApplication(ApplicationInfo appInfo) {
    for (int i = mTransitionInfoList.size() - 1; i >= 0; i--) {
        final TransitionInfo info = mTransitionInfoList.get(i);

        // App isn't attached to record yet, so match with info.
        if (info.mLastLaunchedActivity.info.applicationInfo == appInfo) {
            info.mBindApplicationDelayMs = info.calculateCurrentDelay();
            if (info.mProcessRunning) {
                // It was HOT/WARM launch, but the process was died somehow right after the
                // launch request.
                info.mProcessRunning = false;
                info.mTransitionType = TYPE_TRANSITION_COLD_LAUNCH;
                final String msg = "Process " + info.mLastLaunchedActivity.info.processName
                        + " restarted";
                Slog.i(TAG, msg);
                if (info.mLaunchingState.mTraceName != null) {
                    Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, msg + "#"
                            + LaunchingState.sTraceSeqId);
                }
            }
        }
    }
}

4)转场开始:notifyTransitionStarting 记录 currentTransitionDelayMs 与 reason

转场开始并不是由 ActivityStarter 直接告诉 metrics logger,而是由窗口转场控制器在“transition good to go”时刻通知,这一点非常符合用户感知:用户看到动画开始了,才意味着“启动进入展示阶段”。

老的 app transition 路径在 AppTransitionController.handleAppTransitionReady() 结束处调用:

  • AppTransitionControllerActivityMetricsLogger.notifyTransitionStarting(mTempTransitionReasons)
    调用点见
    public class AppTransitionController {
         void handleAppTransitionReady() {
              mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting(
                  mTempTransitionReasons);
         }
    }
    

Shell transitions 路径在 Transition.reportStartReasonsToLogger() 中也会调用同名方法,并且会根据参与者是否仅 ready due to starting-window 来把 reason 标记成 APP_TRANSITION_SPLASH_SCREEN

notifyTransitionStarting(...) 会对每个相关 WindowContainer 找到对应的 active TransitionInfo,填充 mCurrentTransitionDelayMsmReason,并把 mLoggedTransitionStarting 置为 true。如果此时 windows 已经 drawn(mIsDrawn),会立即进入 done;否则继续等待 windows drawn 事件

5)starting window 绘制:notifyStartingWindowDrawn(可选但很有用)

starting window(启动窗口/闪屏窗口)的绘制完成往往比主窗口更早,它能作为“用户最早看到像素”的一个参考点。系统在 WindowState.finishDrawing(...) 遇到 TYPE_APPLICATION_STARTING 且关联到 ActivityRecord 时通知 metrics logger:

  • WindowState.finishDrawing(...)notifyStartingWindowDrawn(mActivityRecord)
boolean finishDrawing(SurfaceControl.Transaction postDrawTransaction, int syncSeqId) {
       if (mOrientationChangeRedrawRequestTime > 0) {
           final long duration =
                   SystemClock.elapsedRealtime() - mOrientationChangeRedrawRequestTime;
           Slog.i(TAG, "finishDrawing of orientation change: " + this + " " + duration + "ms");
           mOrientationChangeRedrawRequestTime = 0;
       } else if (mActivityRecord != null && mActivityRecord.mRelaunchStartTime != 0
               && mActivityRecord.findMainWindow(false /* includeStartingApp */) == this) {
           final long duration =
                   SystemClock.elapsedRealtime() - mActivityRecord.mRelaunchStartTime;
           Slog.i(TAG, "finishDrawing of relaunch: " + this + " " + duration + "ms");
           mActivityRecord.finishOrAbortReplacingWindow();
       }
       if (mActivityRecord != null && mAttrs.type == TYPE_APPLICATION_STARTING) {
           mWmService.mAtmService.mTaskSupervisor.getActivityMetricsLogger()
                   .notifyStartingWindowDrawn(mActivityRecord);
       }
}

notifyStartingWindowDrawn(...) 会写入 mStartingWindowDelayMs,且通过 mLoggedStartingWindowDrawn 确保只记录一次

6)主窗口绘制完成:notifyWindowsDrawn 形成“首帧耗时”的闭环

真正定义“Time To First Frame”的时刻,是 Activity 的 windows drawn。它来自 ActivityRecord.onWindowsDrawn()

  • ActivityRecord.onWindowsDrawn()ActivityMetricsLogger.notifyWindowsDrawn(this)
 private void onWindowsDrawn() {
       final TransitionInfoSnapshot info = mTaskSupervisor
               .getActivityMetricsLogger().notifyWindowsDrawn(this);
       final boolean validInfo = info != null;
       final int windowsDrawnDelayMs = validInfo ? info.windowsDrawnDelayMs : INVALID_DELAY;
       final @WaitResult.LaunchState int launchState =
               validInfo ? info.getLaunchState() : WaitResult.LAUNCH_STATE_UNKNOWN;
       // The activity may have been requested to be invisible (another activity has been launched)
       // so there is no valid info. But if it is the current top activity (e.g. sleeping), the
       // invalid state is still reported to make sure the waiting result is notified.
       if (validInfo || this == getDisplayArea().topRunningActivity()) {
           mTaskSupervisor.reportActivityLaunched(false /* timeout */, this,
                   windowsDrawnDelayMs, launchState);
       }
       finishLaunchTickingLocked();
       if (task != null) {
           setTaskHasBeenVisible();
       }
       // Clear indicated launch root task because there's no trampoline activity to expect after
       // the windows are drawn.
       mLaunchRootTask = null;
   }

notifyWindowsDrawn(...) 里会算出 mWindowsDrawnDelayMs,把 mIsDrawn 标记为 true,并构造 TransitionInfoSnapshot 作为“脱离全局锁、避免 ActivityRecord 被并发修改”的安全快照

是否立刻结束一次 transition 取决于两个条件:要么转场已经 logged starting,要么这个 activity 并不在 openingApps 且不在 collecting transition 中。满足时会进入 done(false, ...) 结束 transition,并触发最终 logging

 TransitionInfoSnapshot notifyWindowsDrawn(@NonNull ActivityRecord r) {
       ...
        final TransitionInfoSnapshot infoSnapshot = new TransitionInfoSnapshot(info);
        if (info.mLoggedTransitionStarting || (!r.mDisplayContent.mOpeningApps.contains(r)
                && !r.mTransitionController.isCollecting(r))) {
            done(false /* abort */, info, "notifyWindowsDrawn", timestampNs);
        }

       ...
        return infoSnapshot;
    }

done(...) 是收尾中枢,它会停止 trace、通知 observer、输出 StatsD/MetricsLogger/EventLog、并把 TransitionInfo 从活跃列表移除。真正写日志与 StatsD 的实现集中在 logAppTransitionFinished(...) / logAppTransition(...),其中 APP_START_OCCURRED atom 会携带包括启动类型、reason、transitionDelay、startingWindowDelay、bindApplicationDelay、windowsDrawnDelay、sourceType/sourceEventDelay 等关键字段

7)fully drawn:notifyFullyDrawn 将“可用态”补齐

首帧并不等于可用。应用可以在 Activity#reportFullyDrawn 时机主动上报“我已经 fully drawn”。系统通过 binder 回到 ActivityClientController.reportActivityFullyDrawn(...),并交给 metrics logger:

  • ActivityClientController.reportActivityFullyDrawn(...)notifyFullyDrawn(r, restoredFromBundle)
   @Override
    public void reportActivityFullyDrawn(IBinder token, boolean restoredFromBundle) {
        final long origId = Binder.clearCallingIdentity();
        try {
            synchronized (mGlobalLock) {
                final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token);
                if (r != null) {
                    mTaskSupervisor.getActivityMetricsLogger().notifyFullyDrawn(r,
                            restoredFromBundle);
                }
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

notifyFullyDrawn(...) 会优先确保 windows drawn 已完成,否则把逻辑挂在 mPendingFullyDrawn 上延后执行,让 fully drawn 更贴近“窗口可交互”的意义。当条件满足时,它会输出 APP_START_FULLY_DRAWN atom 和 APP_TRANSITION_REPORTED_DRAWN 的 MetricsLogger 记录,并通知 LaunchObserver 的 onReportFullyDrawn(...)


为什么它能“抗丢事件”:Visibility/Removal 驱动的 abort

真实世界里,并非每次启动都能顺利等到 windows drawn。Activity 可能被快速覆盖、被移除、或者由于 keyguard/sleep 导致长时间不可见。ActivityMetricsLogger 通过两个入口把“不会再有 window drawn 的 transition”尽早取消,避免统计出离谱的大值:

在 Activity visibility 变化时,ActivityRecord.setVisibility(...) 会调用 notifyVisibilityChanged(this),logger 会在目标变为不可见或 finishing 时安排一次 checkActivityToBeDrawn(...),如果 task 内已经不存在“仍可见且未 drawn 的 activity”,就取消 transition 并记录 APP_START_CANCELED

  */
    void notifyVisibilityChanged(@NonNull ActivityRecord r) {
        final TransitionInfo info = getActiveTransitionInfo(r);
        if (info == null) {
            return;
        }
        if (DEBUG_METRICS) {
            Slog.i(TAG, "notifyVisibilityChanged " + r + " visible=" + r.isVisibleRequested()
                    + " state=" + r.getState() + " finishing=" + r.finishing);
        }
        if (r.isState(ActivityRecord.State.RESUMED) && r.mDisplayContent.isSleeping()) {
            // The activity may be launching while keyguard is locked. The keyguard may be dismissed
            // after the activity finished relayout, so skip the visibility check to avoid aborting
            // the tracking of launch event.
            return;
        }
        if (!r.isVisibleRequested() || r.finishing) {
            // Check if the tracker can be cancelled because the last launched activity may be
            // no longer visible.
            scheduleCheckActivityToBeDrawn(r, 0 /* delay */);
        }
    }

在 Activity 被移除时,ActivityRecord.onRemovedFromDisplay() 会调用 notifyActivityRemoved(this),logger 会清理引用并 abort 掉仍在等待的 transition

  void notifyActivityRemoved(@NonNull ActivityRecord r) {
        mLastTransitionInfo.remove(r);
        final TransitionInfo info = getActiveTransitionInfo(r);
        if (info != null) {
            abort(info, "removed");
        }

        final int packageUid = r.info.applicationInfo.uid;
        final PackageCompatStateInfo compatStateInfo = mPackageUidToCompatStateInfo.get(packageUid);
        if (compatStateInfo == null) {
            return;
        }

        compatStateInfo.mVisibleActivities.remove(r);
        if (compatStateInfo.mLastLoggedActivity == r) {
            compatStateInfo.mLastLoggedActivity = null;
        }
    }

mProcessSwitch 不是“是否跨应用”,复用 TransitionInfo 时更容易误判

TransitionInfo 里有一个非常“好用但危险”的字段:mProcessSwitch。它被定义为 final boolean,在 notifyActivityLaunched 计算后传入构造函数,并且在 TransitionInfo.isInterestingToLoggerAndObserver() 中被直接返回,它的原始语义是“启动目标进程是否处于没有 started activity 的状态”,用于筛选“更值得关注的启动”(通常意味着更多缓存被回收、恢复成本更高)。

问题在于,启动链路支持“连续启动合并”:当发现新启动可以归并进已有 transition 时,logger 会复用同一个 TransitionInfo,仅更新 mLastLaunchedActivitysetLatestLaunchedActivity)并继续沿用之前的统计上下文。此时 mProcessSwitch 不会重新计算,因为它是 final,也没有任何更新逻辑。

“作者发现一个问题,仅使用mProcessSwitch来判断用户页面跳转是否跨应用不准确,因为在复用TransiationInfo时,mProcessSwitch值不会被重新赋值。“

这个案例在实践里很容易出现:例如一次用户操作触发了一个 trampoline 序列,第一跳是“冷/温启动特征明显”的启动(mProcessSwitch=true),随后又在同一转场上下文里跳到另一个页面(可能同应用,也可能跨包,但被 coalesce 进同一个 TransitionInfo)。如果业务侧或埋点侧把 mProcessSwitch 当成“是否跨应用”的判断条件,那么第二跳的语义会被第一跳污染;反过来,如果第一跳是 mProcessSwitch=false 的热启动序列,后续 coalesce 的启动即便确实跨应用或确实发生了进程层面的切换,也可能被误判为“不重要”。

更隐蔽的是,mProcessSwitch 还影响 trace 的结束文案分支:LaunchingState.stopTrace(...) 会根据 mAssociatedTransitionInfo.mProcessSwitch 来决定写 completed-same-process 还是 completed-*-*。当 TransitionInfo 被复用且最后的 mLastLaunchedActivity 已经变化时,trace 上的“语义标签”也可能与用户实际看到的“跨应用/同应用”不一致,给问题分析带来额外噪声。

从代码本身的意图来看,mProcessSwitch 被用作“是否值得记录启动指标”的过滤条件(isInterestingToLoggerAndObserver()),而不是跨应用判断。把它拿来代表“跨应用跳转”属于语义外推;在 transition 复用场景下,这种外推会更快失真。


用一张时序图把调用链收束起来

sequenceDiagram
  participant AS as ActivityStarter
  participant AML as ActivityMetricsLogger
  participant ATMS as ActivityTaskManagerService
  participant ATC as AppTransitionController/Transition
  participant WS as WindowState
  participant AR as ActivityRecord
  participant ACC as ActivityClientController

  AS->>AML: notifyActivityLaunching(intent, caller, callingUid)
  AS->>AML: notifyActivityLaunched(launchingState, result, newCreated, activity, options)
  ATMS->>AML: notifyBindApplication(appInfo)
  ATC->>AML: notifyTransitionStarting(reasons)
  WS->>AML: notifyStartingWindowDrawn(activity) (optional)
  AR->>AML: notifyWindowsDrawn(activity)
  ACC->>AML: notifyFullyDrawn(activity, restoredFromBundle) (optional)

结语

理解 ActivityMetricsLogger 最有效的方法,是把它当成“一个状态机 + 一条时间线”,而不是一堆回调函数。LaunchingState 定义起点,TransitionInfo 承接过程,notifyTransitionStarting/notifyStartingWindowDrawn/notifyWindowsDrawn/notifyFullyDrawn 依次补齐关键里程碑,done/abort 负责在成功与失败两条路径上收口输出。只要沿着这些里程碑去看“谁在什么时刻调用”,就能把系统启动耗时统计的全貌串起来,并且能更快识别哪些字段可以用于业务判断、哪些字段只是为了系统内部做指标筛选。通常手机厂商会使用此类入手来做一些耗时埋点上报。