Android 性能优化之启动优化

2,459 阅读14分钟

1. 启动优化分类

启动优化分为三类:

  1. 冷启动
  2. 温启动
  3. 热启动

1.1 冷启动

App 首次启动或系统将 App 进程杀死之后启动。

此时,将经历下面的流程:

  1. 加载、启动 App;
  2. 展示一个空白 Window;
  3. 创建 App 进程
    • 创建 Application 对象;
    • 创建主 Activity;
    • Inflating View;
    • View Measure、Layout、Draw;

在以上步骤中,开发者可以干涉的步骤有:

  • 展示一个空白窗口;
  • 创建 Application 对象;
  • 创建主 Activity;
  • Inflating View;
  • View Measure、Layout、Draw;

另外,相比于温启动和热启动,冷启动的过程更复杂,且经历的是完整的步骤,所以只要处理好了冷启动,温启动和热启动自然而然也变好了。

1.2 温启动

App 启动之后,用户将 App 切至后台,过了一会,再切回来,系统将 App 中正在显示的 Activity 杀死,但 App 所在进程依然存在。

此时,主要经历 Activity 的生命周期函数调用:

  1. onCreate();
  2. onStart();
  3. onResume();

1.3 热启动

App 启动之后,用户将 App 切至后台,过了一会,再切回来,App 所在的进程依然存在,App 正在显示的 Activity 未被杀死。

此时,主要经历 Activity 的生命周期函数调用:

  • onResume();

2. 启动优化

由前面的分析可知,在 App 启动过程中,开发者可以干涉的步骤有:

  • 展示一个空白窗口;
  • 创建 Application 对象;
  • 创建主 Activity;
  • Inflating View;
  • View Measure、Layout、Draw;

接下来,咱们就从这些方面讲解如何进行启动优化?

2.1 启动时长测量

启动时间检测的方法有三种:

  1. Android Vitals(Google Play Console);
  2. Logcat
    • By Google(Android Studio 默认提供);
    • Custom(开发者手动埋点);
  3. ADB

2.1.1 Android Vitals

Google Play Console 会提供 App 的启动时间,但这个功能对于广大的中国开发者实际上并没有太大的意义,所以,不赘述。

2.1.2 Logcat

2.1.2.1 By Google(Android Studio 默认提供)

默认情况下,应用启动之后,Android Studio 会显示该 App 当前界面启动时长和启动总时长。此处有两个启动时长的主要原因是:
有时候,当前界面并不是主 Activiy 对应的界面,而是经过跳转之后的界面。因此,此时 Android Studio 就会显示「当前界面启动时长」和「启动总时长」。如果当前界面就是主 Activity 对应的界面,那 Android Studio 只会显示一个时长,因为此时「当前界面启动时长」和「启动总时长」一样。

//1. 当前界面就是主 Activity 对应的界面
2019-09-12 12:10:41.491 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.MainActivity: +1s121ms

//2. 当前界面并不是主 Activiy 对应的界面,而是经过跳转之后的界面
2019-09-12 12:16:13.385 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.second.SecondActivity: +296ms (total +2s224ms)

Android Studio 默认提供的显示当前 App 启动时长的功能对当前设备的上的所有 App 均起作用,所以,如果开发者想要对比竟品和自家 App 启动时长的差异的话,就可以通过此方法。

另外,需要注意的是,Android Studio 默认提供的 App 「启动时长展示功能」日志对应的日志类型是「Verbose」,Tag 是「Displayed」。

2.1.2.2 Custom(开发者手动埋点)

自定义 Application,在自定义 Application 中重写 attachBaseContext() 方法,并在其中记录起始时间,在 MainActivity 中重写 onWindowFocusChanged() 方法,并在其中记录结束时间。

//1. 自定义 Application
public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        LogUtils.recordStartTime();
    }

}

//2. MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        LogUtils.recordEndTime();
    }

}

//3. 起始时间(手动埋点)  
2019-09-12 14:01:53.668 12323-12323/com.smart.a15_start_up_optimization E/Displayed: 486

//4. Android Studio 默认提供  
//系统计算的起始时间比开发者手动埋点计算的时间长的主要原因是系统计算的启动时间包括:
//- 加载、启动 App
//- 展示空白 Window
//- 创建 App 进程
//而开发者手动埋点只包括:
//- 创建 App 进程
2019-09-12 14:01:53.745 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.MainActivity: +885ms

2.1.3 ADB

除了前面的两种方法之外,我们还能通过 ADB 获取指定 Activity 的启动时长。

//1. 语法
adb shell am start -S -W package/activity name

//2. 示例
adb shell am start -S -W com.smart.a15_start_up_optimization/com.smart.a15_start_up_optimization.MainActivity
Stopping: com.smart.a15_start_up_optimization
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.smart.a15_start_up_optimization/.MainActivity }
Status: ok
Activity: com.smart.a15_start_up_optimization/.MainActivity
ThisTime: 479
TotalTime: 479
WaitTime: 505
Complete

//3. Log
//3.1 开发者手动埋点
2019-09-12 16:01:46.069 5315-5315/com.smart.a15_start_up_optimization E/Displayed: 334
//3.2 Android Studio 默认提供
2019-09-12 16:01:46.101 1510-1536/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.MainActivity: +479ms

ThisTime:启动当前 Activity 所用时长。App 启动时,当 App 中显示的界面并不是主 Activiy 对应的界面,而是经过跳转之后的界面时,这个时长(ThisTime)就是当前界面对应的 Activity 的启动所用时长,此时它将跟 TotalTime 不同。App 启动时,当 App 中显示的界面是主 Activiy 对应的界面时,这个时长就是主 Activity 启动所用时长,此时它将跟 TotalTime 相同。

//1. 在 MainActivity 中直接启动 SecondActivity  
//1.1 ADB
adb shell am start -S -W com.smart.a15_start_up_optimization/com.smart.a15_start_up_optimization.MainActivity
Stopping: com.smart.a15_start_up_optimization
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.smart.a15_start_up_optimization/.MainActivity }
Status: ok
Activity: com.smart.a15_start_up_optimization/.second.SecondActivity
ThisTime: 170  //SecondActivity 启动时长
TotalTime: 818  //从加载、启动 App 直至 SecondActivity 启动所用时长
WaitTime: 548  //启动主 Activity 所用时长
Complete
zhangjihuidembp:MyApplication2019CustomView zhangjianhui$ 
//1.2 Log Android Studio 默认提供  
2019-09-12 16:18:36.757 1510-1536/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.second.SecondActivity: +170ms (total +818ms)


TotalTime:启动 App 中第一个能与用户交互的 Activity 所用时长。App 启动时,当 App 中显示的界面并不是主 Activiy 对应的界面,而是经过跳转之后的界面时,这个时长(TotalTime)将跟 ThisTime 不同。App 启动时,当 App 中显示的界面是主 Activiy 对应的界面时,这个时长就是主 Activity 启动所用时长,此时它将跟 ThisTime 相同。

WaitTime:启动第一个 Activity 等待时长,也就是从加载、启动 App 到最终显示主 Activity 所用时长。因此,App 启动时,当 App 中显示的界面并不是主 Activiy 对应的界面,而是经过跳转之后的界面时,三个时长的关系是:

TotalTime > WaitTime > ThisTime

App 启动时,当 App 中显示的界面是主 Activiy 对应的界面时,这个时长就是主 Activity 启动所用时长,此时它将跟 ThisTime 相同。

WaitTime > Total = ThisTime

2.2 启动时间分析工具

Android Studio 提供了两种分析 App 启动时间的分析工具:

  1. traceview;
  2. systrace;

2.2.1 traceview

2.2.1.1 特点
  1. 以图形的形式展示执行时间、调用栈等信息;
  2. 信息全面,包含所有线程;
2.2.1.2 缺点
  1. 运行时开销比较严重,整体都会变慢(展示相关所有线程信息);
  2. 可能会带偏优化方向;
2.2.1.3 使用方法
  1. Debug.startMethodTracing(Constants.TRACE)(开始追踪);
  2. Debug.stopMethodTracing()(停止追踪);
  3. 最终生成的文件在 「sdcard/Android/data/package name/files/xxx.trace」;
  4. 直接在 Android Studio 里面打开生成的 「xxx.trace」 文件查看方法调用情况;
  5. 在 「TopDown」 栏,直接查看每个方法的调用时长,进而找到耗时最多的方法,并进行相应的优化;

举个例子,假如现在想查看 SecondActivity 的 onCreate() 方法执行情况:

//1. SecondActivity
public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //1. 开始追踪
        Debug.startMethodTracing(Constants.TRACE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        showDeviceMemory();
        //2. 结束追踪
        Debug.stopMethodTracing();
    }

    private void showDeviceMemory(){
        ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        int memory = activityManager.getMemoryClass();
        int largeMemory = activityManager.getLargeMemoryClass();
        Log.e(Constants.TAG, "Memory:  " + memory + "  LargeMemory:  " + largeMemory);
        int runtimeTotalMemory = (int)(Runtime.getRuntime().totalMemory() / (1024 * 1024));
        int runtimeFreeMemory = (int)(Runtime.getRuntime().freeMemory() / (1024 * 1024));
        int runtimeMaxMemory = (int)(Runtime.getRuntime().maxMemory() / (1024 * 1024));
        Log.e(Constants.TAG, "Runtime TotalMemory:  " + runtimeTotalMemory +
                "  Runtime FreeMemory:  " + runtimeFreeMemory +
                "  Runtime MaxMemory:  " + runtimeMaxMemory);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

Trace 路径:

Trace 解析:

2.2.2 systrace

2.2.2.1 特点
  1. 轻量级,开销小(只展示埋点的线程);
  2. 直观反映 CPU 利用率;
2.2.2.2 缺点
  1. 轻量级,开销小;
  2. 直观反映 CPU 利用率;
2.2.2.3 使用方法
  1. TraceCompat.beginSection(Constants.TRACE);(开始追踪);
  2. TraceCompat.endSection();(停止追踪);
  3. 启动 App;
  4. 在「终端」执行 python systrace.py --time=10 -o mynewtrace.html;
  5. 操作 App 中想要检测的界面;
  6. 最终生成的文件在 「file:///Users/xxx/Library/Android/sdk/platform-tools/systrace/mynewtrace.html」;
  7. 在 Chrome 浏览器中打开生成的 「mynewtrace.html」 文件查看 CPU、主线程使用情况;
  8. 在 「Alerts」 栏,直接定位可能造成卡顿的原因,并进行相应的优化;

举个例子,假如现在想查看 SixActivity 的 setContentView() 方法执行情况:

//1. SixActivity
public class SixActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //1. 开始追踪
        TraceCompat.beginSection("SixActivity");
        setContentView(R.layout.activity_six);
        //2. 结束追踪
        TraceCompat.endSection();
    }
}

Systrace 解析:

2.3 启动时长优化

2.3.1 展示一个自定义窗口

  1. 定义 WindowBackground;
  2. 定义 Theme;
  3. 在 AndroidManifest 文件中应用 Theme;
  4. 在 Activity 中恢复实际的 Theme;
//1. window_background
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <!-- The background color, preferably the same as your normal theme -->
    <item android:drawable="@android:color/white" />
    <!-- Your product logo - 144dp color version of your app icon -->
    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/bird_woodpecker" />
    </item>
</layer-list>

//2. WindowBackgroundTheme
<style name="WindowBackgroundTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen">
    <item name="android:windowIsTranslucent">false</item>
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowBackground">@drawable/window_background</item>
</style>

//3. 在 AndroidManifest 文件中应用 Theme
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.smart.a15_start_up_optimization">

    <application
        android:name=".framework.MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".third.ThirdActivity"
            android:label="@string/third"
            android:theme="@style/WindowBackgroundTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

//4. 在 Activity 中恢复实际的 Theme
public class ThirdActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setTheme(R.style.AppTheme);
        setContentView(R.layout.activity_third);
    }

}

2.3.2 Application 相关

自定义 Application 启动时,可能存在的问题

  1. 繁重的初始化工作;
  2. 初始化非必须资源;
  3. I/O;
  4. 频繁初始化相同资源;

处理方法

  1. 异步加载(可以通过线程池创建线程,以便重复使用,创建线程的数量根据设备的 CPU 核数动态改变);
  2. 懒加载;
  3. 单例;

举个例子:

//1. 自定义 Application,在主线程执行耗时操作
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing(Constants.APPLICATION_TRACE);
        initData();
        Debug.stopMethodTracing();
    }

    private void initData(){
        //1. 在主线程执行耗时操作
        showDeviceMemory();

        //2. 在自线程执行耗时操作
//        new Thread(){
//            @Override
//            public void run() {
//                showDeviceMemory();
//            }
//        }.start();
    }

    private void showDeviceMemory(){
        ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        int memory = activityManager.getMemoryClass();
        int largeMemory = activityManager.getLargeMemoryClass();
        Log.e(Constants.TAG, "Memory:  " + memory + "  LargeMemory:  " + largeMemory);
        int runtimeTotalMemory = (int)(Runtime.getRuntime().totalMemory() / (1024 * 1024));
        int runtimeFreeMemory = (int)(Runtime.getRuntime().freeMemory() / (1024 * 1024));
        int runtimeMaxMemory = (int)(Runtime.getRuntime().maxMemory() / (1024 * 1024));
        Log.e(Constants.TAG, "Runtime TotalMemory:  " + runtimeTotalMemory +
                "  Runtime FreeMemory:  " + runtimeFreeMemory +
                "  Runtime MaxMemory:  " + runtimeMaxMemory);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

//执行结果(在主线程执行耗时操作):  
2019-09-13 13:51:10.576 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +3s264ms
2019-09-13 13:51:18.192 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +3s333ms
2019-09-13 13:51:24.713 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +3s319ms
//2. 自定义 Application,在子线程执行耗时操作
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing(Constants.APPLICATION_TRACE);
        initData();
        Debug.stopMethodTracing();
    }

    private void initData(){
        //1. 在主线程执行耗时操作
//        showDeviceMemory();

        //2. 在子线程执行耗时操作
        new Thread(){
            @Override
            public void run() {
                showDeviceMemory();
            }
        }.start();
    }

    private void showDeviceMemory(){
        ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        int memory = activityManager.getMemoryClass();
        int largeMemory = activityManager.getLargeMemoryClass();
        Log.e(Constants.TAG, "Memory:  " + memory + "  LargeMemory:  " + largeMemory);
        int runtimeTotalMemory = (int)(Runtime.getRuntime().totalMemory() / (1024 * 1024));
        int runtimeFreeMemory = (int)(Runtime.getRuntime().freeMemory() / (1024 * 1024));
        int runtimeMaxMemory = (int)(Runtime.getRuntime().maxMemory() / (1024 * 1024));
        Log.e(Constants.TAG, "Runtime TotalMemory:  " + runtimeTotalMemory +
                "  Runtime FreeMemory:  " + runtimeFreeMemory +
                "  Runtime MaxMemory:  " + runtimeMaxMemory);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

//执行结果(在子线程执行耗时操作):  
2019-09-13 13:53:41.625 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s390ms
2019-09-13 13:53:44.739 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s228ms
2019-09-13 13:53:47.906 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s237ms
//3. 自定义 Application,在线程池中执行耗时操作
public class MyApplication extends Application {

    //获取当前设备上的可用 CPU 数量
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    //保证最多有四条线程、最少有两条线程在后台运行,以避免 CPU 在后台工作时饱和
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static ExecutorService executorService;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // ★ 1. 手动埋点起点
        //终点在 MainActivity
        LogUtils.recordStartTime();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing(Constants.APPLICATION_TRACE);
        initData();
        Debug.stopMethodTracing();
    }

    private void initData(){
        //1. 在主线程执行耗时操作
//        showDeviceMemory();

        //2. 在自线程执行耗时操作
//        new Thread(){
//            @Override
//            public void run() {
//                showDeviceMemory();
//            }
//        }.start();

        //3. 自定义线程池,以便线程资源可被重复利用
        executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                showDeviceMemory();
            }
        });
    }

    private void showDeviceMemory(){
        ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        int memory = activityManager.getMemoryClass();
        int largeMemory = activityManager.getLargeMemoryClass();
        Log.e(Constants.TAG, "Memory:  " + memory + "  LargeMemory:  " + largeMemory);
        int runtimeTotalMemory = (int)(Runtime.getRuntime().totalMemory() / (1024 * 1024));
        int runtimeFreeMemory = (int)(Runtime.getRuntime().freeMemory() / (1024 * 1024));
        int runtimeMaxMemory = (int)(Runtime.getRuntime().maxMemory() / (1024 * 1024));
        Log.e(Constants.TAG, "Runtime TotalMemory:  " + runtimeTotalMemory +
                "  Runtime FreeMemory:  " + runtimeFreeMemory +
                "  Runtime MaxMemory:  " + runtimeMaxMemory);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

//执行结果(在线程池中执行耗时操作):  
2019-09-16 10:40:39.050 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s487ms
2019-09-16 10:40:43.604 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s246ms
2019-09-16 10:40:48.099 1865-1887/? I/ActivityManager: Displayed com.smart.a15_start_up_optimization/.forth.ForthActivity: +1s377ms

2.3.3 主 Activity 相关

主 Activity 启动时,可能存在的问题

  1. 加载复杂布局;
  2. 直接加载 Bitmap;
  3. 在主线程加载资源;
  4. I/O;
  5. 初始化 Activity 子系统;

处理方法

  1. 减少 View 嵌套层级(Inflating View、View Measure、Layout、Draw);
  2. 不加载不显示的内容;
  3. 延迟加载 Bitmap;
  4. 异步加载;

举个例子:

//1. 嵌套布局实现  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <LinearLayout
        android:id="@+id/login_username_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_large"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_username" />

        <EditText
            android:id="@+id/login_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_username_hint"
            android:inputType="text"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>


    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_medium"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_password" />

        <EditText
            android:id="@+id/login_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_password_hint"
            android:inputType="textPassword"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</LinearLayout>
//2. 未经嵌套实现
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <TextView
        android:id="@+id/login_username_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginTop="@dimen/padding_large"
        android:text="@string/login_username" />

    <EditText
        android:id="@+id/login_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_toRightOf="@id/login_username_label"
        android:background="@null"
        android:hint="@string/login_username_hint"
        android:inputType="text"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />


    <View
        android:id="@+id/login_username_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_username_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login_password_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginTop="@dimen/padding_medium"
        android:text="@string/login_password" />

    <EditText
        android:id="@+id/login_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_medium"
        android:layout_toRightOf="@id/login_password_label"
        android:background="@null"
        android:hint="@string/login_password_hint"
        android:inputType="textPassword"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />

    <View
        android:id="@+id/login_password_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_password_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_password_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</RelativeLayout>

上面两个 XML 布局文件最终实现的效果是一样的,唯一不同的是,前者嵌套层级相对较多,后者没有嵌套层级。当然,这只是在简单的布局文件中,如果是在复杂的布局文件中,这种优化的效果是显而易见的。因此,当主 Activity 中的布局嵌套层级较多时,App 的启动时间将会受到影响,所以,减少布局文件的嵌套层级势在必行。

3. 总结

App 的启动速度是用户对 App 的第一体验,所以,启动很重要。

在 App 的启动过程中,有三个地方开发者是可以进行优化的:

  1. 创建空白窗口;
  2. 创建 Application;
  3. 创建 Activity;

因此,开发者进行启动优化的大方向也就确定了,所以,当检测到 App 启动速度变慢的时候,只要从这三个方面分析就够啦!


参考文档

  1. Launch Time
  2. 爱奇艺Android客户端启动优化与分析
  3. 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」