Android性能優化 - 內存優化

性能優化系列閱讀

爲什麼內存優化?

在一個商業項目中,很有可能因爲工程師的疏忽,導致代碼質量不佳,影響到程序的運行效率,從而讓用戶感知到應用的卡頓、崩潰。而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的引用,所以此時不能再去使用外部類中的方法、變量。除非外部類的那些方法、變量是靜態的

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這兩個可見性屬性會被正常解析。

發佈了273 篇原創文章 · 獲贊 266 · 訪問量 123萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章