android項目中為了界面的展示和效果详拙,不可避免的用到圖片和動畫,所以我會分三個模塊來講解自己所知道的圖像處理和各種的動畫視覺顯示,學(xué)習(xí)中诲侮,虛心接受大神們的建議。
本文中主要講解如何對大圖片進(jìn)行壓縮避免OOM(OutOfMemory)異常箱蟆,圖片加載到內(nèi)存中占多大內(nèi)存的問題沟绪,如何避免UI線程阻塞。如果我們不注意這些很容易導(dǎo)致圖片占用大量的可用內(nèi)存導(dǎo)致程序崩潰空猜。出現(xiàn)下面的異常: java.lang.OutofMemoryError: bitmap size exceeds VM budget.
為什么要處理Bitmap
簡單的來說就是三點(diǎn):
1.手機(jī)設(shè)備內(nèi)存有限绽慈,Android設(shè)備對于單個程序至少需要16MB的內(nèi)存,所以要盡量優(yōu)化程序的內(nèi)存辈毯,提高效率坝疼。
2.Bitmap特別消耗內(nèi)存,特別是圖片豐富的程序谆沃。
3.有時候需要一次性加載多張圖片钝凶,例如在ListView, GridView 與ViewPager 等控件中.需要預(yù)加載一些圖片達(dá)到用戶滑動時體驗(yàn)順暢的效果。
獲取大圖像的尺寸
為了減少內(nèi)存的利用唁影,我們在屏幕中展示圖片的時候不需要那么高的分辨率耕陷,只需要根據(jù)控件的大小壓縮展示。BitmapFactory提供了一些解碼(decode)的方法(decodeByteArray, decodeFile, decodeResource等)据沈,用來創(chuàng)建一個Bitmap對象哟沫。們應(yīng)該根據(jù)圖片的來源選擇合適的方法。比如SD卡中的圖片可以使用decodeFile方法锌介,網(wǎng)絡(luò)上的圖片可以使用decodeStream方法嗜诀,資源文件中的圖片可使用decodeResource方法。讓我們可以在加載圖片之前就獲取到圖片的長寬值和MIME類型掏湾,從而根據(jù)情況對圖片進(jìn)行壓縮裹虫。
<small>注意:
一定要設(shè)置BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;
因?yàn)閐ecode方法構(gòu)建bitmap分配內(nèi)存,很容易導(dǎo)致OOM異常融击。只需要知道圖片大小的情形下筑公,可以不完整加載圖片到內(nèi)存.這時候就需要引用.為此每一種解析方法都提供了一個可選BitmapFactory.Options參數(shù),將這個參數(shù)的inJustDecodeBounds屬性設(shè)置為true就可以讓解析方法禁止為bitmap分配內(nèi)存尊浪,返回值也不再是一個Bitmap對象匣屡,而是null封救。雖然Bitmap是null了,但是BitmapFactory.Options的outWidth捣作、outHeight和outMimeType屬性都會被賦值誉结。</small>
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;```
**按照比例壓縮圖片**
已經(jīng)知道圖片的尺寸,可以通過設(shè)置BitmapFactory.Options中inSampleSize的值來按照比例進(jìn)行壓縮券躁,減少占用的內(nèi)存惩坑。下面代碼計算出適當(dāng)?shù)膲嚎s比例。
/*涼菇?jīng)?br>
reqWidth reqHeight目標(biāo)寬高
*/
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源圖片的高度和寬度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 計算出實(shí)際寬高和目標(biāo)寬高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 選擇寬和高中最小的比率作為inSampleSize的值也拜,這樣可以保證最終圖片的寬和高
// 一定都會大于等于目標(biāo)的寬和高以舒。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}```
使用上述方法先你要將BitmapFactory.Options的inJustDecodeBounds屬性設(shè)置為true,解析一次圖片慢哈。然后將BitmapFactory.Options連同期望的寬度和高度一起傳遞到到calculateInSampleSize方法中蔓钟,就可以得到合適的inSampleSize值了。之后再解析一次圖片卵贱,使用新獲取到的inSampleSize值滥沫,并把inJustDecodeBounds設(shè)置為false,就可以得到壓縮后的圖片了键俱。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 將BitmapFactory.Options的inJustDecodeBounds屬性設(shè)置為true兰绣,解析一次圖片
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 計算出 inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 再解析一次圖片,使用新獲取到的inSampleSize值方妖,并把inJustDecodeBounds設(shè)置為false
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}```
壓縮圖片像素是100*100的縮略圖
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.image, 100, 100));```
本篇主要講解一些壓縮圖片的方法狭魂,以后會更新從本地圖庫中讀取所有的圖片裁剪壓縮并且顯示在imagview控件中的實(shí)例。
Bitmap緩存處理
android中處理圖片的基礎(chǔ)類是Bitmap党觅,顧名思義雌澄,就是位圖。占用內(nèi)存的算法如:圖片的widthheightConfig杯瞻。 如果Config設(shè)置為ARGB_8888镐牺,那么上面的Config就是4。一張480320的圖片占用的內(nèi)存就是480320*4 byte魁莉。如果單個bitmap加載到imagview控件中挺簡單的睬涧,但是如果需要一次性的加載大量的圖片,為了避免OOM需要考慮兩個問題旗唁,內(nèi)存回收的問題和圖片緩存的問題畦浓。
1.內(nèi)存問題:為了保證內(nèi)存的使用始終維持在一個合理的范圍,通常會把被移除屏幕的圖片進(jìn)行回收處理检疫。此時垃圾回收器也會認(rèn)為你不再持有這些圖片的引用讶请,從而對這些圖片進(jìn)行GC操作。
2.圖片緩存問題:大量的圖片加載到listview 屎媳、gridview夺溢、viewpager這樣的控件中论巍,需要在滑動的時候界面上快速展示圖片,保證流暢的用戶體驗(yàn)风响,為了避免重復(fù)處理相同的圖片嘉汰,需要用到內(nèi)存緩存機(jī)制∽辞冢可以使用LruCache或者磁盤緩存來處理這個問題鞋怀,快速的加載已經(jīng)處理過得圖片。
Note: 在過去持搜,一種比較流行的內(nèi)存緩存實(shí)現(xiàn)方法是使用軟引用(SoftReference)或弱引用(WeakReference)對Bitmap進(jìn)行緩存接箫,然而我們并不推薦這樣的做法。從Android 2.3 (API Level 9)開始朵诫,垃圾回收機(jī)制變得更加頻繁,這使得釋放軟(弱)引用的頻率也隨之增高薄扁,導(dǎo)致使用引用的效率降低很多剪返。而且在Android 3.0 (API Level 11)之前,備份的Bitmap會存放在Native Memory中邓梅,它不是以可預(yù)知的方式被釋放的脱盲,這樣可能導(dǎo)致程序超出它的內(nèi)存限制而崩潰。
LruCache緩存
LruCache類(在API Level 4的Support Library中也可以找到)特別適合用來緩存Bitmaps日缨,它使用一個強(qiáng)引用(strong referenced)的LinkedHashMap保存最近引用的對象钱反,并且在緩存超出設(shè)置大小的時候剔除最近最少使用到的對象。
當(dāng)加載Bitmap顯示到ImageView 之前匣距,會先從LruCache 中檢查是否存在這個Bitmap面哥。如果確實(shí)存在,它會立即被用來顯示到ImageView上毅待,如果沒有找到尚卫,會觸發(fā)一個后臺線程去處理顯示該Bitmap任務(wù)。
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
// 獲取到可用內(nèi)存的最大值尸红,使用內(nèi)存超出這個值會引起OutOfMemory異常吱涉。
// LruCache通過構(gòu)造函數(shù)傳入緩存值,以KB為單位外里。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用內(nèi)存值的1/8作為緩存的大小怎爵。
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 重寫此方法來衡量每張圖片的大小,默認(rèn)返回圖片數(shù)量盅蝗。
return bitmap.getByteCount() / 1024;
}
};
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}```
注意:有1/8的內(nèi)存空間被用作緩存鳖链。 這意味著在常見的設(shè)備上(hdpi),最少大概有4MB的緩存空間(32/8)风科。如果一個填滿圖片的GridView控件放置在800x480像素的手機(jī)屏幕上撒轮,大概會花費(fèi)1.5MB的緩存空間(800x480x4 bytes)乞旦,因此緩存的容量大概可以緩存2.5頁的圖片內(nèi)容。
**磁盤緩存**
如果listview或者gridview中的圖片量太多题山,使用內(nèi)存緩存還是會報內(nèi)存溢出的問題兰粉,或者狀態(tài)發(fā)生改變的時候比如打電話的時候?qū)е聲和2⑼顺龊笈_,那么內(nèi)存緩存也會被清除顶瞳,恢復(fù)應(yīng)用的時候就會重新處理這些圖片玖姑。所以考慮磁盤緩存,還可以減少那些不再內(nèi)存緩存中的Bitmap的加載次數(shù)慨菱,必須在后臺進(jìn)行緩存焰络,因?yàn)榇疟P緩存讀取圖片的信息比內(nèi)存緩存要慢。
> **Note:**如果圖片會被更頻繁的訪問符喝,使用[ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html)或許會更加合適闪彼,比如在圖庫應(yīng)用中。
這是我直接復(fù)制的別人寫從[Android源碼](https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java)中剝離出來的DiskLruCache协饲。非Google官方編寫畏腕,但獲得官方認(rèn)證。Android Doc中并沒有對DiskLruCache的用法給出詳細(xì)的說明茉稠,但是平時咱們熟悉的網(wǎng)易新聞里面的圖片緩存就用到了DiskLruCache緩存描馅。有興趣的可以詳細(xì)去了解一下,[下載源碼地址](http://download.csdn.net/detail/qq_31927865/9768201)ps:磁盤緩存都是在I/O進(jìn)程中
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
@Override
protected void onCreate(Bundle savedInstanceState) {
//初始化磁盤緩存DiskCacheDir
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // 完成初始化
mDiskCacheLock.notifyAll(); // 等待線程
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后臺緩存處理圖片
@Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { //圖片在磁盤中沒有找到
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// 添加到磁盤中
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// 先添加到內(nèi)存緩存中
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// 再添加到磁盤中
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
//
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
}
// 創(chuàng)建一個獨(dú)特的指定應(yīng)用程序緩存目錄的子目錄而线。嘗試使用外部
//如果沒有安裝,內(nèi)部存儲器铭污。
public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}```
注意:DiskLruCache緩存地址前面通常都會存放在 /sdcard/Android/data/<application package>/cache 這個路徑下面,但同時我們又需要考慮如果這個手機(jī)沒有SD卡膀篮,或者SD正好被移除了的情況嘹狞,因此比較優(yōu)秀的程序都會專門寫一個方法來獲取緩存地址,獲取到的是 /data/data/<application package>/cache 這個路徑各拷。
Bitmap回收機(jī)制
通常Activity或者Fragment在onStop/onDestroy時候就可以釋放圖片資源:在釋放資源時刁绒,需要注意釋放的Bitmap或者相關(guān)的Drawable是否有被其它類引用。如果正常的調(diào)用烤黍,可以通過Bitmap.isRecycled()方法來判斷是否有被標(biāo)記回收知市;而如果是被UI線程的界面相關(guān)代碼使用,就需要特別小心避免回收有可能被使用的資源速蕊,不然有可能拋出系統(tǒng)異常: E/AndroidRuntime: java.lang.IllegalArgumentException: Cannot draw recycled bitmaps 并且該異常無法有效捕捉并處理嫂丙。
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();```
**優(yōu)化Dalvik虛擬機(jī)的堆內(nèi)存分配 **
對于[Android](http://lib.csdn.net/base/android)平臺來說,其托管層使用的Dalvik [Java ](http://lib.csdn.net/base/java)VM從目前的表現(xiàn)來看還有很多地方可以優(yōu)化處理规哲,比如我們在開發(fā)一些大型游戲或耗資源的應(yīng)用中可能考慮手動干涉GC處理跟啤,使用 dalvik.system.VMRuntime類提供的setTargetHeapUtilization方法可以增強(qiáng)程序堆內(nèi)存的處理效率。
當(dāng)然具體原理我們可以參考開源工程,這里我們僅說下使用方法:
private final static float TARGET_HEAP_UTILIZATION = 0.75f;
// 在程序onCreate時就可以調(diào)用
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);```
不過要注意的是隅肥,VMRuntime這個類在2.2以上的版本已經(jīng)去掉了,可能是因?yàn)橐?guī)范統(tǒng)一管理內(nèi)存的原因吧,需要更多地從優(yōu)化程序本身出發(fā)
Manifest中處理
在Manifest.xml文件里面的<application 里面添加Android:largeHeap="true"
簡單粗暴竿奏。這種方法允許應(yīng)用需要耗費(fèi)手機(jī)很多的內(nèi)存空間,但卻是最快捷的解決辦法
寫了這么多需要對大家有所幫助P确拧7盒ァ!
這篇文章參考于http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/index.html