Develop -- Training(十五) -- 显示高效位图

学习怎样使用常见的技术处理并加载一个 Bitmap 对象,保持用户界面(UI)组件的响应,并且避免超过应用程序的内存限制。如果你不小心的话,Bitmap 可以快速的消耗内存预算,主要会导致应用程序崩溃由于一个可怕的异常:

java.lang.OutofMemoryError: bitmap size exceeds VM budget.

下面有很多原因,为什么在你的应用程序加载 Bitmap 的时候会很复杂:

1.移动设备通常具有有限的系统资源。Android 设备的单个应用程序最少有16M的内存可用。在 Android的兼容性定义文件(CDD)的第3.7节,虚拟机的兼容性给出了不同的屏幕大小和密度所需的申请最低内存。应用程序应该优化这个最小的内存限制来执行。但是,很多设备的配置限制是很高的(也就是可能会超过16M)。

2.Bitmap 会占用大量的内存。特别是丰富的图像,像照片。例如,在Galaxy Nexus的相机拍照采用的照片高达2592x1936像素(5百万像素)。如果这个 Bitmap 的配置使用 ARGB_8888(从Android 2.3以后默认),加载这张图片的内存将需要19M(2592*1936*4 bytes),马上会耗尽在某些设备上的每个应用程序的内存限制。

3.Android 应用程序的 UI 界面会频繁的一次加载几张 Bitmap。组件像ListView、GridView、ViewPager通常包括多个 Bitmap 在屏幕上同时出现,更多的潜在关屏幕时,手指轻弹显现图像。

高效加载大型位图


————————————————————————————

图像有各种形状和大小。在许多情况下,它们比用于典型的应用程序的用户界面(UI)需要更大。例如,系统相册应用程序显示照片使用 Android 设备的相机,通常比设备的屏幕密度更高的分辨率。

应用程序工作的时候是有内存限制的,在理想情况下,你只想在内存里加载一个低分辨率的版本。低分辨率的版本应该适配UI组件显示的大小。一张高分辨率的图片并没有提供可见的好处,但是还要占用珍贵的内存和由于额外的动态缩放所带来的额外的性能开销。

本课程将引导你解码大的位图,不超过每个应用程序的内存限制,通过在内存里加载一个小的子采样版本。

1.阅读位图尺寸和类型

BitmapFactory 类提供了几个解码方法(decodeByteArray(), decodeFile(), decodeResource())用来从各种资源中创建一个 Bitmap。根据图片数据资源选择最合适的解码方法。这些方法试图为构建Bitmap对象分配内存,因此很容易造成 OutOfMemory 异常。每种类型的解码方法都有额外的签名,让你指定通过解码选项 BitmapFactory.Options 类。设置 inJustDecodeBounds 为 true 当解码的时候避免内存分配,返回了 null 的 Bitmap 对象,但是设置了 outWidth,outHeight 和 outMimeType。这个技术允许你在阅读图像数据的尺寸和类型前去构建一个 Bitma 对象(避免内存分配)。

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;

避免了 java.lang.OutOfMemory 异常,在解码前检查图像尺寸,除非你绝对信任其来源为你提供了在可用内存里正适合的图像数据。

2.加载一个缩小的版本到内存

现在知道了图像尺寸,他们可以被决定使用是否全图加载到内存,或者用加载采样版本来代替。下面有一些考虑因素:

1.估计加载整个图像到内存中,内存的使用情况

2.内存量。你愿意承担加载这张图像在应用程序中的任何内存要求。

3.图像被加载到 ImageView 或者UI组件的目标尺寸。

4.当前设备的屏幕大小和密度。

例如,加载1024x768 像素的图片到内存中,如果它最终在 ImageView上显示128x96的缩略图,这是不值得的。

告诉解码器样本图像,加载一个小版本到内存中,如果你使用 BitmapFactory.Options 对象的话,设置 inSampleSize 为 true。例如,对于2048×1536分辨率的图像,它解码产生4个大约512x384的位图。加载到内存中使用0.75MB,而不是12MB的完整图像(假设的Bitmap 配置为ARGB_8888)。下面方法计算一个样本大小值的比率是基于目标宽度和高度的二值化:

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,在ImageView 显示 100x100 的缩略图。

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
处理位图要关闭UI线程


————————————————————————————

BitmapFactory.decode* 方法,在 Load Large Bitmaps Efficiently 这节课已经讨论过了,不应该在主UI线程中执行,如果从磁盘中或者网络上读取图像数据(或者内存以外的任何来源)。这个数据加载时间是不可预测的,并取决于多种因素(从磁盘或网络,图像的大小,CPU的功率等读取速度)。如果这些任务中的一个阻塞了UI线程,应用程序会出现一个无响应的系统标志,用户有一个选项能关闭它(更多信息请看 Designing for Responsiveness)。

本课程指导你处理位图在后台线程中,使用AsyncTask,并告诉你如何处理并发问题。

1.使用AsyncTask

AsyncTask 类提供了一个简单的方法,在后台线程中执行一些工作,并将结果发布回UI线程上。要使用它,创建一个子类,并重写所提供的方法。这里有一个例子,使用的AsyncTask和decodeSampledBitmapFromResource() 加载一张大图到 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 的 WeakReference 确保 AsyncTask不妨碍 ImageView的任何关于它的引用被垃圾回收。不需要保证 ImageView 一直存在当任务完成的时候,所以你必须要在 onPostExecute() 检查引用。这个 ImageView 可能不再存在,如果用户从 Activity 回到导航,或者配置发生改变在任务完成之前。

要启动异步加载位图,只需创建一个新的任务,并执行它:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

2.处理并发

常见的View组件,像ListView、GridView和AsyncTask一起使用时会引入另一个问题,如前一节中所示。为了高效内存,用户在滚动时,这些组件要重复利用子View。如果每个子View引发一个AsyncTask,也不能保证它完成时,相关联的View尚未再循环用于另一子View的使用。此外,这里并不能保证异步任务启动的顺序是它们完成的顺序。

博客 Multithreading for Performance 进一步讨论处理并发性,并提供了解决方案,当任务完成之后检查最近的AsyncTask中大量ImageView的引用的地方。使用一个类似的方法,所述的AsyncTask从上一节可以扩展到遵循类似的模式。

创建一个专用Drawable子类来存储引用,用来回到工作任务中。在这种情况下,一个BitmapDrawable作为一个占位符图像显示在ImageView中当任务完成时:

static class AsyncDrawable extends 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();
    }
}

在执行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 方法的代码引用,检查另一个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 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() 是一个辅助方法,是用于检索与特定任务相关的图片。

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

最后一步是在onPostExecute() 方法中更新,如果当前任务匹配关联ImageView,检查 BitmapWorkerTask 任务是不是被取消。

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

实现这个现在适合使用ListView和GridView组件以及任何其他组件,回收子视图。只需调用 loadBitmap() ,当你通常在ImageView上设置一张图片。例如,在一个GridView实现它,只需要getview()方法中返回支持的适配器。

缓存位图


————————————————————————————

加载单张 Bitmap 到用户界面是很直接的,然而,事情变得更复杂,如果你需要加载一个更大的一组图像。在许多情况下,(像ListView、GridView、ViewPager),这些图片的总数基本上是无限的,结合这些图像可以快速的从屏幕上滚动。

内存使用时保持不变的,这些组件通过回收滚出屏幕的子View来保存内存。垃圾回收器也释放你加载的图片,假如你没有保持着一个长时间活着的引用。这一切都很好,但是为了保持流畅的和快速加载的UI,你要避免不停地处理加载到屏幕中的图片。内存和磁盘缓存能有所帮助,让组件能够快速加载处理的图像。

本课程将教你使用的是内存和硬盘位图缓存加载多个图片时,改善用户界面的响应性和流动性。

1.使用内存缓存

内存缓存提供了可以快速的访问图片,代价是占用应用程序宝贵的内存。

LruCache 类特别适合用来缓存图片,保持最近引用的对象在 LinkedHashMap 中有个强引用,在缓存超过指定大小之前,赶出最近最少使用的成员。

注意:在过去,一个流行的内存缓存是通过 SoftReference 或者WeakReference 图片缓存来实施的,然而,它不是被推荐的。自从 Android 2.3 开始,垃圾收集器更具侵略性,收集软/弱引用,这使他们相当无效。此外,在 Android 3.0 之前,图片支持的数据被存储在本地内存,并没有用可预测的方式来释放,潜在的造成应用程序短暂的超过内存限制而引起的崩溃。

为了选择一个合适的尺寸LruCache,应考虑到许多因素,例如:

1.内存密集型是Activity或者应用程序的其余部分?

2.有多少图片同时出现在屏幕上?有多少准备好在屏幕上显示?

3.设备的屏幕尺寸和密度是多少?一个额外的高密度屏幕的设备(像Galaxy Nexus xhdpi)需要大的缓存来维持内存中相同数量的图片,相比较于 Nexus S hdpi。

4.什么尺寸和配置的图片,因此会占用多少内存?

5.频繁的访问图片?有比其他人更频繁的访问吗?如果是这样的话,也许你可能想在内存中保持某些条目,甚至出现了多个LruCache对象在不同的图片组中。

6.你能平衡质量和数量吗?有时它可以更有用的去存储大量低质量的图片,可能加载一个高质量的版本在另一个后台的任务中。

没有指定的大小和公式来适合所有的应用程序,这取决于对你的使用进行分析,并提出一个合适的解决方案。缓存很小引起额外的开销,也是不好的,缓存太大可能再次引起 java.lang.OutOfMemory 异常,留下应用程序剩余的很少的一点内存来工作。

下面是一个的例子给图片建立一个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);
}

注意:在这个例子中,应用程序的内存八分之一被分配给我们的缓存。在一个正常的hdpi的设备,最低是4MB左右(32/8)。全屏幕GridView充满图像的设备上,使用800x480分辨率将使用1.5MB左右内存(800 x480x4),因此在内存中最少缓存2.5页图像。

当在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.使用磁盘缓存

内存缓存加速访问最近查看过的位图是有用的,然而,你不能依赖于缓存中可用的图像。像GridView 组件有大量的数据,很容易填满内存缓存。你的应用程序不能够中断,当另一个任务在跑的时候(像电话来了),应用程序就退到后台,可能会被杀死,内存缓存被破坏。一旦用户重启,你的应用程序就会再次处理每个图片。

磁盘缓存可以用于这些情况,持续处理位图和有助于减少加载时间,图像不再可用在内存缓存。当然,从磁盘中读取图像比从内存中加载慢,并且应该在后台线程中完成,因为磁盘读取时间是不可预测的。

注意:ContentProvider可能是一个更加合适的来存储缓存图像的地方,如果他们访问更频繁,例如图像应用程序。

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

注意:初始化磁盘缓存需要磁盘操作,因此不应该发生在主线程中。但是,这并不意味着在初始化之前能有访问缓存的机会。为了解决这个问题,在上面的实现中,一个锁对象确保应用程序不会从磁盘缓存中读取,直到缓存已被初始化。

内存缓存检查在UI线程中,但是磁盘缓存检查在后台线程中。磁盘操作从不应该在UI线程中。当一张图片处理完成,最后的位图被添加到内存和磁盘高速缓存中,以供将来使用。

3.处理配置改变

运行配置改变,比如屏幕方向,造成运行Activity的销毁和重启配置新的Activity(有关此问题的详细信息,请参阅处Handling Runtime Changes)。你要避免再次处理图片,使用户在配置改变时有一个极致的体验。

幸运地是,你的位图有好的内存缓存,因为你建立了 Use a Memory Cache 部分。缓存通过 setRetainInstance(true) 方法来保存Activity实例使用的一个Fragment。在Activity再次被创建的时候,保留的Fragment能够被连接上,你能访问存在的缓存对象,允许图片能够快速的获取和填充在ImageView对象上。

这里的例子保留了LruCache对象,在配置更改使用的Fragment:

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。当你保留缓存时,你应该注意到几乎没有任何延迟,因为Activity几乎立即从内存中取出来填充图片。没有找到在内存缓存中的任何图像,就希望在磁盘缓存中能找到,如果没有,他们像往常一样处理。

管理位图内存


————————————————————————————

上一节处理描述缓存位图,你做一些帮助垃圾回收器和图片重用的事情。推荐的策略取决你的目标的于Android系统版本。BitmapFun 这个App展示了如何设计你的应用在不同Android版本的提高工作效率。

设置这个课的阶段,Android对位图内存的管理是怎样发展的:

1.在Android2.2(API 8)和更低,当垃圾回收器触发时,你应用的线程会被停止。这会导致一个延迟,会降低性能。Android 2.3增加了一个并行垃圾回收器,这意味着内存被回收后不久,位图就不再被引用。

2.在Android 2.3.3(API 10)和更低,位图支持像素数据存储在本地内存中。它独立于bitmap本身,这是存储在Dalvik堆。本地内存中的像素数据不能以可预测的方式释放,可能会导致应用程序短暂的超过它的内存限制并崩溃。作为Android 3(API 11),像素数据存储在Dalvik堆以及相关的位图。

下面的章节描述了如何优化内存的位图管理在不同的Andr​​oid版本上。

1.在Android 2.3.3和更低上的内存管理

在Android 2.3.3和更低上的内存管理,使用recycle()方法是比较推荐的。如果你在你的应用程序上显示大量的图片数据,你可能或出现 OutOfMemoryError 错误。recycle()方法允许应用程序尽可能的回收内存。

当你确定图片不再被使用的时候,才调用recycle()方法,如果你使用了recycle()方法,之后再去绘制位图,你将会得到一个错误“Canvas: trying to use a recycled bitmap”

下面的代码片段是一个调用recycle()的例子,它使用引用计数(在变量mdisplayrefcount和mcacherefcount)来跟踪当前位图是否显示或缓存。代码回收位图时,这些条件都满足:

1.引用计数mDisplayRefCount 和 mCacheRefCount 都为0。

2.位图不空,而且尚未回收。

private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            mDisplayRefCount++;
            mHasBeenDisplayed = true;
        } else {
            mDisplayRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            mCacheRefCount++;
        } else {
            mCacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

private synchronized void checkState() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

2.在Android 3.0和更高上管理内存

Android 3.0(API 11)推出 BitmapFactory.Options.inBitmap 属性。如果这个选项被设置,当加载内容的时候,解码方法将根据 Options对象尝试重用现有的位图。这就意味着位图内存被重用,提高性能,删除内存分配和解除分配。然而,有一定的限制,就是inBitmap怎样被使用。特别是在Android 4.4(API 19)之前,只支持同等大小的位图。有关详细信息,请参阅 inBitmap文档。

保存位图给以后使用

下面的代码演示了存储已经存在的位图在示例的应用程序中,供以后使用。当应用程序在Android 3或更高版本上运行,位图是从LruCache取出,位图的软引用被放置在一个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()));
            }
        }
    }
....
}

使用一个存在的位图

在运行的应用程序中,解码方​​法进行检查,看是否有存在的位图,有就可以使用。例子:

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()方法的。为存在的位图设置inBitmap 的值。注意,此方法只设置一个值 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;
}

最后,该方法确定候选位图满足尺寸标准用于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;
}
位图显示在UI


————————————————————————————

这节课把前面的课程内容集中在一起,在GridView或者ViewPager上加载多个位图,使用后台线程和位图缓存,处理并发和配置改变。

1.加载位图到ViewPager的实现

swipe view pattern 是一个浏览图片库的详细视图的很好的方式。你能够实现这个模式使用ViewPager组件支持,通过PagerAdapter。然而,一个更合适的支持适配器是 FragmentStatePagerAdapter 类,自动保存和销毁ViewPager中Fragment的状态,当他们消失在屏幕的时候,保持内存使用。

注意:如果你有图像的数量较少,并有信心他们都在应用的内存限制之内,则使用常规的PagerAdapter或FragmentPagerAdapter可能更合适。

下面实现ViewPager中ImageView,主Activity持有ViewPager和适配器:

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的实现,这可能看起来是一个完全合理的方法,但你能看到这个实现的缺点吗?如何改进?

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

希望你注意到这些问题,从UI线程获取图片资源,可能导致应用程序挂起和焦点关闭。使用 的AsyncTask类,直接在后台线程中加载和处理图像。

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

任何额外的处理(如调整或获取来自网络图片),只要发生在 BitmapWorkerTask 中都不影响主界面的响应。如果后台线程正在做的不仅仅是直接从磁盘上加载一个图像,它有利于增加内存或磁盘高速缓存。下面是一个内存缓存的补充修改:

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache<String, Bitmap> 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
}

把所有这些碎片拼凑起来给你一个回应,ViewPager 是以最小的图像加载等待时间,并根据你的图像需求尽可能多的在后台处理能力实现是很好的。

2.加载位图到GridView的实现

网格列表构造块 显示图片数据集市很有用的,通过GridView组件实现,其中许多图像可以在屏幕上在任何一个时间随时出现,如果用户向上或向下滚动。实现这种控制的时候,你必须确保UI依然流畅,要控制内存的使用和正确的并发处理(由于GridView回收子View)。

开始,这里是一个标准的GridView实现与ImageView放在Fragment。然后,这似乎是一个完全合理的方法,但你会做的更好?

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

再一次又出现这个问题,图片在UI线程中设置。虽然这个工作量可能很小,图像简单(由于系统资源加载和缓存),如果图像需要做额外的处理,你的UI卡住。

相同的异步处理和缓存方法在前面已经实现过了。然后,你也需要警惕并发问题,当GridView回收子View的时候。这里是最新的解决方案:

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<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(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

这个实现允许图像处理和加载是很灵活性,而不会阻碍用户界面的流畅性。在后台任务中,可以从网络加载图像或调整相机的照片,任务处理完成时显示图片。

本课中有一个完整的例子,请参阅包含的示例应用程序。

官方实例demo:http://download.csdn.net/detail/u012301841/9563307

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值