編輯:關於Android編程
學習如何處理和加載Bitmap,顯示在UI上非常的重要。如果你不重視這塊,Bitmap講很快耗盡你的內存資源,最終導致oom內存溢出。
移動設備的內存資源很稀缺,很多時候每個應用只能分配到16MB的內存空間。部分機型可能分配的會更多,但是我們必須保證不超過最大內存的限制。 Bitmaps本身就非常占用資源。比如一個Galaxy Nexus拍一張照片2592x1936分辨率。如果使用ARGB_8888(2.3版本以後默認值)加載bitmap的話,加載這張圖將耗費將近19MB(2592*1936*4 bytes)的內存,直接就超過了很多機器的最大內存。 有時候針對ListView, GridView 和 ViewPager 這種控件,我們會有顯示很多圖片的需求,也要為即將可能顯示在屏幕上的圖片做處理,讓圖片為顯示做好准備。圖片什麼大小和形狀都有。經常圖片比你的UI控件大得多。例如攝像頭拍出來的照片比你手機屏幕的分辨率高得多。
BitmapFactory提供了很多通過各種各樣渠道解碼的方法(decodeByteArray(), decodeFile(), decodeResource(), 等等.) 。這些方法都很容易引起oom內存溢出。每個方法都可以通過BitmapFactory.Options這個類去指定解碼選項。把inJustDecodeBounds設定成true,然後進行解碼,這個時候就不會真的去分配內存,而是返回空的bitmap同時也會返回outWidth, outHeight 和 outMimeType.有了這三個值,我們就可以按照需要對圖片進行壓縮。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
除非你對圖片的來源有絕對的信心,不然建議每次解碼都需要檢查圖片的大小和類型。
你需要注意這些問題
- 預估加載整個圖片需要多少內存
- 願意從整個應用中,分配多少內存給這張圖
- 目標UI控件比如ImageView 的大小
- 當前設備的屏幕分辨率和尺寸
例如把一個1024x768的圖片全部加載顯示在一個128x96像素的ImageView.上是沒有價值的。
下面是根據目標控件傳入的寬和高,計算出合適的inSampleSize值的方法。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
使用這個方法,第一次解碼設置inJustDecodeBounds為true,得到合適的inSampleSize後,設置inJustDecodeBounds為false,然後第二次解碼。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
這個方法可以把很大的圖,很方便的展示在很小的ImageView上。
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
如果圖片來源在磁盤或者網絡上(或者其他內存之外的媒介),上面討論的BitmapFactory.decode*方法不應該在主UI線程上執行。加載圖片需要的時間是不可預知的,依賴於很多因素(磁盤讀取速度,網速,圖片大小,CPU速度等)。任何一個工作都可以阻塞UI線程,造成無響應。下面說說用子線程加載bitmaps的問題。
AsyncTask應該是大家都很熟悉的子線程通知UI線程修改的方法。
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
這段代碼很簡單,但是可以看到好的代碼習慣,比如使用軟引用避免AsyncTask的引用導致內存洩漏。判斷是否為空,避免空指針異常。使用代碼如下:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
如果我們使用ListView 和 GridView這種重復利用子控件的UI控件時,如果我們使用上面的AsyncTask,當任務完成的時候 無法保證該子控件是否已經被重用了。此外,任務開始的順序和完成的順序也無法保證。
下面是解決方案,創建一個專門的 Drawable 子類來儲存載入圖片的任務引用。這樣使用BitmapDrawable,當任務完成的時候placeholder 中的圖片就能在ImageView顯示了。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
在執行 BitmapWorkerTask前,創建一個AsyncDrawable 並且綁定到目標 ImageView中:
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
cancelPotentialWork方法用來檢查是否已經有任務綁定到,如果已經有一個任務了就嘗試取消這個任務(調用 cancel()方法)。
在少數情況下,如果新的任務和已經存在任務的數據一樣,則不需要特殊額外的處理。下面是 cancelPotentialWork 方法的一種實現:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
方法 getBitmapWorkerTask(),獲取和 ImageView關聯的任務:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
最後一步是在BitmapWorkerTask執行更新 onPostExecute() 。
檢查任務是否取消了,和當前的任務和 ImageView引用的任務是否為同一個任務。
class BitmapWorkerTask extends AsyncTask {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
這套實現適用於ListView 和 GridView ,也適用於其他有子控件回收機制的控件。簡單的在設置ImageView圖片的地方調用loadBitmap 方法就可以。比如在GridView的getView方法中調用loadBitmap。
加載一張圖片很簡單,加載一大堆圖片就麻煩了,比如 ListView, GridView 或者 ViewPager。LruCache 是官方推薦的緩存圖片的類,低版本可以使用v4支持包。
你的設備可以為每個應用程序分配多大的內存? 設備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進行預加載,因為有可能很快也會顯示在屏幕上? 你的設備的屏幕大小和分辨率分別是多少?一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。 圖片的尺寸和大小,還有每張圖片會占據多少內存空間。 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在內存當中,或者使用多個LruCache 對象來區分不同組的圖片。 你能維持好數量和質量之間的平衡嗎?有些時候,存儲多個低像素的圖片,而在後台去開線程加載高像素的圖片會更加的有效。下面是一個例子:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache. 使用最大可以內存的八分之一作為LruCache的緩存大小
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than 這個是必須實現的,返回的是每個bitmap的大小,每次添加bitmap時都會調用
// number of items.
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);
}
當加載bitmap到ImageView時候,LruCache先檢查如果找到了鍵,就直接更新ImageView,否則在開啟線程處理圖片。
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
BitmapWorkerTask 也需要把新加載的圖片的鍵值對放到緩存中。
class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
如果使用GridView LruCache很容易超出內存限制, DiskLruCache是官方推薦做持久化的緩存類,把圖片緩存到磁盤內避免反復網絡下載圖片也能提高加載速度。
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) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask {
@Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask {
...
// Decode image in background.
@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) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
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;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
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);
}
當圖片加載完畢,圖片應該同時緩存到memory和磁盤中,以便以後加載。
當配置發生變化,例如橫豎屏切換的時候,安卓會銷毀使用新的配置重建activity。為了避免圖片重新處理,我們需要對代碼做一些修改。
這是一個Fragment中保留LruCache不因為配置變化而重新加載的例子。
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
處理圖片是每個開發者都無法避免的大問題,幸運的是已經有很多非常成熟的第三方圖片加載庫。筆者用過市面上主流的四種加載庫。ImageLoader,Picasso,Glide,Fresco,甚至Volley的圖片加載也嘗試過。每個加載庫各有千秋,可以根據需求做選擇,大大提升開發速度,減少oom異常的概率。筆者更推薦來自facebook的Fresco,在使用過程中,漸進式顯示,三級內存在低端機上體驗更好。送上github上的鏈接: https://github.com/facebook/fresco 。中文文檔非常詳盡,地址: http://fresco-cn.org/ 。開發者可以根據自己的需求,選擇適合自己項目的庫。
過度繪制(Overdraw)是指在一幀的時間內像素被繪制了多次;理論上一個像素每次只繪制一次是最優的,但是由於層疊的布局導致一些像素會被多次繪制,而每次繪制都會對應到CP
下面是簡單的流程圖,從java到kernel層。 ShutdownThread.java文件 stop playing mus
1、寫在前面:雖然demo中程序框架已搭建完成,但是由於筆者時間原因,暫時只完成了核心部分:多線程下載的部分,其他數據庫、服務通知、暫停部分還未添加到項目中。2、相關知識
我們都知道對於屬性動畫可以對某個屬性做動畫,而 插值器(TimeInterpolator)和 估值器(TypeEvaluator)在其中扮演了重要角色,下面先了解下 Ti