转自:
============================================================================================
一、为什么要使用软引用
在上面关于软引用的介绍中,已经提到了软引用的特性。使用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 实例的强引用 :
1 aRef = null ;//断开对象的强引用
此后, 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 ) {3 // 清除 ref4 }
四、实例分析 理解了 Java中的引用机制之后就可以在Android中构造缓存器(cache)了,在Android中应用比较多的控件是ListView,通常会使用ListView显示网络数据列表,同时会包含图片缩略图,当数据量很大的时候,为了让用户能更流畅地流量信息,可以使用异步加载和缓存机制处理网络图片。通过以上对于Java软引用类型的了解,可以知道使用软引用来构建缓存是比较合适的。虽然软引用能够延长数据对象的生命周期,但是对于移动设备来说,内存资源相对来说比较紧缺,仅使用软引用未必能达到最佳的缓存效果。通常会使用一些组合方式来进行数据缓存,最常用的是强引用、软引用加本地缓存的方式。 Android提供了一个AsyncTask类,它封装了基本的异步操作模型,只需要实现几个最基本的方法就可以很容易的实现异步加载图片,主要的方法是doInBackground方法和onPostExecute方法。AsyncTask类会启动一个新的线程执行doInBackground方法,所以我们所有的网络操作都应该在这个方法中实现,当doInBackground方法执行完成后,AsyncTask类会使用内置的Handler发送消息在主线程中执行onPostExecute方法,所以关于对UI的操作都应该放在onPostExecute方法中实现。 对于缓存的处理,主要思路是:在开始时,创建两个缓存区域:强引用缓存区域和软引用缓存区域。在强引用缓存区中保存有限的图片对象,根据LRU策略把一些最不常用的图片对象移到软引用缓存区,当缓存区域中都没有图片对象时从网络加载图片。完成后把图片数据保存到SDCard中,并根据LRU策略进行管理SDCard中保存的图片文件。 下图为ListView异步加载远程图片的流程图:
下面通过一个ListView的使用实例来说明如何在Android应用程序中使用异步加载图片,并且在内存和本地缓存它们。
第一步,首先建立一个Android工程,名称为AsyncListImage,由于应用需要访问网络所以需要修改AndroidManifest.xml文件,添加网络连接的权限,代码如下:1
第二步,修改main.xml文件添加listview控件,并设置listview的一些基本属性信息,如下面的xml代码:
01 0206 11
第三步,修改AsyncListImage Activity类并覆盖oncreate方法,初始化listview,并创建listview控件使用的Adapter。在AsyncListImage中定义了两种缓存区域A和B,A代表强引用缓存区域,B代表软引用缓存区域,由于使用强引用缓存区域保存数据只能保存一定的数量,而不能一直往里面存放,需要设置数据的过期时间、LRU等算法。这里有一个方法是把常用的数据放到缓存A中,不常用的放到另外一个缓存B中。当要获取数据时先从A中去获取,如果A中不存在那么再去B中获取。B中的数据主要是A中经过LRU生成的数据,这里的内存回收主要针对B内存,从而保持A中的数据可以有效的被命中。
下面是完整的Java代码:1 package com.devdiv.android.asynimagelist; 2 3 import java.io.File; 4 import java.lang.ref.SoftReference; 5 import java.util.HashMap; 6 import java.util.LinkedHashMap; 7 import java.util.concurrent.ConcurrentHashMap; 8 9 import android.app.Activity; 10 import android.graphics.Bitmap; 11 import android.os.Bundle; 12 import android.util.Log; 13 import android.view.View; 14 import android.view.ViewGroup; 15 import android.widget.BaseAdapter; 16 import android.widget.ImageView; 17 import android.widget.ListView; 18 import android.widget.ImageView.ScaleType; 19 20 @SuppressWarnings("serial") 21 public class AsyncListImage extends Activity implements RemoteImageCallback { 22 private ListView list; 23 private static final String TAG = AsyncListImage.class.getSimpleName(); 24 private static final int HARD_CACHE_CAPACITY = 10; 25 26 private final HashMapmHardBitmapCache = new LinkedHashMap (HARD_CACHE_CAPACITY / 2, 0.75f, true) { 27 28 @Override 29 protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) { 30 if (size() > HARD_CACHE_CAPACITY) { 31 //当map的size大于10时,把最近不常用的key放到mSoftBitmapCache中,从而保证mHardBitmapCache的效率 32 mSoftBitmapCache.put(eldest.getKey(), new SoftReference (eldest.getValue())); 33 return true; 34 } else 35 return false; 36 } 37 }; 38 39 /** 40 *当mHardBitmapCache的key大于10的时候,会根据LRU算法把最近没有被使用的key放入到这个缓存中。 41 * Bitmap使用了SoftReference,当内存空间不足时,此cache中的bitmap会被垃圾回收掉 42 */ 43 private final static ConcurrentHashMap > mSoftBitmapCache = new ConcurrentHashMap >( 44 HARD_CACHE_CAPACITY / 2); 45 46 @Override 47 public void onCreate(Bundle savedInstanceState) { 48 super.onCreate(savedInstanceState); 49 setContentView(R.layout.main); 50 list = (ListView) findViewById(R.id.list); 51 52 initCacheDir(); 53 54 MyListAdapter adapter = new MyListAdapter(); 55 list.setAdapter(adapter); 56 } 57 58 private void initCacheDir() { 59 String cacheDir = "/data/data/com.devdiv.android.asynimagelist/files/caches"; 60 File f = new File(cacheDir); 61 if (!f.exists()) { 62 f.mkdirs(); 63 } 64 } 65 66 private class MyListAdapter extends BaseAdapter { 67 private String[] urls = new String[] { 68 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Add%20Icon.jpg", 69 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Adobe%20Illustator%20Icon.jpg", 70 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Attach%20Icon.jpg", 71 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Applications%20Cascade%20Icon.jpg", 72 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Administrator%20Icon.jpg", 73 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Clients%20Icon.jpg", 74 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Coinstack%20Icon.jpg", 75 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Download%20Icon.jpg", 76 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Help%20Icon.jpg", 77 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Home%20Icon.jpg", 78 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Pen%20Icon.jpg", 79 "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Statistics%20Icon.jpg" 80 }; 81 82 @Override 83 public int getCount() { 84 return urls.length; 85 } 86 87 @Override 88 public String getItem(int position) { 89 return urls[position]; 90 } 91 92 @Override 93 public long getItemId(int position) { 94 return position; 95 } 96 97 @Override 98 public View getView(int position, View convertView, ViewGroup parent) { 99 if (convertView == null) {100 convertView = new ImageView(AsyncListImage.this);101 }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)});109 } else {110 iv.setImageBitmap(bitmap);111 }112 iv = null;113 return convertView;114 }115 116 }117 118 /** 119 * 从缓存中获取图片 120 */ 121 private Bitmap getBitmapFromCache(String url) { 122 // 先从mHardBitmapCache缓存中获取 123 synchronized (mHardBitmapCache) { 124 final Bitmap bitmap = mHardBitmapCache.get(url); 125 if (bitmap != null) { 126 //如果找到的话,把元素移到linkedhashmap的最前面,从而保证在LRU算法中是最后被删除 127 mHardBitmapCache.remove(url); 128 Log.d(TAG, "move bitmap to the head of linkedhashmap:" + url);129 mHardBitmapCache.put(url,bitmap); 130 return bitmap; 131 } 132 } 133 //如果mHardBitmapCache中找不到,到mSoftBitmapCache中找 134 SoftReference 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);139 return bitmap; 140 } else { 141 mSoftBitmapCache.remove(url); 142 Log.d(TAG, "remove bitmap with key:" + url);143 } 144 } 145 return null; 146 }147 148 @Override149 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);152 if (iv != null) {153 iv.setImageBitmap(bitmap);154 mHardBitmapCache.put(url, bitmap);155 }156 } 157 158 }
1 package com.devdiv.android.asynimagelist; 2 3 import java.io.BufferedOutputStream; 4 import java.io.ByteArrayOutputStream; 5 import java.io.Closeable; 6 import java.io.File; 7 import java.io.FileNotFoundException; 8 import java.io.FileOutputStream; 9 import java.io.IOException; 10 import java.io.InputStream; 11 import java.io.OutputStream; 12 import java.lang.ref.WeakReference; 13 import java.util.Arrays; 14 import java.util.Comparator; 15 16 import org.apache.http.HttpEntity; 17 import org.apache.http.HttpResponse; 18 import org.apache.http.HttpStatus; 19 import org.apache.http.client.methods.HttpGet; 20 import org.apache.http.impl.client.DefaultHttpClient; 21 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.os.AsyncTask; 25 import android.os.Environment; 26 import android.os.StatFs; 27 import android.util.Log; 28 29 public class ImageDownloaderTask extends AsyncTask{ 30 private static String TAG = ImageDownloaderTask.class.getSimpleName(); 31 private static final int IO_BUFFER_SIZE = 4 * 1024; 32 private static final int MB = 1024 * 1024; 33 private static final int CACHE_SIZE = 1024 * 1024; 34 private static final int mTimeDiff = 5 * 24 * 60 * 60 * 1000; 35 private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 30; 36 private static final String WHOLESALE_CONV = "/data/data/com.devdiv.android.asynimagelist/files/caches"; 37 private String url; 38 private final WeakReference activityReference; 39 40 public ImageDownloaderTask(AsyncListImage activity) { 41 activityReference = new WeakReference (activity); 42 } 43 44 @Override 45 protected Bitmap doInBackground(String... params) { 46 url = params[0]; 47 String filename = convertUrlToFileName(url); 48 String dir = getDirectory(filename); 49 File file = new File(dir + "/" + filename); 50 if (file.exists()) { 51 removeExpiredCache(dir, filename); 52 updateFileTime(dir, filename); 53 Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); 54 if (bitmap != null) 55 return bitmap; 56 } 57 58 final DefaultHttpClient client = new DefaultHttpClient(); 59 60 final HttpGet getRequest = new HttpGet(url); 61 try { 62 HttpResponse response = client.execute(getRequest); 63 final int statusCode = response.getStatusLine().getStatusCode(); 64 if (statusCode != HttpStatus.SC_OK) { 65 Log.w(TAG, "从" + url + "中下载图片时出错!,错误码:" + statusCode); 66 return null; 67 } 68 final HttpEntity entity = response.getEntity(); 69 if (entity != null) { 70 InputStream inputStream = null; 71 OutputStream outputStream = null; 72 try { 73 inputStream = entity.getContent(); 74 final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); 75 outputStream = new BufferedOutputStream(dataStream, 76 IO_BUFFER_SIZE); 77 copy(inputStream, outputStream); 78 outputStream.flush(); 79 final byte[] data = dataStream.toByteArray(); 80 final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 81 0, data.length); 82 83 saveBmpToSd(bitmap, url); 84 85 return bitmap; 86 } finally { 87 closeStream(inputStream); 88 closeStream(outputStream); 89 entity.consumeContent(); 90 } 91 } 92 } catch (IOException e) { 93 getRequest.abort(); 94 Log.w(TAG, "I/O error while retrieving bitmap from " + url, e); 95 } catch (IllegalStateException e) { 96 getRequest.abort(); 97 Log.w(TAG, "Incorrect URL:" + url); 98 } catch (Exception e) { 99 getRequest.abort();100 Log.w(TAG, "Error while retrieving bitmap from " + url, e);101 } 102 return null;103 }104 105 @Override106 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);111 }112 }113 114 115 /**116 * Copy the content of the input stream into the output stream, using a temporary117 * byte array buffer whose size is defined by { @link #IO_BUFFER_SIZE}.118 *119 * @param in The input stream to copy from.120 * @param out The output stream to copy to.121 *122 * @throws java.io.IOException If any error occurs during the copy.123 */124 public static void copy(InputStream in, OutputStream out) throws IOException {125 byte[] b = new byte[IO_BUFFER_SIZE];126 int read;127 while ((read = in.read(b)) != -1) {128 out.write(b, 0, read);129 }130 }131 132 /**133 * Closes the specified stream.134 *135 * @param stream The stream to close.136 */137 public static void closeStream(Closeable stream) {138 if (stream != null) {139 try {140 stream.close();141 } catch (IOException e) {142 android.util.Log.e(TAG, "Could not close stream", e);143 }144 }145 }146 147 private void saveBmpToSd(Bitmap bm, String url) {148 if (bm == null) {149 Log.w(TAG, " trying to savenull bitmap");150 return;151 }152 // 判断sdcard上的空间153 if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {154 Log.w(TAG, "Low free space onsd, do not cache");155 removeCache(WHOLESALE_CONV);156 return;157 }158 String filename = convertUrlToFileName(url);159 String dir = getDirectory(filename);160 File file = new File(dir + "/" + filename);161 try {162 file.createNewFile();163 OutputStream outStream = new FileOutputStream(file);164 bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream);165 outStream.flush();166 outStream.close();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");172 }173 }174 175 private String convertUrlToFileName(String url) {176 int lastIndex = url.lastIndexOf('/');177 return url.substring(lastIndex + 1);178 }179 180 private String getDirectory(String filename) {181 return WHOLESALE_CONV;182 }183 184 /**185 * 计算sdcard上的剩余空间186 * 187 * @return188 */189 private int freeSpaceOnSd() {190 StatFs stat = new StatFs(Environment.getExternalStorageDirectory()191 .getPath());192 double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat193 .getBlockSize())194 / MB;195 return (int) sdFreeMB;196 }197 198 /**199 * 修改文件的最后修改时间200 * 201 * @param dir202 * @param fileName203 */204 private void updateFileTime(String dir, String fileName) {205 File file = new File(dir, fileName);206 long newModifiedTime = System.currentTimeMillis();207 file.setLastModified(newModifiedTime);208 }209 210 /**211 *计算存储目录下的文件大小,212 * 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定213 * 那么删除40%最近没有被使用的文件214 * 215 * @param dirPath216 * @param filename217 */218 private void removeCache(String dirPath) {219 File dir = new File(dirPath);220 File[] files = dir.listFiles();221 if (files == null) {222 return;223 }224 int dirSize = 0;225 for (int i = 0; i < files.length; i++) {226 if (files.getName().contains(WHOLESALE_CONV)) {227 dirSize += files.length();228 }229 }230 if (dirSize > CACHE_SIZE * MB231 || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) {232 int removeFactor = (int) ((0.4 * files.length) + 1);233 234 Arrays.sort(files, new FileLastModifSort());235 236 Log.i(TAG, "Clear some expiredcache files ");237 238 for (int i = 0; i < removeFactor; i++) {239 240 if (files.getName().contains(WHOLESALE_CONV)) {241 242 files.delete();243 244 }245 246 }247 248 }249 250 }251 252 /**253 * TODO 根据文件的最后修改时间进行排序 *254 */255 class FileLastModifSort implements Comparator {256 public int compare(File arg0, File arg1) {257 if (arg0.lastModified() > arg1.lastModified()) {258 return 1;259 } else if (arg0.lastModified() == arg1.lastModified()) {260 return 0;261 } else {262 return -1;263 }264 }265 }266 267 /**268 * 删除过期文件269 * 270 * @param dirPath271 * @param filename272 */273 private void removeExpiredCache(String dirPath, String filename) {274 275 File file = new File(dirPath, filename);276 277 if (System.currentTimeMillis() - file.lastModified() > mTimeDiff) {278 279 Log.i(TAG, "Clear some expiredcache files ");280 281 file.delete();282 283 }284 285 }286 }
第五步、运行工程之后,运行效果如下图示。
运行工程,最开始时,ListView使用默认的图片填充imageview。然后使用异步方式获取网络图片,当获取到网络图片后,使用最新的网络图片替换默认图片。
当用户拖动ListView,显示效果如下图所示:
再次运行工程后,加载图片的速度会非常块,因为程序在手机内存中存储了相应的图片资源,直接加载这些资源就好了,不需要再访问网络。
下图是在DDMS的File Explorer中的显示: