使用软引用构建缓存(转载)

一、为什么要使用软引用

       在上面关于软引用的介绍中,已经提到了软引用的特性。使用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     // 清除 ref
4 }


四、实例分析

        理解了 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"?>
02 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
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"/>
11 </LinearLayout>


        第三步,修改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;
002   
003 import java.io.File;
004 import java.lang.ref.SoftReference;
005 import java.util.HashMap;
006 import java.util.LinkedHashMap;
007 import java.util.concurrent.ConcurrentHashMap;
008   
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;
019   
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;
025   
026         private final HashMap<String, Bitmap> mHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) { 
027   
028                 @Override 
029         protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) { 
030             if (size() > HARD_CACHE_CAPACITY) { 
031                //当map的size大于10时,把最近不常用的key放到mSoftBitmapCache中,从而保证mHardBitmapCache的效率 
032                mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue())); 
033                 return true
034             } else 
035                 return false
036         
037     }; 
038       
039         /**
040          *当mHardBitmapCache的key大于10的时候,会根据LRU算法把最近没有被使用的key放入到这个缓存中。
041          * Bitmap使用了SoftReference,当内存空间不足时,此cache中的bitmap会被垃圾回收掉
042          */
043         private final static ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(
044                         HARD_CACHE_CAPACITY / 2);
045   
046         @Override
047         public void onCreate(Bundle savedInstanceState) {
048                 super.onCreate(savedInstanceState);
049                 setContentView(R.layout.main);
050                 list = (ListView) findViewById(R.id.list);
051                   
052                 initCacheDir();
053                   
054                 MyListAdapter adapter = new MyListAdapter();
055                 list.setAdapter(adapter);
056         }
057           
058         private void initCacheDir() {
059                 String cacheDir = "/data/data/com.devdiv.android.asynimagelist/files/caches";
060                 File f = new File(cacheDir);
061                 if (!f.exists()) {
062                         f.mkdirs();
063                 }
064         }
065           
066         private class MyListAdapter extends BaseAdapter {
067                 private String[] urls = new String[] {
080                 };
081                   
082                 @Override
083                 public int getCount() {
084                         return urls.length;
085                 }
086   
087                 @Override
088                 public String getItem(int position) {
089                         return urls[position];
090                 }
091   
092                 @Override
093                 public long getItemId(int position) {
094                         return position;
095                 }
096   
097                 @Override
098                 public View getView(int position, View convertView, ViewGroup parent) {
099                         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<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);
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         @Override
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);
152                 if (iv != null) {
153                         iv.setImageBitmap(bitmap);
154                         mHardBitmapCache.put(url, bitmap);
155                 }
156         
157           
158 }


        第四步,定义AsyncTask 类的子类ImageDownloaderTask类并覆盖doInBackground 方法和onPostExecute方法。在doInBackground方法中进行网络操作和文件操作,在onPostExecute方法中执行回调函数,把获取的bitmap数据发送到UI线程与ListView中的imageView进行关联,Java代码如下。

001 package com.devdiv.android.asynimagelist;
002   
003 import java.io.BufferedOutputStream;
004 import java.io.ByteArrayOutputStream;
005 import java.io.Closeable;
006 import java.io.File;
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;
015   
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;
021   
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;
028   
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";
037         private String url;
038         private final WeakReference<AsyncListImage> activityReference;
039   
040         public ImageDownloaderTask(AsyncListImage activity) {
041                 activityReference = new WeakReference<AsyncListImage>(activity);
042         }
043   
044         @Override
045         protected Bitmap doInBackground(String... params) {
046                 url = params[0];
047                 String filename = convertUrlToFileName(url);
048                 String dir = getDirectory(filename);
049                 File file = new File(dir + "/" + filename);
050                 if (file.exists()) {
051                         removeExpiredCache(dir, filename);
052                         updateFileTime(dir, filename);
053                         Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
054                         if (bitmap != null)
055                                 return bitmap;
056                 }
057                   
058                 final DefaultHttpClient client = new DefaultHttpClient(); 
059                   
060                 final HttpGet getRequest = new HttpGet(url);
061                 try {
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);
066                                 return null;
067                         }
068                         final HttpEntity entity = response.getEntity();
069                         if (entity != null) {
070                                 InputStream inputStream = null;
071                                 OutputStream outputStream = null;
072                                 try {
073                                         inputStream = entity.getContent();
074                                         final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
075                                         outputStream = new BufferedOutputStream(dataStream,
076                                                         IO_BUFFER_SIZE);
077                                         copy(inputStream, outputStream);
078                                         outputStream.flush();
079                                         final byte[] data = dataStream.toByteArray();
080                                         final Bitmap bitmap = BitmapFactory.decodeByteArray(data,
081                                                         0, data.length);
082                                           
083                                         saveBmpToSd(bitmap, url);
084                                           
085                                         return bitmap;
086                                 } finally {
087                                         closeStream(inputStream);
088                                         closeStream(outputStream);
089                                         entity.consumeContent();
090                                 }
091                         }
092                 } catch (IOException e) {
093                         getRequest.abort();
094                         Log.w(TAG, "I/O error while retrieving bitmap from " + url, e);
095                 } catch (IllegalStateException e) {
096                         getRequest.abort();
097                         Log.w(TAG, "Incorrect URL:" + url);
098                 } catch (Exception e) {
099                         getRequest.abort();
100                         Log.w(TAG, "Error while retrieving bitmap from " + url, e);
101                
102                 return null;
103         }
104           
105         @Override
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);
111                 }
112         }
113           
114           
115     /**
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}.
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          * @return
188          */
189         private int freeSpaceOnSd() {
190                 StatFs stat = new StatFs(Environment.getExternalStorageDirectory()
191                                 .getPath());
192                 double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat
193                                 .getBlockSize())
194                                 / MB;
195                 return (int) sdFreeMB;
196         }
197   
198         /**
199          * 修改文件的最后修改时间
200          
201          * @param dir
202          * @param fileName
203          */
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 dirPath
216          * @param filename
217          */
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 * MB
231                                 || 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<File> {
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 dirPath
271          * @param filename
272          */
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 }
posted @ 2012-12-21 16:29  crazywenza  阅读(313)  评论(0编辑  收藏  举报