一、为什么要使用软引用
在上面关于软引用的介绍中,已经提到了软引用的特性。使用SoftReference引用的对象会有很长的生命周期,只有当系统的内存不足的时候,才会去释放这些软引用对象。所以可以使用软引用来缓存一些比较昂贵的资源,比如获取的网络图片数据。
当应用从网络中获取网络图片数据时,用户完全有可能做一些重复性的操作去查看相同的图片信息。对于这样的问题,通常会有两种解决方法: 一种是把过去查看过的图片信息保存在内存中,每一个存储了图片信息的 Java 对象的生命周期都贯穿整个应用程序生命周期,另一种是当用户开始查看其他图片信息的时候,把存储了当前的图片信息的 Java 对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该图片信息的时候,重新获取图片信息。
很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含图片信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。
像访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的 Java 对象的引用,必将减少不必要的访问,大大提高程序的运行速度。
这样看来,使用软引用是非常有必要的一件事情。
二、如何使用软引用
SoftReference 的特点是它的一个实例保存着一个 Java 对象的软引用,该软引用的存在不妨碍垃圾收集器线程对该 Java 对象的回收。也就是说,一旦SoftReference 保存着一个 Java 对象的软引用之后,在垃圾收集器线程对这个 Java 对象回收之前, SoftReference 类所提供的 get() 方法都会返回 这个Java 对象的强引用。另外,一旦垃圾线程回收该 Java 对象之后, get() 方法将返回 null 。
软引用的使用方法如下面的Java代码所示 :
1 |
MyObject aRef = new MyObject(); |
2 |
SoftReference aSoftRef = new SoftReference( aRef ); |
上面的代码执行后,对于MyObject 对象,有两个引用路径,一个是来自 aSoftRef对象的软引用,一个来自变量 aRef 的强引用,所以 MyObject对象是强可及对象。紧跟着,可以使用下面的java的代码结束 aReference 对 MyObject 实例的强引用 :
此后, MyObject 对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个 SoftReference 对该对象的引用而始终保留该对象。 Java 虚拟机的垃圾收集线程对软可及对象和其他一般 Java 对象进行了区别对待 ,软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出 OutOfMemoryError 之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可及对象会被虚拟机尽可能保留。如果想获取软引用中包含的对象,可以使用下面的Java代码:
1 |
MyObject anotherRef =(MyObject) aSoftRef .get(); |
在回收这些对象之前,可以通过上面的代码重新获得对该实例的强引用。而回收之后,当调用软引用的get() 方法时,返回的是 null 。
三、如何使用 ReferenceQueue
作为一个 Java 对象, SoftReference 对象除了具有保存软引用的特殊性之外,也具有 Java 对象的一般性。所以,当软可及对象被回收之后,虽然这个 SoftReference 对象的 get() 方法返回 null, 但这个 SoftReference 对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量 SoftReference 对象带来的内存泄漏。在 java.lang.ref 包里还提供了 ReferenceQueue 。如果在创建 SoftReference 对象的时候,使用了带有一个 ReferenceQueue 对象作为参数的构造方法,如下面的Java代码 :
1 |
ReferenceQueue queue = new ReferenceQueue(); |
2 |
SoftReference ref = new SoftReference( aMyObject, queue ); |
当这个 SoftReference 所软引用的 aMyOhject 被垃圾收集器回收的同时,ref 所强引用的 SoftReference 对象被列入 ReferenceQueue 。也就是说, ReferenceQueue 中保存的对象是 Reference 对象,而且是已经失去了它所软引用的对象的 Reference 对象。另外从 ReferenceQueue 这个名字也可以看出,它是一个队列,当调用它的 poll() 方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个 Reference 对象。
在任何时候,都可以调用 ReferenceQueue 的 poll() 方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个 null, 否则该方法返回队列中最前面一个 Reference 对象。利用这个方法,可以检查哪个 SoftReference 所软引用的对象已经被回收。可以把这些失去所软引用的对象的 SoftReference 对象清除掉,如下面的Java代码所示。:
1 |
SoftReference ref = null ; |
2 |
while ((ref = (EmployeeRef) q .poll()) != null ) { |
四、实例分析
理解了 Java中的引用机制之后就可以在Android中构造缓存器(cache)了,在Android中应用比较多的控件是ListView,通常会使用ListView显示网络数据列表,同时会包含图片缩略图,当数据量很大的时候,为了让用户能更流畅地流量信息,可以使用异步加载和缓存机制处理网络图片。
通过以上对于Java软引用类型的了解,可以知道使用软引用来构建缓存是比较合适的。虽然软引用能够延长数据对象的生命周期,但是对于移动设备来说,内存资源相对来说比较紧缺,仅使用软引用未必能达到最佳的缓存效果。通常会使用一些组合方式来进行数据缓存,最常用的是强引用、软引用加本地缓存的方式。
Android提供了一个AsyncTask类,它封装了基本的异步操作模型,只需要实现几个最基本的方法就可以很容易的实现异步加载图片,主要的方法是doInBackground方法和onPostExecute方法。AsyncTask类会启动一个新的线程执行doInBackground方法,所以我们所有的网络操作都应该在这个方法中实现,当doInBackground方法执行完成后,AsyncTask类会使用内置的Handler发送消息在主线程中执行onPostExecute方法,所以关于对UI的操作都应该放在onPostExecute方法中实现。
对于缓存的处理,主要思路是:在开始时,创建两个缓存区域:强引用缓存区域和软引用缓存区域。在强引用缓存区中保存有限的图片对象,根据LRU策略把一些最不常用的图片对象移到软引用缓存区,当缓存区域中都没有图片对象时从网络加载图片。完成后把图片数据保存到SDCard中,并根据LRU策略进行管理SDCard中保存的图片文件。
下面通过一个ListView的使用实例来说明如何在Android应用程序中使用异步加载图片,并且在内存和本地缓存它们。
第一步,首先建立一个Android工程,名称为AsyncListImage,由于应用需要访问网络所以需要修改AndroidManifest.xml文件,添加网络连接的权限,代码如下:
1 |
< uses-permission android:name = "android.permission.INTERNET" /> |
第二步,修改main.xml文件添加listview控件,并设置listview的一些基本属性信息,如下面的xml代码:
01 |
<? xml version = "1.0" encoding = "utf-8" ?> |
03 |
android:orientation = "vertical" |
04 |
android:layout_width = "fill_parent" |
05 |
android:layout_height = "fill_parent" > |
06 |
< ListView android:id = "@+id/list" |
07 |
android:layout_width = "fill_parent" |
08 |
android:layout_height = "fill_parent" |
09 |
android:background = "#ffffffff" |
10 |
android:cacheColorHint = "#00000000" /> |
第三步,修改AsyncListImage Activity类并覆盖oncreate方法,初始化listview,并创建listview控件使用的Adapter。在AsyncListImage中定义了两种缓存区域A和B,A代表强引用缓存区域,B代表软引用缓存区域,由于使用强引用缓存区域保存数据只能保存一定的数量,而不能一直往里面存放,需要设置数据的过期时间、LRU等算法。这里有一个方法是把常用的数据放到缓存A中,不常用的放到另外一个缓存B中。当要获取数据时先从A中去获取,如果A中不存在那么再去B中获取。B中的数据主要是A中经过LRU生成的数据,这里的内存回收主要针对B内存,从而保持A中的数据可以有效的被命中。
下面是完整的Java代码:
001 |
package com.devdiv.android.asynimagelist; |
004 |
import java.lang.ref.SoftReference; |
005 |
import java.util.HashMap; |
006 |
import java.util.LinkedHashMap; |
007 |
import java.util.concurrent.ConcurrentHashMap; |
009 |
import android.app.Activity; |
010 |
import android.graphics.Bitmap; |
011 |
import android.os.Bundle; |
012 |
import android.util.Log; |
013 |
import android.view.View; |
014 |
import android.view.ViewGroup; |
015 |
import android.widget.BaseAdapter; |
016 |
import android.widget.ImageView; |
017 |
import android.widget.ListView; |
018 |
import android.widget.ImageView.ScaleType; |
020 |
@SuppressWarnings ( "serial" ) |
021 |
public class AsyncListImage extends Activity implements RemoteImageCallback { |
022 |
private ListView list; |
023 |
private static final String TAG = AsyncListImage. class .getSimpleName(); |
024 |
private static final int HARD_CACHE_CAPACITY = 10 ; |
026 |
private final HashMap<String, Bitmap> mHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2 , 0 .75f, true ) { |
029 |
protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) { |
030 |
if (size() > HARD_CACHE_CAPACITY) { |
032 |
mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue())); |
040 |
*当mHardBitmapCache的key大于10的时候,会根据LRU算法把最近没有被使用的key放入到这个缓存中。 |
041 |
* Bitmap使用了SoftReference,当内存空间不足时,此cache中的bitmap会被垃圾回收掉 |
043 |
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>( |
044 |
HARD_CACHE_CAPACITY / 2 ); |
047 |
public void onCreate(Bundle savedInstanceState) { |
048 |
super .onCreate(savedInstanceState); |
049 |
setContentView(R.layout.main); |
050 |
list = (ListView) findViewById(R.id.list); |
054 |
MyListAdapter adapter = new MyListAdapter(); |
055 |
list.setAdapter(adapter); |
058 |
private void initCacheDir() { |
059 |
String cacheDir = "/data/data/com.devdiv.android.asynimagelist/files/caches" ; |
060 |
File f = new File(cacheDir); |
066 |
private class MyListAdapter extends BaseAdapter { |
067 |
private String[] urls = new String[] { |
083 |
public int getCount() { |
088 |
public String getItem( int position) { |
089 |
return urls[position]; |
093 |
public long getItemId( int position) { |
098 |
public View getView( int position, View convertView, ViewGroup parent) { |
099 |
if (convertView == null ) { |
100 |
convertView = new ImageView(AsyncListImage. this ); |
102 |
ImageView iv = (ImageView)convertView; |
103 |
iv.setScaleType(ScaleType.FIT_START); |
104 |
Bitmap bitmap = getBitmapFromCache(getItem(position)); |
105 |
if (bitmap == null ) { |
106 |
iv.setImageResource(R.drawable.default_image); |
107 |
iv.setTag(getItem(position)); |
108 |
new ImageDownloaderTask(AsyncListImage. this ).execute( new String[]{getItem(position)}); |
110 |
iv.setImageBitmap(bitmap); |
121 |
private Bitmap getBitmapFromCache(String url) { |
123 |
synchronized (mHardBitmapCache) { |
124 |
final Bitmap bitmap = mHardBitmapCache.get(url); |
125 |
if (bitmap != null ) { |
127 |
mHardBitmapCache.remove(url); |
128 |
Log.d(TAG, "move bitmap to the head of linkedhashmap:" + url); |
129 |
mHardBitmapCache.put(url,bitmap); |
134 |
SoftReference<Bitmap> bitmapReference = mSoftBitmapCache.get(url); |
135 |
if (bitmapReference != null ) { |
136 |
final Bitmap bitmap = bitmapReference.get(); |
137 |
if (bitmap != null ) { |
138 |
Log.d(TAG, "get bitmap from mSoftBitmapCache with key:" + url); |
141 |
mSoftBitmapCache.remove(url); |
142 |
Log.d(TAG, "remove bitmap with key:" + url); |
149 |
public void onComplete(String url, Bitmap bitmap) { |
150 |
Log.d(TAG, "onComplete after got bitmap from remote with key:" + url); |
151 |
ImageView iv = (ImageView)list.findViewWithTag(url); |
153 |
iv.setImageBitmap(bitmap); |
154 |
mHardBitmapCache.put(url, bitmap); |
第四步,定义AsyncTask 类的子类ImageDownloaderTask类并覆盖doInBackground 方法和onPostExecute方法。在doInBackground方法中进行网络操作和文件操作,在onPostExecute方法中执行回调函数,把获取的bitmap数据发送到UI线程与ListView中的imageView进行关联,Java代码如下。
001 |
package com.devdiv.android.asynimagelist; |
003 |
import java.io.BufferedOutputStream; |
004 |
import java.io.ByteArrayOutputStream; |
005 |
import java.io.Closeable; |
007 |
import java.io.FileNotFoundException; |
008 |
import java.io.FileOutputStream; |
009 |
import java.io.IOException; |
010 |
import java.io.InputStream; |
011 |
import java.io.OutputStream; |
012 |
import java.lang.ref.WeakReference; |
013 |
import java.util.Arrays; |
014 |
import java.util.Comparator; |
016 |
import org.apache.http.HttpEntity; |
017 |
import org.apache.http.HttpResponse; |
018 |
import org.apache.http.HttpStatus; |
019 |
import org.apache.http.client.methods.HttpGet; |
020 |
import org.apache.http.impl.client.DefaultHttpClient; |
022 |
import android.graphics.Bitmap; |
023 |
import android.graphics.BitmapFactory; |
024 |
import android.os.AsyncTask; |
025 |
import android.os.Environment; |
026 |
import android.os.StatFs; |
027 |
import android.util.Log; |
029 |
public class ImageDownloaderTask extends AsyncTask<String, Void, Bitmap> { |
030 |
private static String TAG = ImageDownloaderTask. class .getSimpleName(); |
031 |
private static final int IO_BUFFER_SIZE = 4 * 1024 ; |
032 |
private static final int MB = 1024 * 1024 ; |
033 |
private static final int CACHE_SIZE = 1024 * 1024 ; |
034 |
private static final int mTimeDiff = 5 * 24 * 60 * 60 * 1000 ; |
035 |
private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 30 ; |
036 |
private static final String WHOLESALE_CONV = "/data/data/com.devdiv.android.asynimagelist/files/caches" ; |
038 |
private final WeakReference<AsyncListImage> activityReference; |
040 |
public ImageDownloaderTask(AsyncListImage activity) { |
041 |
activityReference = new WeakReference<AsyncListImage>(activity); |
045 |
protected Bitmap doInBackground(String... params) { |
047 |
String filename = convertUrlToFileName(url); |
048 |
String dir = getDirectory(filename); |
049 |
File file = new File(dir + "/" + filename); |
051 |
removeExpiredCache(dir, filename); |
052 |
updateFileTime(dir, filename); |
053 |
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); |
058 |
final DefaultHttpClient client = new DefaultHttpClient(); |
060 |
final HttpGet getRequest = new HttpGet(url); |
062 |
HttpResponse response = client.execute(getRequest); |
063 |
final int statusCode = response.getStatusLine().getStatusCode(); |
064 |
if (statusCode != HttpStatus.SC_OK) { |
065 |
Log.w(TAG, "从" + url + "中下载图片时出错!,错误码:" + statusCode); |
068 |
final HttpEntity entity = response.getEntity(); |
069 |
if (entity != null ) { |
070 |
InputStream inputStream = null ; |
071 |
OutputStream outputStream = null ; |
073 |
inputStream = entity.getContent(); |
074 |
final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); |
075 |
outputStream = new BufferedOutputStream(dataStream, |
077 |
copy(inputStream, outputStream); |
078 |
outputStream.flush(); |
079 |
final byte [] data = dataStream.toByteArray(); |
080 |
final Bitmap bitmap = BitmapFactory.decodeByteArray(data, |
083 |
saveBmpToSd(bitmap, url); |
087 |
closeStream(inputStream); |
088 |
closeStream(outputStream); |
089 |
entity.consumeContent(); |
092 |
} catch (IOException e) { |
094 |
Log.w(TAG, "I/O error while retrieving bitmap from " + url, e); |
095 |
} catch (IllegalStateException e) { |
097 |
Log.w(TAG, "Incorrect URL:" + url); |
098 |
} catch (Exception e) { |
100 |
Log.w(TAG, "Error while retrieving bitmap from " + url, e); |
106 |
protected void onPostExecute(Bitmap result) { |
107 |
super .onPostExecute(result); |
108 |
AsyncListImage act = activityReference.get(); |
109 |
if (act != null && result != null ) { |
110 |
act.onComplete(url, result); |
116 |
* Copy the content of the input stream into the output stream, using a temporary |
117 |
* byte array buffer whose size is defined by {@link #IO_BUFFER_SIZE}. |
119 |
* @param in The input stream to copy from. |
120 |
* @param out The output stream to copy to. |
122 |
* @throws java.io.IOException If any error occurs during the copy. |
124 |
public static void copy(InputStream in, OutputStream out) throws IOException { |
125 |
byte [] b = new byte [IO_BUFFER_SIZE]; |
127 |
while ((read = in.read(b)) != - 1 ) { |
128 |
out.write(b, 0 , read); |
133 |
* Closes the specified stream. |
135 |
* @param stream The stream to close. |
137 |
public static void closeStream(Closeable stream) { |
138 |
if (stream != null ) { |
141 |
} catch (IOException e) { |
142 |
android.util.Log.e(TAG, "Could not close stream" , e); |
147 |
private void saveBmpToSd(Bitmap bm, String url) { |
149 |
Log.w(TAG, " trying to savenull bitmap" ); |
153 |
if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { |
154 |
Log.w(TAG, "Low free space onsd, do not cache" ); |
155 |
removeCache(WHOLESALE_CONV); |
158 |
String filename = convertUrlToFileName(url); |
159 |
String dir = getDirectory(filename); |
160 |
File file = new File(dir + "/" + filename); |
162 |
file.createNewFile(); |
163 |
OutputStream outStream = new FileOutputStream(file); |
164 |
bm.compress(Bitmap.CompressFormat.JPEG, 100 , outStream); |
167 |
Log.i(TAG, "Image saved tosd" ); |
168 |
} catch (FileNotFoundException e) { |
169 |
Log.w(TAG, "FileNotFoundException" ); |
170 |
} catch (IOException e) { |
171 |
Log.w(TAG, "IOException" ); |
175 |
private String convertUrlToFileName(String url) { |
176 |
int lastIndex = url.lastIndexOf( '/' ); |
177 |
return url.substring(lastIndex + 1 ); |
180 |
private String getDirectory(String filename) { |
181 |
return WHOLESALE_CONV; |
189 |
private int freeSpaceOnSd() { |
190 |
StatFs stat = new StatFs(Environment.getExternalStorageDirectory() |
192 |
double sdFreeMB = (( double ) stat.getAvailableBlocks() * ( double ) stat |
195 |
return ( int ) sdFreeMB; |
204 |
private void updateFileTime(String dir, String fileName) { |
205 |
File file = new File(dir, fileName); |
206 |
long newModifiedTime = System.currentTimeMillis(); |
207 |
file.setLastModified(newModifiedTime); |
212 |
* 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定 |
218 |
private void removeCache(String dirPath) { |
219 |
File dir = new File(dirPath); |
220 |
File[] files = dir.listFiles(); |
225 |
for ( int i = 0 ; i < files.length; i++) { |
226 |
if (files.getName().contains(WHOLESALE_CONV)) { |
227 |
dirSize += files.length(); |
230 |
if (dirSize > CACHE_SIZE * MB |
231 |
|| FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { |
232 |
int removeFactor = ( int ) (( 0.4 * files.length) + 1 ); |
234 |
Arrays.sort(files, new FileLastModifSort()); |
236 |
Log.i(TAG, "Clear some expiredcache files " ); |
238 |
for ( int i = 0 ; i < removeFactor; i++) { |
240 |
if (files.getName().contains(WHOLESALE_CONV)) { |
253 |
* TODO 根据文件的最后修改时间进行排序 * |
255 |
class FileLastModifSort implements Comparator<File> { |
256 |
public int compare(File arg0, File arg1) { |
257 |
if (arg0.lastModified() > arg1.lastModified()) { |
259 |
} else if (arg0.lastModified() == arg1.lastModified()) { |
273 |
private void removeExpiredCache(String dirPath, String filename) { |
275 |
File file = new File(dirPath, filename); |
277 |
if (System.currentTimeMillis() - file.lastModified() > mTimeDiff) { |
279 |
Log.i(TAG, "Clear some expiredcache files " ); |