Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 淺談Android應用的內存優化及Handler的內存洩漏問題

淺談Android應用的內存優化及Handler的內存洩漏問題

編輯:關於Android編程

一、Android內存基礎
物理內存與進程內存
物理內存即移動設備上的RAM,當啟動一個Android程序時,會啟動一個Dalvik VM進程,系統會給它分配固定的內存空間(16M,32M不定),這塊內存空間會映射到RAM上某個區域。然後這個Android程序就會運行在這塊空間上。Java裡會將這塊空間分成Stack棧內存和Heap堆內存。stack裡存放對象的引用,heap裡存放實際對象數據。
在程序運行中會創建對象,如果未合理管理內存,比如不及時回收無效空間就會造成內存洩露,嚴重的話可能導致使用內存超過系統分配內存,即內存溢出OOM,導致程序卡頓甚至直接退出。

內存洩露(Memory Leak)
Java內存洩漏指的是進程中某些對象(垃圾對象)已經沒有使用價值了,但是它們卻可以直接或間接地引用到gc roots導致無法被GC回收。Dalvik VM具備的GC機制(垃圾回收機制)會在內存占用過多時自動回收,嚴重時會造成內存溢出OOM。

內存溢出OOM
當應用程序申請的java heap空間超過Dalvik VM HeapGrowthLimit時,溢出。
注意:OOM並不代表內存不足,只要申請的heap超過Dalvik VM HeapGrowthLimit時,即使內存充足也會溢出。效果是能讓較多進程常駐內存。

如果RAM不足時系統會做什麼?
Android的Memory Killer會殺死優先級較低的進程,讓高優先級進程獲取更多內存。

Android系統默認內存回收機制

進程優先級:Foreground進程、Visible進程、Service進程、Background進程、Empty進程;
如果用戶按Home鍵返回桌面,那麼該app成為Background進程;如果按Back返回,則成為Empty進程
ActivityManagerService直接管理所有進程的內存資源分配。所有進程要申請或釋放內存都需要通過ActivityManagerService對象。
垃圾回收不定期執行。當內存不夠時就會遍歷heap空間,把垃圾對象刪除。
堆內存越大,則GC的時間更長

201621162057205.png (439×249)

二、優化
Bitmap優化
Bitmap非常消耗內存,而且在Android中,讀取bitmap時, 一般分配給虛擬機的圖片堆棧只有8M,所以經常造成OOM問題。所以有必要針對Bitmap的使用作出優化:

圖片顯示:加載合適尺寸的圖片,比如顯示縮略圖的地方不要加載大圖。
圖片回收:使用完bitmap,及時使用Bitmap.recycle()回收。
問題:Android不是自身具備垃圾回收機制嗎?此處為何要手動回收。
Bitmap對象不是new生成的,而是通過BitmapFactory生產的。而且通過源碼可發現是通過調用JNI生成Bitmap對象(nativeDecodeStream()等方法)。所以,加載bitmap到內存裡包括兩部分,Dalvik內存和Linux kernel內存。前者會被虛擬機自動回收。而後者必須通過recycle()方法,內部調用nativeRecycle()讓linux kernel回收。
捕獲OOM異常:程序中設定如果發生OOM的應急處理方式。
圖片緩存:內存緩存、硬盤緩存等
圖片壓縮:直接使用ImageView顯示Bitmap時會占很多資源,尤其當圖片較大時容易發生OOM。可以使用BitMapFactory.Options對圖片進行壓縮。
圖片像素:android默認顏色模式為ARGB_8888,顯示質量最高,占用內存最大。若要求不高時可采用RGB_565等模式。圖片大小:圖片長度*寬度*單位像素所占據字節數
ARGB_4444:每個像素占用2byte內存
ARGB_8888:每個像素占用4byte內存 (默認)
RGB_565:每個像素占用2byte內存
對象引用類型

強引用 strong:Object object=new Object()。當內存不足時,Java虛擬機寧願拋出OOM內存溢出異常,也不會輕易回收強引用對象來解決內存不足問題;
軟引用 soft:只有當內存達到某個阈值時才會去回收,常用於緩存;
弱引用 weak :只要被GC線程掃描到了就進行回收;
虛引用
如果想要避免OOM發生,則使用軟引用對象,即當內存快不足時進行回收;如果想盡快回收某些占用內存較大的對象,例如bitmap,可以使用弱引用,能被快速回收。不過如果要對bitmap作緩存就不要使用弱引用,因為很快就會被GC回收,導致緩存失敗。
關於java對象引用類型,具體可參加本人另一篇文章
池 pool

對象池:如果某個對象在創建時,需要較大的資源開銷,那麼可以將其放入對象池,即將對象保存起來,下次需要時直接取出使用,而不用再次創建對象。當然,維護對象池也需要一定開銷,故要衡量。
線程池:與對象池差不多,將線程對象放在池中供反復使用,減少反復創建線程的開銷。

三、Handler內存洩漏分析及解決
1、介紹
首先,請浏覽下面這段handler代碼:

public class SampleActivity extends Activity {
 private final Handler mLeakyHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
   // ... 
  }
 }
}

在使用handler時,這是一段很常見的代碼。但是,它卻會造成嚴重的內存洩漏問題。在實際編寫中,我們往往會得到如下警告:

  In Android, Handler classes should be static or leaks might occur.
那麼,handler是如何造成內存洩漏的呢?

2、分析
(1)、Android角度
當Android應用程序啟動時,framework會為該應用程序的主線程創建一個Looper對象。這個Looper對象包含一個簡單的消息隊列Message Queue,並且能夠循環的處理隊列中的消息。這些消息包括大多數應用程序framework事件,例如Activity生命周期方法調用、button點擊等,這些消息都會被添加到消息隊列中並被逐個處理。
另外,主線程的Looper對象會伴隨該應用程序的整個生命周期。

然後,當主線程裡,實例化一個Handler對象後,它就會自動與主線程Looper的消息隊列關聯起來。所有發送到消息隊列的消息Message都會擁有一個對Handler的引用,所以當Looper來處理消息時,會據此回調[Handler#handleMessage(Message)](http://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message)方法來處理消息。

(2)、Java角度
在java裡,非靜態內部類 和 匿名類 都會潛在的引用它們所屬的外部類。但是,靜態內部類卻不會。

(3)、洩漏來源
請浏覽下面一段代碼:

public class SampleActivity extends Activity {

 private final Handler mLeakyHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
   // ...
  }
 }

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // Post a message and delay its execution for 10 minutes.
  mLeakyHandler.postDelayed(new Runnable() {
   @Override
   public void run() { /* ... */ }
  }, 1000 * 60 * 10);

  // Go back to the previous Activity.
  finish();
 }
}

當activity結束(finish)時,裡面的延時消息在得到處理前,會一直保存在主線程的消息隊列裡持續10分鐘。而且,由上文可知,這條消息持有對handler的引用,而handler又持有對其外部類(在這裡,即SampleActivity)的潛在引用。這條引用關系會一直保持直到消息得到處理,從而,這阻止了SampleActivity被垃圾回收器回收,同時造成應用程序的洩漏。
注意,上面代碼中的Runnable類--非靜態匿名類--同樣持有對其外部類的引用。從而也導致洩漏。

3、洩漏解決方案
首先,上面已經明確了內存洩漏來源:

只要有未處理的消息,那麼消息會引用handler,非靜態的handler又會引用外部類,即Activity,導致Activity無法被回收,造成洩漏;
Runnable類屬於非靜態匿名類,同樣會引用外部類。
為了解決遇到的問題,我們要明確一點:靜態內部類不會持有對外部類的引用。所以,我們可以把handler類放在單獨的類文件中,或者使用靜態內部類便可以避免洩漏。
另外,如果想要在handler內部去調用所在的外部類Activity,那麼可以在handler內部使用弱引用的方式指向所在Activity,這樣統一不會導致內存洩漏。
對於匿名類Runnable,同樣可以將其設置為靜態類。因為靜態的匿名類不會持有對外部類的引用。

public class SampleActivity extends Activity {

 /**
  * Instances of static inner classes do not hold an implicit
  * reference to their outer class.
  */
 private static class MyHandler extends Handler {
  private final WeakReference<SampleActivity> mActivity;

  public MyHandler(SampleActivity activity) {
   mActivity = new WeakReference<SampleActivity>(activity);
  }

  @Override
  public void handleMessage(Message msg) {
   SampleActivity activity = mActivity.get();
   if (activity != null) {
    // ...
   }
  }
 }

 private final MyHandler mHandler = new MyHandler(this);

 /**
  * Instances of anonymous classes do not hold an implicit
  * reference to their outer class when they are "static".
  */
 private static final Runnable sRunnable = new Runnable() {
   @Override
   public void run() { /* ... */ }
 };

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  // Post a message and delay its execution for 10 minutes.
  mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

  // Go back to the previous Activity.
  finish();
 }
}

4、小結
雖然靜態類與非靜態類之間的區別並不大,但是對於Android開發者而言卻是必須理解的。至少我們要清楚,如果一個內部類實例的生命周期比Activity更長,那麼我們千萬不要使用非靜態的內部類。最好的做法是,使用靜態內部類,然後在該類裡使用弱引用來指向所在的Activity。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved