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

Android性能优化(二)- 丝般顺滑地加载大量图片

2015-03-31 11:43 441 查看

一、SoftReference

备注:其实SoftReference已经弃用了,具体原因下面有讲到,但是为何还要讲它,下面也有原因,哈哈!

首先什么是softreference呢?直译就是软引用,看样子就是说,这是一个非强势的引用,当别人强硬起来时候,它就不行了,嘿嘿,确实是这样的!

软引用(SoftReference):

如果一个对象它只具有软引用,当内存足够的时候,GC是不会去回收它的,但是当内存不足时,就会回收这些对象所占用的内存啦。只要GC没有回收它,那么它就可以被程序使用。因此,软应用可以用来实现内存敏感的高速缓存。说得简单一点就是,当涉及会占用大量内存的图片操作时,比如加载相册,如果将其声明为软引用,那在报OOM之前,GC就去释放内存,所以就不会发生OOM的惨剧。

既然有软引用,自然就该有”硬引用”,好吧,其实没有硬,是强应用(StrongReference):结合软引用这个软柿子,这个就十分好理解了。如果一个对象具有强引用,那么GC是不会去回收它的,即便当内存不足时,JVM宁可抛出OOM的异常使程序crash,也不会靠回收强引用对象的内存来解决内存不足的问题。

软引用的实现:

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

public void addBitmapToCache(String path) {

// 强引用的Bitmap对象

Bitmap bitmap = BitmapFactory.decodeFile(path);

// 软引用的Bitmap对象

SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);

// 添加该对象到Map中使其缓存

imageCache.put(path, softBitmap);

}

public Bitmap getBitmapByPath(String path) {

// 从缓存中取软引用的Bitmap对象

SoftReference<Bitmap> softBitmap = imageCache.get(path);

// 判断是否存在软引用

if (softBitmap == null) {

return null;

}

// 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空

Bitmap bitmap = softBitmap.get();

if(bitmap==null){

return null;

}

return bitmap;

}


看到这里觉得SoftReference真的还不赖,因为会经常遇到加载大量强引用的对象啊,如果只是String那还好,但是套图啥的,额,这种东西吃内存的祖宗,OOM不来才怪,但是SoftReference确实可以有效避免OOM,万事大吉了!

BUT!!!….

二、Lrucache

Google从Android 2.3+开始宣布说,他们要从此版本开始,让GC更加频繁地去回收具有软引用对象的内存,好吧。。。动不动就被GC回收了,那我们的对象岂不就会经常丢失?对的,这样的话,SoftReference虽然不会造成OOM,但是我们的数据就会丢失,就会变的十分不可靠了(你看一组套图,结果关键部分丢了。。。++!)

可是,那怎么办呢?

果然哇,挖掘机技术还是Google强,Google提出了一个叫做LruCache的东西,这又是个什么鬼?

所谓LRU,即为 Least recently used,近期最少使用策略,唉,翻译成汉语果然就不高大上了…当然,其实很熟悉啦,操作系统还是学过的,嘿嘿。

这个LruCache要取代之前的SoftReference还是有两下子的。首先,它会把最近最常使用的对象的强引用放在LinkedHashMap里面,(因为软引用不可靠啊,既然最近常使用,一定要保证它们,嗯!),还有那些不常使用的对象兄弟们呢?就只能在内存到达警戒值得时候,被强行踢出去了。。。

LruCache出场之前,首先要知道风险啊,不然一不小心以强引用的方式存了太多的对象,那就OOM了,所以呢,首先要获得JVM能够给我分配多少内存,Runtime().getRuntime().maxmemory(),哎呀,这下我就放心了,先留个1/8给以强引用方式来存储对象,这样,这些危险的东西被锁在盒子里就不会OOM了~

但是咱也不能太小气了,如果一毛不拔的,只留给一丁点儿内存,那就还是要不断加载到内存,释放内存,加载到内存,释放内存。。。好了,这个问题,=嘛,究竟分多少内存合适呢?!好吧,咱自己心里清楚就行了,JVM给每个程序分配了32MB(有的机器不一样),结合每个对象可能占据的内存,大概能够知道该给分个多大的窝了。。。

好了,LruCache的具体实现:

private LruCache<String, Bitmap> mMemoryCache;

@Override

protected void onCreate(Bundle savedInstanceState) {

// 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。

// LruCache通过构造函数传入缓存值,以KB为单位。

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

// 使用最大可用内存值的1/8作为缓存的大小。

int cacheSize = maxMemory / 8;

mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

@Override

protected int sizeOf(String key, Bitmap bitmap) {

// 必须重写此方法来衡量每张图片的大小,默认返回图片数量。

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

}


假设一个全屏幕的GridView使用4张800*480的图片来填充,这时大约会占用1.5MB的内存(800 * 480 * 4 / 1024 / 1024 MB),所以这个设置的LruCache可以用来缓存2.5页,所以,分配的大小判断就是这么来的。

这里可以简单看一下,分配了1/8的内存,即4MB的缓存空间,根据需要将对象存进去,存取就是键值对啦,键的话就是url,而值就是这个Bitmap的对象。

但是问题来了,我分配的这1/8的内存满了怎么办?

Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。当cache已满的时候加入新的item时,在队列尾部的item会被回收。

Soga!!原来如此~这样看来,我们貌似并没有必要去考虑这4MB内存的心酸,他自己知道怎么做,嗯!我们在实现的时候,只要首先去这块缓存中查找,如果有,那就太好了,它在LruCache中的位置也会被提升,但是如果没有的话,那我们只能去加载了。。。然后再把它添加到缓存里面。到这里貌似可以结束了。。

BUT!!!。。。

三、结合SoftReference和LruCache的二级缓存结构

这里废话这么多,终究说的是LruCache,即为缓存,既然缓存嘛,之前还听过一个东西叫做多级缓存的,不知道有木有关联呢。。。。

难道LruCache中木有,我就只能屁颠屁颠地跑去加载么。。。。既然SoftReference不可靠,那我不依赖它,但是如果作为一个辅助还是可以的嘛,至少比直接去加载资源到内存,不停地加载,释放,加载,释放要好嘛,当SoftReference实在也没有了,那我再去加载资源到内存也行啊!对啊,我貌似可以把LruCache作为一级缓存,SoftReference作为二级缓存呀!好像挺高大上的,其实就是个补丁,哈哈!这里借助用了一下SoftReferenceManager(其实就是个map嘛)

试试嘛~具体实现:

import android.graphics.Bitmap;

import android.support.v4.util.LruCache;

public class LruCacheManager {

private static LruCacheManager lruCacheManager;

private SoftReferenceCacheManager softReferenceCacheManager;

private LruCache<String, Bitmap> lruCache;

// 配置一级缓存设置LruCache

private LruCacheManager() {

// (Runtime.getRuntime().maxMemory()运行时系统所分配的内存资源

lruCache = new LruCache<String, Bitmap>((int) (Runtime.getRuntime()

.maxMemory() / 8)) {

@Override

protected int sizeOf(String key, Bitmap value) {

if (value != null) {

return value.getRowBytes() * value.getHeight();

}

return 0;

}

// 当内存资源不足时 进行调用

@Override

protected void entryRemoved(boolean evicted, String key,

Bitmap oldValue, Bitmap newValue) {

if (evicted) {

// 放到软引用当中

softReferenceCacheManager.put(key, oldValue);

}

}

};

// 初始化softRefrenceCacheManager

softReferenceCacheManager = SoftReferenceCacheManager

.getSoftReferenceCacheManager();

}

// 初始化LruCachaManager操作

public static LruCacheManager getLruCacheManager() {

if (lruCacheManager == null) {

lruCacheManager = new LruCacheManager();

}

return lruCacheManager;

}

// 将图片添加到lrucache中

public void putBitmap(String url, Bitmap bm) {

lruCache.put(url, bm);

}

// 从lrucache中取出图片

public Bitmap getBitmap(String url) {

Bitmap bm = null;

bm = lruCache.get(url);

if (bm == null) {

// 软引用当中取

bm = softReferenceCacheManager.get(url);

// 将从软引用中取出的图片添加到lrucache中,并将软引用中的删除掉

if (bm != null) {

lruCache.put(url, bm);

softReferenceCacheManager.remove(url);

}

}

return bm;

}


改到这里,貌似异步加载吃内存的图片已经可以飞起了,OOM神马的,就不提了!到这里可以结束了嘛~~

BUT!!!…

四、DiskLruCache

从始至终,都是在内存里面啊,无论是软引用还是LruCache,万一内存吃紧,又来了个优先级超级高的phoneCall,好了,全shi了,啥都没了,从零开始。。。

但是为了有效避免这种情况,DiskCache又出场了!

DiskCache和LruCache用法是相似的:

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

//初始化DiskLruCache对象

mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);

mDiskCacheStarting = false; // Finished initialization

mDiskCacheLock.notifyAll(); // Wake any waiting threads

}

return null;

}

}

//在需要获取图片的时候,先去DiskCache中找,实在没有再去加载到内存

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:源码是有缺漏的,其实应该是先去内存中//找该图,比如LruCache,找不到再去DiskCache中,都没有才去加载,并且要//把它加进这两个缓存数据结构

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

}


好吧,这个google的源码,是有缺漏的,其实应该是先去内存中找该图,比如LruCache,找不到再去DiskCache中,都没有才去加载,并且要把它加进这两个缓存数据结构,以便下次的使用。

还有个问题,为啥这里加了锁(synchronized ),这个因为在DiskCache初始化以及置值的时候,可能是一个耗时操作,在其还没有完成的时候,程序就可能去找图片了,这可不行,所以就在该过程执行过程中,将Diskcache锁上,这样就让这个请求乖乖等着咯,不过对于人类使用来说,还是非常快的,等不了多少毫秒!

五、结合SoftReference、LruCache,DiskLruCache的三级缓存结构

但是,是不是可以再结合呢,所以呢。。。结合DiskCache,我们可以建立一个三级缓存啦!

类似于Lrucache和SoftReference的二级缓存的逻辑,我们再在此基础上加上DiskCache的第三级缓存,即过程便是:

首先去LruCache一级缓存中查找该图,如果没有,就去保存有SoftReference的map二级缓存中找,如果还没有,那就去DiskCache的三级缓存中找,最后实在没有了就去加载或者下载咯。具体用法,其实是一致的,就是在最初的加载或者下载时,需要将该对象同时加到LruCache、SoftReference的Map结构以及DiskCache中。

六、ContentProvider

到这里,基本上已经可以完结了,但是google又来个挑逗的note:

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还Cache我的图片呢?

好吧,关于如果使用ContentProvider以及其原理,下篇接着讲。

BUT!!!…

七、Lrucache源码分析

额,太罗嗦了,还是不说了,附上LruCache的源码,哈哈!

packageandroid.util;

importjava.util.LinkedHashMap;
importjava.util.Map;

/**
* 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.
* Cache保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。
* 当cache已满的时候加入新的item时,在队列尾部的item会被回收。
* <p>If your cached values hold resources that need to be explicitly released,
* override {@link #entryRemoved}.
* 如果你cache的某个值需要明确释放,重写entryRemoved()
* <p>If a cache miss should be computed on demand for the corresponding keys,
* override {@link #create}. This simplifies the calling code, allowing it to
* assume a value will always be returned, even when there's a cache miss.
* 如果key相对应的item丢掉啦,重写create().这简化了调用代码,即使丢失了也总会返回。
* <p>By default, the cache size is measured in the number of entries. Override
* {@link #sizeOf} to size the cache in different units. For example, this cache
* is limited to 4MiB of bitmaps: 默认cache大小是测量的item的数量,重写sizeof计算不同item的
*  大小。
* <pre>   {@code
*   int cacheSize = 4 * 1024 * 1024; // 4MiB
*   LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
*       protected int sizeOf(String key, Bitmap value) {
*           return value.getByteCount();
*       }
*   }}</pre>
*
* <p>This class is thread-safe. Perform multiple cache operations atomically by
* synchronizing on the cache: <pre>   {@code
*   synchronized (cache) {
*     if (cache.get(key) == null) {
*         cache.put(key, value);
*     }
*   }}</pre>
*
* <p>This class does not allow null to be used as a key or value. A return
* value of null from {@link #get}, {@link #put} or {@link #remove} is
* unambiguous: the key was not in the cache.
* 不允许key或者value为null
*  当get(),put(),remove()返回值为null时,key相应的项不在cache中
*/
publicclass LruCache<K, V> {
privatefinal LinkedHashMap<K, V> map;

/** Size of this cache in units. Not necessarily the number of elements. */
privateint size; //已经存储的大小
privateint maxSize; //规定的最大存储空间

privateint putCount;  //put的次数
privateint createCount;  //create的次数
privateint evictionCount;  //回收的次数
privateint hitCount;  //命中的次数
privateint missCount;  //丢失的次数

/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
*     the maximum number of entries in the cache. For all other caches,
*     this is the maximum sum of the sizes of the entries in this cache.
*/
publicLruCache(intmaxSize) {
if(maxSize <= 0) {
thrownew IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = newLinkedHashMap<K, V>(0, 0.75f, true);
}

/**
* Returns the value for {@code key} if it exists in the cache or can be
* created by {@code #create}. If a value was returned, it is moved to the
* head of the queue. This returns null if a value is not cached and cannot
* be created. 通过key返回相应的item,或者创建返回相应的item。相应的item会移动到队列的头部,
* 如果item的value没有被cache或者不能被创建,则返回null。
*/
publicfinal V get(K key) {
if(key == null) {
thrownew NullPointerException("key == null");
}

V mapValue;
synchronized(this) {
mapValue = map.get(key);
if(mapValue != null) {
hitCount++;  //命中
returnmapValue;
}
missCount++;  //丢失
}

/*
* Attempt to create a value. This may take a long time, and the map
* may be different when create() returns. If a conflicting value was
* added to the map while create() was working, we leave that value in
* the map and release the created value.
* 如果丢失了就试图创建一个item
*/

V createdValue = create(key);
if(createdValue == null) {
returnnull;
}

synchronized(this) {
createCount++;//创建++
mapValue = map.put(key, createdValue);

if(mapValue != null) {
// There was a conflict so undo that last put
//如果前面存在oldValue,那么撤销put()
map.put(key, mapValue);
} else{
size += safeSizeOf(key, createdValue);
}
}

if(mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
returnmapValue;
} else{
trimToSize(maxSize);
returncreatedValue;
}
}

/**
* Caches {@code value} for {@code key}. The value is moved to the head of
* the queue.
*
* @return the previous value mapped by {@code key}.
*/
publicfinal V put(K key, V value) {
if(key == null|| value == null) {
thrownew NullPointerException("key == null || value == null");
}

V previous;
synchronized(this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if(previous != null) {  //返回的先前的value值
size -= safeSizeOf(key, previous);
}
}

if(previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
returnprevious;
}

/**
* @param maxSize the maximum size of the cache before returning. May be -1
*     to evict even 0-sized elements.
*  清空cache空间
*/
privatevoid trimToSize(int maxSize) {
while(true) {
K key;
V value;
synchronized(this) {
if(size < 0|| (map.isEmpty() && size != 0)) {
thrownew IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}

if(size <= maxSize) {
break;
}

Map.Entry<K, V> toEvict = map.eldest();
if(toEvict == null) {
break;
}

key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}

entryRemoved(true, key, value, null);
}
}

/**
* Removes the entry for {@code key} if it exists.
* 删除key相应的cache项,返回相应的value
* @return the previous value mapped by {@code key}.
*/
publicfinal V remove(K key) {
if(key == null) {
thrownew NullPointerException("key == null");
}

V previous;
synchronized(this) {
previous = map.remove(key);
if(previous != null) {
size -= safeSizeOf(key, previous);
}
}

if(previous != null) {
entryRemoved(false, key, previous, null);
}

returnprevious;
}

/**
* Called for entries that have been evicted or removed. This method is
* invoked when a value is evicted to make space, removed by a call to
* {@link #remove}, or replaced by a call to {@link #put}. The default
* implementation does nothing.
* 当item被回收或者删掉时调用。改方法当value被回收释放存储空间时被remove调用,
* 或者替换item值时put调用,默认实现什么都没做。
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* @param evicted true if the entry is being removed to make space, false
*     if the removal was caused by a {@link #put} or {@link #remove}.
* true---为释放空间被删除;false---put或remove导致
* @param newValue the new value for {@code key}, if it exists. If non-null,
*     this removal was caused by a {@link #put}. Otherwise it was caused by
*     an eviction or a {@link #remove}.
*/
protectedvoid entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}

/**
* Called after a cache miss to compute a value for the corresponding key.
* Returns the computed value or null if no value can be computed. The
* default implementation returns null.
* 当某Item丢失时会调用到,返回计算的相应的value或者null
* <p>The method is called without synchronization: other threads may
* access the cache while this method is executing.
*
* <p>If a value for {@code key} exists in the cache when this method
* returns, the created value will be released with {@link #entryRemoved}
* and discarded. This can occur when multiple threads request the same key
* at the same time (causing multiple values to be created), or when one
* thread calls {@link #put} while another is creating a value for the same
* key.
*/
protectedV create(K key) {
returnnull;
}

privateint safeSizeOf(K key, V value) {
intresult = sizeOf(key, value);
if(result < 0) {
thrownew IllegalStateException("Negative size: "+ key + "="+ value);
}
returnresult;
}

/**
* Returns the size of the entry for {@code key} and {@code value} in
* user-defined units.  The default implementation returns 1 so that size
* is the number of entries and max size is the maximum number of entries.
* 返回用户定义的item的大小,默认返回1代表item的数量,最大size就是最大item值
* <p>An entry's size must not change while it is in the cache.
*/
protectedint sizeOf(K key, V value) {
return1;
}

/**
* Clear the cache, calling {@link #entryRemoved} on each removed entry.
* 清空cacke
*/
publicfinal void evictAll() {
trimToSize(-1); // -1 will evict 0-sized elements
}

/**
* For caches that do not override {@link #sizeOf}, this returns the number
* of entries in the cache. For all other caches, this returns the sum of
* the sizes of the entries in this cache.
*/
publicsynchronized final int size() {
returnsize;
}

/**
* For caches that do not override {@link #sizeOf}, this returns the maximum
* number of entries in the cache. For all other caches, this returns the
* maximum sum of the sizes of the entries in this cache.
*/
publicsynchronized final int maxSize() {
returnmaxSize;
}

/**
* Returns the number of times {@link #get} returned a value that was
* already present in the cache.
*/
publicsynchronized final int hitCount() {
returnhitCount;
}

/**
* Returns the number of times {@link #get} returned null or required a new
* value to be created.
*/
publicsynchronized final int missCount() {
returnmissCount;
}

/**
* Returns the number of times {@link #create(Object)} returned a value.
*/
publicsynchronized final int createCount() {
returncreateCount;
}

/**
* Returns the number of times {@link #put} was called.
*/
publicsynchronized final int putCount() {
returnputCount;
}

/**
* Returns the number of values that have been evicted.
* 返回被回收的数量
*/
publicsynchronized final int evictionCount() {
returnevictionCount;
}

/**
* Returns a copy of the current contents of the cache, ordered from least
* recently accessed to most recently accessed. 返回当前cache的副本,从最近最少访问到最多访问
*/
publicsynchronized final Map<K, V> snapshot() {
returnnew LinkedHashMap<K, V>(map);
}

@Overridepublic synchronized final String toString() {
intaccesses = hitCount + missCount;
inthitPercent = accesses != 0? (100* hitCount / accesses) : 0;
returnString.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
maxSize, hitCount, missCount, hitPercent);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐