两个定律
2-5-8原则
在性能优化中存在启动时间2-5-8原则:
- 当用户在0-2秒之间得到响应时,会感觉系统的响应很快
- 当用户在2-5秒之间得到响应时,会感觉系统的响应速度还可以
- 当用户在5-8秒之间得到响应时,会感觉系统的响应速度很慢,但是还可以接受
- 而当用户在超过8秒后仍然无法得到响应时,会感觉系统糟透了,或者认为系统已经失去响应
八秒定律
八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过了8秒,就有超过70%的用户放弃等待。
启动方式
在应用的启动过程中,有3中启动方式,冷启动、热启动、温启动,每种状态都会影响我们的启动耗时。
冷启动
冷启动是指应用进程不存在,然后从进程创建开始。一般启动优化都是针对冷启动进行优化。包含以下两种情况:
- 系统启动后应用进程首次启动
- 应用进程被杀后,需要的再次启动
冷启动流程:
- 点击Launcher桌面app图标启动程序,调用SystemServer进程的ActivityManagerService.startActivity
- ActivityManagerService通知zygote进程fork孵化出应用进程,然后分配内存空间等
- 执行该应用ActivityThread的main()方法
- 应用程序通知ActivityManagerService它已经启动,ActivityManagerService保存一个该应用的代理对象,ActivityManagerService通过它可以控制应用进程
- ActivityManagerService通知应用进程创建入口的Activity实例,执行它的生命周期
在ActivityThread.main方法中,主要执行的初始化工作有:
- 反射创建Application
- 调用Application.attachBaseContext()
- 调用Application.onCreate()
- 反射创建Activity
- 调用Activity.onCreate()
- 调用Activity.onResume()
- 调用Activity.onAttachToWindow()
- 调用Activity.onWindowFocusChanged()
public class MyApplication extends Application {
private final String TAG = getClass().getSimpleName();
public MyApplication() {
Log.d(TAG, "MyApplication: ");
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
Log.d(TAG, "attachBaseContext: ");
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.appstartdemo">
<application
android:name=".MyApplication">
</application>
</manifest>
public class MainActivity extends AppCompatActivity {
private final String TAG = getClass().getSimpleName();
public MainActivity() {
Log.d(TAG, "MainActivity: ");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "onCreate: ");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: ");
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d(TAG, "onAttachedToWindow: ");
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.d(TAG, "onWindowFocusChanged: ");
}
}
MyApplication: MyApplication:
MyApplication: attachBaseContext:
MyApplication: onCreate:
MainActivity: MainActivity:
MainActivity: onCreate:
MainActivity: onResume:
MainActivity: onAttachedToWindow:
MainActivity: onWindowFocusChanged:
热启动
热启动是指应用进程还存在,Acivity也没有被回收,无需再次执行Activity.onCreate方法。此时应用的Activity仍然存在内存中,无需重复执行Activity的初始化、布局加载和绘制。在热启动中,系统的所有工作就是将 Activity 带到前台。
温启动
温启动是指应用进程还存在,但是Activiyt已经被回收了,需要重新执行Activity的初始化、布局加载和绘制。有以下几种情况:
- 用户在退出应用后又重新启动应用。进程可能未被销毁,继续运行,但应用需要从头开始重新创建 Activity并执行onCreate() 。
- 系统将应用从内存中释放,然后用户又重新启动它。进程和 Activity 需要重启,但传递到onCreate() 的已保存的实例
savedInstanceState 对于完成此任务有一定助益。
一般情况下,温启动耗时比冷启动要快,比热启动要慢。
启动耗时统计
系统日志
在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。
ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +401ms
Android不同版本略有不同,有的是ActivityManager,有的是ActivityTaskManager。
如果我们使用异步懒加载的方式来提升程序画面的显示速度,这通常会导致的一个问题是,程序画面已经显示,同时 Displayed 日志已经打印,可是内容却还在加载中。为了衡量这些异步加载资源所耗费的时间,我们可以在异步加载完毕之后调用activity.reportFullyDrawn() 方法来让系统打印到调用此方法为止的启动耗时。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
setContentView(R.layout.activity_main);
return false;
}
});
}
@Override
public void reportFullyDrawn() {
super.reportFullyDrawn();
Log.d(TAG, "reportFullyDrawn: ");
}
几种启动方式测试对比:
ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +293ms
ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +287ms
ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +84ms
adb命令
通过以下adb命令来启动并打印耗时。
adb shell am start -S -W com.example.appstartdemo/.MainActivity
-S:表示先杀掉该app进程,重新冷启动
-W:表示打印启动耗时日志
命令执行后,命令窗口会打印以下日志
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.appstartdemo/.MainActivity }
Warning: Activity not started, its current task has been brought to the front
Status: ok
Activity: com.example.appstartdemo/.MainActivity
ThisTime: 67
TotalTime: 67
WaitTime: 87
Complete
相关数据说明:
名称 | 说明 |
---|
ThisTime | 一连串的Activity启动后,最后一个Activity的启动耗时,也就是当前Activity的耗时。可以作为重点优化目标。 | TotalTime | 应用冷启动耗时,包含进程的创建、当前Activity的启动,不包含上一个Activity.onPause耗时。 | WaitTime | 应用冷启动耗时,包含进程的创建、当前Activity的启动,包含了上一个Activity.onPause耗时。 |
几种启动耗时对比:
adb shell am start -S -W com.example.appstartdemo/.MainActivity
Stopping: com.example.appstartdemo
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.appstartdemo/.MainActivity }
Status: ok
Activity: com.example.appstartdemo/.MainActivity
ThisTime: 293
TotalTime: 293
WaitTime: 305
Complete
adb shell am start -W com.example.appstartdemo/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.appstartdemo/.MainActivity }
Warning: Activity not started, its current task has been brought to the front
Status: ok
Activity: com.example.appstartdemo/.MainActivity
ThisTime: 61
TotalTime: 61
WaitTime: 71
Complete
adb shell am start -W com.example.appstartdemo/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.appstartdemo/.MainActivity }
Status: ok
Activity: com.example.appstartdemo/.MainActivity
ThisTime: 84
TotalTime: 84
WaitTime: 96
Complete
启动耗时分析
在app启动的过程中,可以有多种方式来对启动耗时进行分析,可以使用CPU Profile/Traceview工具,也可以使用系统提供的Debug API,或者开启StrictMode严苛模式。
CPU Profile
工具介绍
Android Studio 性能分析器官网介绍:https://developer.android.google.cn/studio/profile/cpu-profiler?hl=zh
您可以使用 CPU 性能分析器在与应用交互时实时检查应用的 CPU 使用率和线程活动,也可以检查记录的方法轨迹、函数轨迹和系统轨迹的详情。CPU 性能分析器记录和显示的详细信息取决于您选择的记录配置:
- System Trace:捕获精细的详细信息,以便您检查应用与系统资源的交互情况。
- Method and function traces:对于应用进程中的每个线程,您可以了解一段时间内执行了哪些方法 (Java) 或函数 (C/C++),以及每个方法或函数在其执行期间消耗的 CPU 资源。您还可以使用方法和函数轨迹来识别调用方和被调用方。调用方是指调用其他方法或函数的方法或函数,而被调用方是指被其他方法或函数调用的方法或函数。您可以使用这些信息来确定哪些方法或函数过于频繁地调用通常会消耗大量资源的特定任务,并优化应用的代码以避免不必要的工作。
记录方法跟踪数据时,您可以选择“sampled”或“instrumented”记录方式。记录函数跟踪数据时,只能使用“sampled”记录方式。
使用方式
1. 在AS工具中打开 Run/Debug Configurations 配置界面。
2. 选择app,打开Profiling ,勾选 Start recording CPU activity on startup 选项,选择Trace Java Methods。
几个参数说明:
参数 | 说明 |
---|
Simple Java Methods | 对java方法进行采样,在应用java代码执行期间,频繁的采集应用调用堆栈,分析器会收集java代码执行的时间和相关的资源使用信息。 | Trace Java Methods | 跟踪java方法,在应用运行时,在每个方法调用的开始和结束记录一个时间戳,系统会收集并比较这些时间戳,以生成方法跟踪数据,包括时间信息和 CPU 使用率。 | Simple C/C++ Functions | 对C/C++函数进行采样,捕获应用的原生线程的采样跟踪数据。 | Trace System Calls | 跟踪系统调用,捕获非常详细的细节,用于检查应用和系统资源的交互情况。可以检测线程状态的确切时间和执行时间,也可以查看所有内核的CPU,并添加自定义跟踪事件。 |
3. 配置好之后,然后依次点击 Run—Profile
4. 当工具运行起来后,我们可以手动选择停止捕获,然后工具会自动分析并生成文件
5. 点击stop按钮后,会自动生成一份Java Method Trace Record 文件
数据分析
分析数据时,有四种方式可以选择,分别是Call Chart、Flame Chart、Top Down、Bottom Up。
Call Chart
以图形的方式来显示方法跟踪数据,虽然可读性比原数据好很多,但是不适用于运行时间很长的代码,可以使用Flame Chart进行分析。
坐标说明:
- 横坐标:显示调用时间段和时间
- 纵坐标:显示调用的方法
颜色说明:
- 橙色:显示系统API的调用
- 绿色:显示应用自有方法的调用
- 蓝色:显示第三方API(包含Java语言API)的调用
Flame Chart
简称火焰图,一个倒置的一个调用图表,用来汇总完全相同的调用栈,将具有相同调用方顺序的完全相同的方法收集起来,并在火焰图中将它们表示为一个较长的横条。
坐标说明:
- 横坐标:显示每次调用消耗的时间占用整个记录时间的百分比
- 纵坐标:被调用的方法依次显示,底部展示调用者,顶部展示被调用者。
Top Down
显示一个调用列表,在该列表中可以追踪到具体方法以及耗时,可以显示精确的时间。
坐标说明:
- 横坐标:标题分类
- 纵坐标:显示方法的调用栈,可以非常方便的看到耗时最长的方法调用栈
横坐标参数说明:
参数 | 说明 |
---|
Name | 方法的名称,调用者和被调用方法。 | Total(μs) | 单位:微秒(microsecond)即百万分之一秒(10的负6次秒),简称μs。 该方法调用的代码耗时,加上调用了其他方法的耗时,也可以说该方法的一个整体耗时。 | Self(μs) | 该方法运行自己的代码消耗的时间。 | Children Time(μs) | 该方法调用其他方法消耗的时间。 |
Bottom Up
可以很方便的找到某个方法的调用栈,在该列表中可以看到有哪些方法调用了自己。
例如loop() 函数的调用,在ActivityThread.main 中被调用。坐标以及参数说明与Top Down一致。
Traceview
Traceview是android平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到每个方法的执行时间。但是目前Traceview 已弃用。如果使用 Android Studio3.2 或更高版本,则应改为使用 CPU Profiler。
Debug API
使用系统提供的android.os.Debug ,可以很方便的在代码上进行捕获,需要申请SD卡文件读写权限,默认生成一个**.trace**文件,保存在SD卡目录下。
public class MyApplication extends Application {
private final String TAG = getClass().getSimpleName();
public MyApplication() {
Log.d(TAG, "MyApplication: ");
Debug.startMethodTracing("test_trace");
}
}
public class MainActivity extends AppCompatActivity {
private final String TAG = getClass().getSimpleName();
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Log.d(TAG, "onWindowFocusChanged: ");
Debug.stopMethodTracing();
}
}
在sdcard目录下会生成一个test_trace.trace文件,将文件拖到Android Studio的编辑框中,会打开文件,分析方式和CPU Profile是一样的。
android.os.Debug 常用函数说明:
函数 | 说明 |
---|
startMethodTracing | 对方法进行跟踪 | startMethodTracingSampling | 对方法进行跟踪并采样,可以设置采样率、收集的数据量大小。 | stopMethodTracing | 停止方法跟踪 |
startMethodTracing
public static void startMethodTracing(String tracePath) {
startMethodTracing(tracePath, 0, 0);
}
public static void startMethodTracing(String tracePath, int bufferSize, int flags) {
VMDebug.startMethodTracing(fixTracePath(tracePath), bufferSize, flags, false, 0);
}
String tracePath :文件名称,默认保存在sd卡目录下
int bufferSize :可以收集的最大数据,默认是8M
int flags :用于控制方法跟踪的标志。
startMethodTracingSampling
public static void startMethodTracingSampling(String tracePath, int bufferSize,
int intervalUs) {
VMDebug.startMethodTracing(fixTracePath(tracePath), bufferSize, 0, true, intervalUs);
}
String tracePath :要创建的跟踪日志文件的路径。如果 {@code null}, 这将默认为“dmtrace.trace”。如果文件已经存在,它将 被截断。如果给定的路径中没有以“.trace”结尾,它将为您附加。
int bufferSize :收集的最大跟踪数据量。如果没有给出,则默认为 8MB。
int intervalUs :每个样本之间的时间量(以微秒为单位)。
StrictMode严苛模式
StrictMode又称严苛模式,严苛模式时一个开发人员工具,它可以检测出我们可能无意间做的事情,并提醒我们,以便我们进行修复。最常用于捕获在主线程上执行IO操作或者网络访问。由系统提供android.os.StrictMode ,分为线程检测策略、VM虚拟机检测策略。
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.detectUnbufferedIo()
.detectCustomSlowCalls()
.detectResourceMismatches()
.detectAll()
.penaltyLog()
.penaltyDialog()
.penaltyDeath()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectAll()
.penaltyLog()
.build());
举例说明:假如在Activity.onCreate方法中进行了文件读写操作,然后打印违规日志。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.penaltyLog()
.build());
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(new File(Environment.getExternalStorageDirectory(), "test.txt"));
fos.write(0x10);
fos.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != fos) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
过滤日志StrictMode 查看即可。
优化方案
启动黑白屏优化
? 如果你的application或activity启动的过程太慢,导致系统的BackgroundWindow没有及时被替换,就会出现启动时白屏或黑屏的情况(取决于Theme主题是Dark还是Light)。
? 当系统加载并启动 App 时,需要耗费相应的时间,这样会造成用户会感觉到当点击 App 图标时会有 “延迟” 现象,为了解决这一问题,Google 的做法是在 App 创建的过程中,先展示一个空白页面,让用户体会到点击图标之后立马就有响应。
方案一:将预览界面去掉
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowDisablePreview">true</item>
</style>
方案二:将预览界面改为透明
? 缺点很明显,点击了图标之后,过一会才会显示界面,用户可能会觉得产生了卡顿。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
方案三:使用图片代替背景
该方案,当界面启动后,背景图片仍在存在,不同的场景需要谨慎使用。
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/launch_bg</item>
</style>
</resources>
方案四:主题优化方案(推荐)
# 创建新的主题
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
</style>
<style name="MyTheme" parent="AppTheme">
<item name="android:windowFullscreen">true</item>
<item name="android:windowBackground">@drawable/launch_bg</item>
</style>
</resources>
# 在清单文件中给Activity设置主题
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.appstartdemo">
<application
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:theme="@style/MyTheme">
</activity>
</application>
</manifest>
# Activity启动后,设置回默认的主题
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// 模拟耗时操作,显示更加的明显
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
setTheme(R.style.AppTheme);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
以上的所有方案都只是提高了用户体验,并没有真正的加快启动速度。
主线耗时优化
应该尽量避免在主线程进行耗时操作,例如:
- IO操作
- 第三方SDK耗时初始化操作
- 加载SharePreferences
- 初始化数据库、耗时资源
优化方案:
- 异步加载:提前异步执行类加载、AsyncLayoutInflater异步加载布局
- 懒加载:只有当数据需要使用时,才进行加载
- 合理使用IdleHandler进行延迟初始化
布局优化
减少布局层级、降低布局嵌套
使用Layout Inspector工具进行分析
会显示当前界面的布局嵌套情况,可以通过进行分析删掉不必要的布局来达到优化的目的。
includeb标签
includeb标签,常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。
<include layout="@layout/title_layout" />
viewstub标签
viewstub标签同include标签一样可以用来引入一个外部布局,不同的是,viewstub引入的布局默认不会扩张,即既不会占用显示也不会占用位置,从而在解析layout时节省cpu和内存。
public final class ViewStub extends 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(); // 需要显示时,调用该方法
}
}
merge标签
merge标签用于降低View树的层次来优化Android的布局。可以适用于以下场景:
- 布局顶结点是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当作该布局的顶节点,这样在被引入时顶结点会自动被忽略,而将其子节点全部合并到主布局中。
<?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>
<?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>
|