天天看點

android記憶體不足,Android OutOfMemoryError:記憶體不足問題的排查與解決

程式出現了記憶體不足的問題。經過DDMS的監控,發現作為一個apk才十來M的程式,竟然把系統最多配置設定的128M記憶體占光光。經過一系列曲折的過程,基本查明了所有的問題所在。

記憶體洩露

為了便于了解,這裡把一個對象裡引用另一個對象的現象,稱為“持有”;把垃圾回收器及其動作,統稱為GC;如果A對象持有B對象,B對象又持有C對象,形成一個鍊條樣結構,稱為一個持有鍊。

GC的一個基本方法是“标記-清掃”,就是從堆棧和靜态存儲區出發,周遊所有的對象和它們所持有的對象,形成一個樹形結構(稱為引用樹),對于被周遊到的對象進行标記,最後釋放掉那些沒有被标記的對象。

堆棧和靜态存儲區裡的對象,在這裡通常被稱為GC Roots。根據這篇文章,靜态存儲區存儲存的對象通常有以下幾類:

Class - 由系統類加載器(system class loader)加載的對象

Thread - 活着的線程

Stack Local - Java方法的local變量或參數

JNI Local - JNI方法的local變量或參數

JNI Global - 全局JNI引用

Monitor Used - 用于同步的監控對象

Held by JVM - 用于JVM特殊目的由GC保留的對象,但實際上這個與JVM的實作是有關的。

如果一個對象,直接或者間接地被上面這些GC Roots所持有的話,即使已經沒有用了,也不會被GC清理掉,記憶體洩漏就這樣産生了。

這種不合理的持有鍊通常有以下這些類型:

Android特有的隐式持有

與Java不同地,Android中會發生一些隐式的持有。比如,在一個Activity裡,通過調用findById()、getDrawable()等函數的方式,擷取到一個畫面元素,那麼這個元素就會隐式地持有目前的Activity作為mContext。一般情況下,這樣并沒有問題;但如果這個元素是作為靜态對象定義在Activity的類中,就不會被釋放,進而導緻Activity也不能釋放。

Android記憶體洩漏排查工具:Leak Canary

這個工具引用到項目中後,在程式運作過程中,如果出現了記憶體洩露,就會自動提示在手機螢幕上;點選通知區域中的相應條目,就可以看到洩露的Activity和相應的持有鍊,對于分析記憶體洩漏十分有效。

需要注意的是,因為需要以DebugCompile的方式編譯到程式中去,是以這個庫隻有在Gradle建構的項目中才可以引用,基于Ant的項目就請先遷移到Gradle上去吧。

不合理的圖檔-DPI對應

現在手機螢幕的效果越來越細膩,除了顔色越來越鮮豔之外,像素密度——DPI也越來越高(所謂的視網膜屏)。Android為了正确處理不同DPI下圖檔顯示的效果,規定了程式的圖檔按不同的DPI設計多套,分别分别存放在各自的目錄下的方法。在程式運作時,由系統挑選最合适的目錄進行顯示。

按DPI從小到大,這些目錄分别為: drawable-ldpi(低)→ drawable-mdpi(中)→ drawable-hdpi(高)→ drawable-xhdpi(超高)→ drawable-xxhdpi(超超高)→ ……

DPI并不等同于螢幕分辨率,也不等同于螢幕尺寸,它們之間的關系簡單來說就是: DPI×螢幕尺寸=分辨率。

但是由于這幾年的手機尺寸大部分都在5寸上下(需要兩隻手抱着打電話的那種奇葩沒法說),是以剩下的兩個變量,DPI和分辨率之間是有一個粗略的對應關系的,如下表:

在設計圖檔的時候,如果能按照上面這幾種尺寸分别設計好圖檔,一般來說在分辨率上就不會有太大的偏差。

以上内容其實有點跑題了,那麼圖檔DPI和記憶體占用之間有什麼關系呢?

非常有關系!經過dump heap,找到一些占用記憶體比較兇狠(5M起)的BitMap對象,對其中mData對象進行儲存,并用 XXXXXXX打開(顯示出來),找到這張圖檔後大吃一驚:一個10KB左右的圖檔,在記憶體中竟然占到7MB!

上面這一系列操作的詳細方法點這裡XXXXXXXXXX和這裡XXXXXXXXX(待補充)。 順便吐槽下,IntelliJ比起Android Studio要先進好幾個版本,但是隻要程式稍稍多占用點記憶體的時候,用IntelliJ dump heap完全不行,直接卡死,耽誤了好長時間,最後換了Android才完成。

言歸正傳,為什麼一張圖檔會占用這麼大的記憶體空間?有些老舊的項目,最初不注意DPI的問題,甚至就直接把所有的圖檔放在drawable下。這種情況下,其實是當作mdpi來處理的。但現在的手機螢幕,動辄都是xhdpi起,和mdpi之間相差兩三個檔次。