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,计算出首帧绘制时长。


流程图如下:


image.png

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


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



本文源码基于android-30

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


image.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()


image.png


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()


image.png

ActivityRecord.updateReportedVisibilityLocked()


//ActivityRecord.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


image.png

几个重要的时间节点
  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()
    }
}


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


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


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


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


  1. ContentProvider初始化时间,时间单位毫秒。 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,并写入日志语句。

image.png

image.png

image.png


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4月前
|
移动开发 监控 前端开发
构建高效Android应用:从优化布局到提升性能
【7月更文挑战第60天】在移动开发领域,一个流畅且响应迅速的应用程序是用户留存的关键。针对Android平台,开发者面临的挑战包括多样化的设备兼容性和性能优化。本文将深入探讨如何通过改进布局设计、内存管理和多线程处理来构建高效的Android应用。我们将剖析布局优化的细节,并讨论最新的Android性能提升策略,以帮助开发者创建更快速、更流畅的用户体验。
67 10
|
5月前
|
Java Android开发
Android面试题经典之Glide取消加载以及线程池优化
Glide通过生命周期管理在`onStop`时暂停请求,`onDestroy`时取消请求,减少资源浪费。在`EngineJob`和`DecodeJob`中使用`cancel`方法标记任务并中断数据获取。当网络请求被取消时,`HttpUrlFetcher`的`cancel`方法设置标志,之后的数据获取会返回`null`,中断加载流程。Glide还使用定制的线程池,如AnimationExecutor、diskCacheExecutor、sourceExecutor和newUnlimitedSourceExecutor,其中某些禁止网络访问,并根据CPU核心数动态调整线程数。
148 2
|
3月前
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
68 20
Android经典面试题之图片Bitmap怎么做优化
|
2月前
|
调度 Android开发 开发者
构建高效Android应用:探究Kotlin多线程优化策略
【10月更文挑战第11天】本文探讨了如何在Kotlin中实现高效的多线程方案,特别是在Android应用开发中。通过介绍Kotlin协程的基础知识、异步数据加载的实际案例,以及合理使用不同调度器的方法,帮助开发者提升应用性能和用户体验。
54 4
|
26天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
3月前
|
Java Android开发 UED
安卓应用开发中的内存管理优化技巧
在安卓开发的广阔天地里,内存管理是一块让开发者既爱又恨的领域。它如同一位严苛的考官,时刻考验着开发者的智慧与耐心。然而,只要我们掌握了正确的优化技巧,就能够驯服这位考官,让我们的应用在性能和用户体验上更上一层楼。本文将带你走进内存管理的迷宫,用通俗易懂的语言解读那些看似复杂的优化策略,让你的开发之路更加顺畅。
64 2
|
3月前
|
Java Android开发 开发者
安卓应用开发中的线程管理优化技巧
【9月更文挑战第10天】在安卓开发的海洋里,线程管理犹如航行的风帆,掌握好它,能让应用乘风破浪,反之则可能遭遇性能的暗礁。本文将通过浅显易懂的语言和生动的比喻,带你探索如何优雅地处理安卓中的线程问题,从基础的线程创建到高级的线程池运用,让你的应用运行更加流畅。
|
4月前
|
Ubuntu Android开发
安卓系统调试与优化:(一)bootchart 的配置和使用
本文介绍了如何在安卓系统中配置和使用bootchart工具来分析系统启动时间,包括安装工具、设备端启用bootchart、PC端解析数据及分析结果的详细步骤。
198 0
安卓系统调试与优化:(一)bootchart 的配置和使用
|
3月前
|
监控 算法 数据可视化
深入解析Android应用开发中的高效内存管理策略在移动应用开发领域,Android平台因其开放性和灵活性备受开发者青睐。然而,随之而来的是内存管理的复杂性,这对开发者提出了更高的要求。高效的内存管理不仅能够提升应用的性能,还能有效避免因内存泄漏导致的应用崩溃。本文将探讨Android应用开发中的内存管理问题,并提供一系列实用的优化策略,帮助开发者打造更稳定、更高效的应用。
在Android开发中,内存管理是一个绕不开的话题。良好的内存管理机制不仅可以提高应用的运行效率,还能有效预防内存泄漏和过度消耗,从而延长电池寿命并提升用户体验。本文从Android内存管理的基本原理出发,详细讨论了几种常见的内存管理技巧,包括内存泄漏的检测与修复、内存分配与回收的优化方法,以及如何通过合理的编程习惯减少内存开销。通过对这些内容的阐述,旨在为Android开发者提供一套系统化的内存优化指南,助力开发出更加流畅稳定的应用。
76 0
|
4月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
62 4