天天看點

android開發之詳解ANR與OOM

ANR(Application Not Responding)

ANR定義: 在Android上,如果你的應用程式有一段時間内響應不夠靈敏,系統會向使用者顯示一個對話框,這個對話框稱作為應用程式無響應(ANR: Application Not Responding)對話框.使用者可以選擇”等待”而讓程式繼續運作,也可以選擇”強制關閉”.是以一個流暢的合理的應用程式中不能出現ANR,而讓使用者每次都要處理這個對話框.是以,在程式裡對響應性能的設計很重要,這樣系統不會顯示ANR給使用者.

預設情況下,在Android中Activity的最長執行時間是5秒,BroadcastReceiver的最長執行時間則是10秒.

第一: 為什麼會引發ANR?

在Android裡,應用程式的響應性是由Activity Manager和WindowManager系統服務監視的 。當它監測到以下情況中的一個時,Android就會針對特定的應用程式顯示ANR:

  1. 在5秒内沒有響應輸入的事件(例如,按鍵按下,螢幕觸摸)
  2. BroadcastReceiver在10秒内沒有執行完畢

造成以上兩點的原因有很多,比如在主線程中做了非常耗時的操作,比如說是下載下傳,io異常等。

潛在的耗時操作,例如網絡或資料庫操作,或者高耗時的計算如改變位圖尺寸,應該在子線程裡(或者以資料庫操作為例,通過異步請求的方式)來完成。而不是說你的主線程阻塞在那裡等待子線程的完成——也不是調用 Thread.wait()或是Thread.sleep()。替代的方法是,主線程應該為子線程提供一個Handler,以便完成時能夠送出給主線程。以這種方式設計你的應用程式,将能保證你的主線程保持對輸入的響應性并能避免由于5秒輸入事件的逾時引發的ANR對話框。

第二: 如何避免ANR?

  1. 運作在主線程裡的任何方法都盡可能少做事情。特别是,Activity應該在它的關鍵生命周期方法(如onCreate()和onResume())裡盡可能少的去做建立操作。(可以采用重新開啟子線程的方式,然後使用Handler+Message的方式做一些操作,比如更新主線程中的ui等)
  2. 應用程式應該避免在BroadcastReceiver裡做耗時的操作或計算。但不再是在子線程裡做這些任務(因為 BroadcastReceiver的生命周期短),替代的是,如果響應Intent廣播需要執行一個耗時的動作的話,應用程式應該啟動一個 Service。(此處需要注意的是可以在BroadcastReceiver中啟動Service,但是卻不可以在Service中啟動BroadcastReceiver)
  3. 避免在Intent Receiver裡啟動一個Activity,因為它會建立一個新的畫面,并從目前使用者正在運作的程式上搶奪焦點。如果你的應用程式在響應Intent廣播時需要向使用者展示什麼,你應該使用Notification Manager來實作。

總結:ANR異常也是在程式中自己經常遇到的問題,主要的解決辦法自己最常用的就是不要在主線程中做耗時的操作,而應放在子線程中來實作,比如采用Handler+Mesage的方式,或者是有時候需要做一些和網絡互相互動的耗時操作就采用AsynTask異步任務的方式(它的底層其實Handler+Mesage有所差別的是它是線程池)等,在主線程中更新UI。

OOM(Out Of Memory)

Android oom 有時出現很頻繁,這一般不是Android設計的問題,一般是我們的問題。

出現OOM,無非主要是以下幾個方面造成的:

  一、加載對象過大

  二、相應資源過多,沒有來不及釋放。

解決這樣的問題,也有以下幾個方面:

  一:在記憶體引用上做些處理,常用的有軟引用、強化引用、弱引用

  二:在記憶體中加載圖檔時直接在記憶體中做處理,如:邊界壓縮.

  

  三:優化Dalvik虛拟機的堆記憶體配置設定

  

  四:自定義堆記憶體大小

  五:動态記憶體管理

可真有這麼簡單嗎,不見得,看我娓娓道來:

  一:在記憶體引用上做些處理,常用的有軟引用、強化引用、弱引用

  軟引用(SoftReference)、虛引用(PhantomRefrence)、弱引用(WeakReference),這三個類是對heap中java對象的應用,通過這個三個類可以和gc做簡單的互動,除了這三個以外還有一個是最常用的強引用.

  強引用,例如下面代碼:

  Object o=new Object();

  Object o1=o;

  上面代碼中第一句是在heap堆中建立新的Object對象通過o引用這個對象,第二句是通過o建立o1到new Object()這個heap堆中的對象的引用,這兩個引用都是強引用.隻要存在對heap中對象的引用,gc就不會收集該對象.如果通過如下代碼:

  o=null;   

  o1=null;

  heap中對象有強可及對象、軟可及對象、弱可及對象、虛可及對象和不可到達對象。應用的強弱順序是強、軟、弱、和虛。對于對象是屬于哪種可及的對象,由他的最強的引用決定。如下:

String abc=new String("abc");  //1       
SoftReference<String> abcSoftRef=new SoftReference<String>(abc);  //2       
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //3       
abc=null; //4       
abcSoftRef.clear();//5在此例中,透過 get() 可以取得此 Reference 的所指到的對象,如果傳回值為 null 的話,代表此對象已經被清除。這類的技巧,在設計 Optimizer 或 Debugger 這類的程式時常會用到,因為這類程式需要取得某對象的資訊,但是不可以影響此對象的垃圾收集。
           

  虛引用,就是沒有的意思,建立虛引用之後通過get方法傳回結果始終為null,通過源代碼你會發現,虛引用通向會把引用的對象寫進referent,隻是get方法傳回結果為null.先看一下和gc互動的過程在說一下他的作用. 不把referent設定為null, 直接把heap中的new String(“abc”)對象設定為可結束的(finalizable).與軟引用和弱引用不同, 先把PhantomRefrence對象添加到它的ReferenceQueue中.然後在釋放虛可及的對象. 你會發現在收集heap中的new String(“abc”)對象之前,你就可以做一些其他的事情.通過以下代碼可以了解他的作用.

  雖然這些常見引用,能夠使其gc回收,但是gc又不是非常的智能了,因而oom難免。

  二:在記憶體中加載圖檔時直接在記憶體中做處理

  盡量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource來設定一張大圖,因為這些函數在完成decode後,最終都是通過java層的createBitmap來完成的,需要消耗更多記憶體。

  是以,改用先通過BitmapFactory.decodeStream方法,建立出一個bitmap,再将其設為ImageView的 source,decodeStream最大的秘密在于其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,進而節省了java層的空間。

  如果在讀取時加上圖檔的Config參數,可以更有效減少加載的記憶體,進而更有效阻止抛Out Of Memory異常,另外,decodeStream直接拿的圖檔來讀取位元組碼了,不會根據機器的各種分辨率來自動适應, 使用了decodeStream之後,需要在hdpi和mdpi,ldpi中配置相應的圖檔資源, 否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。

  另外,以下方式也大有幫助:

InputStream is = this.getResources().openRawResource(R.drawable.pic1); 
BitmapFactory.Options options=new BitmapFactory.Options(); 
options.inJustDecodeBounds = false; 
options.inSampleSize = ;   //width,hight設為原來的十分一 
Bitmap btp =BitmapFactory.decodeStream(is,null,options); 

if(!bmp.isRecycle() ){ 
    bmp.recycle()   //回收圖檔所占的記憶體 
    system.gc()  //提醒系統及時回收 
}
           

  以下奉上一個方法,以最省記憶體的方式讀取本地資源的圖檔

public static Bitmap readBitMap(Context context, int resId){ 
    BitmapFactory.Options opt = new BitmapFactory.Options(); 
    opt.inPreferredConfig = Bitmap.Config.RGB_565; 
    opt.inPurgeable = true; 
    opt.inInputShareable = true; 
    //擷取資源圖檔 
    InputStream is = context.getResources().openRawResource(resId); 
    return BitmapFactory.decodeStream(is,null,opt); 
}
           

  昨天在模拟器上給Gallery放入圖檔的時候,出現java.lang.OutOfMemoryError: bitmap size exceeds VM budget 異常,圖像大小超過了RAM記憶體。

  

  模拟器RAM比較小,隻有8M記憶體,當我放入的大量的圖檔(每個100多K左右),就出現上面的原因。

由于每張圖檔先前是壓縮的情況,放入到Bitmap的時候,大小會變大,導緻超出RAM記憶體,具體解決辦法如下:

//解決加載圖檔 記憶體溢出的問題 
//Options 隻儲存圖檔尺寸大小,不儲存圖檔到記憶體 
BitmapFactory.Options opts = new BitmapFactory.Options(); 
//縮放的比例,縮放是很難按準備的比例進行縮放的,其值表明縮放的倍數,SDK中建議其值是的指數值,值越大會導緻圖檔不清晰 
opts.inSampleSize = ; 
Bitmap bmp = null; 
bmp = BitmapFactory.decodeResource(getResources(), mImageIds[position],opts);                              

...               

//回收 
bmp.recycle(); 
           

  關于圖檔的加載和壓縮,詳情請看:http://blog.csdn.net/zanelove/article/details/44278783

  

  通過上面的方式解決了,但是這并不是最完美的解決方式。

  通過一些了解,得知如下:

  三:優化Dalvik虛拟機的堆記憶體配置設定

  對于Android平台來說,其托管層使用的Dalvik JavaVM從目前的表現來看還有很多地方可以優化處理,比如我們在開發一些大型遊戲或耗資源的應用中可能考慮手動幹涉GC處理,使用dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程式堆記憶體的處理效率。當然具體原理我們可以參考開源工程,這裡我們僅說下使用方法: private final static floatTARGET_HEAP_UTILIZATION = 0.75f; 在程式onCreate時就可以調用VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);即可。

  四:Android堆記憶體也可自己定義大小

  對于一些Android項目,影響性能瓶頸的主要是Android自己記憶體管理機制問題,目前手機廠商對RAM都比較吝啬,對于軟體的流暢性來說RAM對性能的影響十分敏感,除了 優化Dalvik虛拟機的堆記憶體配置設定外,我們還可以強制定義自己軟體的對記憶體大小,我們使用Dalvik提供的dalvik.system.VMRuntime類來設定最小堆記憶體為例:

  private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;

  VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE); //設定最小heap記憶體為6MB大小。當然對于記憶體吃緊來說還可以通過手動幹涉GC去處理

  注意了,這個設定dalvik虛拟機的配置的方法對Android4.0 設定無效。

  五:動态記憶體管理

  動态記憶體管理DMM(Dynamic Memory Management)是從Heap中直接配置設定記憶體和回收記憶體。

  有兩種方法實作動态記憶體管理。

  一是: 顯示記憶體管理EMM(Explicit Memory Management)。

在EMM方式,記憶體從Heap中進行配置設定,用完後手動回收。程式使用malloc()函數配置設定整數數組,并使用free()函數釋放配置設定的記憶體。

  二是: 自動記憶體管理AMM(Automatic Memory Management)。

AMM也可叫垃圾回收器(Garbage Collection)。Java程式設計語言實作了AMM,與EMM不同,Run-time system關注已配置設定的記憶體空間,一旦不再使用,立即回收。

  無論是EMM還是AMM,所有的Heap管理計劃都面臨一些共同的問題和前在的缺陷:

  1)内部碎片(Internal Fragmentation)

當記憶體有浪費時,内部碎片出現。因為記憶體請求可導緻配置設定的記憶體塊過大。比如請求128位元組的存儲空間,結果Run-time system配置設定了512位元組。

  2)外部碎片(External Fragmentation)

當一系列的記憶體請求留下了數個有效的記憶體塊,但這些記憶體塊的大小均不能滿足新請求服務,此時出現外部碎片。

  3)基于定位的延遲(Location-based Latency)

延遲問題出現在兩個資料值存儲得相隔很遠,導緻通路時間增加。

  EMM往往比AMM更快。

  EMM與AMM比較表:

——————————————————————————————————————

          EMM                AMM

——————————————————————————————————————

Benefits  尺寸更小、速度更快、易控制    stay focused on domain issues

Costs  複雜、記賬、記憶體洩露、指針懸空    不錯的性能

——————————————————————————————————————

  早期的垃圾回收器非常慢,往往占用50%的執行時間。

  垃圾回收器理論産生于1959年,Dan Edwards在Lisp程式設計語言的開發時實作了第一個垃圾回收器。

  垃圾回收器有三種基本的經典算法:

  1)Reference counting(引用計數)

  基本思想是:當對象建立并指派時該對象的引用計數器置1,每當對象給任意變量指派時,引用記數+1;一旦退出作用域則引用記數-1。一旦引用記數變為0,則該對象可以被垃圾回收。

引用記數有其相應的優勢:對程式的執行來說,每次操作隻需要花費很小塊的時間。這對于不能被過長中斷的實時系統來說有着天然的優勢。

但也有其不足:不能夠檢測到環(兩個對象的互相引用);同時在每次增加或者減少引用記數的時候比較費時間。

在現代的垃圾回收算法中,引用記數已經不再使用。

  2)Mark-sweep(标記清理)

  基本思想是:每次從根集出發尋找所有的引用(稱為活對象),每找到一個,則對其做出标記,當追蹤完成之後,所有的未标記對象便是需要回收的垃圾。

也叫追蹤算法,基于标記并清除。這個垃圾回收步驟分為兩個階段:在标記階段,垃圾回收器周遊整棵引用樹并标記每一個遇到的對象。在清除階段,未标記的對象被釋放,并使其在記憶體中可用。

  3)Copying collection(複制收集)

  基本思想是:将記憶體劃分為兩塊,一塊是目前正在使用;另一塊是目前未用。每次配置設定時使用目前正在使用記憶體,當無可用記憶體時,對該區域記憶體進行标記,并将标記的對象全部拷貝到目前未用記憶體區,這是反轉兩區域,即目前可用區域變為目前未用,而目前未用變為目前可用,繼續執行該算法。

拷貝算法需要停止所有的程式活動,然後開始冗長而繁忙的copy工作。這點是其不利的地方。

  近年來還有兩種算法:

  1)Generational garbage collection(分代)

  其思想依據是:

     (1) 被大多數程式建立的大多數對象有着非常短的生存期。

    (2) 被大多數程式建立的部分對象有着非常長的生存期。

  簡單拷貝算法的主要不足是它們花費了更多的時間去拷貝了一些長期生存的對象。

而分代算法的基本思想是:将記憶體區域分兩塊(或更多),其中一塊代表年輕代,另一塊代表老的一代。針對不同的特點,對年輕一代的垃圾收集更為頻繁,對老代的收集則較少,每次經過年輕一代的垃圾回收總會有未被收集的活對象,這些活對象經過收集之後會增加成熟度,當成熟度到達一定程度,則将其放進老代記憶體塊中。

分代算法很好的實作了垃圾回收的動态性,同時避免了記憶體碎片,是目前許多JVM使用的垃圾回收算法。

  2)Conservative garbage collection(保守)

  哪一種算法最好?答案是沒有最好。

  EMM作為很常用的垃圾回收算法,有5種基本方法:

    1)Table-driven algorithms

    表驅動算法把記憶體分為固定尺寸的塊集合。這些塊使用抽象資料結構進行索引。比如一個bit對應一個塊,用0和1表示是否配置設定。不利因素:位映射依賴于記憶體塊的尺寸;另外,搜尋一系列的空閑記憶體塊可能需要搜尋整個bit映射表,這影響性能。

    2)Sequential fit

    順序适應算法允許記憶體分為不同的尺寸。此算法跟蹤已配置設定和空閑的Heap,标記空閑塊的起始位址和結束位址。它有三種子分類:

  (1) First fit(首次适應)——配置設定找到的第一個适合記憶體請求的塊

  (2) Best fit(最佳适應)——配置設定最适合記憶體請求的塊

  (3) Worst fit(最不适應)——配置設定最大的塊給記憶體請求

  3)Buddy systems

    Buddy systems算法的主要目的是加速已配置設定記憶體在釋放後的合并速度。顯示記憶體管理EMM使用Buddy systems算法可能導緻内部碎片。

  4)Segregated storage

    隔離存儲技術涉及到把Heap分成多個區域(zone),并為每個區域采用不同的記憶體管理計劃。這是很有效的方法。

  5)Sub-allocators

    子配置技術嘗試解決在Run-time System下配置設定大塊記憶體并單獨管理的記憶體配置設定問題。換句話說,程式完全負責自己的私有存儲堆(stockpile)的記憶體配置設定和回收,無需run-time System的幫助。它可能帶來額外的複雜性,但是你可以顯著地提高性能。在1990年的《C Compiler Design》一書中,Allen Holub就極好地利用了Sub-allocators來加速其編譯器的實作。

  注意,顯示記憶體管理EMM必須是靈活的,能夠響應數種不同類型的請求。

  最後,使用EMM還是使用AMM?這是一個Religious question,憑個人喜好。EMM在複雜的開銷下實作了速度和控制。AMM犧牲了性能,但換來了簡單性。

  無論是emm,amm配置設定記憶體,出現了oom的問題,由于加載記憶體過大也是在所難免。

  這就是我對Android的oom的一點看法.

繼續閱讀