Android训练课程(Android Training) - 高效的显示图片
高效的显示图片(Displaying BitmapsEfficiently)
了解如何使用通用的技术来处理和读取位图对象,让您的用户界面(UI)组件是可响应的,并避免超过你的应用程序内存限制的方式。如果你不小心,位图可以快速消耗可用的内存预算而导致应用程序崩溃,引发可怕的异常:
java.lang.OutofMemoryError: bitmap size exceeds VM budget
.
下面是一些 为什an么在你的Android应用程序加载位图是棘手的原因 :
- 移动设备通常拥有受限的系统资源。Android设备分配给每个应用的可用内存空间只不过16MB。在 Android兼容性定义文档[ Android Compatibility Definition Document (CDD)], 3.7章节. 虚拟设备的兼容性一文 为了适应多屏幕尺寸和密度指定了最小应用内存需求。应用程序需要优化去处理最小的内存限制。然而,要记住很多设备被设置成更高的限制。
- 位图占据大量的内存,特别是那些丰富的图像,比如照片。例如,硬件类型为 Galaxy Nexus 的设备 的相机应用拍的照片达到了 2592x1936 像素 (5百万像素).如果位图被配置为使用
ARGB_8888
(在 Android 2.3 以前是默认的),加载这个图像到内存里需要19MB的内存(2592*1936*4 字节),马上就会耗尽一些设备汇总的单个应用的内存限制。 - Android应用的UI 需要即时地加载多个位图。像
ListView
,GridView
和ViewPager
组件 通常包含多个位图在屏幕上,更多可能性在关闭屏幕时,使用手指拨动,立即准备去显示。
课程
- 高效的加载大尺寸位图 (Loading Large Bitmaps Efficiently)
- 本课将引导您在不超过每个应用程序的内存限制下,解码大位图。
- 在UI线程外处理位图(Processing Bitmaps Off the UI Thread)
- 位图处理(调整大小,从远程资源下载等)不应该占用主UI线程。这节课将引导你通过使用AsyncTask在后台线程中处理图像,和解释如何处理并发问题。
- 位图缓存 (Caching Bitmaps)
- 这节课将引导你 在读取多个位图时,使用内存和硬盘缓存来提高你的UI的 响应性 和流畅性。
- 管理位图内存 (Managing Bitmap Memory)
- 这节课将引导你 如何管理位图的内存以最大化你的应用的性能。
- 在UI上显示位图 (Displaying Bitmaps in Your UI)
- 这节课将所有的综合在一起,向你展示如何加载多个图片到你的组件中(比如
ViewPager
andGridView),并使用一个后台线程和位图缓存。
高效的加载大尺寸位图
图片有各种形状和大小. 在很多情况下,它们有更大的需要超过一个典型的应用程序的界面。例如,Gallery(画廊)系统应用在显示图片时,使用了设备的摄像头,它(摄像头)通常的分辨率要高于你的设备的屏幕密度。
既然你正在使用有限的内存,理想情况下,你只应该在内存中加载一个低分辨率的版本的图片。低分辨率版本的图片应该匹配你要显示的UI组件的尺寸。一个更高分辨率的图片不能提供更多可见的好处,但是仍然占据珍贵的内存空间,和由于额外的缩放而导致额外的性能开销。
这节课教你 解码大尺寸的图片而不越过每个应用的内存限制,以在内存中加载一个更小的 样本版本(缩略图)的方式。
读取位图的尺寸大小和类型
BitmapFactory类提供了多个对图片解码的方法 (
,以从不同的数据源创建位图对象。基于你的图像数据源来选择合适的解码方法。这些方法的作用是为结构化的位图分配内存,因此很容易的返回decodeByteArray()
, decodeFile()
,decodeResource()
, 等.)OutOfMemory
异常。每种类型的解码方法都有扩展的方法签名参数,可以通过BitmapFactory.Options类来帮助你指定解码选项(参数)。设置 inJustDecodeBounds
属性为 true可以忽略内存分配的步骤,它会返回 null
的位图对象,但是为选项outWidth
, outHeight
和 outMimeType 赋值了。这个技术允许你读取位图数据的尺寸和类型而不构造位图对象(分配内存)。
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;
为了不出现 java.lang.OutOfMemory
异常,在解码前先要检查位图的尺寸,除非你绝对的信任你的数据源以一种可预见的图片尺寸大小与最小的可用内存是合适的。
读取缩放后的图像到内存
现在我们知道了图像的尺寸,他们可被用于决定是否使用完整的图像加载到内存或者采用缩略图加载到内存。下面是一些考虑的因素:
- 估计记载整个图片到内存后的内存占用(使用)量
- 基于你的应用的其他内存需要, 你愿意的分配给的 加载图片的内存占用量
- 目标
ImageView
的尺寸 或者 你要加载到显示用的UI组件的 尺寸。 - 当前设备的屏幕尺寸和密度
例如,加载分辨率为 1024x768 像素的图像到内存,最后却只显示在一个 ImageView上的 128x96的缩放后图像,是非常不值得的。
要告诉解码器来抽样(缩放)一个图像,设置BitmapFactory.Options
对象的 inSampleSize
为 true。例如,
一个分辨率为2048x1536 的图像在使用
inSampleSize
等于4 时,产生一个 大约512x384 的位图。加载这个倒内存需要0.75MB,全部加载整图则需要12MB(假设使用 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; }
注意: 一个比率 是被计算出来的,因为解码器使用了一个固定值。通过舍入到最接近的 比率。按照 inSampleSize
文档。
要使用这个方法, 第一次解码使用 inJustDecodeBounds
设置为 true
, 传入设置的参数。然后再次解码使用 inSampleSize
value 和 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); }
这个方法很容易的加载一个 未知的大尺寸图像 到 一个 "需要显示100x100像素的缩略图的 imageView"上:
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
你可以使用类似的方法去处理其他数据源的位图,有需要时用来替换 BitmapFactory.decode*方法。
在非UI线程上处理图像
BitmapFactory.decode*系列方法,在 Load Large Bitmaps Efficiently 这节课里就讨论过,如果源数据时需要从硬盘或者网络位置读取时(或者其他真实的不是内存的数据源),不应该在主UI线程执行。加载图片所用的时长是不可预测的,和依赖多个因素(从硬盘或者网络的读取速度,图像尺寸,CPU的能力等等)。如果一个任务阻塞的UI线程,那么系统就会标记你的应用为 未响应的,用户就会收到一个关闭选项的对话框(更多请阅读 Designing for Responsiveness )。
这节课引导 使用AsyncTask
在后台线程中处理图片的处理方式和,展示如果处理并发问题。
使用一个异步任务 AsyncTask
AsyncTask
提供了一个简单的方式来在后台线程中执行工作,和发布处理结果回调到UI线程中。要使用它,只需创建一个子类和重载提供的方法。下面是一个家在大图像到ImageView 中的示例,它使用了AsyncTask
和 上一节课中提到的decodeSampledBitmapFromResource()
:
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); }
处理并发
一般的视图组件,比如ListView
and GridView
当结合AsyncTask 使用时会有一些其他问题。为了有效的利用内存,这些组件在滚动时会回收重用它们的子视图控件。如果每个子控件都在AsyncTask中引发,那么当任务完成时就无法得到保证,导致被关联到的视图还没有被回收,就使用在其他子视图中了。此外,这也无法保证异步任务开始的顺序和它结束的顺序是一致的。
在Multithreading for Performance(多线程任务性能)这篇博客中讨论了并发的处理,和提供了一个解决方案,在ImageView上存储一个 指向最近的一次的异步任务AsyncTask的 引用,而当任务完成后再次检测该引用。使用类似的方法,我们随着这样的模式,扩展我们上一章节中提到的AsyncTask方法。
创建一个专用的Drawable子类,以存储一个 工作任务(AsyncTask)对象的引用。在这种方式中,一个 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()
方法去终止上一次的任务。在很少的情况下,新任务的数据匹配已经存在的任务,并且不在需要触发。下面是一个cancelPotentialWork的实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; // If bitmapData is not yet set or it differs from the new data if (bitmapData == 0 || bitmapData != data) { // Cancel previous task bitmapWorkerTask.cancel(true); } else { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true; }
辅助方法 getBitmapWorkerTask()
, 在上面被用于 获得指定ImageView 关联到的 任务对象:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; }
最后一步是在 BitmapWorkerTask
的 onPostExecute()方法中的更新操作,它检查了 任务是否被终止过了和 当前的任务是否是 ImageView关联的任务。
The last step is updating onPostExecute()
in BitmapWorkerTask
so that it checks if the task is cancelled and if the current task matches the one associated with the 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); } } } }
这样的实现适用于 ListView
和 GridView
组件及其他需要回收他们子视图的组件。在你平时设置图像到ImageView的地方简单的调 loadBitmap
方法。比如,在一个 GridView
中实现方式就是 在 adapter中的 getView()方法中调用。
缓存图像
加载一张图像到你的UI很简单,然而如果你需要一次性加载一批图片就会很复杂。在很多情形下(比如ListView
, GridView
或 ViewPager
),屏幕上的图像总数,结合那些不久后滚动后显示再屏幕的图片,根本就是无限的。
有些组件 通过回收移除屏幕的子视图的方式 可以保持较少的内存使用 。加入你没有或者更长久的活动引用,垃圾回收器将会释放你加载的图片。这是好的情况,但是为了保持流畅性和 快速加载UI,你不需要再处理 那些 “再次回到屏幕上的图像 ”。在这里一个内存和磁盘缓存常常是有帮助的,允许组件哭诉的重新加载处理过的图像。
这节课将引导你,当加载多个图像时,使用一个内存和磁盘图像缓存来提高UI的响应性和流畅性。
使用一个内存缓存
一个内存缓存提供了快速访问位图的方式,更好的占用珍贵的应用程序内存。LruCache 类(在Support Library 安卓支持可 API 4 中)很适合 缓存图像的任务,它以LinkedHashMap
中的强引用方式 保持最近被引用的对象和 在缓存数量超过指定的数量时移除最近最少使用的成员。
注意: 在过去,流行的内存缓存的实现是使用SoftReference
或 WeakReference
位图缓存,然而现在已经不再推荐使用。从Android 2.3(API 级别 9)开始,垃圾回收器更激进的回收 软引用/弱引用,使得相当于无效。另外 在 Android 3.0 (API 级别 11)之前,一个位图的后台数据被存放在原始内存中,它不能以可预见的方式被释放,它潜在性的导致一个应用临时的超出它的内存限制而崩溃。
为了选择一个合适的LruCache 的尺寸,
一些因素必须要考虑到,比如:
- 你的剩余的activity或者应用程序 是如何 集中 你的内存的?How memory intensive is the rest of your activity and/or application?
- 一次加载多少图像到屏幕上显示? 有多少图片即将准备显示到屏幕上?
- 设备的屏幕尺寸和密度是多少?在内存中显示相同数量的图片,一个更高级的高密度屏幕 (xhdpi)设备比如 Galaxy Nexus 比起 Nexus S (hdpi)设备 需要更多的缓存。
- 这些图片的尺寸规格和配置是什么,每个将占据多大的内存?
- 图像被访问的频率?是否有些图像被访问的频率比其他的高?如果这样,或许你需要一直在内存中保持某些图像,后者使用多个
LruCache
对象对应多个图像的分组。 - 你能在质量和数量之间保持平衡么? 有时 存储大量的低质量图像更有用,潜在的在其他后台线程中加载高质量的图像版本。
没有适用于所有应用程序的绝对的指定尺寸和准则,由你分析你的使用情况来决定,并上升到一个合适的方案。一个缓存如果太小,则导致额外的无益的超过限额,如果过大而再次导致java.lang.OutOfMemory
异常或者为你的app提供更少的剩余内存可工作。
下面的示例演示了 如何为图像配置 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). 在一个 800x480分辨率 的设备中,一个全屏的 GridView被填满图像的话,大约需要1.5MB (800*480*4 bytes), 这样将可以在内存中缓存大约 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; } ... }
使用一个磁盘缓存
对于快速的访问最近呈现过的图像,一个内存缓存是非常用于的,然而你不能完全保证 图片在这个缓存中。像GridView这样的组件有大量的数据集合,可以很容易的填满整个内存缓存。你的应用可能被其他任务(比如电话来了)所打断,和在后台时会背傻吊和内存缓存被销毁。一旦用户恢复了应用,你的应用需要再次处理每一个图像。
一个磁盘缓存可以被应用到这些场景,当图像无法在内存缓存中可用时,可以持续访问图像和帮助减少加载图像的次数。当然,从磁盘缓存中提取图像相比较于从内存中来说是较慢的,并且最好在后台任务中处理,磁盘读取次数可能不可预知。
注意: 如果访问很频繁,一个 ContentProvider 可能更适合用于缓存图像,
在gallery 应用示例中 有更多演示。
下面的演示代码使用了一个 DiskLruCache
的磁盘缓存实现,它来自于 安卓源代码 Android source. 下面是一个更新后的示例,它在内存缓存之外,又增加了一个磁盘缓存:
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线程中使用。当图像处理完成后,最后的图片被添加到内存缓存和磁盘缓存中。
处理配置的变化
运行时配置变化,比如屏幕方向改变,导致Android销毁和 使用新的配置 重新启动运行中的activity(更多信息参考Handling Runtime Changes)。当一个配置改变发生时,你可能想不再重新处理你所有的图片,以获得平滑快速的用户体验。
幸运的是,在 使用内存缓存(Use a Memory Cache ) 一节中你拥有了一个很好的图片内存缓存。通过一个 调用了setRetainInstance(true)的
Fragment保存缓存对象,再将Fragment 传到新的Activity示例中。在activity被重新创建后,这个重新创建的(保留的)的 Fragment 被重新附加,这样你重新通过它获得到缓存对象,允许图像被快速提取和重新填充到 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); } }
为了验证这一点,使用 有或者没有保留的Fragmen 方式来尝试旋转一个设备。你可以注意到,在图像填充到activity上时几乎没有滞后,在你获得缓存时是即刻从内存中的。一些图像没有从内存中被找到,也是有希望在磁盘缓存中找到,如果没有找到,就会像平常那样处理。
管理图片内存
除了在 缓存图像(Caching Bitmaps) 章节描述的步骤,这里有些明确的事情可以做,以帮助垃圾回收和重用图像。根据不同的Android版本不同有不同的推荐策略。BitmapFun
示例包含了一些类,展示了如何设计你的程序以在不同的Android版本中更有效率的工作。
为了对这节课划分段落, 先了解Android如何管理图片内存的演变过程:
- 在 Android 2.2 (API 级别 8) 及以下,当垃圾回收发生时,你的应用的线程会暂停。这导致了延迟,降低了性能。Android 2.3添加了并发的垃圾回收,这意味着,失去引用的图像的内存很快被回收。
- 在 Android 2.3.3 (API 级别 10) 及以下,位图的后备的像素数据被存储在原生内存中。它被和位图本身分开,它被存储在Dalvik 的堆中。 在原生内存中的像素数据部能以可预知的方式被释放,可能导致一个应用临时的越过内存限制而崩溃。 Android 3.0 (API 级别 11)中,像素数据也被存储在Dalvik 的堆中,和它关联到的位图一起了。
下面的章节描述了 在不同的Android版本中如何优化内存的管理。
Android 2.3.3 及以下 的内存管理
在 Android 2.3.3 (API 级别 10)及以下,推荐使用 recycle()
方法。如果你在你的应用中显示大量的图像数据,或许你遇到过 OutOfMemoryError
错误。recycle()
方法允许你尽快的回收内存。
警告: 当你确定你的位图对象不再使用的时候,你可以调用 recycle()
如果你调用了 lrecycle()
,而又试图绘制这个位图,你将会受到一个错误: "Canvas: trying to use a recycled bitmap"
.
下面的代码片段提供了一个 调用 recycle()
. 的演示。它使用了引用计数(通过变量 mDisplayRefCount
和 mCacheRefCount
)来追踪 一个位图当前被显示或者在缓存中。当下列条件成立时回收图像:
- 引用计数
mDisplayRefCount
和mCacheRefCount
都为 0. - 位图不是
null
, 并且还未被回收
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();
}
Android 3.0 及 更高版本上 管理内存
Android 3.0 (API 级别 11) 提供了 BitmapFactory.Options.inBitmap
字段。如果这个选项被设置了,在加载内容时,使用了这个选项的解码方法将会试图去重用已经存在的位图。这意味着,位图内存被重用了,而提升了性能,它移除了内存分配和回收的步骤。然而,inBitmap
也是有些已知的局限,具体的说,在Android 4.4 (API 级别 19)之前,只能尺寸大小相同的图片才被支持重用。更多信息请阅读 inBitmap
文档。
保存位图以备后用
下面的代码片段演示了 如何保持一个位图以备将来使用。在运行在Android 3.0或者更高版本上的一个应用中,一个图片被从 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 上显示位图
这节课总结了上面课程的内容,向你展示了如何加载多个图像到 ViewPager
和 GridView 组件中,使用了后台线程,图片缓存,处理并发和配置的改变。
加载图像到 ViewPager 的实现
滑动屏幕模式 ( swipe view pattern ) 是一个极好的方式来导航图像画廊的详细视图页。你可以使用 一个PagerAdapter支持的ViewPager
组件来 实现这个模式。 然而,可能的更适合的支持适配器是 FragmentStatePagerAdapter
的子类,在从屏幕上不可见,内存较低时,它自动的销毁和保存 ViewPager
中的 Fragments
的状态。
注意: 如果你只有很少的数量的图像和确信 它们适用于应用的内存限制内,那么一个普通的 PagerAdapter
或 FragmentPagerAdapter 可能更合适。
下面是一个 拥有ImageView子元素的 ViewPager的实现,主Actvity 持有了 ViewPager和 adapter。
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 } }
希望你注意到问题: 图像从资源文件中读取的过程 是在主UI线程的,它可能导致应用挂起和被强行关闭。使用一个 AsyncTask
,像上面的课程 在UI线程外处理图像 一课中描述的那样,简单的移动图像加载和处理的过程到后台线程中:
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
中,不会影响主UI线程的响应性。如果后台线程要很多次直接从磁盘中加载图像,那么添加一个内存或者磁盘缓存是很有益的,像课程 缓存位图 中描述的那样。下面是使用内存缓存做了额外的修改:
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的实现,使用了更好图像记载延迟,并且有能力的 尽可能多或者尽可能少的在后台处理图像。
加载图像到 GridView 中的实现
网格列表构造块( grid list building block )对于展示图像数据集合是十分有用的,它可以通过GridView组件方式的实现。很多图像需要一次性被加载到屏幕上,当上下滚动时很多图像还需要准备好被显示。当实现这样的控件类型时,你一定要确保UI仍然流畅,内存使用率在可控内和正确的处理并发(由于 GridView 回收它们的子视图 的方式导致)
要开始,下面是一个标准的 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 回收它们的子视图,你仍然需要一个并发问题的方式。要处理它,使用 在UI线程外处理图像 课程中讨论过的技术,下面是个更新后的方案:
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
注意: 同样的代码可以轻易的适配到 ListView 中。
这个实现允许很灵活的处理 图像的处理和加载,而不阻止UI的平滑。在后台任务中,你可以从网络加载图像或者 改变大的相机照片的图像尺寸,在任务完成后,图像即呈现出来。
关于这的一个完整的示例,和这节课其他概念的讨论,请阅读包含的示例应用。
(本课程完,张云飞,2015-08-25)