Android效能優化 - 記憶體優化

weixin_33850890發表於2017-05-25

效能優化系列閱讀

為什麼記憶體優化?

在一個商業專案中,很有可能因為工程師的疏忽,導致程式碼質量不佳,影響到程式的執行效率,從而讓使用者感知到應用的卡頓、崩潰。而Android開發中,每個Android應用在手機上申請的記憶體空間都是有限的。雖然手機發展越來越快,可申請到的記憶體越來越大,但是也不能大手大腳,隨便浪費應用可使用的記憶體空間。記憶體一旦不夠時,你這個應用就會因為OOM(out of memory)而崩潰。因此,記憶體優化這一塊內容,在開發應用時是非常重要的。

1. 記憶體優化的關鍵點—避免記憶體洩露

記憶體優化中非常關鍵的一點,就是避免記憶體洩露。因為記憶體洩露會嚴重的導致記憶體浪費,所以避免記憶體洩露,是記憶體優化中必不可少的。

2. java中的四種引用型別

java引用型別不是指像int、char等這些基本的資料型別。java中的引用型別有四種:強引用、軟引用、弱引用、虛引用。這四種引用型別,它們關於物件的可及性是由強到弱的。

public class ReferenceDemo {

    public static void main(String[] args) {
        // 強引用:物件型別 物件的名字(例項) = 物件的構造方法;
        String str = "abc"; // 常量池
        // String str = new String("abc"); // 堆記憶體

        // 軟引用,當記憶體不足的時候,才會釋放掉它引用的物件
        SoftReference<String> softReference = new SoftReference<String>(str);

        // 弱引用,只要系統產生了GC(垃圾回收),它引用的物件就會被釋放掉
        WeakReference<String> weakReference = new WeakReference<String>(str);

        // 虛引用,實際用的不多,就是判斷物件已被回收

        // PhantomReference<String> phantomReference = new PhantomReference<String>(referent,q);

        str = null;
        System.out.println("強引用:" + str);

        softReference.clear();
        System.out.println("軟引用:" + softReference.get());

        // 通過GC,將String物件回收了,那你引用中的物件也會變成null,gc只回收堆記憶體
        System.gc();
        System.out.println("弱引用:" + weakReference.get());
    }
}

2.1 強引用

最常見的強引用方式如下:

//強引用  物件型別 物件名 = new 物件構造方法();
//比如下列程式碼
String str = new String("abc");

在上述程式碼中,這個str物件就是強可及物件。強可及物件永遠不會被GC回收。它寧願被丟擲OOM異常,也不會回收掉強可及物件。

清除強引用物件中的引用鏈如下:

String str = new String("abc");
//置空
str = null;

2.2 軟應用

軟引用方式如下:

//軟引用SoftReference
SoftReference<String> softReference = new SoftReference<String>(str);

在上述程式碼中,這個str物件就是軟可及物件。當系統記憶體不足時,軟可及物件會被GC回收。

清除軟引用物件中的引用鏈可以通過模擬系統記憶體不足來清除,也可以手動清除,手動清除如下:

SoftReference<String> softReference = new SoftReference<String>(str);
softReference.clear();

2.3 弱引用

弱引用方式如下:

//弱引用WeakReference
WeakReference<String> weakReference = new WeakReference<>(str);

在上述程式碼中,這個str物件就是弱可及物件。當每次GC時,弱可及物件就會被回收。

清除弱引用物件中的引用鏈可以通過手動呼叫gc程式碼來清除,如下:

WeakReference<String> weakReference = new WeakReference<>(str);
System.gc();

當然,也可以通過類似軟引用,呼叫clear()方法也可以。

2.4 虛引用

虛引用方式如下:

//虛引用PhantomReference
PhantomReference phantomReference = new PhantomReference<>(arg0, arg1);

虛引用一般在程式碼中出現的頻率極低,主要目的是為了檢測物件是否已經被系統回收。它在一些用來檢測記憶體是否洩漏的開源專案中使用到過,如LeakCanary。

2.5 補充

  • 一個物件的可及性由最強的那個來決定。
  • System.gc()方法只會回收堆記憶體中存放的物件。
String str = "abc";
//弱引用WeakReference
WeakReference<String> weakReference = new WeakReference<>(str);
System.gc();

像這樣的程式碼,即使gc後,str物件仍然可以通過弱引用拿到。因為像"abc"這種,並沒有存放在堆內 存中,它被存放在常量池裡,所以gc不會去回收。

3. 記憶體洩露的原因

對無用物件的引用一直未被釋放,就會導致記憶體洩露。如果物件已經用不到了,但是因為疏忽,導致程式碼中對該無用物件的引用一直沒有被清除掉,就會造成記憶體洩露。

比如你按back鍵關掉了一個Activity,那麼這個Activity頁面就暫時沒用了。但是某個後臺任務如果一直持有著對該Activity物件的引用,這個時候就會導致記憶體洩露。

4. 檢測記憶體洩露—LeakCanary

在全球最大的同性交友網站github中,有一個非常流行的開源專案LeakCanary,它能很方便的檢測到當前開發的java專案中是否存在記憶體洩露。

5. LeakCanary的使用

5.1 官方使用文件描述

從LeakCanary的文件描述中,可以得知使用方式,簡單翻譯為如下步驟:

1.在你的專案中,找到moudle級別的build.gradle檔案,並在dependencies標籤里加上以下程式碼:

 dependencies {
    //... 你專案中以前宣告的一些依賴
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
 }

2.在你Android專案中,找到先前寫的Application類(PS:如果沒有,那麼請自行新建並在AndroidManifest中宣告),並新增如下程式碼:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

3.匯入完畢!當你的應用出現記憶體洩露時,LeakCanary會在通知欄上進行通知,注意檢視。下圖是一個LeakCanary檢測到記憶體洩露時的實示例。

LeakCanary檢測到記憶體洩露

5.2 檢測Fragment

上述步驟預設會檢測Activity,但是不會去檢測Fragment,如果需要對某個Fragment檢測的話,需要利用到LeakCanary的其他寫法。

首先,在先前的Application類中,改寫為以下程式碼:

public class MyApplication extends Application {

    public static RefWatcher mRefWatcher;

    @Override public void onCreate() {
        super.onCreate();
        //...
        mRefWatcher = LeakCanary.install(this);
        // Normal app init code...
    }
}   

然後在Fragment中的onDestroy方法中,去使用這個靜態的RefWatcher進行觀察,如果onDestroy了當前這個Fragment還沒被回收,說明該Fragment產生了記憶體洩露。

@Override
public void onDestroy() {
    super.onDestroy();
    MyApplication.mRefWatcher.watch(this);
}

5.3 檢測某個特定物件

有時候如果需要檢測某個特定的可疑物件在某個時機下是否記憶體洩露,那麼只需要執行如下程式碼

(假如物件名為someObjNeedGced):

//...
RefWatcher refWatcher = MyApplication.refWatcher;
refWatcher.watch(someObjNeedGced);
//...

當執行了refWatcher.watch方法時,如果這個物件還在記憶體中被其他物件引用,就會在 logcat 裡看到記憶體洩漏的提示。

6. LeakCanary的原理簡介

LeakCanary的程式碼執行流程圖如下:

leakCanary

LeakCanary 的機制如下:

  1. RefWatcher.watch() 會以監控物件來建立一個KeyedWeakReference 弱引用物件

  2. AndroidWatchExecutor的後臺執行緒裡,來檢查弱引用已經被清除了,如果沒被清除,則執行一次 GC

  3. 如果弱引用物件仍然沒有被清除,說明記憶體洩漏了,系統就匯出 hprof 檔案,儲存在 app 的檔案系統目錄下

  4. HeapAnalyzerService啟動一個單獨的程式,使用HeapAnalyzer來分析 hprof 檔案。它使用另外一個開源庫 HAHA

  5. HeapAnalyzer 通過查詢KeyedWeakReference 弱引用物件來查詢內在洩漏

  6. HeapAnalyzer計算KeyedWeakReference所引用物件的最短強引用路徑,來分析記憶體洩漏,並且構建出物件引用鏈出來。

  7. 記憶體洩漏資訊送回給DisplayLeakService,它是執行在 app 程式裡的一個服務。然後在裝置通知欄顯示記憶體洩漏資訊。

7. 常見的記憶體洩露

7.1 內部類導致記憶體洩露

內部類例項會隱式的持有外部類的引用。

比如說在Activity中去建立一個內部類例項,然後在內部類例項中去執行一些需要耗時間的任務。任務在執行過程中,將Activity關掉,這個時候Activity物件是不會被釋放的,因為那個內部類還持有著對Activity的引用。但是Activity此時已經是個沒用的Activity了,所有這時,記憶體洩露就出現了。

隱式持有外部類的說明:內部類可以直接去呼叫外部類的方法,如果沒有持有外部類的引用,內部類是沒辦法去呼叫外部類的屬性和方法的,但是內部類又沒有明顯的去指定和宣告引用,所以稱之為隱式引用。

7.1.1 Thread執行緒

在Activity中建立一個內部類去繼承Thread,然後讓該Thread執行一些後臺任務,未執行完時,關閉Activity,此時會記憶體洩露。核心程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startThread();
            }
        });
    }

    private void startThread() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    SystemClock.sleep(1000);
                }
            }
        };
        thread.start();
    }

}

當點選頁面按鈕執行startThread()後,再按下back鍵關閉Activity,幾秒後LeakCanary就會提示記憶體洩露了。

為了避免此種Thread相關記憶體洩露,只需要避免這個內部類去隱式引用外部類Activity即可。

解決方案:讓這個內部類宣告為靜態類。程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...與先前相比未做變化,不再描述
    }

    private void startThread() {
        Thread thread = new MyStaticThread();
        thread.start();
    }

    private static class MyStaticThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 200; i++) {
                SystemClock.sleep(1000);
            }
        }
    }
}

這樣宣告為靜態類後,該內部類將不會再去隱式持有外部類的應用。

如果像這樣的迴圈操作,為了效率和優化,建議通過申明一個boolean型別的標誌位來控制後臺任務。比如在外部類Activity的onDestory退出方法中,將boolean值進行修改,使後臺任務退出迴圈。程式碼如下:

public class MainActivity extends AppCompatActivity {

    ...
    //Activity頁面是否已經destroy
    private static boolean isDestroy = false;

    private static class MyStaticThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if(!isDestroy){
                    SystemClock.sleep(1000);
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isDestroy = true;
    }
}

因為申明為了靜態內部類,該內部類不再持有外部類Activity的引用,所以此時不能再去使用外部類中的方法、變數。<u>除非外部類的那些方法、變數是靜態的</u>。

Q:在防止記憶體洩露的前提下,如果一定要去使用那些外部類中非靜態的方法、變數,該怎麼做?

A:通過使用弱引用或者軟引用的方式,來引用外部類Activity。程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
    }

    private void startThread() {
        Thread thread = new MyStaticThread(MainActivity.this);
        thread.start();
    }

    private  boolean isDestroy = false;//Activity頁面是否已經destroy

    private static class MyStaticThread extends Thread {

        private WeakReference<MainActivity> softReference = null;

        MyStaticThread(MainActivity mainActivity){
            this.softReference = new WeakReference<MainActivity>(mainActivity);
        }

        @Override
        public void run() {
            //能夠isDestroy變數是非靜態的,它屬於MainActivity,我們只要拿到了MainActivity物件,就能拿到isDestroy
            MainActivity mainActivity = softReference.get();
            for (int i = 0; i < 200; i++) {
                //使用前最好對MainActivity物件做非空判斷,如果它已經被回收,就不再執行後臺任務
                if(mainActivity!=null&&!mainActivity.isDestroy){
                    SystemClock.sleep(1000);
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isDestroy = true;
    }
}

7.1.2 Handler

在使用Handler時,經常可以看到有人在Activity、Fragment中寫過內部類形式的Handler,比如說寫一個內部類形式的handler來執行一個延時的任務,像這樣:

public class MainActivity extends AppCompatActivity {

    private static final int MESSAGE_DELAY = 0;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startDelayTask();
            }
        });
    }

    private void startDelayTask() {
        //傳送一條訊息,該訊息會被延時10秒後才處理
        Message message = Message.obtain();
        message.obj = "按鈕點選15秒後再彈出";
        message.what = MESSAGE_DELAY;
        mHandler.sendMessageDelayed(message, 15000);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_DELAY:
                    Toast.makeText(MainActivity.this, (String) msg.obj, Toast.LENGTH_SHORT).show();
                    mButton.setText("延時修改了按鈕的文字");
                    break;
            }
        }
    };
}

當點選了按鈕後會傳送出一條訊息,該訊息將會15秒後再進行處理,如果中途退出Activity,不一會LeakCanary就會檢測到記憶體洩露。

上述程式碼發生記憶體洩露也是因為內部類持有外部類的引用。這個內部類Handler會拿著外部類Activity的引用,而那個Message又拿著Handler的引用。這個Message又要在訊息佇列裡排隊等著被handler中的死迴圈來取訊息。從而形成了一個引用鏈,最後導致關於外部類Activity的引用不會被釋放。

該情況的的解決方案,是與上一節的Thread執行緒相同的。只要將Handler設定為static的靜態內部類方式,就解決了handler持有外部類引用的問題。

如果handler已申明為靜態內部類,那麼Handler就不再持有外部類的引用,無法使用外部類中非靜態的方法、變數了。

如果想在避免記憶體洩露的同時,想使用非靜態的方法、變數,同樣可以用弱(軟)引用來做。

public class MainActivity extends AppCompatActivity {

    private static final int MESSAGE_DELAY = 0;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
    }

    private void startDelayTask() {
        //傳送一條訊息,該訊息會被延時10秒後才處理
        ...
    }

    private Handler mHandler = new InsideHandler(MainActivity.this);

    private static class InsideHandler extends Handler {
        private WeakReference<MainActivity> mSoftReference;

        InsideHandler(MainActivity activity) {
            mSoftReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity mainActivity = mSoftReference.get();
            if (mainActivity != null) {
                switch (msg.what) {
                    case MESSAGE_DELAY:
                        Toast.makeText(mainActivity, (String) msg.obj, Toast.LENGTH_SHORT).show();
                        //通過軟引用中的mainActivity可以拿到那個非靜態的button物件
                        mainActivity.mButton.setText("延時修改了按鈕的文字");
                        break;
                }
            }
        }
    }
}

最後,更完美的做法是在這些做法的基礎上,再新增這段邏輯:當Activity頁面退出時,將handler中的所有訊息進行移除,做到滴水不漏。

其實就是在onDestroy中寫上:

@Override
protected void onDestroy() {
    super.onDestroy();
    //引數為null時,handler中所有訊息和回撥都會被移除
    mHandler.removeCallbacksAndMessages(null);
}

PS:弱引用和軟引用的區別:弱引用會很容易被回收掉,軟引用沒那麼快。如果你希望能儘快清掉這塊記憶體使用就使用弱引用;如果想在記憶體實在不足的情況下才清掉,使用軟引用。

下圖是在內部類Handler使用軟引用時LeakCanary出現的提示。

記憶體洩漏

因為使用軟引用,GC會有點偷懶,所以leakCanary會檢測到一些異常,出現這樣的提示。

7.1.3 非靜態內部類的靜態例項

有時候會使用,程式碼如下:

public class MainActivity extends AppCompatActivity {

    private static User sUser = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
    }

    private void initData() {
        if(sUser==null){
            sUser = new User();
        }
    }

    private class User{
        User(){
        }
    }
}

在程式碼中,非靜態的內部類建立了一個靜態例項。非靜態內部類會持有外部類Activity的引用,後來又建立了一個這個內部類的靜態例項。

這個靜態例項不會在Activity被關掉時一塊被回收(靜態例項的生命週期跟Activity可不一樣,你Activity掛了,但是寫在Activity中的靜態例項還是會在,靜態例項的生命週期跟應用的生命週期一樣長)。

非靜態內部類持有外部引用,而該內部類的靜態例項不會及時回收,所以才導致了記憶體洩露。

解決方案:將內部類申明為靜態的內部類。

public class MainActivity extends AppCompatActivity {

    ...

    private static class User{
        ...
    }
}

7.2 Context導致記憶體洩露

有時候我們會建立一個靜態類,比如說AppManager、XXXManager。這些靜態類可能還是以單例的形式存在。而這些靜態類需要做一個關於UI的處理,所以傳遞了一個Context進來,程式碼如下:

public class ToastManager {
    private Context mContext;
    ToastManager(Context context){
        mContext = context;
    }

    private static ToastManager mManager = null;

    public void showToast(String str){
        if(mContext==null){
            return;
        }
        Toast.makeText(mContext, str, Toast.LENGTH_SHORT).show();
    }

    public static ToastManager getInstance(Context context){
        if(mManager==null){
            synchronized (ToastManager.class){
                if(mManager==null){
                    mManager = new ToastManager(context);
                }
            }
        }
        return mManager;
    }
}

而在使用時是這樣寫的:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        ToastManager instance = ToastManager.getInstance(MainActivity.this);
    }
}

這個時候程式碼也會發生記憶體洩露。因為靜態例項比Activity生命週期長,你在使用靜態類時將Activity作為context引數傳了進來,即時Activity被關掉,但是靜態例項中還保有對它的應用,所以會導致Activity沒法被及時回收,造成記憶體洩露。

解決方案:在傳Context上下文引數時,儘量傳跟Application應用相同生命週期的Context。比如getApplicationContext(),因為靜態例項的生命週期跟應用Application一致。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ToastManager instance = ToastManager.getInstance(getApplicationContext());
    }
}

7.2.1 Context的作用域

系統中的Context的具體實現子類有:Activity、Application、Service。

雖然Context能做很多事,但並不是隨便拿到一個Context例項就可以為所欲為,它的使用還是有一些規則限制的。在絕大多數場景下,Activity、Service和Application這三種型別的Context都是可以通用的。不過有幾種場景比較特殊,比如啟動Activity,還有彈出Dialog。

出於安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert型別的Dialog),因此在這種場景下,我們只能使用Activity型別的Context,否則將會出錯。

Context

上圖中Application和Service所不推薦的兩種使用情況:

1.如果我們用ApplicationContext去啟動一個LaunchMode為standard的Activity的時候會報錯

android.util.AndroidRuntimeException: 
Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. 
Is this really what you want?

這是因為非Activity型別的Context並沒有所謂的任務棧,所以待啟動的Activity就找不到棧了。解決這個問題的方法就是為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK標記位,這樣啟動的時候就為它建立一個新的任務棧,而此時Activity是以singleTask模式啟動的。所有這種用Application啟動Activity的方式不推薦使用,Service的原因跟Application一致。

2.在Application和Service中去layout inflate也是合法的,但是會使用系統預設的主題樣式,如果你自定義了某些樣式可能不會被使用。所以這種方式也不推薦使用。一句話總結:凡是跟UI相關的,都建議使用Activity做為Context來處理;其他的一些操作,Service,Activity,Application等例項Context都可以,當然了,注意Context引用的持有,防止記憶體洩漏。

8. 記憶體優化—減少記憶體使用(Reduce)

如果減少某些不必要記憶體的使用,也可以達到記憶體優化的目的。

比如說Bitmap。它在使用時會花掉較多的記憶體。那我們就可以考慮在應用bitmap時減少某些不必要記憶體的使用。

邊界壓縮

一張拍出來的圖片解析度可能會很大,如果不做壓縮去展示的話,會消耗大量記憶體,可能造成OOM,通過BitmapFactory.Options去設定inSampleSize,可以對圖片進行邊界的壓縮,減少記憶體開銷。

做法:先設定BitmapFactory.inJustDecodeBounds為true,然後decodeFile,這樣將會只去解析圖片大小等資訊,避免了將原圖載入進記憶體。拿到原圖尺寸資訊後,根據業務邏輯換算比例,設定inSampleSize,接著設定BitmapFactory.inJustDecodeBounds為false,最後再去decodeFile,從而實現對圖片邊界大小進行了壓縮再展示。

inSampleSize

色彩壓縮

除此之外,還可以通過設定Bitmap圖片的Config配置來減少記憶體使用。配置有以下四種:

壓縮配置 說明
ALPHA_8 Alpha由8位組成,代表8位Alpha點陣圖
ARGB_4444 由4個4位組成即16位,代表16位ARGB點陣圖
ARGB_8888 由4個8位組成即32位,代表32位ARGB點陣圖,圖片質量最佳
RGB_565 R為5位,G為6位,B為5位,共16位,它是沒有透明度的

如果配置不一樣,需要的記憶體也不同。比如ARGB4444、ARGB8888、RGB565。配置的位數越高,圖片質量越佳,當然需要的記憶體就越多。如果圖片不需要透明度,就採用RGB565的配置。通過Bitmap.Config配置,也可以起到壓縮圖片大小作用。

在實際中,可以通過以下程式碼來進行圖片轉bitmap解碼時的Config。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_menu_add, options);

如果通過在列表中展示縮圖的形式來載入圖片,如果需要檢視高清圖片,另啟動一個頁面(對話方塊)來載入高清圖片,這樣可以避免在列表中載入太多高清圖片,減少記憶體開銷。

9. 記憶體優化—回收(Recycle)

一些資源時使用時記得回收,比如說BraodcastReceiver,ContentObserver,File,Cursor,Stream,BitmapTypeArray等資源的程式碼,應該在使用之後或者Activity銷燬時及時關閉或者登出,否則這些資源可能將不會被回收,造成記憶體洩漏。

10. 記憶體優化—重用(Reuse)

10.1 物件池

在程式裡面經常會遇到的一個問題是短時間內建立大量的物件,導致記憶體緊張,從而觸發GC導致效能問題。對於這個問題,我們可以使用物件池技術來解決它。通常物件池中的物件可能是bitmaps,views,messages等等。

比如說Message.obtain()方法。通過handler去發訊息Message時,通過Message.obtain()來獲得一個訊息,就比直接通過new一個Message要更好。因為Message中內部就維護了一個物件池用來存放訊息,通過obtain方法來取訊息的話,會先從內部的物件池中去取,如果取不到,再去新建立一個訊息進行使用。

關於物件池的操作原理,請看下面的圖示:

object pool

使用物件池技術有很多好處,它可以避免記憶體抖動,提升效能,但是在使用的時候有一些內容是需要特別注意的。通常情況下,初始化的物件池裡面都是空白的,當使用某個物件的時候先去物件池查詢是否存在,如果不存在則建立這個物件然後加入物件池。

但是我們也可以在程式剛啟動的時候就事先為物件池填充一些即將要使用到的資料,這樣可以在需要使用到這些物件的時候提供更快的首次載入速度,這種行為就叫做預分配

使用物件池也有不好的一面,我們需要手動管理這些物件的分配與釋放,所以我們需要慎重地使用這項技術,避免發生物件的記憶體洩漏。為了確保所有的物件能夠正確被釋放,我們需要保證加入物件池的物件和其他外部物件沒有互相引用的關係。

10.2 快取

無論是為了提高CPU的計算速度還是提高資料的訪問速度,在絕大多數的場景下,我們都會使用到快取。

例如快取到記憶體裡面的圖片資源,網路請求返回資料的快取等等。凡是可能需要反覆讀取的資料,都建議使用合適的快取策略。比如圖片三級快取、ListView中的Adapter使用contentView進行復用、使用holder避免重複的findViewById。再比如以下的程式碼,都是快取的體現。

        //原始碼
        for (int i = 0; i < 1024; i++) {
            if(i<getCount()){
                Log.d("TAG", "some log" + i);
            }
        }

        //有快取體現的程式碼,避免重複呼叫1024次getCount方法
        int count = getCount();
        for (int i = 0; i < 1024; i++) {
            if(i<count){
                Log.d("TAG", "some log" + i);
            }
        }

10.2.1 快取中的Lru演算法

lru演算法(Least Recently Use),即最近最少使用演算法,在Android中比較常用。當記憶體超過限定大小時,凡是近時間內最少使用的那一個物件,就會從快取容器中被移除掉。

LRU Cache的基礎構建用法如下:

//往快取中新增圖片,PicUrl是圖片的地址,將其作為key,bitmap點陣圖則作為value
bitmapLRUCache.put(picUrl,bitmap);
//通過picUrl圖片地址,從快取中取bitmap
bitmapLRUCache.get(picUrl);

為了給LRU Cache設定一個比較合理的快取大小值,我們通常是用下面的方法來做界定的:

//當前應用最大可用記憶體
long maxMemory = Runtime.getRuntime().maxMemory();
//建立一個LRUCache,設定快取大小界限為最大可用記憶體的八分之一
BitmapLRUCache bitmapLRUCache = new BitmapLRUCache((int)maxMemory / 8);

使用LRU Cache時為了能夠讓Cache知道每個加入的Item的具體大小,我們需要Override下面的方法:

public class BitmapLRUCache extends LruCache<String,Bitmap> {

    public BitmapLRUCache(int maxSize) {
        super(maxSize);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        int byteCount = value.getByteCount();//該bitmap點陣圖所佔用的記憶體位元組數
        return byteCount;
    }
}

11. 記憶體優化—檢查(Review)

程式碼寫完了只是個開始。比較規範的編碼,都需要Review的。程式碼檢查時的注意點可參考上述內容。

接下來要提到的是UI檢查。

11.1 檢視UI佈局是否過度繪製(overdraw)

檢視的前提是:移動裝置已經開啟了開發者選項

在開發者選項中,點選“除錯GPU過度繪製”,將彈出對話方塊,然後選擇“顯示過度繪製區域”,如下圖所示:

overdraw

螢幕這時候會變得花花綠綠的. 這些顏色是用來幫助你診斷應用程式的顯示行為的。

overdraw02

這些顏色用於表示每個畫素被重繪的次數, 含義如下:

真實顏色: 沒有被重繪

藍色: 重繪一次

綠色: 重繪兩次

粉色: 重繪三次

紅色: 重繪四次或更多次

overdraw03

通過這個工具,可以實現這些事情:

  • 展示一個APP在何處做了不必要的渲染繪製。
  • 幫助你檢視在哪裡可以減少渲染繪製。

有些重繪是不可避免的. 儘量調整APP的使用者介面, 目標是讓大部分的螢幕都是真實的顏色以及重繪一次的藍色。

11.2 檢視UI佈局的渲染速度

檢視的前提是:移動裝置已經開啟了開發者選項

在開發者選項中,點選“GPU呈現模式分析”,將彈出對話方塊,然後選擇“在螢幕上顯示為條形圖”,如下圖所示:

GPU Monitor

這時,將會在螢幕下方出現條形圖,如下圖所示:

GPU Monitor2

該工具會為每個可見的APP顯示一個圖表,水平軸即時間流逝, 垂直軸表示每幀經過的時間,單位是毫秒。

在與APP的互動中, 垂直欄會顯示在螢幕上, 從左到右移動, 隨著時間推移,繪製幀的效能將會迅速體現出來。

綠色的線是用於標記16毫秒的分隔線(PS:人眼的原因, 1秒24幀的動畫才能感到順暢. 所以每幀的時間大概有41ms多一點點(1000ms/24). 但是但是, 注意了, 這41ms不是全都留給你Java程式碼, 而是所有java native 螢幕等等的, 最後留給我們用java級別程式碼發揮的時間, 只有16~17ms),只要有一幀超過了綠線, 你的APP就會丟失一幀。

11.3 檢視UI佈局的層級和實現方式

有的UI介面寫的效率比較低,我們可以通過一些工具來進行UI方面的檢視檢查。Hierarchy Viewer工具可以展示當前手機介面的View層級。

使用該工具的前提是:只能在模擬器或開發版手機上才能用,普通的商業手機是無法連上的。主要是出於安全考慮,普通商業手機中view server這個服務是沒有開啟的. Hierarchy Viewer就無法連線到機器獲取view層級資訊。

PS:如果願意花功夫搗鼓,也可以在真機上強行開啟View Server,詳情見網上資料

先開啟模擬器執行要檢視的頁面,然後開啟Hierarchy Viewer工具,它位於android的sdk所在目錄中,具體位置為...\sdk\tools\hierarchyviewer.bat。開啟後如圖所示:

hierarchy viewer

列表展示手機中已開啟的頁面(包括狀態列等)。這裡以電話應用中的DialtactsActivity為例,雙擊DialtactsActivity,將會開啟關於該頁面的樹狀圖。如下圖所示:

hierarchy viewer tree

圖中標出了3個部分:

  • Tree View:

樹狀圖的形式展示該Activity中的View層級結構。可以放大縮小,每個節點代表一個View,點選可以彈出其屬性的當前值,並且在LayoutView中會顯示其在介面中相應位置。

  • Tree Overview

它是Tree View的概覽圖。有一個選擇框, 可以拖動選擇檢視。選中的部分會在Tree View中顯示

  • Layout View

匹配手機螢幕的檢視,如果在Tree View中點選了某個節點,呢麼這個節點在手機中的真是位置將會在Layout View中以紅框的形式被標出。

接下來介紹點選Tree View中某個節點時,它所展示的資訊類似於下圖:

Tree View Args

下面的三個圓點,依次表示Measure、Layout、Draw,可以理解為對應View的onMeasure,onLayout,onDraw三個方法的執行速度。

  • 綠色:表示該View的此項效能比該View Tree中超過50%的View都要快。
  • 黃色:表示該View的此項效能比該View Tree中超過50%的View都要慢。
  • 紅色:表示該View的此項效能是View Tree中最慢的。

如果介面中的Tree View中紅點較多,那就需要注意了。一般的佈局可能有以下幾點:

  • Measure紅點,可能是佈局中多次巢狀RelativeLayout,或是巢狀的LinearLayout都使用了weight屬性。
  • Layout紅點,可能是佈局層級太深。
  • Draw紅點,可能是自定義View的繪製有問題,複雜計算等。

12. UI佈局優化

12.1 避免過度繪製(Overdraw)

12.2 減少佈局層級

12.3 複用(id、style)

12.4 使用include、merge、viewStub標籤

12.4.1 include標籤

include標籤常用於將佈局中的公共部分提取出來供其他layout共用,以實現佈局模組化,這在佈局編寫上提供了大大的便利。

下面以在一個佈局main.xml中用include引入另一個佈局foot.xml為例。main.mxl程式碼如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ListView
        android:id="@+id/simple_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="@dimen/dp_80" />
    <include layout="@layout/foot.xml" />
</RelativeLayout>

其中include引入的foot.xml為公用的頁面底部,foot.xml程式碼如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/text"/>
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />
</RelativeLayout>

<include>標籤唯一需要的屬性是layout屬性,指定需要包含的佈局檔案。在該標籤中,還可以定義android:id和android:layout_*屬性來覆蓋被引入佈局根節點的對應屬性值。注意重新定義android:id後,子佈局的頂結點i就變化了。

12.4.2 merge標籤

在使用了include後可能導致佈局巢狀過多,多餘不必要的layout節點,從而導致解析變慢,不必要的節點和巢狀可通過上文中提到的hierarchy viewer來檢視。而merge標籤可以消除那些include時不必要的layout節點。

merge標籤可用於兩種典型情況:

  1. 佈局頂結點是FrameLayout且不需要設定background或padding等屬性,可以用merge代替,因為Activity內容試圖的parent view就是個FrameLayout,所以可以用merge消除只剩一個。

  2. 某佈局作為子佈局被其他佈局include時,使用merge當作該佈局的頂節點,這樣在被引入時頂結點會自動被忽略,而將其子節點全部合併到主佈局中

以上一節中的<include>標籤的示例為例,用hierarchy viewer檢視main.xml佈局如下圖:

merge

可以發現多了一層沒必要的RelativeLayout,將foot.xml中RelativeLayout改為merge,如下:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/text"/>
    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />
</merge>

執行後再次用hierarchy viewer檢視main.xml佈局如下圖:

merge02

這樣就不會有多餘的RelativeLayout節點了。

12.4.3 viewStub標籤

viewstub標籤同include標籤一樣可以用來引入一個外部佈局,不同的是,viewstub引入的佈局預設不會擴張,即既不會佔用顯示也不會佔用位置,從而在解析layout時節省cpu和記憶體。

viewstub常用來引入那些預設不會顯示,只在特殊情況下顯示的佈局,如進度佈局、網路失敗顯示的重新整理佈局、資訊出錯出現的提示佈局等。

下面以在一個佈局main.xml中加入網路錯誤時的提示頁面network_error.xml為例。main.mxl程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    ……
    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_error" />
</RelativeLayout>

其中network_error.xml為只有在網路錯誤時才需要顯示的佈局,預設不會被解析,示例程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <Button
        android:id="@+id/network_setting"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/network_setting" />
    <Button
        android:id="@+id/network_refresh"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_below="@+id/network_setting"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/dp_10"
        android:text="@string/network_refresh" />
</RelativeLayout>

在java中通過(ViewStub)findViewById(id)找到ViewStub,通過stub.inflate()展開ViewStub,然後得到子View,如下

private View networkErrorView;
private void showNetError() {
    // not repeated infalte
    if (networkErrorView != null) {
        networkErrorView.setVisibility(View.VISIBLE);
        return;
    }
    ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
    networkErrorView = stub.inflate();
    Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
    Button refresh = (Button)findViewById(R.id.network_refresh);
}
private void showNormal() {
    if (networkErrorView != null) {
        networkErrorView.setVisibility(View.GONE);
    }
}

在上面showNetError()中展開了ViewStub,同時我們對networkErrorView進行了儲存,這樣下次不用繼續inflate。

上面展開ViewStub部分程式碼

ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
networkErrorView = stub.inflate();

也可以寫成下面的形式

View viewStub = findViewById(R.id.network_error_layout);
viewStub.setVisibility(View.VISIBLE);   // ViewStub被展開後的佈局所替換
networkErrorView =  findViewById(R.id.network_error_layout); // 獲取展開後的佈局

兩者效果一致,只是不用顯示的轉換為ViewStub。通過viewstub的原理我們可以知道將一個view設定為GONE不會被解析,從而提高layout解析速度,而VISIBLE和INVISIBLE這兩個可見性屬性會被正常解析。

相關文章