其他分享
首页 > 其他分享> > 性能优化之启动优化

性能优化之启动优化

作者:互联网

安卓性能优化之启动优化

真香定律

2-5-8原则

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

八秒定律

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

启动方式

在应用的启动过程中,有3中启动方式,冷启动、热启动、温启动,每种状态都会影响我们的启动耗时。

冷启动

冷启动是指应用进程不存在,然后从进程创建开始。一般启动优化都是针对冷启动进行优化。包含以下两种情况:

冷启动流程:

  1. 点击Launcher桌面app图标启动程序,调用SystemServer进程的ActivityManagerService.startActivity
  2. ActivityManagerService通知zygote进程fork孵化出应用进程,然后分配内存空间等
  3. 执行该应用ActivityThread的main()方法
  4. 应用程序通知ActivityManagerService它已经启动,ActivityManagerService保存一个该应用的代理对象,ActivityManagerService通过它可以控制应用进程
  5. ActivityManagerService通知应用进程创建入口的Activity实例,执行它的生命周期

在ActivityThread.main方法中,主要执行的初始化工作有:

  1. 反射创建Application
  2. 调用Application.attachBaseContext()
  3. 调用Application.onCreate()
  4. 反射创建Activity
  5. 调用Activity.onCreate()
  6. 调用Activity.onResume()
  7. 调用Activity.onAttachToWindow()
  8. 调用Activity.onWindowFocusChanged()
# 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的初始化、布局加载和绘制。有以下几种情况:

一般情况下,温启动耗时比冷启动要快,比热启动要慢。

启动耗时统计

系统日志

在 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命令来启动并打印耗时。

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严苛模式。

CPU Profile

工具介绍

Android Studio 性能分析器官网介绍:https://developer.android.google.cn/studio/profile/cpu-profiler?hl=zh

您可以使用 CPU 性能分析器在与应用交互时实时检查应用的 CPU 使用率和线程活动,也可以检查记录的方法轨迹、函数轨迹和系统轨迹的详情。CPU 性能分析器记录和显示的详细信息取决于您选择的记录配置:

记录方法跟踪数据时,您可以选择“sampled”或“instrumented”记录方式。记录函数跟踪数据时,只能使用“sampled”记录方式。

使用方式

1. 在AS工具中打开 Run/Debug Configurations 配置界面。

image-20220306191145806

2. 选择app,打开Profiling ,勾选 Start recording CPU activity on startup 选项,选择Trace Java Methods。

image-20220306191218239

几个参数说明:

参数 说明
Simple Java Methods 对java方法进行采样,在应用java代码执行期间,频繁的采集应用调用堆栈,分析器会收集java代码执行的时间和相关的资源使用信息。
Trace Java Methods 跟踪java方法,在应用运行时,在每个方法调用的开始和结束记录一个时间戳,系统会收集并比较这些时间戳,以生成方法跟踪数据,包括时间信息和 CPU 使用率。
Simple C/C++ Functions 对C/C++函数进行采样,捕获应用的原生线程的采样跟踪数据。
Trace System Calls 跟踪系统调用,捕获非常详细的细节,用于检查应用和系统资源的交互情况。可以检测线程状态的确切时间和执行时间,也可以查看所有内核的CPU,并添加自定义跟踪事件。

3. 配置好之后,然后依次点击 Run---Profile

image-20220306191736483

4. 当工具运行起来后,我们可以手动选择停止捕获,然后工具会自动分析并生成文件

image-20220306191909045

5. 点击stop按钮后,会自动生成一份Java Method Trace Record 文件

image-20220306192008044

数据分析

分析数据时,有四种方式可以选择,分别是Call Chart、Flame Chart、Top Down、Bottom Up。

image-20220306192144078

Call Chart

以图形的方式来显示方法跟踪数据,虽然可读性比原数据好很多,但是不适用于运行时间很长的代码,可以使用Flame Chart进行分析。

image-20220306192430537

坐标说明:

颜色说明:

Flame Chart

简称火焰图,一个倒置的一个调用图表,用来汇总完全相同的调用栈,将具有相同调用方顺序的完全相同的方法收集起来,并在火焰图中将它们表示为一个较长的横条。

image-20220306192712585

坐标说明:

Top Down

显示一个调用列表,在该列表中可以追踪到具体方法以及耗时,可以显示精确的时间。

image-20220306192908042

坐标说明:

横坐标参数说明:

参数 说明
Name 方法的名称,调用者和被调用方法。
Total(μs) 单位:微秒(microsecond)即百万分之一秒(10的负6次秒),简称μs。
该方法调用的代码耗时,加上调用了其他方法的耗时,也可以说该方法的一个整体耗时。
Self(μs) 该方法运行自己的代码消耗的时间。
Children Time(μs) 该方法调用其他方法消耗的时间。

Bottom Up

可以很方便的找到某个方法的调用栈,在该列表中可以看到有哪些方法调用了自己。

image-20220306193421584

例如loop()函数的调用,在ActivityThread.main中被调用。坐标以及参数说明与Top Down一致。

Traceview

Traceview是android平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到每个方法的执行时间。但是目前Traceview 已弃用。如果使用 Android Studio3.2 或更高版本,则应改为使用 CPU Profiler。

Debug API

使用系统提供的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是一样的。

image-20220306194120540

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()            // 检查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工具进行分析

image-20220306202622499

会显示当前界面的布局嵌套情况,可以通过进行分析删掉不必要的布局来达到优化的目的。

image-20220306202738586

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的布局。可以适用于以下场景:

<?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>

标签:调用,启动,性能,MainActivity,Activity,onCreate,优化,public
来源: https://www.cnblogs.com/chendd870172464/p/15973359.html