Android启动优化之精确测量启动各个阶段的耗时

news2024/9/9 7:22:58

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()
  4. 启动Activity(执行 onCreate、onStart、onResume等方法)
  5. ViewRootImpl执行doFrame()绘制View,计算出首帧绘制时长。
    流程图如下:
    在这里插入图片描述
    我们可以看出:阶段1和2都是由系统控制的。App开发者对这两个阶段的耗时能做的优化甚微。

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


    本文源码基于android-30
    我们在cs.android.com源码阅读网站上全局搜索
    在这里插入图片描述
    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()
    在这里插入图片描述
    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);
}

notifyWindowsDraw方法正是通过查找mTransitionInfoList中对应的TransitionInfo获取到Activity的启动开始时间。
启动完成调用堆栈如下
• ActivityRecord.onFirstWindowDrawn()
• ➡️ActivityRecord.updateReportedVisibilityLocked()
• ➡️ActivityRecord.onWindowsDrawn()
• ➡️ActivityMetricsLogger.notifyWindowsDrawn()
在这里插入图片描述
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
    在这里插入图片描述
    几个重要的时间节点
  5. ActivityManagerService接收到startActivity信号时间,等价于launchingState.mCurrentTransitionStartTimeNs。时间单位纳秒。
  6. 进程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());
	  ...
}

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

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

5.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()花费时间,时间单位毫秒。很简单方法开始和结束时间差。
  2. 首帧绘制时间,比较复杂,使用到了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,并写入日志语句。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1918907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

水库大坝安全监测险情主要内容

水库常见险情主要包括洪水漫顶、脱坡滑坡、坝体裂缝、 散浸、渗漏、漏洞、陷坑、管涌等,此外风浪冲击、水流冲刷等也会加剧险情的扩大。大坝险情万一抢护不及时,易导致发 生溃坝事故,造成极为严重的灾难性后果。要做到及时有效地 抢护大坝险情…

智慧金融-数据可视化

智慧金融-数据可视化 导入所需的库 import numpy as np import numpy_financial as npf import matplotlib.pyplot as plt from pylab import mpl mpl.rcParams[font.sans-serif][FangSong] mpl.rcParams[axes.unicode_minus]False单图曲线图 r 0.05 # 贷款的年利率 n 30…

28.IP核理论知识(Xilinx)

(1)ip核是什么? IP(Intellectual Property)即知识产权,在半导体产业中,将IP核定义为“用于ASIC或FPGA中的预先设计好的电路功能模块”,简而言之,这里的IP即电路功能模块。…

使用 `useAppConfig` :轻松管理应用配置

title: 使用 useAppConfig :轻松管理应用配置 date: 2024/7/11 updated: 2024/7/11 author: cmdragon excerpt: 摘要:本文介绍了Nuxt开发中useAppConfig的使用,它便于访问和管理应用配置,支持动态加载资源、环境配置切换、权限…

MacOS 开发 — Packages 程序 macOS新版本 演示选项卡无法显示

MacOS 开发 — Packages 程序 macOS新版本 演示选项卡无法显示 问题描述 : 之前写过 Packages 的使用以及如何打包macOS程序。最近更新了新的macOS系统,发现Packages的演示选项卡无法显示,我尝试从新安转了Packages 也是没作用,…

医院人员管理系统03_上午:JDBC连接数据库,完成简单的增删改查

文章目录 jdbc连接数据库什么是jdbc完成jdbc的步骤导入jar包写三个类DBConn.java加载驱动类:找到对应的然后写上获取连接关闭连接代码解释 最后写一个main方法调用测试一下运行结果 Students.javaStudentDao.java 运行结果![](https://i-blog.csdnimg.cn/direct/ba2…

bC一体化:推拉联动双向引擎促增长

在数字化浪潮的推动下,快消品行业正面临着前所未有的变革。在过去的几十年里,快速消费品的渠道分销模式发展经历了几个重要的阶段。传统渠道分销模式,从多级分销商的“批发制胜”到“深度分销”的人海战术,在过去的几十年内虽各有…

工业智能网关的功能特点及应用

工业智能网关的功能特点及应用 工业智能网关作为工业互联网架构中的关键组件,扮演着数据桥梁与边缘计算核心的角色,它不仅促进了物理设备与云端平台之间的高效通信,还通过集成的高级功能推动了工业4.0时代的智能化转型。以下是对其功能特点及…

【北京迅为】《i.MX8MM嵌入式Linux开发指南》-第一篇 嵌入式Linux入门篇-第二十五章 Source Insight 的安装和使用

i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT…

智慧景区建设方案PPT(54页)

智慧景区建设方案摘要 背景简述智慧景区建设旨在通过信息技术改进和创新传统旅游景区,整合旅游资源和产业链,实现精准网络营销,提升旅游品牌价值,改善信息共享和业务协同,提高旅游产业链效率。基础设施包括高速光纤网络…

FunAudioLLM SenseVoice语音转录(ASR)与CosyVoice语音合成(TTS)及语音克隆使用案例;webui可视化页面操作使用

参考: https://fun-audio-llm.github.io/ 1、SenseVoice语音转录 在线体验:https://modelscope.cn/studios/iic/CosyVoice-300M 参考:https://github.com/FunAudioLLM/SenseVoice 下载: pip install -U funasr使用: from funasr import AutoModelmodel_dir = "…

Nature Climate Change | 气候暖化使土壤碳库易受微生物快速降解影响

本文首发于“生态学者”微信公众号! 整个北半球土壤有机碳总量的一半富集在北极地区,其原因是气温较低导致微生物对永久冻土带土壤有机碳的分解缓慢,有利于有机碳的积累。但由于人类活动的影响,近几十年来北极出现了明显的升温&am…

第三方软件测试报告内容详解,如何获取专业软件测试报告?

在现代社会中,软件已经渗透到了各个行业的方方面面,而软件的质量也成为了企业成功的重要因素之一。然而,在软件开发过程中,由于人力、物力、时间等各种因素的限制,难免会出现一些问题和bug,而这些问题又可能…

【Linux系统】信号量(初次理解)

五个概念 多个执行流(进程),能看到的一份资源:共享资源被保护起来的资源可以叫临界资源(同步和互斥) --- 用互斥的方式保护共享资源就叫临界资源互斥:任何时刻只能有一个进程在访问共享资源资源…

【待补充】【来可USB-CAN】个人使用总结

文章目录 EVN简介10. 环境搭建20. 环境测试 EVN windows7 LIKE USB-CAN简介 官网介绍、资料下载 https://www.njlike.com/product/CAN-interfaces/USBCAN/01121.html 10. 环境搭建 USB-CAN 驱动 下载地址:https://www.njlike.com/uploads/soft/190625/1_1425474881…

Redis实践经验

优雅的Key结构 Key实践约定: 遵循基本格式:[业务名称]:[数据名]:id例:login:user:10长度步超过44字节(版本不同,上限不同)不包含特殊字符 优点: 可读性强避免key冲突方便管理节省内存&#x…

Pandas基础03:数据排序与增删

上一节我们介绍了通过按行索引和按列索引找出相关数据的方法。本章节将进一步介绍如何筛选数据,并对数据进行排序、增删的方法。 示例表格和上一节相同。 1.数据筛选 Python中可以通过区域筛选,即获取某几行某几列的方法得到数据。例如,我要…

2024年了还在学pytestday1

1、按照博主的说法,提出疑问:应该在电脑本地终端安装还是在pythoncharm终端安装? ------在pythoncharm终端安装就行 避免老是忘记,还是记下来比较好。 2、在公司安装不成功,换豆瓣源也不行,连接手机热点尝…

MYSQL审批流程判断同一层级审批人是否全部通过审批

在做流程审批的时候,通常会出现某一层有多个审批人的情况,这个时候需要所有人都通过才会进入到下一步 数据结构如下图表格所示 每一个审批申请对应一个apply_id serial_no相同的代表是同一层级审批人 approval_status是审核状态 下面我们可以用一个SQL来…

Day50:单调栈 LeedCode 739. 每日温度 496.下一个更大元素 I 503. 下一个更大元素 II

739. 每日温度 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。 示例 1: 输…