在性能优化中存在启动时间2-5-8 原则:
- 当用户在
0-2s 之间得到响应时,会感觉系统的响应很快; - 当用户在
2-5s 之间得到响应时,会感觉系统的响应速度还可以; - 当用户在
5-8s 之间得到响应时,会感觉系统的响应速度很慢,但是还可以接受; - 而当用户在超过
8s 后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应;
八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过了8秒 ,就有超过70%的用户放弃等待。
1 启动分类
- 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,然后再根据启动的参数,启动对应的进程组件,这个启动方式就是冷启动;
- 热启动:当启动应用时,后台已有该应用的进程(例如:按
back 键、home 键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),在已有进程的情况下,这种启动会从已有的进程中来启动对应的进程组件,这个方式叫热启动;
冷启动的详细流程可以简单分成三个步骤,其中创建进程步骤是系统做的,启动应用和绘制界面是应用做的:
- 创建进程:启动
APP -> 显示一个空白的启动Window -> 创建应用进程; - 启动应用:创建
Application -> 启动主线程(UI 线程)-> 创建第一个Activity ; - 绘制界面:加载视图布局(
inflating ) -> 计算视图在屏幕上的位置排版(Laying out ) -> 首帧视图绘制(Draw );
不同的启动方式决定了应用UI 对用户可见所需要花费的时间长短,冷启动消耗的时间最长。 基于冷启动方式的优化工作也是最考验产品用户体验的地方。
以下是启动时间,这个启动时间是从应用启动(创建应用进程)开始计算到完成视图的首帧绘制(即Activity 的内容对用户可见)为止:
或者使用指令:adb shell am start -S -W [packageName]/[packageName.MainActivity] ,-S 表示重新启动应用,查看启动时间:
或者是使用埋点的方式,启动时埋点,启动结束埋点,二者的差值就是启动时间。我们能够接触到的最早的启动相关回调方法是Application.attachBaseContext 方法,在这个方法中获取启动时间,在Activity.onWindowFocusChanded 方法中获取结束时间,此时,View 已经完成了measure 、layout ,但是还没有draw 。
以下是三种方式对比:
2 优化方案
2.1 启动窗口优化
启动窗口,指的是应用启动时候的预览窗口。Android 默认有一个启动页,用户点击桌面APP 图标之后,系统会立即显示这个启动窗口,等APP 主页加载好之后再显示主页面。
这个启动窗口是可以禁用的,部分开发者会禁用系统默认的启动窗口,自己定义。但是自定义的启动窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动APP 的时候,有一定的延迟, 表现在点击图标过了一段时间才进行窗口动画进入APP ,我们要尽量避免这种情况。
- 不要禁止系统默认的启动窗口:即不要在主题里面设置
android:windowDisablePreview 为true - 自己定制启动窗口的内容时,可以将启动窗口的背景设置成和闪屏页一样,或者尽量使闪屏页面的主题和主页一致。 可以参考知乎、抖音的做法;
- 合并闪屏和主页面的
Activity :微信的做法,不过由于微信设置了android:windowDisablePreview , 且他在各个厂商的白名单里面,一般不会被杀,冷启动的机会比较少;
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正在执行...
2.3 页面布局优化
2.3.1 减少层级嵌套,尽量保持层级的扁平化
ConstraintLayout 可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。 使用约束布局的关键点:
- 约束布局采用相对定位原理,即控件相对于另一个控件的约束;
- 一个控件至少三个方向的约束;
constraint [k?n?stre?nt] 限制,束缚;克制,拘束
2.3.2 <merge/>
<merge/> 标签用于降低View 树的层次来优化布局。 可以适用于以下场景:
- 顶层布局是
FrameLayout 且不需要设置background 或padding 等属性的,可以用merge 代替,因为Activity 内容视图的Parent View 就是个FrameLayout ,可以用merge 消除只剩一个;
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试文字" />
</merge>
- 当布局作为子布局被其它布局
<include/> 的时候,使用<merge/> 当作该布局的顶层布局,这样在被引入时顶层布局会自动被忽略,而将其子布局会全部合并到主布局中;
# title_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<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
<?xml version="1.0" encoding="utf-8"?>
<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 <ViewStub/>
在开发中经常会遇到这样的情况,会在程序运行时动态根据条件来决定显示某个View 或某个布局。那么通常做法就是把用到的View 都写在布局中,然后在代码中动态的更改它是否可见。这样的做法在创建布局的时候也会创建View 。这时就可以用到<ViewStub/> 了,它同<include/> 标签一样可以用来引入一个布局<ViewStub/> 是一个轻量级的View ,不占布局位置,占用资源非常小。
比如请求网络加载列表,如果网络异常或者加载失败可以显示一个提示View ,在上面可以点击重新加载。如果一直没有错误,就不显示。
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<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 系统调度优化
启动过程中除了Activity 之外的组件启动要谨慎,因为四大组件的启动都是在主线程的,如果组件启动慢,占用了Message 通道,也会影响应用的启动速度。
2.6 业务梳理
梳理清楚启动过程中的每一个功能,哪些是一定需要的,那些是可以砍掉,那些是可以懒加载的。
懒加载:当页面可见的时候,才加载当前页面, 没有打开的页面,就不会预加载。 也就是说,懒加载就是可见的时候才去请求数据。
参考
[性能] adb shell am start -W 获取应用启动时间 Android冷启动耗时优化 Android App 启动优化全记录 Android性能优化系列:启动优化 Android 性能优化—— 启动优化提升60% Android性能优化系列:启动优化 安卓性能优化之启动优化 Android性能优化系列一:启动优化 Android性能优化之布局优化(使用约束布局) Android一些你需要知道的布局优化技巧
|