【Android应用开发技术:图像处理】Bit地图显示性能优化分析

【Android应用开发技术:图像处理】Bitmap显示性能优化分析

作者:郭孝星
微博:郭孝星的新浪微博
邮箱:allenwells@163.com
博客:http://blog.csdn.net/allenwells
Github:https://github.com/AllenWells

【Android应用开发技术:图像处理】章节列表

Bitmap经常会消耗大量内存而导致程序崩溃,常见的异常如下所示:java.lang.OutofMemoryError:bitmap size extends VM budget,因此为了保证程序的稳定性,我们应该小心处理程序中加载Bitmap的操作。

我们在加载Bitmap时通常会遇到以下问题:

  • 移动设备的系统资源有限。Android设备对于单个程序至少需要16MB的内存。Android Compatibility Definition Document, Section 3.7. Virtual Machine Compatibility给出了对于不同大小与密度的屏幕的最低内存需求。程序应该在这个最低内存限制下最优化程序的效率。当然,大多数设备的都有更高的限制需求。
  • Bitmap会消耗很多内存,特别是对于类似照片等内容更加丰富的图片。例如:Galaxy Nexus的照相机能够拍摄2592x1936 pixels (5 MB)的图片。 如果bitmap的配置是使用ARGB_8888(ARGB_8888是从Android 2.3开始的默认配置),那么加载这张照片到内存会大概需要19MB( 2592*1936*4 bytes) 的内存, 这样的话会迅速消耗掉设备的整个内存。
  • Android App的UI通常会在一次操作中立即加载许多张Bitmap。例如:在ListView, GridView 与ViewPager等组件中通常会需要一次加载许多张Bitmap,而且需要预先加载一些没有在屏幕上显示的内容,为用户滑动时显示做准备。

一 加载大尺寸位图

图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现出来的要大很多。考虑到程序是在有限的内存下工作,我们通常会用低分辨率的缩略图的形式来显示图片,低分辨率的版本应该与我们的UI大小匹配。高分辨率的图片会占用大量内存资源,并且在快读滑动图片时会导致附加的效率问题。

下面我们来介绍以下如何加载一个低分辨率的图片到内存中并解析出高分辨率的图片。

1.1 读取位图的尺寸与类型

BitmapFactory类提供了一些decode的方法,如下所示:

  • public static Bitmap decodeFile(String pathName, Options opts)
/**
 * Decode a file path into a bitmap. If the specified file name is null,
 * or cannot be decoded into a bitmap, the function returns null.
 *
 * @param pathName complete path name for the file to be decoded.
 * @param opts null-ok; Options that control downsampling and whether the
 *             image should be completely decoded, or just is size returned.
 * @return The decoded bitmap, or null if the image data could not be
 *         decoded, or, if opts is non-null, if opts requested only the
 *         size be returned (in opts.outWidth and opts.outHeight)
 */
public static Bitmap decodeFile(String pathName, Options opts);
  • public static Bitmap decodeResource(Resources res, int id, Options opts)
/**
 * Synonym for opening the given resource and calling
 * {@link #decodeResourceStream}.
 *
 * @param res   The resources object containing the image data
 * @param id The resource id of the image data
 * @param opts null-ok; Options that control downsampling and whether the
 *             image should be completely decoded, or just is size returned.
 * @return The decoded bitmap, or null if the image data could not be
 *         decoded, or, if opts is non-null, if opts requested only the
 *         size be returned (in opts.outWidth and opts.outHeight)
 */
public static Bitmap decodeResource(Resources res, int id, Options opts);
  • public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
/**
 * Decode an immutable bitmap from the specified byte array.
 *
 * @param data byte array of compressed image data
 * @param offset offset into imageData for where the decoder should begin
 *               parsing.
 * @param length the number of bytes, beginning at offset, to parse
 * @param opts null-ok; Options that control downsampling and whether the
 *             image should be completely decoded, or just is size returned.
 * @return The decoded bitmap, or null if the image data could not be
 *         decoded, or, if opts is non-null, if opts requested only the
 *         size be returned (in opts.outWidth and opts.outHeight)
 */
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts);
  • public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
/**
 * Decode an input stream into a bitmap. If the input stream is null, or
 * cannot be used to decode a bitmap, the function returns null.
 * The stream's position will be where ever it was after the encoded data
 * was read.
 *
 * @param is The input stream that holds the raw data to be decoded into a
 *           bitmap.
 * @param outPadding If not null, return the padding rect for the bitmap if
 *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
 *                   no bitmap is returned (null) then padding is
 *                   unchanged.
 * @param opts null-ok; Options that control downsampling and whether the
 *             image should be completely decoded, or just is size returned.
 * @return The decoded bitmap, or null if the image data could not be
 *         decoded, or, if opts is non-null, if opts requested only the
 *         size be returned (in opts.outWidth and opts.outHeight)
 *
 * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT},
 * if {@link InputStream#markSupported is.markSupported()} returns true,
 * <code>is.mark(1024)</code> would be called. As of
 * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p>
 */
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
  • public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
/**
 * Decode a bitmap from the file descriptor. If the bitmap cannot be decoded
 * return null. The position within the descriptor will not be changed when
 * this returns, so the descriptor can be used again as-is.
 *
 * @param fd The file descriptor containing the bitmap data to decode
 * @param outPadding If not null, return the padding rect for the bitmap if
 *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
 *                   no bitmap is returned (null) then padding is
 *                   unchanged.
 * @param opts null-ok; Options that control downsampling and whether the
 *             image should be completely decoded, or just its size returned.
 * @return the decoded bitmap, or null
 */
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts);

上述方法用来从不同的资源中创建一个Bitmap. 根据图片数据源来选择合适的decode方法. 那些方法在构造位图的时候会尝试分配内存,因此会容易导致OutOfMemory的异常。每一种decode方法都提供了通过BitmapFactory.Options来设置一些附加的标记来指定decode的选项。设置inJustDecodeBounds属性为true可以在decoding的时候避免内存的分配,它会返回一个null的bitmap,但是outWidth, outHeight与outMimeType 还是可以获取。这个技术可以允许我们在构造bitmap之前先读取图片的尺寸与类型。

举例

在不分配内存的情况下获取图片的尺寸与类型。

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;

1.2 加载按比例缩小的图片到内存中

当通过以上的步骤我们已经获得了图片的尺寸,这些数据可以让我们决定是直接加载整个图片到内存中还是加载一个缩小的版本到内存中,我们通常会做以下的考量:

  • 评估加载完整图片所需要耗费的内存。
  • 程序在加载这张图片时会涉及到其他内存需求。
  • 呈现这张图片的组件的尺寸大小。
  • 屏幕大小与当前设备的屏幕密度。

举例

根据目标图片大小来计算Sample图片大小。

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;

        //设置inSampleSize为2的幂,之所以会这样做是因为decoder最终会对非2的幂进行向下处理,
        //直到获取最靠近2的幂的树。
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}

使用calculateInSampleSize()方法。

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
    options.inJustDecodeBounds = true;
    //传递options的值
    BitmapFactory.decodeResource(res, resId, options);
    //计算inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    //设置 options.inJustDecodeBounds为false
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

载一个任意大小的图片并显示为100*100 pixel的缩略图形式。

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

二 非UI线程处理图片

对于BitmapFactory的decode方法,当数据源是网络或者磁盘时,这些方法都不应该在主UI线程中执行,否则可能会由于诸多因数(例如:磁盘读取数据的速度、图片的大小、CPU的速度等)导致ANR异常。

2.1 使用AsyncTask

AsyncTask 类提供了一个简单的方法来在后台线程执行一些操作,并且可以把后台的结果呈现到UI线程。

举例

加载一个高分辨率图片到内存。

class BitmapWorkerTask extends AsyncTask {

    //创建了imageViewReference的弱引用来保证AsyncTask可以被垃圾回收器回收,因为
    //不能确定当任务结束时,ImageView任然存在(例如:用户已经离开Activity)。
    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);
}

2.2 处理并发问题

一般地,ListView和GridView等视图组件为了更有效的利用内存,视图的子组件会在用户滑动屏幕时循环利用,如果每一个子视图都触发一个AsyncTAsk,那么无法保证当前视图在结束task时,分配的视图已经进入循环队列中给另一个子视图进行重用,而且无法保证所有异步任务都能够按顺序执行完毕。

如何处理这种并发问题呢?

解决方案:当任务结束时ImageView保存一个最近经常使用的AsyncTask引用。

具体做法:创建一个专用的Drawable子类来保存一个可以回到当前工作任务的引用,在这种情况下,BitmapDrawable被用来作为占位图片,它可以在任务结束时显示到ImageView中。

创建一个专用的Drawable的子类来存储返回工作任务的引用,在这种情况下,当任务完成时,BitmaDrawable会被使用,placeholder image才会在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();
    }
}

创建一个 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中运行的任务得以取消,如果是这样,它通过执行cancel()方法来取消之前的一个任务。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.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;
}

在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到UI是简单直接的,但是如果你需要一次加载大量的图片,事情则会变得复杂起来。在大多数情况下(例如:在使用ListView、GridView或ViewPager时), 屏幕上的图片和因滑动将要显示的图片的数量通常是没有限制的。通过循环利用子视图可以抑制内存的使用,GC也会释放那些不再需要使用的bitmap。但是为了保持一个流畅的用户体验,我们想要在屏幕滑回来时避免每次重复处理那些图片。这个时候内存与磁盘缓存操作通常可以起到帮助的作用,允许组件快速的重新加载那些处理过的图片。

3.1 使用内存缓存

内存缓存以花费内存来快速访问位图android.util中提供的LruCache类就适合用来缓存位图,用一个强引用的LinkedHashMap来保存最近引用的对象,并且在缓存超出设置大小的时候移除最近最少使用的对象。

为LruCache设置缓存大小,合适的缓存大小的设置是非常关键的,因为:

  • 缓存太大:大量占用程序内存,会导致OutOfMomory异常,并且使得我们的程序只剩下小部分内存用来工作。
  • 缓存太小:导致额外内存花销却没有起到实际作用。

对于如何设置缓存大小,我们通常会考虑以下因素:

  • 程序剩下了多少可用的内存
  • 多少图片会被一次呈现到屏幕上,多少图片需要准备好以便马上显示到屏幕。
  • 设备的屏幕大小与密度是多少,一个具有特别高密度屏幕(xhdpi)的设备,例如:Galaxy Nexus会比Nexus S(hdpi)需要一个更大的Cache来缓存同样数量的图片。
  • 位图的尺寸与配置是多少,会花费多少内存。
  • 图片被访问的频率如何,是其中一些比另外的访问更加频繁吗,如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache对象。
  • 可以平衡质量与数量吗,某些时候保存大量低质量的位图会非常有用,在加载更高质量图片的任务则交给另外一个后台线程。

举例

为Bitmap建立LruCache。

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

加载位图到ImageView。

//当加载位图到 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<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);
        return bitmap;
    }
    ...
}

2.2 使用磁盘缓存

内存缓存能够提高访问最近查看过的位图的速度,但是不能保证这个图片会在Cache中。像类似GridView等带有大量数据的组件很容易就填满内存Cache。你的程序可能会被类似Phone Call等任务而中断,这样后台程序可能会被杀死,那么内存缓存就会被销毁。一旦用户恢复前面的状态,你的程序就又需要重新处理每个图片。

磁盘缓存可以用来保存那些已经处理好的位图,并且在那些图片在内存缓存中不可用时减少加载的次数。当然从磁盘读取图片会比从内存要慢,而且读取操作需要在后台线程中处理,因为磁盘读取操作是不可预期的。

举例

给已有的内存缓存添加磁盘缓存。

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 处理配置改变

运行时配置改变,例如:屏幕方向的改变会导致App去destory并restart当前运行的Activity,我们需要避免在配置改变时重新处理已经处理过的图片,这样才能给用户一个良好的平滑过渡的体验。

解决方案:使用内存缓存。

具体做法:通过调用setRetainInstance(true)保留一个Fragment实例,这个缓存可以通过被保留的Fragment传递给新的Activity实例。在这个Activity被recreate之后,这个被保留的Fragment会被重新附着到Activity上。这样我们就可以访问缓存对象,从中获取图片信息并快读的重新添加到ImageView上。

举例

配置改变时使用Fragment来保留LruCache。

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

三 管理位图内存占用

在Android 3.0引进了BitmapFactory.Options.inBitmap. 如果这个值被设置了,decode方法会在加载内容的时候去重用已经存在的bitmap. 这意味着bitmap的内存是被重新利用的,这样可以提升性能, 并且减少了内存的分配与回收。

3.1 保存位图供以后使用

举例

已经存在的Bitmap保存起来供以后使用,当一个应用运行在Android 3.0或者更高的平台上并且Bitmap从LruCache中移除时, bitmap的一个软引用会被存放在Hashset中,这样便于之后可能被inBitmap重用:

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

3.2 使用已经存在的位图

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

//为inBitmap查找一个已经存在的Bitmap设置为value,该方法只有在找到合适的值的时候才会为inBitmap设置一个值。
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 to find 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;
        }
    }
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
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;
}

//判断候选Bitmap是否满足inBitmap大小条件
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;
}

通过以上三点的分析,我们了解了Bitmap显示性能优化的手段和方式,下面我们来实际看一下如何咋Android UI上进行高效的Bitmap显示。

举例1

加载图片到ViewPager。使用ViewPager与ImageView作为子视图。

方法一:在UI线程中加载图片,适用于数据量较小的情况。

public class ImageDetailActivity extends FragmentActivity {
    public static final String EXTRA_IMAGE = "extra_image";
    private ImagePagerAdapter mAdapter;
    private ViewPager mPager;
    // A static dataset to back the ViewPager adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
        mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
        mPager = (ViewPager) findViewById(R.id.pager);
        mPager.setAdapter(mAdapter);
    }
    public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
        private final int mSize;
        public ImagePagerAdapter(FragmentManager fm, int size) {
            super(fm);
            mSize = size;
        }
        @Override
        public int getCount() {
            return mSize;
        }
        @Override
        public Fragment getItem(int position) {
            return ImageDetailFragment.newInstance(position);
        }
    }
}

//Fragment 里面包含了ImageView 的子组件:
public class ImageDetailFragment extends Fragment {
    private static final String IMAGE_DATA_EXTRA = "resId";
    private int mImageNum;
    private ImageView mImageView;
    static ImageDetailFragment newInstance(int imageNum) {
        final ImageDetailFragment f = new ImageDetailFragment();
        final Bundle args = new Bundle();
        args.putInt(IMAGE_DATA_EXTRA, imageNum);
        f.setArguments(args);
        return f;
    }
    // Empty constructor, required as per Fragment docs
    public ImageDetailFragment() {}
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // image_detail_fragment.xml contains just an ImageView
        final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
        mImageView = (ImageView) v.findViewById(R.id.imageView);
        return v;
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        final int resId = ImageDetailActivity.imageResIds[mImageNum];
        mImageView.setImageResource(resId); // Load image into ImageView
    }
}

方法二:在异步任务中加载图片,适用于数据量较大的情况。

public class ImageDetailActivity extends FragmentActivity {
    ...
    public void loadBitmap(int resId, ImageView imageView) {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
    ... // include BitmapWorkerTask class
}
public class ImageDetailFragment extends Fragment {
    ...
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (ImageDetailActivity.class.isInstance(getActivity())) {
            final int resId = ImageDetailActivity.imageResIds[mImageNum];
            // Call out to ImageDetailActivity to load the bitmap in a background thread
            ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
        }
    }
}

在异步任务总加载图片不会使UI线程卡顿,如果我们想缓存图片,可以为以上程序再添加缓存,如下所示:

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache mMemoryCache;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        // initialize LruCache as per Use a Memory Cache section
    }
    public void loadBitmap(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);
        final Bitmap bitmap = mMemoryCache.get(imageKey);
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
        } else {
            mImageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
            task.execute(resId);
        }
    }
    ... // include updated BitmapWorkerTask from Use a Memory Cache section
}

举例2

加载图片到GridView,在Fragment里面内置了ImageView作为GridView子视图,

GridView是一种有效显示大量图片的方式。这样能够一次显示许多图片,而且那些即将被显示的图片也处于准备显示状态。如果你想要实现这种效果,你必须确保UI是流畅的,能够控制内存使用,并且正确的处理并发问题。

方法一:在UI线程中加载图片,适用于数据流较小的情况。

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    private ImageAdapter mAdapter;
    // A static dataset to back the GridView adapter
    public final static Integer[] imageResIds = new Integer[] {
            R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
            R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
            R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
    // Empty constructor as per Fragment docs
    public ImageGridFragment() {}
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
    }
    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
        return v;
    }
    @Override
    public void onItemClick(AdapterView parent, View v, int position, long id) {
        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
    }
    private class ImageAdapter extends BaseAdapter {
        private final Context mContext;
        public ImageAdapter(Context context) {
            super();
            mContext = context;
        }
        @Override
        public int getCount() {
            return imageResIds.length;
        }
        @Override
        public Object getItem(int position) {
            return imageResIds[position];
        }
        @Override
        public long getItemId(int position) {
            return position;
        }
        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some attributes
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            } else {
                imageView = (ImageView) convertView;
            }
        //请注意下面的代码
        imageView.setImageResource(imageResIds[position]); // Load image into ImageView
        return imageView;
    }
}

方法二:在异步任务中加载图片,适用于数据量较大的情况。

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...
    private class ImageAdapter extends BaseAdapter {
        ...
        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return 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);
        }
    }
    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();
        }
    }
    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;
    }
    ... // include updated BitmapWorkerTask class

版权声明:当我们认真的去做一件事的时候,就能发现其中的无穷乐趣,丰富多彩的技术宛如路上的风景,边走边欣赏。