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

在 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)这一类"从还不知道最终目标包名开始"的观测

java 复制代码
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)"在统计上看起来像一个事件

java 复制代码
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)
java 复制代码
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(...),因为"开始发起"本身就具有观测价值

java 复制代码
  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 合并)"

java 复制代码
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)
java 复制代码
 // 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 掉

java 复制代码
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(或进程不存在)",它更像"是否需要关注缓存/冷态带来的首帧成本",而不是"是否跨应用"

java 复制代码
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)
java 复制代码
 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),这使得最终统计更接近真实发生的事情

java 复制代码
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)
    调用点见

    java 复制代码
    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)
java 复制代码
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)
java 复制代码
 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

java 复制代码
 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)
java 复制代码
   @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

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

java 复制代码
  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 负责在成功与失败两条路径上收口输出。只要沿着这些里程碑去看"谁在什么时刻调用",就能把系统启动耗时统计的全貌串起来,并且能更快识别哪些字段可以用于业务判断、哪些字段只是为了系统内部做指标筛选。通常手机厂商会使用此类入手来做一些耗时埋点上报。

相关推荐
用户69371750013843 小时前
Android 开发,别只钻技术一亩三分地,也该学点“广度”了
android·前端·后端
唔663 小时前
原生 Android(Kotlin)仅串口「继承架构」完整案例二
android·开发语言·kotlin
一直都在5723 小时前
MySQL索引优化
android·数据库·mysql
代码s贝多芬的音符4 小时前
android mlkit 实现仰卧起坐和俯卧撑识别
android
jwn9995 小时前
Laravel9.x核心特性全解析
android
今天又在写代码6 小时前
数据智能分析平台部署服务器
android·服务器·adb
梦里花开知多少7 小时前
深入谈谈Launcher的启动流程
android·架构
jwn9997 小时前
Laravel11.x新特性全解析
android·开发语言·php·laravel
我就是马云飞7 小时前
停更5年后,我为什么重新开始写技术内容了
android·前端·程序员