Android性能優化(一)啓動優化

        以前做手機的時候,我非常重視app的性能優化。其實一直以來,在工作中我總會去強調性能優化的重要性。但是,很多時候,由於一些外界因素,我們對app的一些性能指標不會那麼重視。但是,性能優化依然是做好一個產品的重中之重。試想一下,如果用戶費了很多時間和流量下載了我們的app,當人家安裝好啓動app時,卻發現我們的app點了之後,很長時間沒反應。那如果我是用戶,我會二話不說卸載掉。因此,app的性能優化還是很重要且很有必要的,我接下來會總結一下Android性能優化的一些相關技術和知識,這篇博客主要總結一下啓動優化。

一、前言

        在我總結啓動優化之前,我先說些題外話。可能有很多朋友,工作很多年了,也沒接觸過或者沒有實際做過性能優化。我說這個,並不是想展示自己多牛逼,相反,恰恰是因爲自己初入職場時太菜。我自己接觸性能優化算是職業生涯中比較早的時候吧,而這竟完全是因爲自己第一份工作時,寫出了非常爛的代碼。校招入職後一個多月,公司6個月的培養計劃我早就提前執行完了,信心滿滿的我,主動找主管要了一個開發工作,是基於某公司的算法庫實現某種照片美化的功能。當然,那時候還沒有正式排期,只是我先拿到算法庫開始集成。JNI,NDK,查一些資料,最後搭建好了NDK開發環境。用了沒幾天時間,開發好了。拿去給我主管看,我主管看後說,你這速度太慢了,掐着秒錶給我計算,足足7S!!!我當時不解了,做功能不就是把功能做出來就好了嗎,什麼是性能,不知道啊。我主管就讓我去嘗試優化一下,看看能不能優化到1S以內。我當時一聽,7S到1S,雖然我數學不好,但是聽到這個,我心裏還是咯噔了一下,這怎麼可能,這已經是動用了我所有的能力了。好,也就是從那時候開始,正式接觸性能優化,這也伴隨了我很長一段時間的職業生涯。

二、app啓動

        首先,我們引用一下谷歌給出的app啓動的三種方式:冷啓動,熱啓動,溫啓動。啥?app還有這麼多啓動方式,冷,熱,溫,難道是跟啓動app時的環境溫度有關?當然了,這麼說我覺得也是說的過去的。只不過,這個環境,不是我們理解的室內外環境,而是系統環境。

1、冷啓動

        什麼是冷啓動,就是在系統中不存在當前app進程的情況下,點擊app圖標啓動app。比如初次安裝完app啓動app或者清除app數據後啓動app,這樣app的啓動需要經過兩個步驟:(1)application的創建(2)activity生命週期。在當前系統中不存在任何該app的進程實例,不存在任何的activity實例,所以說,當前的系統環境是“冷”的,這樣的app啓動速度也是最慢的。

2、溫啓動

        溫啓動,其啓動速度是介於冷啓動和熱啓動之間的。溫啓動,就是說在application存在的情況下去啓動app,這樣只會走activity的生命週期,也就是冷啓動的第二階段。例如:某些手機系統的app,點擊系統返回鍵退出app,再重新啓動app。這種時候,app的進程還是存在的,只執行activity的生命週期。所以說,當前的系統環境是“溫”的,因爲進程還在。

3、熱啓動

        毫無疑問,這是啓動最快的了。熱啓動,就是在application和activity都存在的情況下啓動app,這樣只會走activity生命週期的一部分。例如,最常見的就是點擊系統home鍵或者recent鍵後再次進入app,其實就是前後臺的切換。所以說呢,當前的系統環境是熱的,因爲我的進程和activity都在。

三、優化方向

        在說優化防線之前,再詳細說一下冷啓動的過程。其實,我們上面說要經過兩個步驟,有點不太準確。在創建application之前,系統還會做一些準備工作。

        (1)創建application前,系統會做一些準備工作,具體如下:啓動app——>創建空白Window——>創建進程。

        (2)創建了application後,接下來的一系列流程如下:創建進程——>啓動主線程——>啓動Activity

        (3)啓動Activity後,我們就知道基本的流程了,那就是執行Activity生命週期,在各個生命週期中加載佈局,展示佈局。

        通過上面對冷啓動流程的總結,毫無疑問,我們優化的方向,就是針對application和Activity來進行,更詳細點,就是針對application和activity的生命週期來進行。

四、啓動時間的測量

1、adb命令

        使用如下adb命令可以獲取app啓動的時間:adb shell am start -W [package]/[.MainActivity]。例如:

adb shell am start -W com.example.tuduoptimize/.MainActivity

        使用上述命令,啓動我的tuduoptimize項目,打印出的信息如下:

       

(1)ThisTime:是打開最後一個Activity的時間。

(2)TotalTime:是打開所有Activity的時間。

(3)WaitTime:AMS啓動activity的總耗時。

2、打印activity啓動時間

        這種方式,其實就是在我們認爲開始啓動的時間點打印當前系統時間,在我們認爲啓動完成後的地方打印當前系統時間,取二者的和,得到啓動耗時。

        對於上面打印系統時間的方法,寫一個工具類,方便我們打印:

package com.example.tuduoptimize;

import android.util.Log;

public class LunchTimeUtil {

    private static final String TAG = "LunchTimeUtil";
    private static long startTime;

    public static void startRecord() {
        startTime = System.currentTimeMillis();
    }

    public static void endRecord(String msg) {
        long costTime = System.currentTimeMillis() - startTime;
        Log.d(TAG, "CostTime:" + costTime + "msg:" + msg);
    }

}

(1)開始啓動

        開始啓動的時間,我們一般放在Application的attachBaseContext方法中。

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        LunchTimeUtil.startRecord();
    }

(2)結束啓動

        這個結束啓動的時間,我們需要根據我們具體的項目來確定。例如我的空項目,只是加載了一個佈局,那麼我放在onWindowsFocusChanged()中。

@Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        LunchTimeUtil.endRecord("onWindowFocusChanged");
    }

五、性能優化工具TraceView

        進行性能優化,需要藉助一些工具,幫助我們分析app啓動耗時以及耗時的地方。相信大家都或多或少的用過或者聽說過一些性能優化工具。在這裏呢,我介紹一下TraceView的使用。

        在使用TraceView分析trace文件之前,我們首先得得到一個trace文件。那麼如何生成trace文件呢?其實很簡單,我們在我們需要生成trace文件的方法中,簡單的兩行代碼即可生成trace文件。例如我在Application的onCreate中,分析initSdk這個方法:

@Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing("tudu");
        initSdk();
        Debug.stopMethodTracing();

    }

        寫好上述兩行代碼後,運行我們的app,即可生成trace文件。生成文件的路徑:Android/data/packagename/file,記得在運行後刷新一下:

        雙擊打開trace文件,進入TraceView主界面:

(1)最上面淺藍色區域:就是我們選取抓trace文件的起止時間段

(2)THREADS:顯示當前所有的線程,後面的長條是耗時

(3)下面的四個tab,可以顯示方法及所耗時間,不同的tab有不同的作用,主要介紹Call Chart和Top Down:

Call Chart:

        大家可以發現顏色不一樣,這個顏色是有講究的:綠色的就是我們自己寫的方法,藍色的是系統的方法。

Top Down:

        在我的測試app中,我寫了四個方法模擬sdk初始化(均讓主線程休眠一段時間),這四個方法又放在了initSdk中,通過這個tab,我們可以很清晰的看到各個方法的耗時。

六、啓動優化

        在這個章節中,介紹幾種啓動優化的技巧。當然,我是以一個demo項目來介紹,這與我們實際的項目開發過程中會有點不同。但是,基本的思路是一樣的。不知道大家有沒有遇到測試提過如下問題:應用啓動白屏(或黑屏時間)太久,希望優化。這個啓動白屏(黑屏)時間太久,就是我們啓動優化要做的工作。

1、閃屏

        什麼是閃屏?上面我們提到過,在application啓動前,系統會創建一個空白的window,而我們的閃屏,就是在這個空白的window上做文章。

        首先,我們看一下優化之前,我的demo運行效果,如下圖所示。可以看到,點擊圖標啓動app後,有2秒多的白屏時間。當然,這個是因爲我在代碼中動了手腳,在Application中模擬耗時操作2秒鐘。

        接下來,我們操作一下,如何通過閃屏來優化這個體驗:

       (1)我們在drawable中創建一個drawable,命名爲splash_bg:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item>
        <bitmap
            android:gravity="center"
            android:src="@drawable/splash" />
    </item>

</layer-list>

        (2)自定義Theme,並且在Manifest中爲我們的MainActivity配置好:

    <style name="lunchTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowBackground">@drawable/splash_bg</item>
    </style>

 

<activity android:name=".MainActivity"
            android:theme="@style/lunchTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

 (3)在MainActivity的onCreate中,手動把Theme修改爲我們原有的Theme:

protected void onCreate(Bundle savedInstanceState) {
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        setContentView(R.layout.activity_main);
    }

        我們看一下優化後的效果,是不是給人一種秒開的錯覺?對,實際上,我們只是把原來的白屏替換爲我們自己定義的一個drawable,並且在activity的onCreate中,再替換爲正確的Theme。但,這樣會讓用戶更好去接受。

 

2、異步優化

        先簡單的介紹一下異步優化,異步優化,顧名思義,就是把線性執行的操作改爲異步執行。例如,我們在主線程中做了2000ms的耗時操作。假如我們創建幾個子線程,讓子線程同步去進行sdk初始化等耗時操作。當然,爲了比較優雅的實現多線程,我們簡單的使用一下線程池。

private void initSdkWithThreadPool(){
        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override
            public void run() {
                initSdk1();
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                initSdk2();
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                initSdk3();
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                initSdk4();
            }
        });
    }

         我們看一下這樣做後的優化效果:

        看到上面的優化結果,大家是不是非常開心?是不是覺得不管有多少個耗時操作,我們都可以使用線程池異步來加載,就能達到上面秒開的效果?不好意思,答案是否定的。在很多情況下,我們是不能簡單地使用線程池來達到我們的優化目的的。那麼,什麼情況呢?答案就是,我們的某些操作必須要在主線程中進行或者我們必須在主線程中用到該操作的某個產物。

        針對上面的答案,可能有些朋友不太明白。我舉個簡單的例子,我們要在splash界面使用initSdk1方法的某個產物,而由於是異步執行,很有可能我們需要這個產物的時候,InitSdk1方法尚未執行完成。那這樣的情況下,肯定會得到我們不想要的結果。例如,我們的initSdk1方法會給我們返回一個String值,而我們會通過Toast展示這個字符串,如下:

private String initSdk1() {
        try {
            //模擬sdk初始化等耗時操作
            Thread.sleep(500);
            result = "sdk1 init success";
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mCountDownLatch.countDown();
        return result;
    }

        如果我們異步執行,並且在異步方法後面去打印Toast,那麼肯定拿不到“sdk1 init success”,拿到的是null,這樣是不對的。那麼,initSdk1這個方法我們就不讓他異步執行了。我們單獨把他拿出來,讓他執行完後,我們再去打印我們的Toast。

initSdk1();
initSdkWithThreadPool();
Toast.makeText(this, result, Toast.LENGTH_SHORT).show();

        這篇文章總結了app啓動優化的一些知識,包括啓動的幾種方式,獲取啓動時間以及啓動優化用到的一個性能分析工具TraceView,並且通過一個簡單的demo總結了兩種啓動優化的方法:閃屏和異步優化。其實閃屏和異步優化只是啓動優化的最常規方式,相信很多一線團隊都有自己的一些啓動優化的獨門祕笈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章