宿州市住房和城乡建设局网站,十堰秦楚网主页,宁波网站营销推广制作,软件工程是工学还是理学Android批量加载图片OOM问题 前言使用内存缓存使用磁盘缓存处理配置更改 前言
将单个位图加载到界面中非常简单#xff0c;但如果您需要同时加载较大的一组图片#xff0c;则操作起来会比较复杂。实际上#xff0c;在许多情况下#xff08;比如使用 ListView、GridView 或… Android批量加载图片OOM问题 前言使用内存缓存使用磁盘缓存处理配置更改 前言
将单个位图加载到界面中非常简单但如果您需要同时加载较大的一组图片则操作起来会比较复杂。实际上在许多情况下比如使用 ListView、GridView 或 ViewPager 等组件时屏幕上的图片与可能很快会滚动到屏幕上的图片加起来数量是无限的。
系统通过循环利用移出屏幕的子视图来限制此类组件对内存的占用。垃圾回收器假设您不会保留任何长期的引用因此也会释放已加载的位图。这些都没有问题但是为了确保能够快速、流畅地加载界面您必须避免每次这些图片返回到屏幕上时都要处理这些图片。通常采用内存和磁盘缓存会有所帮助因为这可以让组件快速重新加载经过处理的图片。
使用内存缓存
内存缓存可以提供对位图的快速访问但代价是会占用宝贵的应用内存。LruCache 类支持库中也提供了该类最低可支持 API 级别 4非常适合用于以下任务缓存位图将最近引用的对象保持在强引用的 LinkedHashMap 中并且在缓存超出其指定大小之前移除上次使用时间最早的成员。 如需为 LruCache 选择合适的大小需要考虑多种因素例如
activity 和/或应用的其余部分对内存的占用情况如何一次会在屏幕上显示多少张图片有多少张图片需要准备好随时可以显示在屏幕上设备的屏幕尺寸和密度是多少相比于 Nexus S (hdpi) 这样的设备超高密度屏幕 (xhdpi) 设备如 Galaxy Nexus需要更大的缓存才能在内存中保存相同数量的图片。位图的尺寸和配置如何每个位图会占用多少内存图片的访问频率是多少是否有一些图片的访问频率会高于其他图片 如果是这样您可能需要将某些项始终保留在内存中甚至为不同的位图组创建多个 LruCache 对象。能否在质量和数量之间取得平衡有时存储更多低质量的位图会更有用这样做可能需要在另一个后台任务中加载更高质量的位图。
没有适合所有应用的特定大小或公式你应该自行分析使用情况并找到适合的解决方案。缓存过小会产生额外的开销且没有任何好处缓存过大又会造成 java.lang.OutOfMemory 异常并让应用的其余部分没有多少内存可用。
以下是为位图设置 LruCache 的示例
private LruCacheString, Bitmap memoryCache;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;memoryCache new LruCacheString, Bitmap(cacheSize) {Overrideprotected 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) {memoryCache.put(key, bitmap);}
}public Bitmap getBitmapFromMemCache(String key) {return memoryCache.get(key);
}在本示例中将八分之一的应用内存分配给了缓存。在普通/hdpi 设备上此内存最少为 4MB32/8左右。在分辨率为 800x480 的设备上填充了图片的全屏 GridView 大约会占用 1.5MB800 * 480 * 4 字节的内存这会在内存中缓存至少 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 AsyncTaskInteger, Void, Bitmap {...// Decode image in background.Overrideprotected Bitmap doInBackground(Integer... params) {final Bitmap bitmap decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);return bitmap;}...
}使用磁盘缓存
内存缓存有助于加快对最近查看过的位图的访问但你不能依赖于此缓存中保留的图片。GridView 这样拥有较大数据集的组件很容易将内存缓存填满。应用可能被其他任务如电话中断而在后台时应用可能会被终止而内存缓存则会销毁。用户恢复操作后应用必须重新处理每张图片。
在这些情况下可以使用磁盘缓存来保存经过处理的位图并在图片已不在内存缓存中时帮助减少加载时间。当然从磁盘获取图片比从内存中加载缓慢而且应该在后台线程中完成因为磁盘读取时间不可预测。
这个类的代码示例使用了从 Android 源代码中提取的 DiskLruCache 实现。 以下是更新后的代码示例该示例在现有的内存缓存之外又添加了一个磁盘缓存
private DiskLruCache diskLruCache;
private final Object diskCacheLock new Object();
private boolean diskCacheStarting 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 threadFile cacheDir getDiskCacheDir(this, DISK_CACHE_SUBDIR);new InitDiskCacheTask().execute(cacheDir);...
}class InitDiskCacheTask extends AsyncTaskFile, Void, Void {Overrideprotected Void doInBackground(File... params) {synchronized (diskCacheLock) {File cacheDir params[0];diskLruCache DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);diskCacheStarting false; // Finished initializationdiskCacheLock.notifyAll(); // Wake any waiting threads}return null;}
}class BitmapWorkerTask extends AsyncTaskInteger, Void, Bitmap {...// Decode image in background.Overrideprotected Bitmap doInBackground(Integer... params) {final String imageKey String.valueOf(params[0]);// Check disk cache in background threadBitmap bitmap getBitmapFromDiskCache(imageKey);if (bitmap null) { // Not found in disk cache// Process as normalfinal Bitmap bitmap decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));}// Add final bitmap to cachesaddBitmapToCache(imageKey, bitmap);return bitmap;}...
}public void addBitmapToCache(String key, Bitmap bitmap) {// Add to memory cache as beforeif (getBitmapFromMemCache(key) null) {memoryCache.put(key, bitmap);}// Also add to disk cachesynchronized (diskCacheLock) {if (diskLruCache ! null diskLruCache.get(key) null) {diskLruCache.put(key, bitmap);}}
}public Bitmap getBitmapFromDiskCache(String key) {synchronized (diskCacheLock) {// Wait while disk cache is started from background threadwhile (diskCacheStarting) {try {diskCacheLock.wait();} catch (InterruptedException e) {}}if (diskLruCache ! null) {return diskLruCache.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 dirfinal String cachePath Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :context.getCacheDir().getPath();return new File(cachePath File.separator uniqueName);
}即使是初始化磁盘缓存也需要执行磁盘操作因此不应在主线程上执行。不过这也意味着可能会在初始化之前访问该缓存。为了解决此问题上述实现利用了一个 lock 对象来确保应用在磁盘缓存初始化之前不会从该缓存中读取数据。 虽然内存缓存是在界面线程中检查但磁盘缓存会在后台线程中检查。界面线程上不应执行磁盘操作。图片处理完毕后系统会将最终的位图同时添加到内存缓存和磁盘缓存中以供将来使用。
处理配置更改
运行时配置更改例如屏幕方向更改会导致 Android 销毁并使用新的配置重新启动正在运行的 activity。你需要避免重新处理所有图片以便用户在配置发生更改时能够获得快速、流畅的体验。
幸运的是你在使用内存缓存部分构建了一个实用的位图内存缓存。可以使用通过调用 setRetainInstance(true) 保留的 Fragment 将该缓存传递给新的 activity 实例。重新创建 activity 后系统会重新附加这个保留的 Fragment并且你将可以访问现有的缓存对象从而能够快速获取图片并将其重新填充到 ImageView 对象中。
以下是使用 Fragment 在配置更改时保留 LruCache 对象的示例
private LruCacheString, Bitmap memoryCache;Override
protected void onCreate(Bundle savedInstanceState) {...RetainFragment retainFragment RetainFragment.findOrCreateRetainFragment(getFragmentManager());memoryCache retainFragment.retainedCache;if (memoryCache null) {memoryCache new LruCacheString, Bitmap(cacheSize) {... // Initialize cache here as usual}retainFragment.retainedCache memoryCache;}...
}class RetainFragment extends Fragment {private static final String TAG RetainFragment;public LruCacheString, Bitmap retainedCache;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;}Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);}
}如需对此进行测试尝试在保留和不保留 Fragment 的情况下旋转设备。在保留缓存的情况下你几乎会看不到延迟因为图片会立即从内存填充到 activity 中。在内存缓存中找不到的图片有可能会在磁盘缓存中如果不在系统会照常处理它们。