Android Bitmap图像优化

在Android应用开发中不可避免的会用到图形图像,这样就会生成Bitmap对象。如果在开发过程中没有处理好Bitmap对象就很容易产生Out Of Memory(OOM)的异常。以下列举几点使用Bitmap对象需要注意的地方:
    一个Android应用程序最多只能使用16M的内存,在Android的 Android Compatibility Definition Document (CDD) 3.7节中描述了不同屏幕分辨率及密度的设备在VM中会分配的大小。                                                                   

Screen Size Screen Density Application Memory
small / normal / large   ldpi / mdpi 16MB
small / normal / large tvdpi / hdpi 32MB
small / normal / large xhdpi 64MB
xlarge   mdpi 32MB
xlarge tvdpi / hdpi 64MB
xlarge xhdpi 128MBBitmap

       对象比较占用内存,特别像一些照片。比如使用Google Nexus照一张

分辨率为2592x1936的照片大概为5M,如果采用ARGB_8888的色彩格式(2.3之后默认使用该格式)加载这个图片就要占用19M内存(2592*1936*4 bytes),这样会导致某些设备直接挂掉。



Android中很多控件比如ListView/GridView/ViewPaper通常都会包含很多图片,特别是快速滑动的时候可能加载大量的图片,因此图片处理显得尤为重要。

下面会从四个方向讲述如何优化Bitmap的显示:
优化大图片 -- 注意Bitmap处理技巧,使其不会超过内存最大限值
          通常情况下我们的UI并不需要很精致的图片。例如我们使用Gallery显示照相机拍摄的照片时,你的设备分辨率通常小于照片的分辨率。
          BitmapFactory类提供了几个解码图片的方法(decodeByteArray(),decodeFile(),decodeResource()等),它们都可以通过BitmapFactory.Options指定解码选项。设置inJustDecodeBounds属性为true时解码并不会生成Bitmap对象,而是返回图片的解码信息(图片分辨率及类型:outWidth,outHeight,outMimeType)然后通过分辨率可以算出缩放值,再将inJustDecodeBounds设置为false,传入缩放值缩放图片,值得注意的是inJustDecodeBounds可能小于0,需要做判断。

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;  
          现在我们知道了图片的密度,在BitmapFactory.Options中设置inSampleSize值可以缩小图片。比如我们设置inSampleSize = 4,就会生成一个1/4长*1/4宽=1/16原始图的图片。当inSampleSize < 1的时候默认为1,系统提供了一个calculateInSampleSize()方法来帮我们算这个值:

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) {  
        if (width > height) {  
            inSampleSize = Math.round((float)height / (float)reqHeight);  
        } else {  
            inSampleSize = Math.round((float)width / (float)reqWidth);  
        }  
    }  
    return inSampleSize;  
}  
          创建一个完整的缩略图方法:

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));  
不要在UI线程中处理Bitmap -- 图片下载/调整大小等不要放在UI线程中处理,可以使用AsyncTask处理并发的问题。
        刚刚我们提到过BitmapFactory.decode*的方法,值得注意的是这些方法都不能在UI线程中执行,因为他们的加载过程都是不可靠的,很可能引起应用程序的ANR。
        如何解决这个问题呢?我们需要用到AsyncTask来处理并发。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);  
            }  
        }  
    }  
}  

public void loadBitmap(int resId, ImageView imageView) {  
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
    task.execute(resId);  
}  
当我们在ListView和GridView中使用AsyncTask的时候会引发一些问题,例如ListView快速滑动的时候其child view是循环未被回收的,我们也并不知道AsyncTask什么时候会完成,有可能AsyncTask还没执行完之前childView就已经被回收了,下面我们讲一种方法可以避免这种情况:
创建一个Drawable的子类来引用存储工作任务执行后返回的图片

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);  
    }  
}  
在给ImageView赋值之前会调用cancelPotentialWork方法,它会使用cancel()方法尝试取消已经过期的任务。

public static boolean cancelPotentialWork(int data, ImageView imageView) {  
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);  
  
    if (bitmapWorkerTask != null) {  
        final int bitmapData = bitmapWorkerTask.data;  
        if (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;  
}  

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()方法

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);  
            }  
        }  
    }  
}  
缓存Bitmap -- 使用缓存可以改善图片加载速度提升用户体验
使用内存的Cache
       从Android3.1开始,Google提供了一个缓存类叫LruCache,在此之前我们实现缓存通常都是用软引用或是弱引用,但是Google并不建议我们这样做,因为从Android2.3之后增加了GC回收的频率。
       我们在使用LruCache的时候需要为它设置一个缓存大小,设置小了缓存没有作用,设置大了同样会导致OOM,因此设置缓存大小是一门技术活。

private LruCache mMemoryCache;  
  
@Override  
protected void onCreate(Bundle savedInstanceState) {  
    ...  
    // Get memory class of this device, exceeding this amount will throw an  
    // OutOfMemory exception.  
    final int memClass = ((ActivityManager) context.getSystemService(  
            Context.ACTIVITY_SERVICE)).getMemoryClass();  
  
    // Use 1/8th of the available memory for this memory cache.  
    final int cacheSize = 1024 * 1024 * memClass / 8;  
  
    mMemoryCache = new LruCache(cacheSize) {  
        @Override  
        protected int sizeOf(String key, Bitmap bitmap) {  
            // The cache size will be measured in bytes rather than number of items.  
            return bitmap.getByteCount();  
        }  
    };  
    ...  
}  
  
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
    if (getBitmapFromMemCache(key) == null) {  
        mMemoryCache.put(key, bitmap);  
    }  
}  
  
public Bitmap getBitmapFromMemCache(String key) {  
    return mMemoryCache.get(key);  
}  
            这样当我们在ImageView中使用Bitmap的时候就可以先从缓存中获取,如果缓存没有就从网络中获取:

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;  
    }  
    ...  
}  
        2.使用硬盘的Cache
           我们会使用DiskLruCache来实现硬盘Cache

private DiskLruCache mDiskCache;  
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  
    ...  
    File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);  
    mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);  
    ...  
}  
  
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(String.valueOf(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  
    if (!mDiskCache.containsKey(key)) {  
        mDiskCache.put(key, bitmap);  
    }  
}  
  
public Bitmap getBitmapFromDiskCache(String key) {  
    return mDiskCache.get(key);  
}  
  
// 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 getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED  
            || !Environment.isExternalStorageRemovable() ?  
                    context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();  
  
    return new File(cachePath + File.separator + uniqueName);  
}  
值得一提的是Android API中并没有提供DiskLruCache接口,需要自己从4.x源码中移植至应用程序。源码地址:
libcore/luni/src/main/java/libcore/io/DiskLruCache.java

3.有时候在处理横竖屏切换的时候对象会全部重载,这样缓存就丢失了。为了避免这个问题,我们除了在Manifest中设置横竖屏不更新之外,就是使用Fragment做保存:

private LruCache mMemoryCache;  
  
@Override  
protected void onCreate(Bundle savedInstanceState) {  
    ...  
    RetainFragment mRetainFragment =  
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());  
    mMemoryCache = RetainFragment.mRetainedCache;  
    if (mMemoryCache == null) {  
        mMemoryCache = new LruCache(cacheSize) {  
            ... // Initialize cache here as usual  
        }  
        mRetainFragment.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();  
        }  
        return fragment;  
    }  
  
    @Override  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setRetainInstance(true);  
    }  
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值