Android圖像與動畫<1>

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, GridViewViewPager 等控件中.需要預(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秃症,一起剝皮案震驚了整個濱河市候址,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌种柑,老刑警劉巖岗仑,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異聚请,居然都是意外死亡荠雕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進(jìn)店門驶赏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舞虱,“玉大人,你說我怎么就攤上這事母市。” “怎么了损趋?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵患久,是天一觀的道長。 經(jīng)常有香客問我浑槽,道長蒋失,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任桐玻,我火速辦了婚禮篙挽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镊靴。我一直安慰自己铣卡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布偏竟。 她就那樣靜靜地躺著煮落,像睡著了一般。 火紅的嫁衣襯著肌膚如雪踊谋。 梳的紋絲不亂的頭發(fā)上蝉仇,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼轿衔。 笑死沉迹,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的害驹。 我是一名探鬼主播鞭呕,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼裙秋!你這毒婦竟也來了琅拌?” 一聲冷哼從身側(cè)響起浪默,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤渔期,失蹤者是張志新(化名)和其女友劉穎四康,沒想到半個月后奶浦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灾杰,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡固棚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年斧抱,在試婚紗的時候發(fā)現(xiàn)自己被綠了哮翘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片徐块。...
    茶點(diǎn)故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡未玻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出胡控,到底是詐尸還是另有隱情扳剿,我是刑警寧澤,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布昼激,位于F島的核電站庇绽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏橙困。R本人自食惡果不足惜瞧掺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望凡傅。 院中可真熱鬧辟狈,春花似錦、人聲如沸夏跷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽槽华。三九已至释簿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間硼莽,已是汗流浹背庶溶。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工煮纵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人偏螺。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓行疏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親套像。 傳聞我的和親對象是個殘疾皇子酿联,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容