在应用启动期间,您的应用会给用户留下第一印象。应用启动必须快速加载并显示用户使用您的应用所需的信息。如果您的应用启动时间过长,用户可能会退出您的应用,因为他们等待的时间过长。
我们建议使用 Macrobenchmark 库来衡量启动时间。该库提供了概述和详细的系统跟踪,以便准确了解启动期间发生了什么。
系统跟踪提供了有关设备上发生情况的有用信息,这有助于您了解应用在启动期间执行的操作并识别潜在的优化区域。
要分析您的应用启动,请执行以下操作
- 设置环境 以记录应用启动的跟踪。
- 了解系统跟踪。
- 使用Android Studio Profilers 或Perfetto 浏览跟踪报告。
分析和优化启动的步骤
应用通常需要在启动期间加载对最终用户至关重要的特定资源。非必要资源可以等到启动完成后再加载。
要进行性能权衡,请考虑以下因素
使用 Macrobenchmark 库来衡量每个操作花费的时间,并识别完成时间较长的阻塞。
确认资源密集型操作对于应用启动至关重要。如果操作可以等到应用完全绘制后执行,则可以帮助最大程度地减少启动时的资源限制。
确保您预期此操作在应用启动时运行。通常,不必要的操作可以从遗留代码或第三方库中调用。
如果可能,将长时间运行的操作移到后台。后台进程仍可能影响启动期间的 CPU 使用率。
在完全调查操作后,您可以确定加载时间与将其包含在应用启动中的必要性之间的权衡。请记住,在更改应用的工作流程时,应考虑潜在的回归或重大更改。
优化并重新衡量,直到您对应用的启动时间感到满意为止。有关更多信息,请参阅使用指标检测和诊断问题。
衡量和分析主要操作中花费的时间
当您拥有完整的应用启动跟踪时,请查看跟踪并测量主要操作(如bindApplication
或activityStart
)花费的时间。我们建议使用Perfetto或Android Studio Profilers来分析这些跟踪。
查看应用启动期间的总时间,以识别任何执行以下操作的操作
- 占用大量时间段并且可以进行优化。性能的每一毫秒都很重要。例如,查找
Choreographer
绘制时间、布局膨胀时间、库加载时间、Binder
事务或资源加载时间。对于一般开始,请查看所有花费超过 20 毫秒的操作。 - 阻塞主线程。有关更多信息,请参阅导航 Systrace 报告。
- 无需在启动期间运行。
- 可以等到第一帧绘制完成后再运行。
进一步调查每个跟踪以查找性能差距。
识别主线程上的昂贵操作
最佳实践是将文件 I/O 和网络访问等昂贵操作放在主线程之外。这在应用启动期间同样重要,因为主线程上的昂贵操作会使应用无响应并延迟其他关键操作。StrictMode.ThreadPolicy
可以帮助识别主线程上发生昂贵操作的情况。建议在调试版本中启用StrictMode
,以便尽早识别问题,如下例所示
Kotlin
class MyApplication : Application() { override fun onCreate() { super.onCreate() ... if (BuildConfig.DEBUG) StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build() ) ... } }
Java
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); ... if(BuildConfig.DEBUG) { StrictMode.setThreadPolicy( new StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyDeath() .build() ); } ... } }
使用StrictMode.ThreadPolicy
在所有调试版本上启用线程策略,并在检测到违反线程策略时使应用崩溃,这使得很难错过线程策略违规。
TTID 和 TTFD
要查看应用生成第一帧所需的时间,请测量初始显示时间 (TTID)。但是,此指标不一定反映用户可以开始与您的应用交互之前的时间。完全显示时间 (TTFD) 指标在测量和优化具有完全可用应用状态所需的代码路径方面更有用。
有关在应用 UI 完全绘制时报告策略的信息,请参阅提高启动时间准确性。
针对 TTID 和 TTFD 进行优化,因为两者在其各自领域都很重要。较短的 TTID 可帮助用户看到应用实际上已启动。保持 TTFD 时间短对于确保用户能够快速开始与应用交互非常重要。
分析整体线程状态
选择应用启动时间并查看整体线程切片。主线程需要始终保持响应。
Android Studio Profiler 和Perfetto 等工具提供了主线程的详细概述,以及每个阶段花费的时间。有关可视化 perfetto 跟踪的更多信息,请参阅Perfetto UI 文档。
识别主线程睡眠状态的主要块
如果花费大量时间睡眠,则可能是您的应用主线程等待工作完成的结果。如果您拥有多线程应用,请识别主线程正在等待的线程,并考虑优化这些操作。确保没有不必要的锁争用导致关键路径延迟也很有用。
减少主线程阻塞和不可中断睡眠
查找主线程进入阻塞状态的每个实例。Perfetto 和 Studio Profiler 在线程状态时间线上使用橙色指示符显示此状态。识别操作,探索这些操作是否预期或可以避免,并在必要时进行优化。
与 IO 相关的可中断睡眠可能是一个非常好的改进机会。其他执行 IO 的进程,即使是不相关的应用,也可能与顶级应用正在执行的 IO 发生竞争。
改进启动时间
在识别出优化机会后,探索可能的解决方案以帮助改善启动时间
- 延迟加载内容并异步加载以加快TTID。
- 最大程度地减少调用执行绑定器调用的函数。如果不可避免,请确保通过缓存值而不是重复调用或将非阻塞工作移至后台线程来优化这些调用。
- 为了使您的应用启动看起来更快,您可以尽快向用户显示需要最少渲染的内容,直到屏幕的其余部分加载完成。
- 创建并添加启动配置文件 到您的应用。
- 使用 Jetpack App Startup 库 简化应用启动期间组件的初始化。
分析 UI 性能
应用启动包括启动画面和主页的加载时间。为了优化应用启动,请检查跟踪以了解 UI 绘制所需的时间。
限制初始化工作
某些帧可能比其他帧花费更多时间加载。这些被认为是应用的昂贵绘制。
要优化初始化,请执行以下操作
- 优先考虑缓慢的布局传递并选择这些内容进行改进。
- 通过添加自定义跟踪事件来调查 Perfetto 的每个警告和 Systrace 的每个警报,以减少昂贵的绘制和延迟。
测量帧数据
有多种方法可以测量帧数据。五种主要的收集方法是
- 使用
dumpsys gfxinfo
进行本地收集:并非在 dumpsys 数据中观察到的所有帧都与应用的缓慢渲染有关或对最终用户有任何影响。但是,这是在不同发布周期中查看以了解性能总体趋势的一个好指标。要了解有关使用gfxinfo
和framestats
将 UI 性能测量集成到测试实践中的更多信息,请参阅Android 应用测试基础知识。 - 使用JankStats进行现场收集:使用JankStats 库从应用的特定部分收集帧渲染时间,并记录和分析数据。
- 在使用 Macrobenchmark(底层为 Perfetto)的测试中
- Perfetto FrameTimeline:在 Android 12(API 级别 31)上,您可以从 Perfetto 跟踪中收集帧时间线指标,以了解哪些工作导致帧下降。这可能是诊断帧下降原因的第一步。
- Android Studio Profiler 用于卡顿检测
检查主活动加载时间
您的应用的主活动可能包含大量从多个来源加载的信息。检查主页Activity
布局,并特别查看主页活动的Choreographer.onDraw
方法。
- 使用
reportFullyDrawn
向系统报告您的应用现已完全绘制,以进行优化。 - 使用 Macrobenchmark 库中的
StartupTimingMetric
测量活动和应用启动。 - 查看帧下降。
- 识别渲染或测量时间过长的布局。
- 识别加载时间过长的资产。
- 识别在启动期间膨胀的不必要的布局。
考虑以下可能的解决方案来优化主活动加载时间
- 使您的初始布局尽可能简单。有关更多信息,请参阅优化布局层次结构。
- 添加自定义跟踪点以提供有关帧下降和复杂布局的更多信息。
- 最大程度地减少在启动期间加载的位图资源的数量和大小。
在布局并非立即
VISIBLE
的情况下使用ViewStub
。ViewStub
是一个不可见的、零大小的 View,可用于在运行时延迟膨胀布局资源。有关更多信息,请参阅ViewStub
。如果您使用的是Jetpack Compose,则可以使用状态来延迟加载某些组件,从而获得与
ViewStub
类似的行为var shouldLoad by remember {mutableStateOf(false)} if (shouldLoad) { MyComposable() }
通过修改
shouldLoad
在条件块内加载可组合项LaunchedEffect(Unit) { shouldLoad = true }
这会触发一个重新组合,其中包括第一个代码段中条件块内的代码。
推荐内容
- 注意:当 JavaScript 关闭时显示链接文本
- 捕获 Macrobenchmark 指标
- 测量应用性能概述 * 冻结帧