Processing Bitmaps Off the UI Thread
非ui线程处理位图。
BitmapFactory.decode*方法,在上一篇讨论过的,不应该在ui线程上处理的情况:从硬盘加载或从网络加载。因为加载时间未知,如果时间过久,会导致程序失去响应。
这章节是关于AsyncTask在后台处理图片的。
AsyncTask类提供了一个简易的方法处理后台事务,并通知ui线程。使用它需要创建一个子类,覆盖一些方法这里举一个加载图片到ImageView的例子:
class BitmapWorkerTask extends AsyncTask {
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);
}
}
}
}
WeakReference是为了避免ImageView被回收时由于引用造成无法回收。所以多次判断是否为null值。这种为空的情况如Activity已经到了其它Activity,或配置变化了。
加载图片就简单了:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
Handle Concurrency:
ListView,GridView是另一个麻烦的地方,为了有效地使用内存,这些组件会在用户滚动时回收一些子View,如果每一个View都触发一个AsyncTask,不能保证在操作完成时,相关的View还存在。它可能被回收了
http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html 更详细地说明了并发的问题,提供了一个解决办法。存储最近的AsyncTask。
提供专用的Drawable子类来存储task,
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();
}
}
在执行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方法就是检查是否关联的task已经在运行了。它先调用cancel()结束先前的方法,
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;
}
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;
}
最后一步是在onPostExecute()确认是否任务结束了和当前的关联ImageView匹配:
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);
}
}
}
}
这个实现可以用于listview,gridview这样回收他们的子元素的组件中,只要简单地调用loadBitmap方法就可以了。
这里介绍的方法可能的问题是导致很多的线程创建,销毁,这也算是一个问题吧。
Caching Bitmaps 缓存图片
加载单张图片到ui中,比较容易,更复杂一些,一次加载一系列图片。这种情况屏幕上的图片可能通过滚动,不再显示。
一些组件通过回收子元素来回收内存,垃圾回收器释放你已经加载的位图,非长期的引用。这是必须的,但无法提供流畅的体验,你需要避免一直处理这些图片,把它们 贴到屏幕上,内存或硬盘的缓存就可以提供一些帮助了。
Use a Memory Cache
内存缓存提供位图的快速访问,LruCache类(在Support Library中也可用的)
适合缓存位图,保持最近的引用对象,是强引用,清除早期的对象。
SoftReference or WeakReference在之前的版本最好使用这样的引用保存位图,便于回收,3.0版本以前位图是存储在本地内存中的,不容易回收。
不管是哪个版本,都应该使用软引用或弱弱引用,(据说软引用更适合,但这里用弱引用)
对于LruCache的大小选择,有几个因素需要参考的:
你的应用剩下部分需要多数内存?
你需要一次加载多少图片到屏幕上?
屏幕的大小与设备的解析度,高解析度的设备xhdpi像Nexus相比Galaxy S hdpi需要大的缓存来保存相同数量的图片。
维度与配置决定了位图的占用资源的多少。
图片的访问频率。一些比较常用到,另一些不常用,需要保持一些常用的在缓存中,或建多个LruCache对象来存储图片。
在数量与质量间平衡, 有时缓存大量的低分辨率的图片,而加载大图是用后台线程来处理。
对所有没有固定统一的规则,需要自己分析处理。缓存的大小需要自己试验,不同的系统不同的手机需要适配,找到一个合适的值。
下面提供一个使用LruCache的例子:
private LruCache mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}
};
...
}
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的设备中最小值是4m(32/8),全屏的图片在800*480分辨率中需要消耗1.5m(800*480*4),所以缓存了2.5页图片。
当加载图片时,LruCache会先检查,然后没有才加载其它的图片
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 {
...
// 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;
}
...
}
Use a Disk Cache 磁盘缓存。
内存缓存更快速,当然不能仅靠内存来维持,listview,gridview会很快地占用了内存中的图片,而且你的Activity可能被销毁,然后再加载,这时就需要另一处缓存了。
磁盘缓存主要在下载图片时用到,缓存后不用再次下载,从磁盘中加载当然比从网络中要快得多了
DiskLruCache已经是一种健壮的实现 了,在4.0中提供了源码libcore/luni/src/main/java/libcore/io/DiskLruCache.java,
看个例子:
private DiskLruCache mDiskCache;
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
...
File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
...
}
class BitmapWorkerTask extends AsyncTask {
...
// 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(String.valueOf(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
if (!mDiskCache.containsKey(key)) {
mDiskCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromDiskCache(String key) {
return mDiskCache.get(key);
}
// 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 getCacheDir(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.getExternalStorageState() == Environment.MEDIA_MOUNTED
|| !Environment.isExternalStorageRemovable() ?
context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
虽然这里没有看到DiskLruCache的源码,但是可以想像得出,前面的章节已经了如何加载图片了。
内存缓存在ui线程中检查,磁盘缓存在后台线程中使用。
未完待续
分享到:
相关推荐
Android 6.0的官方开发平台上,有4节是专门来讲BitMap优化的,这是其Word文档。
Android 开发者网站上 Training 部分,Building Apps with Graphics & Animation -> Displaying Bitmaps Efficiently部分的实例 demo
Google官方给出的对于OOM缓存Bitmap以及控制使用内存的方法,使用LruCache和DiskCache,很多有名的图片缓存框架如ImageLoader都有参考这个
Android DisplayingBitmaps示例 该存储库已迁移到 。 请检查该仓库以获取将来的更新。 谢谢!
Android 媒体数据库示例(原始 DisplayingBitmaps 示例 ( ))当前版本 1.0 Beta 示例演示如何从主 UI 线程有效地从 Android sd 卡(图库)加载大型位图、缓存位图(在内存和磁盘中)、管理位图内存并在 UI 元素 - ...
Displaying time series, spatial, and space-time data with R R语言画图,介绍了ggplot2,lattice包的画图功能,以及空间数据可视化
Calculating and Displaying Fatigue Results
Displaying Data on a PC tinyos
(Displaying Triangles) Write an app that displays the following patterns separately, one below the other. Use for loops to generate the patterns. All asterisks (*) should be displayed by a single ...
Simple java library for displaying dates as relative time
Chat User Control with WebBrowser Control as Displaying Engine。
Using Edit box as static Text Control for displaying ToolTip。
Configuration Examples for Displaying CPU Usage of the Device Using SNMP
Check out this article for displaying large bitmaps into the desired area of your dialog box in its original size with a scrolling technique used to show the entire bitmap. 滚动显示位图 在VC++环境下...
A slider widget with a popup bubble displaying the precise value selected..zip,A slider widget with a popup bubble displaying the precise value selected. Android library made by @Ramotion - ...
Displaying a 256 color bitmap在程序中显示256色的位图(6KB)
Displaying_3D_Images_Algorithms_for_Single_Image_Random_Dot_Stereograms
【Audio APIs, Video Playback, Displaying Web Content, Settings】[PDF] [iPhone/iPad/iOS]