Android性能优化 -- 启动优化

1,520 阅读6分钟

或许我们都有过这样的体验,如果打开一个app需要加载很长时间才到首页,有些时候可能因为网络环境问题根本打不开,那么用户就会直接退出不再使用这个app,长此以往用户量就会大规模流失,所以快速启动是app的生存之道,尤其是现在用户对于app的体验要求很高,因为启动慢、白屏受到很多用户的吐槽。

但是启动优化也仅仅是优化,有些时间是必须要耗费在启动上,所以我们能做的就是把不必要的时间消耗给去除,把时间花在刀刃上。

1 App的黑白屏优化

一个app的启动主要分为以下几个流程:

(1)点击app icon按钮,加载并启动app;
(2)启动后,立即为该app显示一个空白的启动窗口(不一定是白色,决定权在windowBackground的颜色);
(3)创建app的进程,在Binder驱动成分配一块内存;
(4)创建主线程,创建主Activity;
(5)执行主Activity的生命周期,到onResume时,显示布局用户可交互。

这是冷启动的主要流程,因为只有在主Activity页面执行onResume时,白屏才会消失,所以中间这个过程时间越长,那么白屏的时间就越长。

1.1 白屏的处理

所以对于白屏的处理,我们先看下目前的效果

device-2022-10-23-134905.gif 我们可以看到,中间有很长时间的一个白屏现象,所以我们想能不能去掉这个白屏窗口,用户打开app后直接看到首页。

<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:windowDisablePreview">true</item>
</style>

当然是可以的,但是用户的直观感受为卡顿,看下效果

2.gif

在点下图标之后,并没有启动而是像卡主一下,然后启动到了app首页,这种用户体验不好。

因此我们可以换一个方案,我们不要白屏,而是使用一个闪屏页将白屏替代

<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:windowBackground">@drawable/ic_launcher_background</item>
</style>

device-2022-10-23-144930.gif 加上闪屏页之后,我们发现首页的主题也跟着改变了,其实在加闪屏页的时候,可以让设计师设计一个跟主题相关的闪屏页作为背景,这样就不需要重新设置页面的背景。

1.2 网易云启动闪屏方案

既然我们不想影响到主Activity,那么我们可以单独设置一个SplashActivity作为闪屏的承接方,从而闪屏完成之后,进入到首页。

首先,我们先自定义一个drawable布局,作为闪屏页的背景

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@color/red" />

    <item android:top="30dp">
        <bitmap
            android:gravity="top"
            android:src="@drawable/ic_launcher_round" />

    </item>


</layer-list>

device-2022-10-23-153034.gif

我们看到当闪屏页启动之后,出现一行字,有没有伙伴们觉得这个是一个动画,其实并不是,而且因为前面我们对于windowBackground属性的使用

<style name="StartupTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    <item name="android:windowBackground">@drawable/splash</item>
    <item name="windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
</style>

当我们把一个页面的windowBackground设置为一张图之后,那么默认的背景也会是这张图会被首先加载进来,然后App完全启动之后,SplashActivity页面才会加载进来,显示文字

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SplashActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="音乐的力量"
        android:fontFamily="sans-serif-medium"
        android:textSize="20dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_marginBottom="30dp"
        android:textColor="#FFFFFF"/>

</androidx.constraintlayout.widget.ConstraintLayout>

当SplashActivity完全加载之后,意味着App已经完全启动了,便可以跳转到首页

override fun onResume() {
    super.onResume()
    startActivity(Intent(this,WebActivity::class.java))
}

device-2022-10-23-154141.gif

2 App启动时间优化

经过前面对于黑白屏的优化,其实仅仅是从用户体验方面来完成的,并没有从启动时间方面做出优化,如果app的启动时间很长,即便是加了闪屏页,也会导致在闪屏页的停留时间过长,用户耐心消耗完成也不再进入app,所以app启动时间优化才是重中之重。

2.1 App启动时间测量

App启动时间测量的方式有多种,其中包括:

(1)系统日志

通过在Logcat窗口中搜索关键字ActivityTaskManager,可以看到启动闪屏页用时1.8s

2022-10-23 15:30:30.780 2008-2046/system_process I/ActivityTaskManager:Displayed com.lay.mvi/.SplashActivity: +1s861ms

(2)adb命令

adb shell am start -W com.lay.mvi/.SplashActivity

通过adb命令,我们可以看到更详细的启动耗时,它的LaunchState(启动状态)是冷启动,总共耗时1.5s左右。

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.lay.mvi/.SplashActivity }
Status: ok
LaunchState: COLD
Activity: com.lay.mvi/.SplashActivity
TotalTime: 1556
WaitTime: 1557
Complete

2.2 方法耗时统计

因为在App启动的时候,像系统层的耗时我们其实很难处理,真正需要优化的就是应用层,也就是说从Application调用onCreate方法开始,所以对于Application中方法调用的耗时,我们可以做一次统计并分析。

override fun onCreate() {
    super.onCreate()
    Log.e("TAG", "Application onCreate")

    Debug.startMethodTracing(”Launcher“)
    threadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
    threadPool!!.execute {
        //....
    }
    Thread.sleep(200)
    Debug.stopMethodTracing()
}

程序运行之后,会生成一个trace文件,记得要开启读写权限

image.png

image.png

从上图中,我们可以看到在main方法中耗时最严重,那么这个是拿某个线上项目来看的,首先耗时最严重的,在初始化某个SDK的时候,耗时占比达到了34%;另外一个是初始化神策埋点的时候,耗时占用了25%,还有一个就是读写存储内容的时候,耗时占用了16%。

其实我们大概能看到耗时点主要分为两种:一种是初始化SDK,还有一种就是读写存储数据。

2.3 启动优化中的取舍

我们的SDK一定要放在主线程中初始化吗?这个答案只能说不一定,假设我们有3个SDK

image.png

耗时情况如下:SDK1耗时0.8s,SDK耗时1.5s,SDK3耗时1.1s,如果按照顺序执行,总共耗时为3.4s,那么如果我们有以下几个场景:

(1)3个SDK都不会立刻用到,那么全部可以懒加载放在子线程,耗时0s;

(2)SDK1和SDK2必须要用到,但是SDK3不会,SDK3放在子线程懒加载,总耗时2.7s;

(3)假设SDK3依赖SDK1和SDK2的结果,只能等到SDK3初始化完成才能进入首页,那么总耗时最大就是3.4s,其实我们可以将SDK1和SDK2都放在子线程,最多耗时1.5s返回全部的结果给SDK3,所以总耗时就是2.6s。

那么这是3个SDK,如果有5个、10ge甚至更多,而且相互之间存在依赖关系,在# Android性能优化 -- 图论在启动优化中的应用这篇文章中做了详细的介绍。

其实对于启动优化,我们能做的事情就是在应用层尽可能多地去减少耗时,有时候10ms、20ms的优化都是质的提升,所以在耗时的处理上如果遇到瓶颈,也别忘记在视觉上给用户好的体验。