Android 性能專題篇 - 內存優化

一、解決所有的內存泄漏

1. 單利泄漏

主要原因還是因爲一般情況下單例都是全局的,有時候會引用一些實際生命週期比較短的變量,導致其無法釋放。

例如 :

activity 的 context賦值到單利對象裏面的成員量變量:

private static volatile ClassXX instance;
    private  Context context;
    private ClassXX(Context context) {
        this.context = context;
    }

    public static ClassXX getInstance(Context context) {
        if (instance == null) {
            synchronized (instance) {
                if(instance == null) {
                    instance =  new ClassXX(context);
                }
            }
        }

        return instance;
    }

如果這個Context是 Activity 的 Context,當你的 Activity finish(); 之後Activity 這個對象的內存還是在堆中,沒有釋放。因爲單利對象持有Activity 的引用,jvm 認爲你這個對象還是在使用中,不敢去 回收掉你的 Activity。那單例什麼時候被回收?那就只有等到整個進程被回收了,單例纔會被回收。

進程殺死(回收):
- Process.killProcess(Process.myPid())
- 用戶手動卡片式摧毀 (親測可行)

解決方法:

  • 傳入和單例一樣生命週期的對象,如context.getApplication();
  • 不將 context保存在單例的成員變量裏面。

2. Handler AsyncTask 等內部類的內存泄漏

主要原因是內部類默認持有外部類的引用

private Handler mMainActivityHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
      super.handleMessage(msg);
 }
};

其實包括我也很喜歡,而且一個Activity 對應一個 Handler,每一個 Handler 負責更新本 Activity 的 UI,一對一關係,分工明確。好用到爆炸。
然而 java 內部類是默認持有一個外部類的引用,因爲 jvm 在把.java 源文件編譯成 .class 字節碼的時候,會在默認的構造函數加入外部類的引用。所以我們在內部類中也能訪問外部類的引用。
然後問題就發生了,當前 Handler 持有當前 Activity 的引用,Handler 不釋放,Activity 也別想釋放了。MMP

(爲什麼 Handler 有時候會不會被釋放?)

解決方法:

  • 構造函數傳入Activity 並用 WeakReferencemActivity;弱引用保存下來。 GC 的時候會不計入Handler對Activity的引用,可以被回收。
  • Activity OnDestroy 的時候 ,把所有的相關請求終止,並且把消息隊列清空 removeCallbacksAndMessages(null); 防止有數據回調到 UI 層。(當然如果不這麼做,Activity照樣被回收,但是 Handler 不及時回收而已)

3. 資源使用完未關閉

  • 廣播(BraodcastReceiver)動態註冊之後要反註冊,推薦在onStart onStop 對應的生命週期執行。
  • 服務(Service)Start 之後 記得 Stop。啓動服務時機看需求。一般不建議在 Application 啓動(啓動Service耗時基本要100ms+)。
  • Bitmap 內存大戶,要記得回收 recycle 一下,當然 90% 的場景Glide 已經幫我們處理的。

4.檢測內存泄漏的工具

LeakCanary
Android Studio profile
MAT

圖片壓縮

大家都知道 bitmap 佔用內存很大,用完之後要 recycle 一下。
不知道大家有沒有用過,圖片加載出來內存就爆掉了(OOM)情況,本寶寶就遇到過了(心中一千萬頭草擬嗎奔騰而過)。
首先一張圖片從網絡獲下來,從 InputStream 轉成 Bitmap,這個 bitmap 佔了多少內存怎麼計算?
獻上代碼:
Bitmap.getAllocationByteCount();
其實就是 ByteCount = 長* 寬 * 4(假設這裏每一個像素點是是RGB888) 那就是 4 個字節。也有一個像素點 RGB565 佔 3 個字節,當然佔更多字節的 RGB888 更加高清無碼。起初版本 Glide 使用 RGB565,目前 Glide4.XX 的默認都是 RGB888,當然自己可以配置一下。

爲了解決這個問題一般都是通過下面代碼:

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inJustDecodeBounds = true;  
// 通過這個bitmap獲取圖片的寬和高 
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options); 
float realWidth = options.outWidth;  
float realHeight = options.outHeight;
//計算出scale
options.inSampleSize = scale;  
options.inJustDecodeBounds = false;  
// 注意這次要把options.inJustDecodeBounds 設爲 false,這次圖片是要讀取出來的。
bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);  
  • 先獲取他的圖片大小,根據自己需要的大小計算出縮放比例。(圖片大小都是放在圖片的頭部,這時候不會去加載整張圖片)
  • 進行縮放,得出符合自己的控件尺寸的大小。
    (當然還有些非法的圖片頭部是獲取不出 長* 寬。這時候記得搞個默認的縮放率,防止 OOM)

有時候爲了優化內存,還不如壓縮一張圖片 所節約的內存來的更快。
譬如 一張 1080 * 1920 圖片再乘以 4 等於 7.9 M。
我壓縮到 一張縮略圖 200*200 等於 156KB。瞬間節約了7M 空間。區別真的太大了,頓時內心 一句 MMP 。

三、解決內存抖動
1.String VS StringBuffer VS StringBuilder

大家應該對着三個類都非常熟悉。那就先看代碼:

long time = System.currentTimeMillis();
String s = new String("JAVA");
for(int i = 0 ;i<10000; i++) {
   s = s+"VERSION";
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));
time = System.currentTimeMillis();

StringBuilder s1 = new StringBuilder("JAVA");
for(int i = 0 ;i<10000; i++) {
   s1.append("VERSION");
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));

輸出

D/TestString: Time consumption:3786
D/TestString: Time consumption:2

很明顯使用 StringBuilder 去拼接字符,效率大大快於用加號,我們帶着問題來找原因。

總結:

通過上面的例子,String 的拼接通過一個 for 循環創建了 10000 個 StringBulider,而且用完就拋棄。特別浪費,在內存吃緊的情況下,很容易引起 gc ,導致 App 卡頓。
也許有同學要問 一個 StringBuilder 的空對象才佔堆內存多大?我們來算一算
一個對象 = 對象頭 + 成員屬性
對象頭 = MardWord + Klass= 12個字節 (數組除外)

上圖:
這裏寫圖片描述

MardWord 字段大全:
這裏寫圖片描述

這個 MardWord 怎麼有這麼多鎖狀態,這些鎖狀態又是什麼?
這就要涉及到 synchronized 同步鎖的知識,這個不在本文討論範圍之內。
那麼 StringBulider 的成員屬性有哪些?清單:

static final long serialVersionUID = 4383685877147921099L;
char[] value;
int count;

對象結構圖
這裏寫圖片描述

計算下來:12+8+8+4+24 = 56 個字節 10000 個對象 那就是要 560KB 內存。不小吧。當然我們實際需求不可能一次搞這麼多個對象,但是多個地方都用 String
去玩的話,積少成多,到時候 APP 內存比別人的高出一大截。那就尷尬了..

四、儘量使用 “池”

我們常見的池有

  • 線程池 Lrucache
  • 緩存池 okhttp 裏面的 ConnectionPool (socket 複用池)
  • okio SegmentPool (buffer 複用池)

池的功能:

可以重複利用對象,並且減少內存開銷,內存抖動,cpu 開銷。

1. 線程池

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

儘量使用線程池去跑任務,而不是動不動就先 new Thread 去跑,這樣子線程是得不到複用的。當任務量一大,使用線程池的效率會超乎你想象(具體自己看源碼),畢竟 開啓一個線程 cpu 內存都是有開銷的。

這裏推薦 Rxjava 的第三方庫,一個將 裝飾者模式 玩到上天的 框架,切換線程方便,支持函數式編程 杜絕回調地獄 等等:

Observable.create(new Action1<Emitter<Integer>>() {
@Override
public void call(Emitter<Integer> subscriber) {}
}, Emitter.BackpressureMode.BUFFER)
.subscribeOn(Schedulers.io()) //切換到 io 線程池
.subscribeOn(Schedulers.computation()) //切換 到計算 線程池
.subscribeOn(Schedulers.immediate()) // 使用當前線程
.observeOn(AndroidSchedulers.mainThread()) //切換到 android UI 主線程
.subscribe();

2. Lrucache 緩存池

Lrucache 緩存池:最近最少使用緩存池,底層原理是用 LinkHashMap 實現。

谷歌的 Glide 圖片加載庫,就是使用了 Lrucache,和 LruDiskCache 對圖片進行緩存,進而提高用戶體驗。

3. ConnectionPool 緩存池

ConnectionPool 緩存池 :複用 tcp socket 套接字,進行網絡通訊,每一次 HTTP 請求結束後,並不結束鏈接,可複用於下次的請求。把網絡傳輸速度極致化。

一次 http 請求分:

  • tcp 三次握手
  • 數據傳輸
  • tcp 四次分手

如果每一次請求都經歷整個流程,可能別人所有數據都加載完畢了,我還在握手中… 這就不能忍。
(當然 http 1.1+ 才支持這個鏈接複用,具體詳細源碼 看 OKhttp,本文不做詳細展開)

4. okio SegmentPool (buffer 複用池)

SegmentPool:同上。

總結

對於一些需要 大量頻繁生成和回收的對象,建議使用池,如果沒有輪子,也是可以手動寫一個。

五、其他

  • 常用數據結構優化
  • xml 層級 和 view

1.常用數據結構優化

內存大用戶 : HashMap (及其子類)
HashMap 是一個典型的 空間換時間,時間複雜度趨近 o(1)
佔用空間 是大於 size / 0.75(負載因子),

/**
* hashMap put 部分源碼,
* size 當前已存入數據數目
* threshold = 容量 *0.75
*/

if (++size > threshold)
resize();

通俗點就是 存入100個數據,要佔用 133 個數據內存(及以上),所在數據量較小,或者對速度沒有那麼要求的時候可用 SparseArray(二叉樹實現) 代替。

2.xml 層級 和 view

xml 層級最好控制在 5 層以內。

view 的使用多用:

ViewStub
Include
merge

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