Android 性能優化之啓動優化

1. 啓動優化分類

啓動優化分爲三類:html

  1. 冷啓動
  2. 溫啓動
  3. 熱啓動

1.1 冷啓動

App 首次啓動或系統將 App 進程殺死以後啓動。python

此時,將經歷下面的流程:android

  1. 加載、啓動 App;
  2. 展現一個空白 Window;
  3. 建立 App 進程
    • 建立 Application 對象;
    • 建立主 Activity;
    • Inflating View;
    • View Measure、Layout、Draw;

在以上步驟中,開發者能夠干涉的步驟有:shell

  • 展現一個空白窗口;
  • 建立 Application 對象;
  • 建立主 Activity;
  • Inflating View;
  • View Measure、Layout、Draw;

另外,相比於溫啓動和熱啓動,冷啓動的過程更復雜,且經歷的是完整的步驟,因此只要處理好了冷啓動,溫啓動和熱啓動天然而然也變好了。瀏覽器

1.2 溫啓動

App 啓動以後,用戶將 App 切至後臺,過了一會,再切回來,系統將 App 中正在顯示的 Activity 殺死,但 App 所在進程依然存在。bash

此時,主要經歷 Activity 的生命週期函數調用:架構

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

1.3 熱啓動

App 啓動以後,用戶將 App 切至後臺,過了一會,再切回來,App 所在的進程依然存在,App 正在顯示的 Activity 未被殺死。app

此時,主要經歷 Activity 的生命週期函數調用:異步

  • onResume();

2. 啓動優化

由前面的分析可知,在 App 啓動過程當中,開發者能夠干涉的步驟有:ide

  • 展現一個空白窗口;
  • 建立 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 客戶端啓動速度優化之「垃圾回收」
相關文章
相關標籤/搜索