Android性能优化(一)—— 启动优化

一个应用的启动速度能够影响用户的首次体验,启动速度较慢的应用可能会导致用户再次开启该应用的意图下降,或者卸载放弃该应用。

在性能优化中存在启动时间 2-5-8 原则:

  • 当用户在 0-2s 之间得到响应时,会感觉系统的响应很快;
  • 当用户在 2-5s 之间得到响应时,会感觉系统的响应速度还可以;
  • 当用户在 5-8s 之间得到响应时,会感觉系统的响应速度很慢,但是还可以接受;
  • 而当用户在超过 8s 后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应;

八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过了 8 秒,就有超过 70% 的用户放弃等待。

1 启动分类

  • 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,然后再根据启动的参数,启动对应的进程组件,这个启动方式就是冷启动;
  • 热启动:当启动应用时,后台已有该应用的进程(例如:按 back 键、home 键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看)。所以在已有进程的情况下,这种启动会从已有的进程中来启动对应的进程组件,这个方式叫热启动;

冷启动的详细流程可以简单分成三个步骤,其中创建进程步骤是系统做的,启动应用和绘制界面是应用做的:

  • 创建进程:启动 APP -> 显示一个空白的启动 Window -> 创建应用进程;
  • 启动应用:创建 Application -> 启动主线程(UI 线程)-> 创建第一个 Activity;
  • 绘制界面:加载视图布局(inflating) -> 计算视图在屏幕上的位置排版(Laying out) -> 首帧视图绘制(Draw);

冷启动和热启动的特点如下:

  • 冷启动:系统会重新创建一个新的进程分配给该应用,从 Application 创建到 UI 绘制等相关流程都会执行一次;
  • 热启动:应用还在后台,因此该启动方式不会重建 Application,只会重新绘制 UI 等相关流程;

不同的启动方式决定了应用 UI 对用户可见所需要花费的时间长短,冷启动消耗的时间最长。 基于冷启动方式的优化工作也是最考验产品用户体验的地方。

以下是启动时间,这个启动时间是从应用启动(创建应用进程)开始计算到完成视图的首帧绘制(即 Activity 的内容对用户可见)为止:

启动时间

或者使用指令:adb shell am start -S -W [packageName]/[packageName.MainActivity],-S 表示重新启动应用,-W 启动并输出相应的数据, 查看启动时间:

Android性能优化(一)—— 启动优化_第1张图片

TotalTime:应用启动时间,包含创建进程 + Application 初始化 + Activity 初始化到界面显示的时间;WaitTime:一般比 TotalTime 大一点,包含系统影响的耗时。

或者是使用埋点的方式,启动时埋点,启动结束埋点,二者的差值就是启动时间。我们能够接触到的最早的启动相关回调方法是 Application.attachBaseContext 方法,在这个方法中获取启动时间,在 Activity.onWindowFocusChanded 方法中获取结束时间,此时,View 已经完成了 measure、layout,但是还没有 draw。

以下是三种方式对比:

Android性能优化(一)—— 启动优化_第2张图片

Displayed

2 优化方案

2.1 启动窗口优化

启动窗口,指的是应用启动时候的预览窗口。Android 默认有一个启动页,用户点击桌面 APP 图标之后,系统会立即显示这个启动窗口,等 APP 主页加载好之后再显示主页面。

这个启动窗口是可以禁用的,部分开发者会禁用系统默认的启动窗口,自己定义。但是自定义的启动窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 APP 的时候,有一定的延迟, 表现在点击图标过了一段时间才进行窗口动画进入 APP,我们要尽量避免这种情况。

  • 不要禁止系统默认的启动窗口:即不要在主题里面设置 android:windowDisablePreview 为 true
  • 自己定制启动窗口的内容时,可以将启动窗口的背景设置成和闪屏页一样,或者尽量使闪屏页面的主题和主页一致。 可以参考知乎、抖音的做法;
  • 合并闪屏和主页面的 Activity,微信的做法,不过由于微信设置了 android:windowDisablePreview, 且它在各个厂商的白名单里面,一般不会被杀,冷启动的机会比较少;

白屏/黑屏问题

当跨进程启动 Activity 时,界面会出现黑屏/白屏的问题。出现这种情况的原因是 Android 创建进程需要准备很多资源,是一个耗时的操作。 进程创建完成之前,新的 Activity 界面没机会展示,如此用户在跳转新的 Activity 时会短暂没反应,这极大的降低用户体验。

Android 团队避免出现这种尴局面,于是系统根据清单文件设置的主题颜色的不同来展示一个白屏或者黑屏。而这个黑(白)屏被称作 Preview Window,即预览窗口。

预览窗口是可以禁用的:

<style name="APPTheme" parent="@android:style/Theme.Holo.NoActionBar">
   "android:windowDisablePreview">true
style>

这样做可以解决部分场景的问题,比如在 A 进程启动 B 进程中的 Activity,但是在另外一个场景就有问题了,在桌面 Launcher 点击应用出现短暂的假死现象。

还可以自定义预览窗口:

<style name="APPTheme" parent="@android:style/Theme.Holo.NoActionBar">
    "android:windowBackground">@drawable/splash_icon
    "android:windowFullscreen">true
    "android:windowNoTitle">true
style>

该解决方案很适合启动一个 APP 场景,android:windowBackground 属性设置 Preview Window 的背景,市面上大部分 APP 都是使用该属性设置启动页背景。但是该解决方案不适合在跨进程启动 Activity 场景了。

还可以设置预览窗口透明属性:

<style name="APPTheme" parent="@android:style/Theme.Holo.NoActionBar">
    "android:windowIsTranslucent">true
    "android:windowBackground">@android:color/transparent    
    "android:windowFullscreen">true
    "android:windowNoTitle">true
style>

该解决方案适合跨进程启动 Activity 场景使用。当然这个解决方案也会引入其他问题,就是:android:windowIsTranslucent 引起 Activity 切换动画无效解决方案。为了追求极致,不能解决一个问题引入一个新问题,该问题的解决方案也有两种:

  • 代码动态设置 Activity 专场动画:
overridePendingTransition(R.anim.anim_right_in,R.anim.anim_left_out); 
  • 给 Window 设置动画 style:
<style name="APPTheme" parent="@android:style/Theme.Holo.NoActionBar">
    "android:windowIsTranslucent">true
    "android:windowBackground">@android:color/transparent    
    "android:windowFullscreen">true
    "android:windowNoTitle">true
    "android:windowAnimationStyle">@styleAnimation.Activity.Translucent.Style/
style>
<style name="Animation.Activity.Translucent.Style" parent="@android:style/Animation.Translucent">  
        "android:windowEnterAnimation">@anim/base_slide_right_in  
        "android:windowExitAnimation">@anim/base_slide_right_out  
style>  

如果白屏/黑屏的问题比较严重,很有可能是 Application 中执行了过多的初始化操作,应该重点从这个方向着手处理问题。

2.2 线程优化

线程优化主要是减少 CPU 调度带来的波动,让启动时间更稳定。如果启动过程中有太多的线程一起启动,会给 CPU 带来非常大的压力,尤其是比较低端的机器。可以使用线程池控制线程数量:

class Test {
  public static void main(String[] args) {
    ExecutorService pool = Executors.newFixedThreadPool(2);
    Thread t1 = new TestThread();
    Thread t2 = new TestThread();
    Thread t3 = new TestThread();
    Thread t4 = new TestThread();
    Thread t5 = new TestThread();
    pool.execute(t1);
    pool.execute(t2);
    pool.execute(t3);
    pool.execute(t4);
    pool.execute(t5);
  }
}

class TestThread extends Thread {
  @Override
  public void run() {
    System.out.println(Thread.currentThread().getName() + "正在执行...");
  }
}

// pool-1-thread-1正在执行...
// pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
pool-1-thread-2正在执行...
pool-1-thread-1正在执行...
2.3 页面布局优化
2.3.1 减少层级嵌套,尽量保持层级的扁平化

ConstraintLayout 可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。 使用约束布局的关键点:

  • 约束布局采用相对定位原理,即控件相对于另一个控件的约束;
  • 一个控件至少三个方向的约束;

constraint [kənˈstreɪnt] 限制,束缚;克制,拘束

2.3.2

标签用于降低 View 树的层次来优化布局。 可以适用于以下场景:

  • 顶层布局是 FrameLayout 且不需要设置 background 或 padding 等属性的,可以用 merge 代替,因为 Activity 内容视图的 Parent View 就是个 FrameLayout ,可以用 merge 消除只剩一个;

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="测试文字" />
    
merge>
  • 当布局作为子布局被其它布局 的时候,使用 当作该布局的顶层布局,这样在被引入时顶层布局会自动被忽略,而将其子布局会全部合并到主布局中;
# title_layout.xml

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="标题显示" />

merge>

# activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
	
    <include layout="@layout/title_layout" />	// 使用include标签引入merge标签布局

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="内容显示" />

LinearLayout>
2.3.3

在开发中经常会遇到这样的情况,会在程序运行时动态根据条件来决定显示某个 View 或某个布局。那么通常做法就是把用到的 View 都写在布局中,然后在代码中动态的更改它是否可见。这样的做法在创建布局的时候也会创建 View。这时就可以用到 了,它同 标签一样可以用来引入一个布局 是一个轻量级的 View,不占布局位置,占用资源非常小。

比如请求网络加载列表,如果网络异常或者加载失败可以显示一个提示 View,在上面可以点击重新加载。如果一直没有错误,就不显示。

布局文件:


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

    <ViewStub
        android:id="@+id/mViewStub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/title_layout" />	// 这里需要引入一个懒加载的布局
        
androidx.constraintlayout.widget.ConstraintLayout>

使用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        ViewStub mViewStub = findViewById(R.id.mViewStub);
        mViewStub.inflate();  // 需要显示时,调用该方法
    }
}
2.4 闲时调用

IdleHandler:主要针对一些优先级不是很高的任务在 CPU 空闲的时候执行。 IdleHandler.queueIdle() 方法返回 true,则会一直执行,返回 false,执行完一次后就会被移除消息队列。比如,我们可以将一些打点任务或者把一些不重要的 View 的加载放到 IdleHandler 中执行。

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
  @Override
  public boolean queueIdle() {
    return false;
  }
});
2.5 CountDownLatch

线程之间可能存在相互等待依赖等相关问题,一个(或者多个)线程,等待另外 N 个线程完成某个事情之后才能执行。

CountDownLatch 是一个同步工具,用来协调线程之间的同步,用来作为线程间的通信而不是互斥作用,能够使一个线程在等待另外一些线程完成各自的工作之后,再继续执行。 使用一个计数器进行实现,计数器的初始值就是线程的数量。当每个被计数的线程完成任务后,计数器值减一,当计数器的值为 0 时,表示多有线程都已经完成了任务,然后在 CountDownLatch 上等待的线程就可以恢复执行。

Android性能优化(一)—— 启动优化_第3张图片

某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为:new CountDownLatch(n),每当一个任务线程执行完毕,计数器减 1,CountDownLatch.countDown(),当计数器的值变为 0 时,在 CountDownLatch.await() 的线程就会被唤醒。一个典型的应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

public static void main(String[] args) {
    CountDownLatch latch = new CountDownLatch(2);
    System.out.println("全班同学开始考试,一共两个学生");
    new Thread(() -> {
        System.out.println("第一个学生交卷,countDownLatch 减 1");
        latch.countDown();
    }).start();
    new Thread(() -> {
        System.out.println("第二个学生交卷,countDownLatch 减 1");
        latch.countDown();
    }).start();
    try {
        latch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("老师清点试卷,在此之前,只要有一个学生没交,countDownLatch 不为 0,不能离开考场");
}

// 全班同学开始考试,一共两个学生
// 第一个学生交卷,countDownLatch 减 1
// 第二个学生交卷,countDownLatch 减 1
// 老师清点试卷,在此之前,只要有一个学生没交,countDownLatch 不为 0,不能离开考场
2.6 CyclicBarrier(循环屏障)

CyclicBarrier,循环屏障,通过它可以实现让一组线程等待至某个状态之后再全部同时执行,叫做循环是因为当所有等待线程执行完成以后,CyclicBarrier 可以被重用。
Android性能优化(一)—— 启动优化_第4张图片

2.7 系统调度优化

启动过程中除了 Activity 之外的组件启动要谨慎,因为四大组件的启动都是在主线程的,如果组件启动慢,占用了 Message 通道,也会影响应用的启动速度。

2.8 业务梳理

梳理清楚启动过程中的每一个功能,哪些是一定需要的,那些是可以砍掉,那些是可以懒加载的。

懒加载:当页面可见的时候,才加载当前页面, 没有打开的页面,就不会预加载。 也就是说,懒加载就是可见的时候才去请求数据。

3 TraceView 工具的使用

TraceView 是 Android Studio 配备的一个很好的性能分析工具,它可以通过图形化的方式让我们了解更过的程序性能,并且能具体到方法。

首先通过 Android SDK 自带的 Debug 方法,对应用进行打点获取相关的信息:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());

  	// 开始记录,该方法可以设置文件大小和路径
    Debug.startMethodTracing("browser.trace");

    setSupportActionBar(binding.toolbar);

    NavController navController = Navigation.findNavController(this, 
                   R.id.nav_host_fragment_content_main);
    appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
    NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

    binding.fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });

  	// 结束记录
    Debug.stopMethodTracing();

}

执行完以上操作后,可以生成以下文件: /sdcard/Android/data/[packageName]/files/browser.trace。

导出文件,通过 Android Studio 的 Profiler 打开:

Android性能优化(一)—— 启动优化_第5张图片

Android性能优化(一)—— 启动优化_第6张图片

可以查看有多少线程、具体的方法耗时以及 CPU 的执行时间等信息。

参考

[性能] adb shell am start -W 获取应用启动时间
Android冷启动耗时优化
Android App 启动优化全记录
Android性能优化系列:启动优化
Android 性能优化—— 启动优化提升60%
Android性能优化系列:启动优化
安卓性能优化之启动优化
Android性能优化系列一:启动优化
Android性能优化之布局优化(使用约束布局)
Android一些你需要知道的布局优化技巧
Android 跨进程启动Activity黑屏(白屏)的三种解决方案
CountDownLatch详解

你可能感兴趣的:(android,性能优化,java)