在 WindowManager/ActivityTaskManager 体系里,ActivityMetricsLogger承担了一个很明确的角色:它把一次“用户感知的页面启动/切换”拆成多个关键时刻(开始发起、转场开始、启动窗口出现、主窗口绘制完成、应用 fully drawn 上报等),将它们在不同线程、不同模块产生的时间戳收敛到同一个 transition 语义里,然后输出到 MetricsLogger/StatsD/EventLog/Logcat,以及在必要时反馈给 WaitResult。
这件事之所以需要一个专门的类,是因为启动链路天然跨域:请求从 ActivityStarter 发起,进程启动/绑定在 ActivityManager/ATMS 侧推进,转场开始由 AppTransition/Transition 控制,最终“画出来”发生在 WindowState/ActivityRecord 的绘制回调点。ActivityMetricsLogger 的价值在于它充当“时间线的粘合层”,让这些分散的信号最终描述同一个 launch。
两个核心对象:LaunchingState 与 TransitionInfo
ActivityMetricsLogger 的内部结构围绕两个对象展开:LaunchingState 和 TransitionInfo。
LaunchingState 是最早创建的“意图发起态”,在 notifyActivityLaunching(...) 中生成。它持有两个时间基准:mStartUptimeNs(uptimeNanos)用于计算耗时差值,mStartRealtimeNs(elapsedRealtimeNanos)用于和 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 匹配),若命中则返回已存在 TransitionInfo 的 mLaunchingState,让“连续启动(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;
}
第二件事是判定启动类型和进程状态,并创建 TransitionInfo。processRunning 和 processSwitch 在这里计算: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(...) 生成对象并加入 mTransitionInfoList 与 mLastTransitionInfo,并启动 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() 结束处调用:
AppTransitionController→ActivityMetricsLogger.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,填充 mCurrentTransitionDelayMs、mReason,并把 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,仅更新 mLastLaunchedActivity(setLatestLaunchedActivity)并继续沿用之前的统计上下文。此时 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 负责在成功与失败两条路径上收口输出。只要沿着这些里程碑去看“谁在什么时刻调用”,就能把系统启动耗时统计的全貌串起来,并且能更快识别哪些字段可以用于业务判断、哪些字段只是为了系统内部做指标筛选。通常手机厂商会使用此类入手来做一些耗时埋点上报。