Android系统是如何计算应用启动耗时的?能否更精准定位性能瓶颈呢?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Android系统是如何计算应用启动耗时的?能否更精准定位性能瓶颈呢?

1. 直观地观察应用启动时长


我们可以通过观察logcat日志查看Android应用启动耗时,过滤关键字"Displayed"

ActivityTaskManager: Displayed com.peter.viewgrouptutorial/.activity.DashboardActivity: +797ms

启动时长(在这个例子中797ms)表示从启动App到系统认为App启动完成所花费的时间。

2. 启动时间包含哪几个阶段


从用户点击桌面图标,到Activity启动并将界面第一帧绘制出来大概会经过以下几个阶段。


  1. system_server展示starting window
  2. Zygote fork Android 进程
  3. ActivityThread handleBindApplication(这个阶段又细分为)
  • 加载程序代码和资源
  • 初始化ContentProvider
  • 执行Application.onCreate()
  1. 启动Activity(执行 onCreate、onStart、onResume等方法)
  2. ViewRootImpl执行doFrame()绘制View,计算出首帧绘制时长。


流程图如下:

640.png


我们可以看出:阶段1和2都是由系统控制的。App开发者对这两个阶段的耗时能做的优化甚微。


3. 系统是如何测量启动时长的?


本文源码基于android-30


我们在cs.android.com源码阅读网站上全局搜索

640.png

  1. 「在ActivityMetricsLogger.logAppDisplayed()方法中发现了打印日志语句」
private void logAppDisplayed(
  TransitionInfoSnapshot info
) {
    if (info.type != TYPE_TRANSITION_WARM_LAUNCH && info.type != TYPE_TRANSITION_COLD_LAUNCH) {
        return;
    }
    EventLog.writeEvent(WM_ACTIVITY_LAUNCH_TIME,
            info.userId, info.activityRecordIdHashCode, info.launchedActivityShortComponentName,
            info.windowsDrawnDelayMs);
    StringBuilder sb = mStringBuilder;
    sb.setLength(0);
    sb.append("Displayed ");
    sb.append(info.launchedActivityShortComponentName);
    sb.append(": ");
    TimeUtils.formatDuration(info.windowsDrawnDelayMs, sb);
    Log.i(TAG, sb.toString());
}
  1. TransitionInfoSnapshot.windowsDrawnDelayMs是启动的时长。它在以下方法中被赋值:」
  • ActivityMetricsLogger.notifyWindowsDrawn()
  • ➡️ TransitionInfo.calculateDelay()
//ActivityMetricsLogger.java
TransitionInfoSnapshot notifyWindowsDrawn(
  ActivityRecord r, 
  long timestampNs
) {  
  TransitionInfo info = getActiveTransitionInfo(r);
  info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
  return new TransitionInfoSnapshot(info);
}
private static final class TransitionInfo {
 int calculateDelay(long timestampNs) {
   long delayNanos = timestampNs - mTransitionStartTimeNs;
   return (int) TimeUnit.NANOSECONDS.toMillis(delayNanos);
 }
}


  1. timestampNs表示启动结束时间,mTransitionStartTimeNs表示启动开始时间。它们分别是在哪赋值的呢?」

mTransitionStartTimeNs启动开始时间在notifyActivityLaunching方法中被赋值。调用堆栈如下:


  • ActivityManagerService.startActivity()
  • ➡️ActivityManagerService.startActivityAsUser()
  • ➡️ActivityStarter.execute()
  • ➡️ActivityMetricsLogger.notifyActivityLaunching()

640.jpg

ActivityMetricsLogger.notifyActivityLaunching(...)
//ActivityMetricsLogger.java
private LaunchingState notifyActivityLaunching(
  Intent intent,
  ActivityRecord caller,
  int callingUid
) {
  ...
  long transitionStartNs = SystemClock.elapsedRealtimeNanos();
  LaunchingState launchingState = new LaunchingState();
  launchingState.mCurrentTransitionStartTimeNs = transitionStartNs;
  ...
  return launchingState; 
}

启动时间记录到LaunchingState.mCurrentTransitionStartTimeNs


ActivityStarter.execute()


//ActivityStarter.java
int execute() {
    try {
        final LaunchingState launchingState;
        synchronized (mService.mGlobalLock) {
            final ActivityRecord caller = ActivityRecord.forTokenLocked(mRequest.resultTo);
            launchingState = mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(
                    mRequest.intent, caller);
        }
        if (mRequest.activityInfo == null) {
            mRequest.resolveActivity(mSupervisor);
        }
        int res;
        synchronized (mService.mGlobalLock) {
            mSupervisor.getActivityMetricsLogger().notifyActivityLaunched(launchingState, res,
                    mLastStartActivityRecord);
            return getExternalResult(mRequest.waitResult == null ? res
                    : waitForResult(res, mLastStartActivityRecord));
        }
    } finally {
        onExecutionComplete();
    }
}


该方法作用如下:


  1. 调用ActivityMetricsLogger().notifyActivityLaunching()生成LaunchingState。将启动时间记录其中
  2. 执行StartActivity逻辑
  3. 调用ActivityMetricsLogger().notifyActivityLaunched()把launchingState和ActivityRecord映射保存起来


ActivityMetricsLogger.notifyActivityLaunched(...)
//ActivityMetricsLogger.java
void notifyActivityLaunched(
   LaunchingState launchingState,
   int resultCode,
   ActivityRecord launchedActivity) {
    ...
    final TransitionInfo newInfo = TransitionInfo.create(launchedActivity, launchingState,
            processRunning, processSwitch, resultCode);
    if (newInfo == null) {
        abort(info, "unrecognized launch");
        return;
    }
    if (DEBUG_METRICS) Slog.i(TAG, "notifyActivityLaunched successful");
    // A new launch sequence has begun. Start tracking it.
    mTransitionInfoList.add(newInfo);
    mLastTransitionInfo.put(launchedActivity, newInfo);
    startLaunchTrace(newInfo);
    if (newInfo.isInterestingToLoggerAndObserver()) {
        launchObserverNotifyActivityLaunched(newInfo);
    } else {
        // As abort for no process switch.
        launchObserverNotifyIntentFailed();
    }
}


该方法将根据LaunchingState和ActivityRecord生成TransitionInfo保存到mTransitionInfoList中。这样就将启动开始时间保存起来了。


ActivityMetricsLogger.notifyWindowsDrawn(...)
//ActivityMetricsLogger.java
TransitionInfoSnapshot notifyWindowsDrawn(
  ActivityRecord r, 
  long timestampNs
) {  
  TransitionInfo info = getActiveTransitionInfo(r);
  info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
  return new TransitionInfoSnapshot(info);
}


//ActivityMetricsLogger.java
private TransitionInfo getActiveTransitionInfo(WindowContainer wc) {
    for (int i = mTransitionInfoList.size() - 1; i >= 0; i--) {
        final TransitionInfo info = mTransitionInfoList.get(i);
        if (info.contains(wc)) {
            return info;
        }
    }
    return null;
}

notifyWindowsDraw方法正是通过查找mTransitionInfoList中对应的TransitionInfo获取到Activity的启动开始时间。


「启动完成调用堆栈如下」


  • ActivityRecord.onFirstWindowDrawn()
  • ➡️ActivityRecord.updateReportedVisibilityLocked()
  • ➡️ActivityRecord.onWindowsDrawn()
  • ➡️ActivityMetricsLogger.notifyWindowsDrawn()

640.jpg


ActivityRecord.updateReportedVisibilityLocked()
tyRecord.java
void updateReportedVisibilityLocked() {
 ...
    boolean nowDrawn = numInteresting > 0 && numDrawn >= numInteresting;
    boolean nowVisible = numInteresting > 0 && numVisible >= numInteresting && isVisible();
    if (nowDrawn != reportedDrawn) {
        onWindowsDrawn(nowDrawn, SystemClock.elapsedRealtimeNanos());
        reportedDrawn = nowDrawn;
    }
 ...
}
void onWindowsDrawn(boolean drawn, long timestampNs) {
    mDrawn = drawn;
    if (!drawn) {
        return;
    }
    final TransitionInfoSnapshot info = mStackSupervisor
            .getActivityMetricsLogger().notifyWindowsDrawn(this, timestampNs);
    ...
}

我们看到在updateReportedVisibilityLocked()方法中把SystemClock.elapsedRealtimeNanos()传递给onWindowsDrawn(nowDrawn, SystemClock.elapsedRealtimeNanos())

4. 调试技巧


通过断点调试记录应用冷启动记录耗时调用栈


  1. 准备一台root的手机(或者非Google Play版本模拟器)
  2. compileSdkVersion、targetSdkVersion与模拟器版本一致(本文30)
  3. notifyActivityLaunching和notifyWindowsDrawn中增加断点
  4. 调试勾选Show all processes选择system_process

640.jpg


几个重要的时间节点


  1. ActivityManagerService接收到startActivity信号时间,等价于launchingState.mCurrentTransitionStartTimeNs。时间单位纳秒。
  2. 进程Fork的时间,时间单位毫秒。可以通过以下方式获取


object Processes {
    @JvmStatic
    fun readProcessForkRealtimeMillis(): Long {
        val myPid = android.os.Process.myPid()
        val ticksAtProcessStart = readProcessStartTicks(myPid)
        // Min API 21, use reflection before API 21.
        // See https://stackoverflow.com/a/42195623/703646
        val ticksPerSecond = Os.sysconf(OsConstants._SC_CLK_TCK)
        return ticksAtProcessStart * 1000 / ticksPerSecond
    }
    // Benchmarked (with Jetpack Benchmark) on Pixel 3 running
    // Android 10. Median time: 0.13ms
    fun readProcessStartTicks(pid: Int): Long {
        val path = "/proc/$pid/stat"
        val stat = BufferedReader(FileReader(path)).use { reader ->
            reader.readLine()
        }
        val fields = stat.substringAfter(") ")
            .split(' ')
        return fields[19].toLong()
    }
}

ActivityThread.handleBindApplication时设置的进程启动时间,单位毫秒。Process.getStartElapsedRealtime()

//ActivityThread.java
private void handleBindApplication(AppBindData data) {
    ...
    // Note when this process has started.
    Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());
   ...
}


程序代码和资源加载的时间,时间单位毫秒。Application类初始化时的时间handleBindApplication的时间差

class MyApp extends Application {
    static {
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            long loadApkAndResourceDuration = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime();
          }
    }
}

ontentProvider初始化时间,时间单位毫秒。 Application.onCreate()Application.attachBaseContext(Context context) 之间的时间差

 class MyApp extends Application {
   long mAttachBaseContextTime = 0L;
   long mContentProviderDuration = 0L;
   @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        mAttachBaseContextTime = SystemClock.elapsedRealtime();
    }
    @Override
    public void onCreate() {
        super.onCreate();
        mContentProviderDuration = SystemClock.elapsedRealtime() - mAttachBaseContextTime;
    }
}


  1. Application.onCreate()花费时间,时间单位毫秒。很简单方法开始和结束时间差。


  1. 首帧绘制时间,比较复杂,使用到了com.squareup.curtains:curtains:1.0.1代码如下,firstDrawTime就是首帧的绘制时间。从ActivityThread.handleBindApplication()到首帧绘制所花费的时间:


class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                Window window = activity.getWindow();
                WindowsKt.onNextDraw(window, () -> {
                    if (firstDraw) return null;
                    firstDraw = true;
                    handler.postAtFrontOfQueue(() -> {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            long firstDrawTime =  (SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime()));
                        }
                    });
                    return null;
                });
            }
        }
    }
}


调试launchingState.mCurrentTransitionStartTimeNs


由于ActivityMetricsLogger是运行在system_process进程中。我们无法在应用进程中获取到transitionStartTimeNs,我们可以用过Debug打印日志。我们需要将断点设置成non-suspending。如图将Suspend反勾选。选中Evaluate and log,并写入日志语句。

640.jpg

640.jpg


日志输出如下:


2021-08-08 12:55:36.295 537-579/system_process D/AppStart: 19113098274557 Intent received

5. 总结


本文主要介绍了Android系统是如何测量应用启动时间以及应用开发者如何测量应用启动各个阶段的启动耗时。有了这些我们能够很好的定位启动过程中的耗时以及性能瓶颈。如果你在应用启动优化有比较好的实践成果欢迎留言讨论哟

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
158 4
|
1月前
|
人工智能 搜索推荐 物联网
Android系统版本演进与未来展望####
本文深入探讨了Android操作系统从诞生至今的发展历程,详细阐述了其关键版本迭代带来的创新特性、用户体验提升及对全球移动生态系统的影响。通过对Android历史版本的回顾与分析,本文旨在揭示其成功背后的驱动力,并展望未来Android可能的发展趋势与面临的挑战,为读者呈现一个既全面又具深度的技术视角。 ####
|
1月前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
20天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
45 14
|
23天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。
|
21天前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
21天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
22天前
|
存储 安全 Android开发
探索Android系统的最新安全特性
在数字时代,智能手机已成为我们生活中不可或缺的一部分。随着技术的不断进步,手机操作系统的安全性也越来越受到重视。本文将深入探讨Android系统最新的安全特性,包括其设计理念、实施方式以及对用户的影响。通过分析这些安全措施如何保护用户免受恶意软件和网络攻击的威胁,我们希望为读者提供对Android安全性的全面了解。
|
28天前
|
安全 Android开发 iOS开发
深入探讨Android与iOS系统的差异及未来发展趋势
本文旨在深入分析Android和iOS两大移动操作系统的核心技术差异、用户体验以及各自的市场表现,进一步探讨它们在未来技术革新中可能的发展方向。通过对比两者的开放性、安全性、生态系统等方面,本文揭示了两大系统在移动设备市场中的竞争态势和潜在变革。
|
21天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
27 0