一个应用的启动速度能够影响用户的首次体验,启动速度较慢的应用可能会导致用户再次开启该应用的意图下降,或者卸载放弃该应用。
在性能优化中存在启动时间 2-5-8 原则:
八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过了 8 秒,就有超过 70% 的用户放弃等待。
冷启动的详细流程可以简单分成三个步骤,其中创建进程步骤是系统做的,启动应用和绘制界面是应用做的:
冷启动和热启动的特点如下:
不同的启动方式决定了应用 UI 对用户可见所需要花费的时间长短,冷启动消耗的时间最长。 基于冷启动方式的优化工作也是最考验产品用户体验的地方。
以下是启动时间,这个启动时间是从应用启动(创建应用进程)开始计算到完成视图的首帧绘制(即 Activity 的内容对用户可见)为止:
或者使用指令:adb shell am start -S -W [packageName]/[packageName.MainActivity],-S 表示重新启动应用,-W 启动并输出相应的数据, 查看启动时间:
TotalTime:应用启动时间,包含创建进程 + Application 初始化 + Activity 初始化到界面显示的时间;WaitTime:一般比 TotalTime 大一点,包含系统影响的耗时。
或者是使用埋点的方式,启动时埋点,启动结束埋点,二者的差值就是启动时间。我们能够接触到的最早的启动相关回调方法是 Application.attachBaseContext 方法,在这个方法中获取启动时间,在 Activity.onWindowFocusChanded 方法中获取结束时间,此时,View 已经完成了 measure、layout,但是还没有 draw。
以下是三种方式对比:
启动窗口,指的是应用启动时候的预览窗口。Android 默认有一个启动页,用户点击桌面 APP 图标之后,系统会立即显示这个启动窗口,等 APP 主页加载好之后再显示主页面。
这个启动窗口是可以禁用的,部分开发者会禁用系统默认的启动窗口,自己定义。但是自定义的启动窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 APP 的时候,有一定的延迟, 表现在点击图标过了一段时间才进行窗口动画进入 APP,我们要尽量避免这种情况。
白屏/黑屏问题
当跨进程启动 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 切换动画无效解决方案。为了追求极致,不能解决一个问题引入一个新问题,该问题的解决方案也有两种:
overridePendingTransition(R.anim.anim_right_in,R.anim.anim_left_out);
<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 中执行了过多的初始化操作,应该重点从这个方向着手处理问题。
线程优化主要是减少 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正在执行...
ConstraintLayout 可以按照比例约束控件位置和尺寸,能够更好地适配屏幕大小不同的机型。 使用约束布局的关键点:
constraint [kənˈstreɪnt] 限制,束缚;克制,拘束
标签用于降低 View 树的层次来优化布局。 可以适用于以下场景:
<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>
在开发中经常会遇到这样的情况,会在程序运行时动态根据条件来决定显示某个 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(); // 需要显示时,调用该方法
}
}
IdleHandler:主要针对一些优先级不是很高的任务在 CPU 空闲的时候执行。 IdleHandler.queueIdle() 方法返回 true,则会一直执行,返回 false,执行完一次后就会被移除消息队列。比如,我们可以将一些打点任务或者把一些不重要的 View 的加载放到 IdleHandler 中执行。
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
return false;
}
});
线程之间可能存在相互等待依赖等相关问题,一个(或者多个)线程,等待另外 N 个线程完成某个事情之后才能执行。
CountDownLatch 是一个同步工具,用来协调线程之间的同步,用来作为线程间的通信而不是互斥作用,能够使一个线程在等待另外一些线程完成各自的工作之后,再继续执行。 使用一个计数器进行实现,计数器的初始值就是线程的数量。当每个被计数的线程完成任务后,计数器值减一,当计数器的值为 0 时,表示多有线程都已经完成了任务,然后在 CountDownLatch 上等待的线程就可以恢复执行。
某一线程在开始运行前等待 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,不能离开考场
CyclicBarrier,循环屏障,通过它可以实现让一组线程等待至某个状态之后再全部同时执行,叫做循环是因为当所有等待线程执行完成以后,CyclicBarrier 可以被重用。
启动过程中除了 Activity 之外的组件启动要谨慎,因为四大组件的启动都是在主线程的,如果组件启动慢,占用了 Message 通道,也会影响应用的启动速度。
梳理清楚启动过程中的每一个功能,哪些是一定需要的,那些是可以砍掉,那些是可以懒加载的。
懒加载:当页面可见的时候,才加载当前页面, 没有打开的页面,就不会预加载。 也就是说,懒加载就是可见的时候才去请求数据。
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 打开:
可以查看有多少线程、具体的方法耗时以及 CPU 的执行时间等信息。
[性能] adb shell am start -W 获取应用启动时间
Android冷启动耗时优化
Android App 启动优化全记录
Android性能优化系列:启动优化
Android 性能优化—— 启动优化提升60%
Android性能优化系列:启动优化
安卓性能优化之启动优化
Android性能优化系列一:启动优化
Android性能优化之布局优化(使用约束布局)
Android一些你需要知道的布局优化技巧
Android 跨进程启动Activity黑屏(白屏)的三种解决方案
CountDownLatch详解