Android APP启动以及性能优化

本文详细介绍了Android应用的启动类型,包括冷启动、温启动和热启动,强调了冷启动优化的重要性。文章分析了应用的启动过程,提出预览窗口优化、业务梳理、多进程优化、线程和GC优化等方法。并讨论了启动优化工具,如adb命令、代码埋点、Nanoscope和Systrace的优缺点。最后,文章总结了启动优化的注意事项,包括主线程工作负载、按需加载、线程管理和对象创建等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一. 启动优化的目的

APP启动如果得到很好的优化,增强用户体验增加用户流量;如果app启动时间过长影响用户体验,从而会造成流失用户。所以做启动优化是有必须的。

二. 应用启动类型
1. 冷启动
  • 概念:当应用在设备开机或者系统主动 kill APP 进程之后,用户点击桌面icon图标启动,称之为冷启动。
  • 场景:开机后第一次启动应用 或者 应用被杀死后再次启动
  • 生命周期:Process.start->Application创建->attachBaseContext->onCreate->onStart->onResume->Activity生命周期
  • 启动速度:在几种启动类型中最慢,也是我们优化启动速度最大的拦路虎
2. 温启动
  • 概念:温启动是APP进程还存活,因为内存不足Activity被回收了,当再次启动APP时就会重新执行Activity生命周期,布局绘制等操作。
  • 场景:应用已经启动,返回键退出
  • 生命周期:onCreate->onStart->onResume->Activity生命周期
  • 启动速度:较快
3. 热启动
  • 概念:当后台存在该应用的进程或者服务时,用户点击icon图标启动,称之为热启动。一般是用户按了home键回到桌面,或者返回键没有杀进程,或者app本身做了进程重启的机制。
  • 场景:Home键最小化应用
  • 生命周期:onResume->Activity生命周期
  • 启动速度:快

从上面的总结可以看出,在应用的启动过程中,APP启动所需的时间为:冷启动时间>温启动时间>热启动时间,冷启动是最慢最耗时的,系统以及应用本身都有大量的工作需要处理,所以,冷启动对于应用的启动速度是最具挑战以及最有必要进行优化的。

三. 应用的启动过程

冷启动启动流程——当点击app的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类(attachBaseContext()–>OnCreate())、创建MainActivity类、加载主题样式Theme中的 windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了,最后才进行contentView的measure/layout/draw显示在界面上,所以直到这里,应用的第一次启动才算完成,这时候看到的界面也就是首帧。
在这里插入图片描述

大致流程如下:

  • 点击桌面图标,Launcher会启动程序默认的Acticity,之后再按照程序的逻辑启动各种Activity。
  • 启动Activity通过应用程序框架层的ActivityManagerService服务启动(Service也是由ActivityManagerService来启动的)。ActivityManagerService是一个非常重要的接口,它负责启动和管理Activity和Service。
  • 无论是通过Launcher来启动Activity,还是通过Activity内部调用startActivity接口来启动新的Activity,都通过Binder进程间通信进入到ActivityManagerService进程中,并且调用ActivityManagerService.startActivity接口。
  • ActivityManagerService调用ActivityStack.startActivityMayWait来做准备要启动的Activity的相关信息。
  • ActivityStack通知ApplicationThread要进行Activity启动调度了,这里的ApplicationThread代表的是调用ActivityManagerService.startActivity接口的进程,对于通过点击应用程序图标的情景来说,这个进程就是Launcher了,而对于通过在Activity内部调用startActivity的情景来说,这个进程就是这个Activity所在的进程了。
  • ApplicationThread不执行真正的启动操作,它通过调用ActivityManagerService.activityPaused接口进入到ActivityManagerService进程中,看看是否需要创建新的进程来启动Activity。
  • 对于通过点击应用程序图标来启动Activity的情景来说,ActivityManagerService在这一步中,会调用startProcessLocked来创建一个新的进程,而对于通过在Activity内部调用startActivity来启动新的Activity来说,这一步是不需要执行的,因为新的Activity就在原来的Activity所在的进程中进行启动。
  • ActivityManagerServic调用ApplicationThread.scheduleLaunchActivity接口,通知相应的进程执行启动Activity的操作。
  • ApplicationThread把这个启动Activity的操作转发给ActivityThreadActivityThread通过ClassLoader导入相应的Activity类,然后把它启动起来。

所以相应的优化方案:

  • 减少 onCreate方法的工作量。
  • 不要让Application参与业务逻辑。
  • 不要在Application中做耗时操作,一些初始化操作可以开启子线程来完成。
  • 不要以静态变量方式在Application中保存数据。
  • 布局优化/mainThread尽量延迟初始化。
  • 启动画面的初始化可以使用设置主题背景的方式,速度回更快。
四. 冷启动流程

冷启动指的是应用程序从进程在系统不存在,到系统创建应用运行进程空间的过程。冷启动通常会发生在一下两种情况:

  • 设备启动以来首次启动应用程序
  • 系统杀死应用程序之后再次启动应用程序

在冷启动的最开始,系统需要负责做三件事:

  • 加载以及启动app
  • app启动之后立刻显示一个空白的预览窗口
  • 创建app进程

一旦系统完成创建app进程后,app进程将要接着负责完成下面的工作:

  • 创建Application对象
  • 创建并且启动主线程ActivityThread
  • 创建启动第一个Activity
  • Inflating views
  • 布局屏幕
  • 执行第一次绘制

一旦app进程完完成了第一次绘制工作,系统进程就会用main activity替换前面显示的预览窗口,这个时候,用户就可以正式开始与app进行交互了。
在这里插入图片描述

从冷启动的流程看,我们无法干预app进程创建等系统操作,我们能够干预的有:

  • 预览窗口
  • Application生命周期回调
  • Activity生命周期回调
五. 优化分析测量工具

对研发人员来说,启动速度是我们的“门面”,它清清楚楚可以被所有人看到,我们都希望自己应用的启动速度可以秒杀所有竞争对手。

“工欲善其事必先利其器”,我们需要先找到一款适合做启动优化分析的工具或者方式。

  • adb shell am start -W [packageName]/[ packageName. AppstartActivity]
    在统计 app 启动时间时,系统为我们提供了 adb 命令,可以输出启动时间。系统在绘制完成后,ActivityManagerService 会回调该方法,但是能够方便我们通过脚本多次启动测量 TotalTime,对比版本间启动时间差异。但是统计时间不如 Systrace 准确。

  • 代码埋点
    通过代码埋点来准确获取记录每个方法的执行时间,知道哪些地方耗时,然后再有针对性地优化。例如通过在 app 启动生命周期中,关键位置加入时间点记录,达到测量目的;又例如可以在 Application 的 attachBaseContext方法中记录开始时间,然后在启动的第一个 Activity 的 onWindowFocusChanged方法记录结束时间。但是从用户点击 app Icon 到 Application 被创建,再到 Activity 的渲染,中间还是有很多步骤的,比如冷启动的进程创建过程,而这个时间用此版本是没办法统计了,必须得承受这点数据的不准确性。

  • Nanoscope
    Nanoscope 非常真实,不过暂时只支持 Nexus 6 和 x86 模拟器。

  • Simpleperf
    Simpleperf 的火焰图并不适合做启动流程分析。

  • TraceView
    通过 TraceView 主要可以得到两种数据:单次执行耗时的方法 以及 执行次数多的方法。但是TraceView 性能耗损太大,不能比较正确反映真实情况。

  • Systrace
    Systrace 能够追踪关键系统调用的耗时情况,如系统的 IO 操作、内核工作队列、CPU 负载、Surface 渲染、GC 事件以及 Android 各个子系统的运行状况等。但是不支持应用程序代码的耗时分析。综上所述,这几种方式都各有各的优点以及缺点,我们都要掌握。

六. 启动优化方法

在拿到整个启动流程的全景图之后,我们可以清楚地看到这段时间内系统、应用各个进程和线程的运行情况,现在我们要开始真正开始“干活”了。具体的优化方式,我把它们分为预览窗口优化、业务梳理、业务优化、多进程优化、线程优化、GC 优化和系统调用优化、布局优化

1. 预览窗口优化

当用户点击应用桌面图标启动应用的时候,利用提前展示出来的 Window,快速展示出一个界面,用户只需要很短的时间就可以看到“预览页”,这种完全“跟手”的感觉在高端机上体验非常好,但对于中低端机,会把总的的闪屏时间变得更长。

如果点击图标没有响应,用户主观上会认为是手机系统响应比较慢。所以比较推荐的做法是,只在 Android 6.0 或者 Android 7.0 以上才启用“预览窗口”方案,让手机性能好的用户可以有更好的体验。

要实现预览窗口的显示,只需要在利用 activity 的windowBackground主题属性提供一个简单的自定义 drawable 给启动的 activity,如下:

Layout XML file:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>

Manifest file:

<activity ...
android:theme="@style/AppTheme.Launcher" />

这样一个 activity 启动的时候,就会先显示一个预览窗口,给用户快速响应的体验。当 activity想要恢复原来 theme,可以通过在调用super.onCreate() 和setContentView()之前调用 setTheme(R.style.AppTheme),如下:

public class MyMainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Make sure this is before calling super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}
2. 业务梳理

不要一股脑把全部初始化工作放在 Application 中做,需要梳理清楚当前启动过程正在运行的每一个模块,哪些是一定需要的、哪些可以砍掉、哪些可以懒加载。但是需要注意的是,懒加载要防止集中化,否则容易出现首页显示后用户无法操作的情形。总的来说,用以下四个维度分整理启动的各个点:

  • 必要且耗时:启动初始化,考虑用线程来初始化。
  • 必要不耗时:首页绘制。
  • 非必要但耗时:数据上报、插件初始化。
  • 非必要不耗时:不用想,这块直接去掉,在需要用的时再加载。

把数据整理出来后,按需实现加载逻辑,采取分步加载、异步加载、延期加载策略,如下图所示:

image.png

一句话概述,要提高应用的启动速度,核心思想是在启动过程中少做事情,越少越好。

3. 业务优化

通过梳理之后,剩下的都是启动过程一定要用的模块。这个时候,我们只能硬着头皮去做进一步的优化。优化前期需要“抓大放小”,先看看主线程究竟慢在哪里。最理想是通过算法进行优化,例如一个数据解密操作需要 1 秒,通过算法优化之后变成 10 毫秒。退而求其次,我们要考虑这些任务是不是可以通过异步线程预加载实现,但需要注意的是过多的线程预加载会让我们的逻辑变得更加复杂。

业务优化做到后面,会发现一些架构和历史包袱会拖累我们前进的步伐。比较常见的是一些事件会被各个业务模块监听,大量的回调导致很多工作集中执行,部分框架初始化“太厚”,例如一些插件化框架,启动过程各种反射、各种 Hook,整个耗时至少几百毫秒。还有一些历史包袱非常沉重,而且“牵一发动全身”,改动风险比较大。但是我想说,如果有合适的时机,我们依然需要勇敢去偿还这些“历史债务”。

4. 多进程优化

Android app 是支持多进程的,在 Manifest 中只要在组件声明中加入android:process属性就可以让组件在启动时运行在不同的进程中。举个例子: 对于多进程 app ,可能拥有主进程,插件进程以及下载进程,但开发者只能在 Manifest 中声明一个 Application 组件,如果对应不同进程的组件启动时,系统会创建三个进程,创建三个 Application 对象,同时attachBaseContext、onCreate等生命周期回调方法也会被调用三次。

但是每个进程需要初始化的内容肯定是不一样的,所以,为了防止资源的浪费,我们需要在Application 中区分进程,对应进程只初始化对应的内容。

5. 线程优化

线程优化分两方面:

第一,耗时任务异步化。子线程处理耗时任务,主线程做的事情越少,越早进入Acitivity绘制阶段,界面越早展现。例如不在主线程做如 IO 、网络等耗时操作。但是要注意,子线程不能阻塞主线程。

第二,线程池管理线程,控制线程的数量。线程数量太多会相互竞争 CPU 资源,导致分给主线程的时间片减少,从而导致启动速度变慢。线程切换的数据我们可以通过卡顿优化中学到的 sched 文件查看,这里特别需要注意 nr_involuntary_switches 被动切换的次数。

proc/[pid]/sched: 
 nr_voluntary_switches:主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是 IO。 
 nr_involuntary_switches:被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占 CPU。 

第三,避免主线程与子线程之间的锁阻塞等待。有一次我们把主线程内的一个耗时任务放到线程中并发执行,但是发现这样做根本没起作用。仔细检查后发现线程内部会持有一个锁,主线程很快就有其他任务因为这个锁而等待。通过 Systrace 可以看到锁等待的事件,我们需要排查这些等待是否可以优化,特别是防止主线程出现长时间的空转。
image.png

特别是现在有很多启动框架,会使用 Pipeline 机制,根据业务优先级规定业务初始化时机。比如微信内部使用的 mmkernel 、阿里最近开源的 Alpha 启动框架,它们为各个任务建立依赖关系,最终构成一个有向无环图。对于可以并发的任务,会通过线程池最大程度提升启动速度。如果任务的依赖关系没有配置好,很容易出现下图这种情况,即主线程会一直等待 taskC 结束,空转 2950 毫秒。
image.png

6. GC优化

在启动过程,要尽量减少 GC 的次数,避免造成主线程长时间的卡顿,特别是对 Dalvik 来说,我们可以通过 Systrace 单独查看整个启动过程 GC 的时间。

启动过程避免进行大量的字符串操作,特别是序列化跟反序列化过程。一些频繁创建的对象,例如网络库和图片库中的 Byte 数组、Buffer 可以复用。如果一些模块实在需要频繁创建对象,可以考虑移到 Native 实现。

Java 对象的逃逸也很容易引起 GC 问题,我们在写代码的时候比较容易忽略这个点。我们应该保证对象生命周期尽量的短,在栈上就进行销毁。

7. 系统调用优化

部分系统的API使用是阻塞性的,文件很小可能无法感知,当文件过大,或者使用频繁时,可能造成阻塞。例如:SharedPreference.Editor 的提交操作建议使用异步的 apply,而不是阻塞的 commit。

通过 systrace 的 System Service 类型,我们可以看到启动过程 System Server 的CPU 工作情况。在启动过程,我们尽量不要做系统调用,例如 PackageManagerService 操作、Binder 调用等待。

在启动过程也不要过早地拉起应用的其他进程,System Server 和新的进程都会竞争 CPU 资源。特别是系统内存不足的时候,当我们拉起一个新的进程,可能会成为“压死骆驼的最后一根稻草”。它可能会触发系统的 low memorykiller 机制,导致系统杀死和拉起(保活)大量的进程,从而影响前台进程的 CPU。举个例子,之前一个程序在启动过程会拉起下载和视频播放进程,改为按需拉起后,线上启动时间提高了 3%,对于 1GB 以下的低端机优化,整个启动时间可以优化 5%~8%,效果还是非常明显的。

8. 布局优化

布局越复杂,测量布局绘制的时间就越长。主要做到以下几点:

  • 布局的层级越少,加载速度越快。
  • 一个控件的属性越少,解析越快,删除控件中的无用属性。
  • 使用< ViewStub/>标签加载一些不常用的布局,做到使用时在加载。
  • 使用 < merge/>标签减少布局的嵌套层次。
  • 尽可能少用wrap_content,wrap_content会增加布局measure时的计算成本,已知宽高为固定值时,不用wrap_content。
总结

启动优化,是一项长期的任务,任重而道远。

开发者要未雨绸缪,在编码过程中尽量减少给启动带来性能损耗的工作,主要注意以下几个事项:

  • 尽量避免启动时在主线程做密集繁重的工作,如:避免 I/O 操作、反序列化、网络操作、锁等待等。
  • 对模块以及第三方库按需加载,采取分步加载、异步加载、延期加载等策略。
  • 利用线程池管理线程,避免创建大量线程,造成 CPU 竞争,导致主线程时间片减少。
  • 启动过程中,尽量避免频繁创建的大量对象,减少 GC 给启动性能带来的卡顿影响。
  • 尽量避免在启动过程中调用阻塞性的系统调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值