您的位置:首页 > 移动开发 > Android开发

【Android】GridView显示大量图片缓存问题(一)

2013-01-22 12:43 375 查看



2012/10/23 15:22:27 | 阅读235次

前面有好几篇博客讨论到大量图片的存储以及缓存,比如:【Android】缩略图Thumbnails【Android】图片的异步加载,尤其是在后者,异步加载我觉得已经很好了,并且后者是做了缓存的,看上去似乎是完美的解决方案,但是根据我的实践,后者是存在问题的:

我在相册里面存入了80张照片,然后使用该方案获取图片显示在GridView里面,之后得到的效果如下:1)异步加载效果非常好,图片加载线程并不会和主线程抢夺资源(源于滑动的时候该方案将所有加载图片的线程暂停了);2)能够保证可以在停下的那一页优先加载图片,这无疑提高了用户体验用户不用等到前面的图片都加载好了再来看本页面的图片。以上两点都是该方案的优点,处理的非常不错,该方案的主要缺点就在于缓存。

根据该方案的实现,我们使用SoftReference对图片进行引用,该引用只有在内存不足的情况下才会释放资源,这样子看起来其实是非常适合做缓存的:能够自动释放所持有的资源,不会像硬引用一样导致OOM错误。但是在实践中,我发现并非如此,因为80张图片,经过缩放之后整体的大小肯定不会超过8M,但是就算是在这样子的情况下,我发现还是有图片资源被释放。这就说明,SoftReference并没有像说明的那样子引用和释放资源。我猜想Android改变了SoftReference的实现。

在网上搜了半天,发现一篇博客,甚好:Image
Caching in Android,里面就说明,Android里面SoftReference的确没有起到应有的作用:

So unfortunately all those recommending SoftReferences have never actually tried using them, they don’t work at all. Immediately upon loading the bitmap into the SoftReference it’s destroyed at the next garbage collection. Why would a SoftReference do this? It turns out in Android SoftReferences are implemented a little differently than one would assume. Check out the explanation on this bug request. Basically what the dev team has decided is that in Android a SoftReference doesn’t really make sense. Your app is competing with all the other apps in memory, and due to the constrained nature of the platform it’s actually hard for the VM to decide when to destroy your SoftReference. There’s no way forit to know that your image cache is more important than the other applications in memory, so the team has just made SoftReferences basically ineffective. I’d have to agree with them, they only make sense when your understanding of the problem is insufficient.


所以,Java里面的东西和Android里面的不完全一样,毕竟,Android是为了移动设备定制的系统,很多东西不能和Java一样。

接着,也是这篇博客,和我之前找的资料提出了另外一个解决方案:LRUCache。

LruCache开发文档:http://developer.android.com/reference/android/util/LruCache.html

还有一篇开发文档是讲述如何缓存Bitmap的:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html#memory-cache (都是宝藏啊有木有!)

慢慢来,第一篇文档是介绍LruCache的,第二篇则讲述了如何使用。

A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that
queue is evicted and may become eligible for garbage collection.(缓存强引用,每次一个value被获取,它就会被移动到队列的最前端,每次往一个全满的缓存中添加一个值,队列最后一个值就会被移除)(讲到这里,很多人应该知道LRU是什么意思了:Least
Recently Used最近最少使用算法

If your cached values hold resources that need to be explicitly released, override
entryRemoved(boolean,
K, V, V)
.(可以调用这个方法显示的移除资源)

If a cache miss should be computed on demand for the corresponding keys, override
create(K)
.
This simplifies the calling code, allowing it to assume a value will always be returned, even when there's a cache miss.

By default, the cache size is measured in the number of entries. Override
sizeOf(K,
V)
to size the cache in different units. For example, this cache is limited to 4MiB of bitmaps:(如何分配存储空间

1 int cacheSize = 4 * 1024 * 1024; // 4MiB 2 LruCache bitmapCache = new LruCache(cacheSize) { 3 protected int sizeOf(String key, Bitmap value) { 4 return value.getByteCount(); 5 } 6 }


This class is thread-safe. Perform multiple cache operations atomically by synchronizing on the cache:(线程安全)

1 synchronized (cache) { 2 if (cache.get(key) == null) { 3 cache.put(key, value); 4 } 5 }


This class does not allow null to be used as a key or value. A return value of null from
get(K)
,
put(K,
V)
or
remove(K)
is
unambiguous: the key was not in the cache.(不得使用null做键或者值,会产生歧义)

以上是第一篇博客的主要内容,至于具体的怎么用还得看第二篇博客:Caching Bitmaps

Loading a single bitmap into your user interface (UI) is straightforward, however things get more complicated if you need to load a larger set of images at once. In
many cases (such as with components like
ListView
,
GridView
or
ViewPager
),
the total number of images on-screen combined with images that might soon scroll onto the screen are essentially unlimited.(可能载入的图片是无限的,也就是说会有很多)

Memory usage is kept down with components like this by recycling the child views as they move off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any long lived references.
This is all good and well, but in order to keep a fluid and fast-loading UI you want to avoid continually processing these images each time they come back on-screen. A memory and disk cache can often help here, allowing components to quickly reload processed
images.(ListView和GridView通过将移出屏幕的View回收来保持内存的低消耗,垃圾回收机制也会回收你载入的Bitmap,不让保持长久生命期的引用。这很好,但是为了保持一个流畅快速的UI界面操作,你必须避免每次当图片重新显示的时候再次处理这些图片。这时候就需要用到内存和磁盘缓存机制了,允许你快速获取载入过的图片)。

This lesson walks you through using a memory and disk bitmap cache to improve the responsiveness and fluidity of your UI when loading multiple bitmaps.(这篇文档就是教你如何在加载大量图片的时候提高界面的流畅性和应答速度)

Use a Memory Cache

A memory cache offers fast access to bitmaps at the cost of taking up valuable application
memory. The
LruCache
class
(also available in the Support Library for
use back to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently
referenced objects in a strong referenced
LinkedHashMap
and
evicting the least recently used member before the cache exceeds its designated size.

Note: In
the past, a popular memory cache implementation was a
SoftReference
or
WeakReference
bitmap
cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective. In addition, prior to Android 3.0 (API Level 11), the
backing data of a bitmap was stored in native memory which is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash.(不要再使用SoftReference!!)

In order to choose a suitable size for a
LruCache
,
a number of factors should be taken into consideration, for example:(如何分配适当的缓存大小)

How memory intensive is the rest of your activity and/or application?
How many images will be on-screen at once? How many need to be available ready to come on-screen?
What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy
Nexus will need a larger cache to hold the same number of images in memory compared to a device likeNexus
S (hdpi).
What dimensions and configuration are the bitmaps and therefore how much memory will each take up?
How frequently will the images be accessed? Will some be accessed more frequently than others? If so, perhaps you may want to keep certain items always in memory or even have multiple
LruCache
objects
for different groups of bitmaps.
Can you balance quality against quantity? Sometimes it can be more useful to store a larger number of lower quality bitmaps, potentially loading a higher quality version in another background task.

There is no specific size or formula that suits all applications, it's up to you to analyze your usage and come up with a suitable
solution. A cache that is too small causes additional overhead with no benefit, a cache that is too large can once again cause
java.lang.OutOfMemory
exceptions and leave
the rest of your app little memory to work with.(没有一个标准的分配方式,太小的话没有意义,太大的话占用很多的内存,会让其他应用无处运行,导致OOM,必须自己想办法解决)

Here’s an example of setting up a
LruCache
for
bitmaps:(下面是一个例子)

1 private LruCache mMemoryCache; 2 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 ... 6 // Get memory class of this device, exceeding this amount will throw an 7 // OutOfMemory exception. 8 final int memClass = ((ActivityManager) context.getSystemService( 9 Context.ACTIVITY_SERVICE)).getMemoryClass(); 10 11 // Use 1/8th of the available memory for this memory cache. 12 final int cacheSize = 1024 * 1024 * memClass / 8;(分配1/8的内存) 13 14 mMemoryCache = new LruCache(cacheSize) { 15 @Override 16 protected int sizeOf(String key, Bitmap bitmap) { 17 // The cache size will be measured in bytes rather than number of items. 18 return bitmap.getByteCount(); 19 } 20 }; 21 ... 22 } 23 24 public void addBitmapToMemoryCache(String key, Bitmap bitmap) { 25 if (getBitmapFromMemCache(key) == null) { 26 mMemoryCache.put(key, bitmap); 27 } 28 } 29 30 public Bitmap getBitmapFromMemCache(String key) { 31 return mMemoryCache.get(key); 32 }


Note: In this example, one eighth of the application memory is allocated for our cache. On a normal/hdpi device this
is a minimum of around 4MB (32/8). A full screen
GridView
filled
with images on a device with 800x480 resolution would use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in memory.(大概缓存2.5页的图片)

When loading a bitmap into an
ImageView
,
the
LruCache
is
checked first. If an entry is found, it is used immediately to update the
ImageView
,
otherwise a background thread is spawned to process the image:(当载入图片的时候,首先检测缓存,有的话直接载入,否则启动线程加载)

1 public void loadBitmap(int resId, ImageView imageView) { 2 final String imageKey = String.valueOf(resId); 3 4 final Bitmap bitmap = getBitmapFromMemCache(imageKey); 5 if (bitmap != null) { 6 mImageView.setImageBitmap(bitmap); 7 } else { 8 mImageView.setImageResource(R.drawable.image_placeholder); 9 BitmapWorkerTask task = new BitmapWorkerTask(mImageView); 10 task.execute(resId); 11 } 12 }


The
BitmapWorkerTask
also
needs to be updated to add entries to the memory cache:

1 class BitmapWorkerTask extends AsyncTask { 2 ... 3 // Decode image in background. 4 @Override 5 protected Bitmap doInBackground(Integer... params) { 6 final Bitmap bitmap = decodeSampledBitmapFromResource( 7 getResources(), params[0], 100, 100)); 8 addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); 9 return bitmap; 10 } 11 ... 12 }


Use a Disk Cache

A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot rely on images being available in this cache. Components like
GridView
with
larger datasets can easily fill up a memory cache. Your application could be interrupted by another task like a phone call, and while in the background it might be killed and the memory cache destroyed. Once the user resumes, your application it has to process
each image again.(内存缓存很容易就被填满了,打电话等任务可能会中断我们的应用,这样当我们的应用恢复的时候,图片又必须被重新处理)

A disk cache can be used in these cases to persist processed bitmaps and help decrease loading times where images are no longer available in a memory cache. Of course, fetching images from disk is slower
than loading from memory and should be done in a background thread, as disk read times can be unpredictable.(磁盘缓存可以被用来在这种时刻缓存处理过的照片,减少内存缓存hit miss的时候图片的加载时间。当然,从磁盘加载的时间比从内存加载时间更长,并且是不确定的)

Note: A
ContentProvider
might
be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.(注意,如果图片被经常取用,ContentProvider或许是一个更好的地方来存储缓存的图片)

Included in the sample code of this class is a basic
DiskLruCache
implementation.
However, a more robust and recommended
DiskLruCache
solution is included in the Android
4.0 source code (4.0里面的更加健壮,已经有人导出来了)(
libcore/luni/src/main/java/libcore/io/DiskLruCache.java
). Back-porting this class for use on previous Android releases should
be fairly straightforward (a quick search shows
others who have already implemented this solution).

Here’s updated example code that uses the simple
DiskLruCache
included
in the sample application of this class:

1 private DiskLruCache mDiskCache; 2 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB 3 private static final String DISK_CACHE_SUBDIR = "thumbnails"; 4 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 ... 8 // Initialize memory cache 9 ... 10 File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); 11 mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); 12 ... 13 } 14 15 class BitmapWorkerTask extends AsyncTask { 16 ... 17 // Decode image in background. 18 @Override 19 protected Bitmap doInBackground(Integer... params) { 20 final String imageKey = String.valueOf(params[0]); 21 22 // Check disk cache in background thread 23 Bitmap bitmap = getBitmapFromDiskCache(imageKey); 24 25 if (bitmap == null) { // Not found in disk cache 26 // Process as normal 27 final Bitmap bitmap = decodeSampledBitmapFromResource( 28 getResources(), params[0], 100, 100)); 29 } 30 31 // Add final bitmap to caches 32 addBitmapToCache(String.valueOf(imageKey, bitmap); 33 34 return bitmap; 35 } 36 ... 37 } 38 39 public void addBitmapToCache(String key, Bitmap bitmap) { 40 // Add to memory cache as before 41 if (getBitmapFromMemCache(key) == null) { 42 mMemoryCache.put(key, bitmap); 43 } 44 45 // Also add to disk cache 46 if (!mDiskCache.containsKey(key)) { 47 mDiskCache.put(key, bitmap); 48 } 49 } 50 51 public Bitmap getBitmapFromDiskCache(String key) { 52 return mDiskCache.get(key); 53 } 54 55 // Creates a unique subdirectory of the designated app cache directory. Tries to use external 56 // but if not mounted, falls back on internal storage. 57 public static File getCacheDir(Context context, String uniqueName) { 58 // Check if media is mounted or storage is built-in, if so, try and use external cache dir 59 // otherwise use internal cache dir 60 final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED 61 || !Environment.isExternalStorageRemovable() ? 62 context.getExternalCacheDir().getPath() : context.getCacheDir().getPath(); 63 64 return new File(cachePath + File.separator + uniqueName); 65 }


While the memory cache is checked in the UI thread, the disk cache is checked in the background thread. Disk operations should never take place on the UI thread. When image processing is complete, the final
bitmap is added to both the memory and disk cache for future use.(内存缓存的检测在UI线程里面进行,磁盘缓存的检测则在后台进程里面进行。磁盘操作不应该发生在UI线程上面。当图片处理完成,图片会同时被缓存到内存和磁盘以便于日后使用)

Handle Configuration Changes

Runtime configuration changes, such as a screen orientation change, cause Android to destroy and restart the running activity with the new configuration (For more information about this behavior, see Handling
Runtime Changes). You want to avoid having to process all your images again so the user has a smooth and fast experience when a configuration change occurs.(运行时配置变化,比如屏幕的方向改变了,这个时候就需要避免再度处理图片以便于用户的体验流畅)

Luckily, you have a nice memory cache of bitmaps that you built in the Use
a Memory Cache section. This cache can be passed through to the new activity instance using a
Fragment
which
is preserved by calling
setRetainInstance(true)
).
After the activity has been recreated, this retained
Fragment
is
reattached and you gain access to the existing cache object, allowing images to be quickly fetched and re-populated into the
ImageView
objects.(内存缓存是可以维持下来的!!!)

1 private LruCache mMemoryCache; 2 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 ... 6 RetainFragment mRetainFragment = 7 RetainFragment.findOrCreateRetainFragment(getFragmentManager()); 8 mMemoryCache = RetainFragment.mRetainedCache; 9 if (mMemoryCache == null) { 10 mMemoryCache = new LruCache(cacheSize) { 11 ... // Initialize cache here as usual 12 } 13 mRetainFragment.mRetainedCache = mMemoryCache; 14 } 15 ... 16 } 17 18 class RetainFragment extends Fragment { 19 private static final String TAG = "RetainFragment"; 20 public LruCache mRetainedCache; 21 22 public RetainFragment() {} 23 24 public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { 25 RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG); 26 if (fragment == null) { 27 fragment = new RetainFragment(); 28 } 29 return fragment; 30 } 31 32 @Override 33 public void onCreate(Bundle savedInstanceState) { 34 super.onCreate(savedInstanceState); 35 setRetainInstance(true); 36 } 37 }


To test this out, try rotating a device both with and without retaining the
Fragment
.
You should notice little to no lag as the images populate the activity almost instantly from memory when you retain the cache. Any images not found in the memory cache are hopefully available in the disk cache, if not, they are processed as usual.(磁盘缓存是不需要做样子的处理的)

(未完待续)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐