Android 性能篇 - 內存優化

Android 性能篇 - 內存優化

來自專欄 Android資料庫1 人贊了文章

內存優化是一個程序員的基本功。有時也要切合項目的實際需求來做選擇。

一、解決所有的內存泄漏

內存泄漏概念:

不再使用的對象沒有被回收,就是內存泄露。

1. 單利泄漏

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

例如 :

  • activity 的 content 賦值到單利對象裡面的成員量變數

code:

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

ActivityContext ,當你的 Activity finish(); 之後Activity 這個對象的內存還是在堆中,沒有釋放。

因為單利對象持有Activity 的引用,jvm 認為你這個對象還是在使用中,不敢去 回收掉你的 Activity。那單例什麼時候被回收?

那就只有等到整個進程被回收了,單例才會被回收。

進程殺死(回收):

  • Process.killProcess(Process.myPid())
  • 用戶手動卡片式摧毀 (親測可行)

解決方法:

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

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

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

大家應該很喜歡吧 Handler寫成一個內部類譬如:

private Handler mMainActivityHandler = new Handler(){@Overridepublic 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 的時候會不計入HandlerActivity的引用,可以被回收。
  • Activity OnDestroy 的時候 ,把所有的相關請求終止,並且把消息隊列清空 removeCallbacksAndMessages(null); 防止有數據回調到 UI 層。(當然如果不這麼做,Activity 照樣被回收,但是 Handler 不及時回收而已)

(什麼叫 強引用 軟引用 弱引用 虛引用 ,以及 Handler 的消息驅動模型是怎麼樣子的,這裡就不展開講,本文著重內存泄漏)

當然 AsyncTask 和其它對象內部類也是有這種問題,解決方法同上。

3. 資源使用完未關閉

主要是:

  • 廣播(BraodcastReceiver)動態註冊之後要反註冊,推薦在onStart onStop 對應的生命周期執行。
  • 服務(ServiceStart 之後 記得 Stop。啟動服務時機看需求。一般不建議在 Application 啟動(啟動 Service 耗時基本要100ms+)。
  • io Cursor 流要記得 close,一定要在 finallyclose,防止拋異常沒執行 close ,那就泄漏了。
  • Bitmap 內存大戶,要記得回收 recycle 一下,當然 90% 的場景 Glide 已經幫我們處理的。

4.檢測內存泄漏的工具

當然有時候不能完全在寫代碼的時候規避掉所有的內存泄漏,就要用一些工具檢測一下:

  • LeakCanary
  • Android Studio profile
  • MAT

選自己喜歡的工具,去研究一下。(網上很多教程)

二、圖片壓縮

1. bitmap 壓縮

大家都知道 bitmap 佔用內存很大,用完之後要 recycle 一下。

不知道大家有沒有用過,圖片載入出來內存就爆掉了(OOM)情況,本寶寶就遇到過了(心中一千萬頭草擬嗎奔騰而過)。

首先一張圖片從網路獲下來,從 InputStream 轉成 Bitmap,這個 bitmap 佔了多少內存怎麼計算?

獻上代碼:

Bitmap.getAllocationByteCount();

其實就是 ByteCount = 長* 寬 * 4(假設這裡每一個像素點是是RGB888) 那就是 4 個位元組。也有一個像素點 RGB565 占 3 個位元組,當然占更多位元組的 RGB888 更加高清無碼。起初版本 Glide 使用 RGB565,目前 Glide 4.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;//計算出scaleoptions.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:3786D/TestString: Time consumption:2

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

那我們看一下用 + 號去拼接的位元組碼:

使用+號去拼接字元,jvm 會創建一個臨時的 StringBuilder

25 new #24 <java/lang/StringBuilder>

  • 然後把上次的結果集,通過構造函數傳入,

29 invokespecial #25 <java/lang/StringBuilder.<init>> //調用構造函數,這串符號引用類似 jni 中反調 java的類查找寫法32 aload_3 //將局變數表Slot 3的元素入棧

  • 再拼接本次需要拼接的字元。然後存到局部變數表中,等待下次循環操作。

44 astore_3

  • 然後跳轉編號17 去繼續循環。這時候又重新創建了一個 StringBulider 去拼接。真是啃爹啊。。。

48 goto 17 (-31)

那我們看一下用 StringBuilder 去拼接的位元組碼:

這個很明顯 new StringBulider 位元組碼在循環體外面,所以並沒有循環新建對象。

總結:

通過上面的例子,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 裡面的 ConnectionPoolsocket 復用池)
  • okio SegmentPoolbuffer 復用池)

池的功能:

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

  • 線程池

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>>() {@Overridepublic 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

推薦閱讀:

快速排序演算法的優化思路總結
【厚積薄發】關於LZMA和LZ4壓縮的疑惑解析
document.write 的痛
MySQL性能優化的最佳21條經驗

TAG:Android | 性能優化 |