Bitmap 內存優化


Android在加載大背景圖或者大量圖片時,經常導致內存溢出(Out of Memory  Error),本文根據我處理這些問題的經歷及其它開發者的經驗,整理解決方案如下(部分代碼及文字出處無法考證):
 方案一、讀取圖片時注意方法的調用,適當壓縮  盡量不要使用setImageBitmapsetImageResourceBitmapFactory.decodeResource來設置一張大圖,因為這些函數在完成decode后,最終都是通過java層的createBitmap來完成的,需要消耗更多內存。 因此,改用先通過BitmapFactory.decodeStream方法,創建出一個bitmap,再將其設為ImageView的  source,decodeStream最大的秘密在於其直接調用JNI>>nativeDecodeAsset()來完成decode,無需再使用java層的createBitmap,從而節省了java層的空間。

         InputStream is = this.getResources().openRawResource(R.drawable.pic1);

         BitmapFactory.Options options = new  BitmapFactory.Options();

         options.inJustDecodeBounds =  false;

         options.inSampleSize =  10;   // widthhight設為原來的十分一

         Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);


如果在讀取時加上圖片的Config參數,可以跟有效減少加載的內存,從而跟有效阻止拋out of Memory異常。

 

   /**

     *  以最省內存的方式讀取本地資源的圖片

     *  @param context

     *  @param resId

     *  @return

      */

    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);

         }


另 外,decodeStream直接拿圖片來讀取字節碼,  不會根據機器的各種分辨率來自動適應,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相應的圖片資源,  否則在不同分辨率機器上都是同樣大小(像素點數量),顯示出來的大小就不對了。
方案二、在適當的時候及時回收圖片占用的內存  通常Activity或者Fragment在onStop/onDestroy時候就可以釋放圖片資源:  

 if(imageView !=  null &&  imageView.getDrawable() != null){     

      Bitmap oldBitmap =  ((BitmapDrawable) imageView.getDrawable()).getBitmap();    

       imageView.setImageDrawable(null);    

      if(oldBitmap !=  null){    

            oldBitmap.recycle();     

            oldBitmap =  null;   

      }    

 }   

 //  Other code.

 System.gc();


在釋放資源時,需要注意釋放的Bitmap或者相關的Drawable是否有被其它類引用。如果正常的調用,可以通過Bitmap.isRecycled()方 法來判斷是否有被標記回收;而如果是被UI線程的界面相關代碼使用,就需要特別小心避免回收有可能被使用的資源,不然有可能拋出系統異常: E/AndroidRuntime: java.lang.IllegalArgumentException: Cannot draw recycled  bitmaps 並且該異常無法有效捕捉並處理。
方案三、不必要的時候避免圖片的完整加載 只需要知道圖片大小的情形下,可以不完整加載圖片到內存。 在使用BitmapFactory壓縮圖片的時候,BitmapFactory.Options設置inJustDecodeBounds為true后,再使用decodeFile()等方法,可以在不分配空間狀態下計算出圖片的大小。示例:   

 BitmapFactory.Options opts =  new  BitmapFactory.Options();     

 //  設置inJustDecodeBounds為false     

 opts.inJustDecodeBounds = false;    

 //  使用decodeFile方法得到圖片的寬和高    

 BitmapFactory.decodeFile(path,  opts);    

 //  打印出圖片的寬和高

 Log.d("example", opts.outWidth + "," + opts.outHeight);

(ps:原理其實就是通過圖片的頭部信息讀取圖片的基本信息)
方案四、優化Dalvik虛擬機的堆內存分配  堆 (HEAP)是VM中占用內存最多的部分,通常是動態分配的。堆的大小不是一成不變的,通常有一個分配機制來控制它的大小。比如初始的HEAP是4M大, 當4M的空間被占用超過75%的時候,重新分配堆為8M大;當8M被占用超過75%,分配堆為16M大。倒過來,當16M的堆利用不足30%的時候,縮減 它的大小為8M大。重新設置堆的大小,尤其是壓縮,一般會涉及到內存的拷貝,所以變更堆的大小對效率有不良影響。 Heap  Utilization是堆的利用率。當實際的利用率偏離這個百分比的時候,虛擬機會在GC的時候調整堆內存大小,讓實際占用率向個百分比靠攏。使用  dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強程序堆內存的處理效率。  

 private final static float  TARGET_HEAP_UTILIZATION = 0.75f;    

 //  在程序onCreate時就可以調用

 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);


方案五、自定義堆(Heap)內存大小  對 於一些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大小。


但是上面方法還是存在問題,函數setMinimumHeapSize其實只是改變了堆的下限值,它可以防止過於頻繁的堆內存分配,當設置最小堆內存大小超過上限值(Max Heap  Size)時仍然采用堆的上限值,對於內存不足沒什么作用。  
在默認情況下android進程的內存占用量為16M,因為Bitmap他除了java中持有數據外,底層C++的  skia圖形庫還會持有一個SKBitmap對象,因此一般圖片占用內存推薦大小應該不超過8M。這個可以調整,編譯源代碼時可以設置參數。

參考資料:http://www.tuicool.com/articles/yemM7zf

方案六:在Manifest.xml文件里面的<application  里面添加Android:largeHeap="true"

簡單粗暴。這種方法允許應用需要耗費手機很多的內存空間,但卻是最快捷的解決辦法

 

 

這篇文章是我很久以前寫的,感覺寫得太官方了,於是我下定決心想要改得更加貼近大家的思維,於是我將把改版后的推薦給大家。
1
、內存泄漏:

出現對ActivityViewdrawable等類的對象長期持有無用的引用,就會造成被引用的對象無法在GC時回收,而是長期占用堆空間,此時就會發生內存泄漏
簡單來說,就是保留下來卻永遠不再使用的對象引用。

2、內存溢出:

如果應用程序在消耗光了所有的可用堆空間(16M48M),那么再試圖在堆上分配新對象時就會引起OOM(Out Of Memory Error)異常,此時應用程序就會崩潰退出。

3、兩者的區別:

簡單的說,就是內存溢出是占用內存太大,超過了其可以承受的范圍;
內存泄漏是回收不及時甚至是沒有被回收,而在推空間中產生的許多無用的引用。
於是過多的內存泄漏就會導致內存溢出,從而迫使程序崩潰退出。

4、四種不同類型的引用:

引用名稱

Strong Reference(強引用)

Soft Reference (軟引用)

Weak Reference (弱引用)

Phantom Reference (虛引用)

引用特點

通常我們編寫的代碼都是Strong Ref,於此對應的是強可達性,只有去掉強可達,對象才被回收。

對應軟可達性,只要有足夠的內存,就一直保持對象,直到發現內存吃緊且沒有Strong Ref時才回收對象。一般可用來實現緩存,

比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收對象而不必等到內存吃緊的時候

根本不會在內存中保持任何對象,你只能使用Phantom Ref本身。一般用於在進入finalize()方法后進行特殊的清理過程

引用方式

 

通過java.lang.ref.

SoftReference類實現

通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現

通過java.lang.ref.PhantomReference實現

 

 

5、持有Context引用造成的泄漏。

Android應用程序中,很多操作都用到了Context對象,但是大多數都是用來加載和訪問資源的。
這就是為什么所有的顯示控件都需要一個Context對象作為構造方法的參數。
Android應用程序中通常可以使用兩種Context對象:ActivityApplication
當類或方法需要Context對象的時候常見的作法是使用第一個作為Context參數。
但這就意味着View對象對整個activity保持引用,因此也就保持對activity內的所有東西的引用,
也就是整個View結構和它所有的資源都無法被及時的回收,而且對activity的長期引用是比較隱蔽的。

當屏幕方向改變時,Android系統默認作法是會銷毀當前的Activity,然后創建一個新的Activity,這個新的Activity會顯示剛才的狀態。
在這樣做的過程中,Android系統會重新加載UI用到的資源。
現在假設的應用程序中有一個比較大的bitmap類型的圖片,每次旋轉時都重新加載圖片所用的時間較多。
為了提高屏幕旋轉時Activity的創建速度,最簡單的方法是用靜態變量的方法。

這樣的代碼執行起來是快速的,但同時是錯誤的:這樣寫會一直保持着對Activity的引用。
當一個Drawable對象附屬於一個View時,這個View就相當於drawable對象的一個回調(引用)。
在上面的代碼片段中,就意味着drawableTextView存在着引用的關系,
TextView自己持有了對ActivityContext對象)的引用,這個Activity又引用了相當多的東西。

有兩種簡單的方法可以避免由引用context對象造成的內存泄露。
首先第一個方法是避免context對象超出它的作用范圍。
上面的例子展示了靜態引用的情況,但是在類的內部,隱式的引用外部的類同樣的危險。
第二種方法是,使用Application對象。這個context對象會隨着應用程序的存在而存在,而不依賴於activity的生命周期。
如果你打算對context對象保持一個長期的引用,請記住這個application對象。
通過調用Context.getApplicationContext() 或者 Activity.getApplication().方法,你可以很容易的得到這個對象。
如果在activity中使用靜態的類時如果需要引用activity,應該采用WeakReference弱引用來引用Activity

總結一下避免Context泄漏應該注意的問題: 盡量使用Application這種Context類型, 

注意對Context的引用不要超過它本身的生命周期,慎重的對Context使用“static”關鍵字,Context里如果有線程,一定要在onDestroy()里及時停掉。 

為了防止內存泄露,我們應該注意以下幾點:
(1)不要讓生命周期長的對象引用activity context,即保證引用activity的對象要與activity本身生命周期是一樣的.
(2)對於生命周期長的對象,可以使用application context
(3)避免非靜態的內部類,盡量使用靜態類,避免生命周期問題,注意內部類對外部對象引用導致的生命周期變化。

6、線程之間通過Handler通信引起的內存泄漏

Android中線程之間進行通信時最常用的作法是通過接收消息的目標線程所持有Handler對象來創建Message對象,然后再向目標線程發送該Message
在目標線程中Handler在執行handleMessage()時會根據相應Message來執行相應不同功能。
另外一種作法是通過Handler對象向目標線程直接發送Runnable對象來執行該Runnable對象中不同的功能代碼。
在通過Handler進行通信時如果不注意,也很有可能引起內存泄漏。

sendMessage完成之后顯示的將msg成員變量置為null,並且在退出整個應用程序之前,將handler置為null

7、將變量的作用域設置為最小

最小化作用域意味着對垃圾收集器更快的可見。讓我們舉一個例子。
我們有一個變量定義在方法級,當方法執行完畢后,也就是說,控制跳出方法后,則該變 量將不會被引用。
這也就意味着它已經可以被垃圾回收。但是如果該變量是類級的,這時垃圾回收器就需要等待直到對該類的所有的引用都被移除后才能進行垃圾回收。
優化屬性的作用域是必須的。同樣這也是封裝原則。最小化作用域降低了通過包訪問變量並減少了耦合。

8、構造Adapter時,沒有使用緩存的convertView

初始時ListView會從BaseAdapter中根據當前的屏幕布局實例化一定數量的view對象,同時ListView會將這些view對象 緩存起來。
當向上滾動ListView時,原先位於最上面的list itemview對象會被回收,然后被用來構造新出現的最下面的list item
這個構造過程就是由getView()方法完成的,getView()的第二個形參View convertView就是被緩存起來的list itemview對象(初始化時緩存中沒有view對象則convertViewnull)

由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新實例化一個View對象的話,
即浪費時間,也造 成內存垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機將不得不給該應用進程分配更多的內存,造成不必要的內存開支。

9、Bitmap的回收和置空。
Bitmap 對象不在使用時調用 recycle()釋放內存,有時我們會手工的操作 Bitmap 對象,
如果一個Bitmap 對象比較占內存, Bitmap對象在不使用時,我們應該先調用recycle()釋放內存,然后才它設置為null
雖然recycle()從源碼上看,調用它應該能立即釋放Bitmap的主要內存,但是測試結果顯示它並沒能立即釋放內存。
但是我它應該還是能大大的加速Bitmap的主要內存的釋放。

10、資源對象沒關閉造成的內存泄露。

資源性對象比如(CursorFile文件等)往往都用了一些緩沖,我們在不使用的時候,應該及時關閉它們,以便它們的緩沖及時回收內存。
它們的 緩沖不僅存在於java虛擬機內,還存在於java虛擬機外。
如果我們僅僅是把它的引用設置為null,而不關閉它們,往往會造成內存泄露。
因為有些資源 性對象,比如SQLiteCursor(在析構函數finalize(),如果我們沒有關閉它,它自己會調close()關閉),
如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。
因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然后才置為null.
在我們的程序退出時一定要確保我們的資源性對象已經關閉。

程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢Cursor后沒有關閉的情況。
如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會復現內存問題,這樣就會給以后的測試和問題排查帶來困難和風險。

11、各種注冊沒取消。

這種情況造成的內存泄露這種Android的內存泄露比純java的內存泄露還要嚴重,因為其他一些Android程序可能引用我們的Anroid程序的對象(比如注冊機制)。
即使我們的Android程序已經結束了,但是別的引用程序仍然還有對我們的Android程序的某個對象的引用,泄露的內存依然不能被垃圾回收
。比如 假設我們希望在鎖屏界面(LockScreen)中,監聽系統中的電話服務以獲取一些信息(如信號強度等)
則可以在LockScreen中定義一個PhoneStateListener的對象,同時將它注冊到TelephonyManager服務中。
對於LockScreen對象,當需要顯示鎖屏界面的時候就會創建一個LockScreen對象,而當鎖屏界面消失的時候LockScreen對象就會被釋放掉。
但是如果在釋放LockScreen對象的時候忘記取消我們之前注冊的PhoneStateListener對象,則會導致LockScreen無法被垃圾回收。
如果不斷的使鎖屏界面顯示和消失,則最終會由於大量的LockScreen對象沒有辦法被回收而引起OutOfMemory,使得system_process進程掛掉。
雖然有些系統程序,它本身好像是可以自動取消注冊的(當然不及時),但是我們還是應該在我們的程序中明確的取消注冊,程序結束時應該把所有的注冊都取消掉。

12、集合容器對象沒清理造成的內存泄露。

我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,
並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
所以要在退出程序之前,將集合里的東西clear,然后置為null,再退出程序。

13static關鍵字的濫用。

當類的成員變量聲明成static后,它是屬於類的而不是屬於對象的,
如果我們將很大的資源對象(Bitmapcontext等)聲明成static,那么這些資源不會隨着對象的回收而回收,
會一直存在,所以在使用static關鍵字定義成員變量的時候要慎重。

14WebView對象沒有銷毀。

當我們不要使用WebView對象時,應該調用它的destory()函數來銷毀它,並釋放其占用的內存,否則其占用的內存長期也不能被回收,從而造成內存泄露。

15GridView的濫用。

GridViewListView的實現方式不太一樣。GridViewView不是即時創建的,而是全部保存在內存中的。
比如一個GridView100項,雖然我們只能看到10項,但是其實整個100項都是在內存中的。
所以在應用程序退出之前,要講gridViewadapter全部置為null


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM