【Android开发】BitMap优化最佳实践及官方Demo“图片墙”实例详解

1.   前言

BitMap(位图)作为Android的所有图片在内存中的抽象,使用频率很高。就对象的内存占用而言,BitMap对象是所有Java对象中最应该关注的。就我的有限的开发经历来讲,遇到的问题包括:

²  ImageView加载大图片不显示,或者报错

²  ListView显示多个图片,滑动过程中内存溢出,导致崩溃

²  GridView显示多个图片,有些图片不能显示

²  ……

   所以,本文总结了BitMap优化策略的一些最佳实践。

2.   官方指南

  在Android 6.0的官方开发平台上,有5节是专门来讲BitMap优化的,地址为【http://developer.android.com/reference/android/graphics/Bitmap.html】,我将这5节内容专门Copy到了Word文档,地址在【http://download.csdn.net/detail/brillianteagle/9415769】,也有人不厌其烦,将其翻译成了中文,地址【http://blog.csdn.net/ohehehou/article/details/40476779

。下面分别介绍这5节内容,也包含了我对于这5节的思考。

2.1LoadingLarge Bitmaps Efficiently

这一节主要讲解如何有效加载大图片。对于图片来讲,可以先读取图片的尺寸信息(BitmapFactory.Options),而不必直接加载图片。

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;

根据尺寸信息来对图像进行一次预判。如果是大图片,则可以根据实际需要的图片尺寸,计算其真实的压缩比率。


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

最后,加载大图片的方式为:

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

【我的思考】这里可以只根据长或宽进行缩放因子的计算,这样就保证了图片的长宽比。

2.2 Processing Bitmaps Off the UI Thread

  这一节讲了利用在后台线程解析BitMap然后加载到UI中,这是大图片、在线下载图片等需要耗时操作等情景的标准方案。

  后台线程机制采用的异步线程类AsyncTask,它特别适合“后台耗时处理,然后通知UI线程”的模式,较“Thread+Handler”的代码稍微简洁一些。

 这里采用了WeakReference类来封装待加载BitMap的ImageView类。对于GridView和ListView,不在可视区域的ImageView极有可能被GC回收,为了保证这一优化机制,所以必须在线程中用弱引用。如果ImageView在某个时刻划入可视区域,后台线程开始解析Bitmap,等到解析结束后极有可能ImageView已经划出了可视区域,且被回收,这样就不必加载该图片了。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;//弱引用
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(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);
            }
        }
    }
}

上面这种方法有个缺陷。上面的逻辑是,如果ImageView被GC回收了,它对应的后台线程仍然在解析图片,等到图片解析完毕后,再通知UI层时时判断ImageView是否有效。而更有效率的逻辑是,一旦ImageView被GC回收,它对应的后台线程就立马取消掉。如何实现呢?这里有一个重要的技巧,就是把每个ImageView对应的后台线程封装到BitmapDrawable中,将此BitmapDrawable对象再添加到ImageView中。即:

static class AsyncDrawable extends BitmapDrawable {//将异步任务封装到BitmapDrawable中
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

这样,ListView或GridView中的ImageView一旦被GC回收,其上的AsyncDrawable也会被立即回收,包括其中的BitmapWorkerTask。

 

加载ImageView时,先判断它有没有对应的异步任务(例如,第一次加载),如果没有,就启动异步任务;如果有,且本次要加载的和正在处理的异步线程一致,就继续该异步线程,否则就取消掉正在处理的异步线程,再启动异步任务。

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {//解除任何附着在imageView的异步任务
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);//生成异步任务
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);//异步任务封装到bitMapDrawable
        imageView.setImageDrawable(asyncDrawable);//异步任务附着到imageView
        task.execute(resId);
    }
}

其中,用于取消ImageView潜在异步任务的方法为:


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

  其中,获得某个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;
}

最后,当异步任务执行完毕后,需要判断其对应的ImageView是否已经更改了异步任务,不再是“自己”了。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

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


2.3 Caching BitMap

  本节主要讲了如何使用缓存来优化Bitmap。包括了三部分内容:

²  内存缓存方案;

²  硬盘缓存方案;

²  Android配置改变时缓存方案;

2.3.1   内存缓存方案

    如果ListView或GridView频繁的滑动时,ImageView在可视区域时,BitMap一旦解析完毕,就显示;ImageView滑出可视区域就被GC,再滑入可视区域就再次启动异步线程来解析加载。这样频繁的GC和解析,效率很低。

缓存本质上就是一个有内存上限的队列,先进先出。缓存的优化策略是,将异步线程中已经解析出来的BitMap加入队列,一旦超过缓存的内存上限,就开始GC缓存中队头的BitMap。在加载图片时,先查下该图片是否在缓存中,如果在,就直接加载,否则开启异步线程进行加载。

Android使用LruCache这个类来生成缓存对象。对于该类,我的博客【深入理解内存缓存类LruCache:源码分析:http://blog.csdn.net/brillianteagle/article/details/50593379】对其有更深入的解析。下面是一个LruCache的构造,缓存大小为本应用运行时内存的1/8。注意,默认运行时内存默认情况下为64M(可以修改得更大),所以这里的缓存大小为8M。

private LruCache<String, Bitmap> 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.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // 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 加入缓存中。

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // 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);//加入bitMap进入缓存
        return 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);
    }
}

上面的内容可以归结为“内存缓存”方案,适用于本地图片加载。加载的图片需要在线下载,那么如果也使用“内存缓存”方案的话,下载的图片对象如果在被GC,就要再下载一遍(与本地再解析一遍不同),效率不高。所以,本节第二个重要内容是“硬盘缓存”方案。

2.3.2 硬盘缓存方案

  该方案依赖于一个开源的类DiskLruCache,我的博客【深入理解硬盘缓存类DiskLruCache:源码分析:http://blog.csdn.net/brillianteagle/article/details/50593434】对其有更深入的解析。这里,主要使用该类就可以了。

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<File, Void, Void> {
    @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<Integer, Void, Bitmap> {
    ...
    // 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);
}
2.3.3 Android配置改变时缓存方案

  最后一部分是介绍当运行时配置改变时(例如,屏幕朝向改变导致Activity被销毁并重新加载)如何保证缓存不用被销毁。这里采用了Fragment的一个关键特性。如果Activty因为运行时配置改变而被销毁重新加载,那么附着在该Activity的所有Fragment都会经历同样的过程。如果不希望Fragemt的某些对象被销毁并重新生成,可以使用Fragment的setRetainInstance(true)来声明。关于这一特性,可以参看【Fragment的非中断保存setRetaineInstance:http://ju.outofmemory.cn/entry/132081】。

所以,将缓存对象放置在Fragment中,并设置不随Activity的销毁而销毁,就能保存该缓存。

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    mMemoryCache = retainFragment.mRetainedCache;
    if (mMemoryCache == null) {
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.mRetainedCache = mMemoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> 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);//关键语句
    }
}
【我的思考】依靠Fragment来存储一些不希望随配置改变而被销毁的对象,这是通用方法。

2.4 Managing Bitmap Memory

  这一节主要讲如何复用已有的BitMap内存空间,这样就免得GC某些不用的BitMap,以及分配新内存给新的BitMap。对于Android3.0及其以上版本,BitmapFactory.Options.inBitmap对应的就是要复用的BitMap对象。

  首先,那些从LruCache中被自动剔除的BitMap会被保存到一个弱引用集合,这个集合内的BitMap可能会被复用。

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

其次,解析待加载的BitMap的属性Options,根据属性从弱引用集合中查找,找到符合条件的重用BitMap。

public static Bitmap decodeSampledBitmapFromFile(String filename, 
        int reqWidth, int reqHeight, ImageCache cache) { 
 
    final BitmapFactory.Options options = new BitmapFactory.Options(); 
    ... 
    BitmapFactory.decodeFile(filename, options); 
    ... 
 
    // If we're running on Honeycomb or newer, try to use inBitmap. 
    if (Utils.hasHoneycomb()) { 
        addInBitmapOptions(options, cache); 
    } 
    ... 
    return BitmapFactory.decodeFile(filename, options); 
}

其中addInBitmapOptions(options, cache):

privatestaticvoid addInBitmapOptions(BitmapFactory.Options options, 
        ImageCache cache) { 
    // inBitmap only workswith mutable bitmaps, so force the decoder to 
    // return mutable bitmaps. 
    options.inMutable = true; 
 
    if (cache != null) { 
        // Try tofind a bitmap to use for inBitmap. 
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options); 
 
        if (inBitmap != null) { 
            // If a suitable bitmap has been found, set it as the value of 
            // inBitmap. 
            options.inBitmap = inBitmap; 
        } 
    } 
} 

其中,getBitmapFromReusableSet(options);:

protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { 
        Bitmap bitmap = null; 
 
    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { 
        synchronized (mReusableBitmaps) { 
            final Iterator<SoftReference<Bitmap>> iterator 
                    = mReusableBitmaps.iterator(); 
            Bitmap item; 
 
            while (iterator.hasNext()) { 
                item = iterator.next().get(); 
 
                if (null != item && item.isMutable()) { 
                    // Check to see it the item can be used for inBitmap. 
                    if (canUseForInBitmap(item, options)) { 
                        bitmap = item; 
 
                        // Remove from reusable set so it can't be used again. 
                        iterator.remove(); 
                        break; 
                    } 
                } else { 
                    // Remove from the set if the reference has been cleared. 
                    iterator.remove(); 
                } 
            } 
        } 
    } 
    return bitmap; 
}

其中,canUseForInBitmap(item,options)

static boolean canUseForInBitmap( 
        Bitmap candidate, BitmapFactory.Options targetOptions) { 
 
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of 
        // the new bitmap is smaller than the reusable bitmap candidate 
        // allocation byte count. 
        int width = targetOptions.outWidth / targetOptions.inSampleSize; 
        int height = targetOptions.outHeight / targetOptions.inSampleSize; 
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); 
        return byteCount <= candidate.getAllocationByteCount(); 
    } 
 
    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 
    return candidate.getWidth() == targetOptions.outWidth 
            && candidate.getHeight() == targetOptions.outHeight 
            && targetOptions.inSampleSize == 1; 
} 
 
/** 
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration. 
 */ 
static int getBytesPerPixel(Config config) { 
    if (config == Config.ARGB_8888) { 
        return 4; 
    } else if (config == Config.RGB_565) { 
        return 2; 
    } else if (config == Config.ARGB_4444) { 
        return 2; 
    } else if (config == Config.ALPHA_8) { 
        return 1; 
    } 
    return 1; 
}

【我的思考】在提升效率方面,这一节不是特别重要。

2.5 Displaying BitMaps in Your UI 

  这一节使用了两个实例来说明如何在UI层展示BitMap,详细内容见官方指南的源文。

2.5.1 Load Bitmaps into a ViewPager Implementation

  思路比较简单。ViewPager添加Fragment为Item的Aapter,每个Fragement就是一个ImageView。加载时利用前两节的异步线程、内存缓存技术。

2.5.2 Load Bitmaps into a GridView Implementation

  与ViewPager类似。

2.6 其他优化

   

    除了官方的优化策略,还有一些注意点,可以查看【Android开发优化之——对Bitmap的内存优化】的4条优化,其中第3、第4条本文已经谈过了。其它2条是主动释放、捕捉异常。这里只提其中一点,即主动释放BitMap内存

  为何要主动释放呢?原因在于,BitMap在new的过程中是一个JNI调用,所以,GC掉的BitMap只是Java JVM控制的内存区域,JNI保持的内存区域没有及时释放。所以,对于BitMap对象,一旦不再使用,就需要主动释放其内存。

if(bitmap != null && !bitmap.isRecycled()){ 
bitmap.recycle();
bitmap = null; 
}
System.gc();

3.   官方Demo详解

    打开Android 6.0SDK中的DisplayingBitmaps工程(路径为:sdk\samples\android-23\ui\)。本工程是一个“图片墙”的应用,用GridView展示大量网络下载的图片缩略图,点击缩略图可以显示高清图片。由于无法下载Google提供的图片,我更换了其中的图片地址。效果如图:

   

3.1缓存分析

  本Demo综合应用了提前解析图片信息、异步线程加载、内存缓存、硬盘缓存、可重用Bitmap等上面5节提到的所有技术,其中缓存的逻辑如下:




 除此之外,还包括了用Fragment来保存Cache对象,防止因为Activity的横竖屏切换而导致的缓存丢失(2.3.3节内容)。

3.2UML分析

3.2.1   ImageCache

  根据缓存方案,本程序的将内存缓存、硬盘缓存、软引用封装成了一个类ImageCache,而且其中还有用于保存ImageCache的静态内部类Fragment(2.3.3节内容),另一个内部类ImageCacheParams用于提供缓存大小等参数。UML如下:

ImageCache提供了将BitMap加入缓存、从内存缓存、硬盘缓存和软引用中取可复用的Bitmap的方法。注意,在加入缓存时,同时加入了内存缓存和硬盘缓存。

3.2.2   ImageFetcher

ImageFetcher是最后封装完毕的类,加载图片即调用:

mImageFetcher.loadImage(mImageUrl, mImageView);

回溯一下,ImageFetcher的UML图为。

整体框架在ImageWorker中已经写好,加载图片是先查询内存缓存,如果不存在则异步处理。

    /**
     * Load an image specified by the data parameter into an ImageView (override
     * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
     * disk cache will be used if an {@link ImageCache} has been added using
     * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
     * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
     * will be created to asynchronously load the bitmap.
     *
     * @param data The URL of the image to download.
     * @param imageView The ImageView to bind the downloaded image to.
     */
    public void loadImage(Object data, ImageView imageView) {
        if (data == null) {
            return;
        }

        BitmapDrawable value = null;

        if (mImageCache != null) {
        //先查询内存缓存
            value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
        }

        if (value != null) {
            // Bitmap found in memory cache
            imageView.setImageDrawable(value);
        } else if (cancelPotentialWork(data, imageView)) {
//生成异步任务
            final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
//异步任务封装到BitMapDrawable中
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(mResources, mLoadingBitmap, task);
            imageView.setImageDrawable(asyncDrawable);

            // NOTE: This uses a custom version of AsyncTask that has been pulled from the
            // framework and slightly modified. Refer to the docs at the top of the class
            // for more info on what was changed.
//执行异步任务
            task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);

        }
    }

 再来细看异步任务。

/**
     * The actual AsyncTask that will asynchronously process the image.
     */
    private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
        private Object mData;
        private final WeakReference<ImageView> imageViewReference;

        public BitmapWorkerTask(Object data, ImageView imageView) {
            mData = data;
//异步任务的句柄为弱引用
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        /**
         * Background processing.
         */
        @Override
        protected BitmapDrawable doInBackground(Void... params) {

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - starting work");
            }

            final String dataString = String.valueOf(mData);
            Bitmap bitmap = null;
            BitmapDrawable drawable = null;

            // Wait here if work is paused and the task is not cancelled
            synchronized (mPauseWorkLock) {
                while (mPauseWork && !isCancelled()) {
                    try {
                        mPauseWorkLock.wait();
                    } catch (InterruptedException e) {}
                }
            }

            // If the image cache is available and this task has not been cancelled by another
            // thread and the ImageView that was originally bound to this task is still bound back
            // to this task and our "exit early" flag is not set then try and fetch the bitmap from
            // the cache
            if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
//图片是否在硬盘缓存
                bitmap = mImageCache.getBitmapFromDiskCache(dataString);
            }

            // If the bitmap was not found in the cache and this task has not been cancelled by
            // another thread and the ImageView that was originally bound to this task is still
            // bound back to this task and our "exit early" flag is not set, then call the main
            // process method (as implemented by a subclass)
//图片如果不在硬盘缓存就直接下载
            if (bitmap == null && !isCancelled() && getAttachedImageView() != null
                    && !mExitTasksEarly) {
                bitmap = processBitmap(mData);
            }

            // If the bitmap was processed and the image cache is available, then add the processed
            // bitmap to the cache for future use. Note we don't check if the task was cancelled
            // here, if it was, and the thread is still running, we may as well add the processed
            // bitmap to our cache as it might be used again in the future
            if (bitmap != null) {
                if (Utils.hasHoneycomb()) {
                    // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
                    drawable = new BitmapDrawable(mResources, bitmap);
                } else {
                    // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
                    // which will recycle automagically
                    drawable = new RecyclingBitmapDrawable(mResources, bitmap);
                }

                if (mImageCache != null) {
//图片加入内存缓存和硬盘缓存
                    mImageCache.addBitmapToCache(dataString, drawable);
                }
            }

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "doInBackground - finished work");
            }

            return drawable;

        }

        /**
         * Once the image is processed, associates it to the imageView
         */
        @Override
        protected void onPostExecute(BitmapDrawable value) {

            // if cancel was called on this task or the "exit early" flag is set then we're done
            if (isCancelled() || mExitTasksEarly) {
                value = null;
            }

            final ImageView imageView = getAttachedImageView();
            if (value != null && imageView != null) {
                if (BuildConfig.DEBUG) {
                    Log.d(TAG, "onPostExecute - setting bitmap");
                }
                setImageDrawable(imageView, value);
            }

        }

    再往下走,如果不在硬盘缓存,就调用的是processBitmap(mData)。这里应该就是直接网络下载了吧?这个函数的实现是在ImageFetcher中。这里的实现有额外使用了一个硬盘缓存,即DiskLruCache mHttpDiskCache。根据网址将突破下载后的图片直接放入到了mHttpDiskCache硬盘缓存中,然后再从mHttpDiskCache硬盘缓存读出文件并加入到内存缓存和硬盘缓存(ImageCache)。

为什么不直接将下载的图片放到内存缓存和硬盘缓存(ImageCache)呢?因为下载的内容是字节流,并不能直接生产BitMap,下载中可能中断,DiskLruCache mHttpDiskCache相当于是一级缓存,准备完毕图片后才加入到内存缓存和硬盘缓存(ImageCache)。

/**
     * The main process method, which will be called by the ImageWorker in the AsyncTask background
     * thread.
     *
     * @param data The data to load the bitmap, in this case, a regular http URL
     * @return The downloaded and resized bitmap
     */
    private Bitmap processBitmap(String data) {
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "processBitmap - " + data);
        }

        final String key = ImageCache.hashKeyForDisk(data);
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        DiskLruCache.Snapshot snapshot;
        synchronized (mHttpDiskCacheLock) {
            // Wait for disk cache to initialize
            while (mHttpDiskCacheStarting) {
                try {
                    mHttpDiskCacheLock.wait();
                } catch (InterruptedException e) {}
            }

            if (mHttpDiskCache != null) {
                try {
                    snapshot = mHttpDiskCache.get(key);
                    if (snapshot == null) {
                        if (BuildConfig.DEBUG) {
                            Log.d(TAG, "processBitmap, not found in http cache, downloading...");
                        }
                        DiskLruCache.Editor editor = mHttpDiskCache.edit(key);
                        if (editor != null) {
                        //将文件下载到mHttpDiskCache.
                            if (downloadUrlToStream(data,
                                    editor.newOutputStream(DISK_CACHE_INDEX))) {
                                editor.commit();
                            } else {
                                editor.abort();
                            }
                        }
                        snapshot = mHttpDiskCache.get(key);
                    }
                    if (snapshot != null) {
                        fileInputStream =
                                (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
//从mHttpDiskCache取得文件描述信息
                        fileDescriptor = fileInputStream.getFD();
                    }
                } catch (IOException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } catch (IllegalStateException e) {
                    Log.e(TAG, "processBitmap - " + e);
                } finally {
                    if (fileDescriptor == null && fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {}
                    }
                }
            }
        }

        Bitmap bitmap = null;
        if (fileDescriptor != null) {
//从mHttpDiskCache读取BitMap
            bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
                    mImageHeight, getImageCache());
        }
        if (fileInputStream != null) {
            try {
                fileInputStream.close();
            } catch (IOException e) {}
        }
        return bitmap;
    }

好了,继续往下走,解析图片时,可以看内存中是否有复用的BitMap空间。

/**
     * Decode and sample down a bitmap from a file input stream to the requested width and height.
     *
     * @param fileDescriptor The file descriptor to read from
     * @param reqWidth The requested width of the resulting bitmap
     * @param reqHeight The requested height of the resulting bitmap
     * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
     * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
     *         that are equal to or greater than the requested width and height
     */
    public static Bitmap decodeSampledBitmapFromDescriptor(
            FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;

        // If we're running on Honeycomb or newer, try to use inBitmap
        if (Utils.hasHoneycomb()) {
            addInBitmapOptions(options, cache);
        }

        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
    }

  这里面的addInBitmapOptions(options,cache)的作用就是查询弱引用中是否有可以复用的BitMap内存空间。请回顾本文的2.4节。


@TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {

        // inBitmap only works with mutable bitmaps so force the decoder to
        // return mutable bitmaps.
        options.inMutable = true;

        if (cache != null) {
            // Try and find a bitmap to use for inBitmap
            Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

            if (inBitmap != null) {
                options.inBitmap = inBitmap;//关键部分。
            }
        }

    }

4   总结

本文以官方文档和Demo为核心,全面讲解了BitMap优化的最佳实践。对于本地大量图片的读取,不需要硬盘缓存,可以优化的点为:

内存缓存;

内存缓存以Fragment为容器,防止因为Activity的横竖屏切换而销毁掉内存缓存;

异步线程解析图片;

异步线程以弱引用的形式封装到BitMapDrawable中,并将其附着到ImageView上

……

对于网络下载,需要考虑地方更多了。除了上面几条,下载时需要直接放入额外的硬盘缓存中,再从额外的硬盘缓存中读取BitMap加入到内存缓存和硬盘缓存。





  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值