安卓性能优化之启动优化
在性能优化中存在启动时间2-5-8原则:
八秒定律是在互联网领域存在的一个定律,即指用户访问一个网站时,如果等待网页打开的时间超过了8秒,就有超过70%的用户放弃等待。
在应用的启动过程中,有3中启动方式,冷启动、热启动、温启动,每种状态都会影响我们的启动耗时。
冷启动是指应用进程不存在,然后从进程创建开始。一般启动优化都是针对冷启动进行优化。包含以下两种情况:
冷启动流程:
在ActivityThread.main方法中,主要执行的初始化工作有:
# 1. 自定义Application 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: "); } } # 2. 添加到清单文件中 <?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> # 3. MainActivity添加日志 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: "); } } # 4. 日志输出 MyApplication: MyApplication: MyApplication: attachBaseContext: MyApplication: onCreate: MainActivity: MainActivity: MainActivity: onCreate: MainActivity: onResume: MainActivity: onAttachedToWindow: MainActivity: onWindowFocusChanged:
热启动是指应用进程还存在,Acivity也没有被回收,无需再次执行Activity.onCreate方法。此时应用的Activity仍然存在内存中,无需重复执行Activity的初始化、布局加载和绘制。在热启动中,系统的所有工作就是将 Activity 带到前台。
温启动是指应用进程还存在,但是Activiyt已经被回收了,需要重新执行Activity的初始化、布局加载和绘制。有以下几种情况:
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 # 热启动,先按home键,然后再点击图标 ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +287ms # 温启动,先按back键,然后再点击图标 ActivityManager: Displayed com.example.appstartdemo/.MainActivity: +84ms
通过以下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 # 热启动,先按home键,然后再执行启动命令 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 # 温启动,先按back键,然后再执行启动命令 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严苛模式。
Android Studio 性能分析器官网介绍:https://developer.android.google.cn/studio/profile/cpu-profiler?hl=zh
您可以使用 CPU 性能分析器在与应用交互时实时检查应用的 CPU 使用率和线程活动,也可以检查记录的方法轨迹、函数轨迹和系统轨迹的详情。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。
以图形的方式来显示方法跟踪数据,虽然可读性比原数据好很多,但是不适用于运行时间很长的代码,可以使用Flame Chart进行分析。
坐标说明:
颜色说明:
简称火焰图,一个倒置的一个调用图表,用来汇总完全相同的调用栈,将具有相同调用方顺序的完全相同的方法收集起来,并在火焰图中将它们表示为一个较长的横条。
坐标说明:
显示一个调用列表,在该列表中可以追踪到具体方法以及耗时,可以显示精确的时间。
坐标说明:
横坐标参数说明:
参数 | 说明 |
---|---|
Name | 方法的名称,调用者和被调用方法。 |
Total(μs) | 单位:微秒(microsecond)即百万分之一秒(10的负6次秒),简称μs。 该方法调用的代码耗时,加上调用了其他方法的耗时,也可以说该方法的一个整体耗时。 |
Self(μs) | 该方法运行自己的代码消耗的时间。 |
Children Time(μs) | 该方法调用其他方法消耗的时间。 |
可以很方便的找到某个方法的调用栈,在该列表中可以看到有哪些方法调用了自己。
例如loop()
函数的调用,在ActivityThread.main
中被调用。坐标以及参数说明与Top Down一致。
Traceview是android平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到每个方法的执行时间。但是目前Traceview 已弃用。如果使用 Android Studio3.2 或更高版本,则应改为使用 CPU Profiler。
使用系统提供的android.os.Debug
,可以很方便的在代码上进行捕获,需要申请SD卡文件读写权限,默认生成一个.trace文件,保存在SD卡目录下。
# 在Application的构造方法中开始捕获 public class MyApplication extends Application { private final String TAG = getClass().getSimpleName(); public MyApplication() { Log.d(TAG, "MyApplication: "); Debug.startMethodTracing("test_trace"); } } # 在MainActivity.onWindowFocusChanged中结束捕获 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又称严苛模式,严苛模式时一个开发人员工具,它可以检测出我们可能无意间做的事情,并提醒我们,以便我们进行修复。最常用于捕获在主线程上执行IO操作或者网络访问。由系统提供android.os.StrictMode
,分为线程检测策略、VM虚拟机检测策略。
// 线程检测策略 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() // 检查文件读取操作 .detectDiskWrites() // 检查文件写入操作 .detectNetwork() // 检查IO访问 .detectUnbufferedIo() .detectCustomSlowCalls() .detectResourceMismatches() .detectAll() // 检测以上所有 .penaltyLog() // 打印log信息 .penaltyDialog() // 弹出警告 .penaltyDeath() // 直接奔溃 .build()); // VM虚拟机检测策略 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() // 检测sql泄露 .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); } }
以上的所有方案都只是提高了用户体验,并没有真正的加快启动速度。
应该尽量避免在主线程进行耗时操作,例如:
优化方案:
使用Layout Inspector工具进行分析
会显示当前界面的布局嵌套情况,可以通过进行分析删掉不必要的布局来达到优化的目的。
includeb标签,常用于将布局中的公共部分提取出来供其他layout共用,以实现布局模块化,这在布局编写方便提供了大大的便利。
<include layout="@layout/title_layout" />
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标签用于降低View树的层次来优化Android的布局。可以适用于以下场景:
<?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>
# 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>