使用软引用构建缓存

来源:互联网 发布:数码暴龙故事 网络侦探 编辑:程序博客网 时间:2024/05/21 09:17

转载:http://www.devdiv.com/Android-%E4%BD%BF%E7%94%A8%E8%BD%AF%E5%BC%95%E7%94%A8%E6%9E%84%E5%BB%BA%E7%BC%93%E5%AD%98-thread-130476-1-1.html


一、为什么要使用软引用

       在上面关于软引用的介绍中,已经提到了软引用的特性。使用SoftReference引用的对象会有很长的生命周期,只有当系统的内存不足的时候,才会去释放这些软引用对象。所以可以使用软引用来缓存一些比较昂贵的资源,比如获取的网络图片数据。

        当应用从网络中获取网络图片数据时,用户完全有可能做一些重复性的操作去查看相同的图片信息。对于这样的问题,通常会有两种解决方法: 一种是把过去查看过的图片信息保存在内存中,每一个存储了图片信息的 Java 对象的生命周期都贯穿整个应用程序生命周期,另一种是当用户开始查看其他图片信息的时候,把存储了当前的图片信息的 Java 对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该图片信息的时候,重新获取图片信息。

        很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含图片信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。

        像访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的 Java 对象的引用,必将减少不必要的访问,大大提高程序的运行速度。

        这样看来,使用软引用是非常有必要的一件事情


二、如何使用软引用

        SoftReference 的特点是它的一个实例保存着一个 Java 对象的软引用,该软引用的存在不妨碍垃圾收集器线程对该 Java 对象的回收。也就是说,一旦SoftReference 保存着一个 Java 对象的软引用之后,在垃圾收集器线程对这个 Java 对象回收之前, SoftReference 类所提供的 get() 方法都会返回 这个Java 对象的强引用。另外,一旦垃圾线程回收该 Java 对象之后, get() 方法将返回 null 。

        软引用的使用方法如下面的Java代码所示 :

1MyObject aRef  =  new   MyObject();//创建一个对象
2SoftReference aSoftRef = new SoftReference( aRef );//创建对象的软引用

   
        上面的代码执行后,对于MyObject 对象,有两个引用路径,一个是来自 aSoftRef对象的软引用,一个来自变量 aRef 的强引用,所以 MyObject对象是强可及对象。紧跟着,可以使用下面的java的代码结束 aReference 对 MyObject 实例的强引用 :

1aRef = null ;//断开对象的强引用

        此后, MyObject 对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个 SoftReference 对该对象的引用而始终保留该对象。 Java 虚拟机的垃圾收集线程对软可及对象和其他一般 Java 对象进行了区别对待 ,软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出 OutOfMemoryError 之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可及对象会被虚拟机尽可能保留。如果想获取软引用中包含的对象,可以使用下面的Java代码:

1MyObject anotherRef =(MyObject) aSoftRef .get();//通过软引用获取对象
    
        在回收这些对象之前,可以通过上面的代码重新获得对该实例的强引用。而回收之后,当调用软引用的get() 方法时,返回的是 null 


三、如何使用 ReferenceQueue

        作为一个 Java 对象, SoftReference 对象除了具有保存软引用的特殊性之外,也具有 Java 对象的一般性。所以,当软可及对象被回收之后,虽然这个 SoftReference 对象的 get() 方法返回 null, 但这个 SoftReference 对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量 SoftReference 对象带来的内存泄漏。在 java.lang.ref 包里还提供了 ReferenceQueue 。如果在创建 SoftReference 对象的时候,使用了带有一个 ReferenceQueue 对象作为参数的构造方法,如下面的Java代码 :

1ReferenceQueue queue = new ReferenceQueue();//创建引用队列
2SoftReference  ref = new SoftReference( aMyObject, queue );// 把引用加入到引用队列

        当这个 SoftReference 所软引用的 aMyOhject 被垃圾收集器回收的同时,ref 所强引用的 SoftReference 对象被列入 ReferenceQueue 。也就是说, ReferenceQueue 中保存的对象是 Reference 对象,而且是已经失去了它所软引用的对象的 Reference 对象。另外从 ReferenceQueue 这个名字也可以看出,它是一个队列,当调用它的 poll() 方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个 Reference 对象。

        在任何时候,都可以调用 ReferenceQueue 的 poll() 方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个 null, 否则该方法返回队列中最前面一个 Reference 对象。利用这个方法,可以检查哪个 SoftReference 所软引用的对象已经被回收。可以把这些失去所软引用的对象的 SoftReference 对象清除掉,如下面的Java代码所示。:

1SoftReference ref = null ;
2while ((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异步加载远程图片的流程图:


jhkghjkghk.png


图14-2  ListView异步加载远程图片的流程图


        下面通过一个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"
04android: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代码:

001package com.devdiv.android.asynimagelist;
002 
003import java.io.File;
004import java.lang.ref.SoftReference;
005import java.util.HashMap;
006import java.util.LinkedHashMap;
007import java.util.concurrent.ConcurrentHashMap;
008 
009import android.app.Activity;
010import android.graphics.Bitmap;
011import android.os.Bundle;
012import android.util.Log;
013import android.view.View;
014import android.view.ViewGroup;
015import android.widget.BaseAdapter;
016import android.widget.ImageView;
017import android.widget.ListView;
018import android.widget.ImageView.ScaleType;
019 
020@SuppressWarnings("serial")
021public 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 = newLinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 20.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[] {
068"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Add%20Icon.jpg",
069"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Adobe%20Illustator%20Icon.jpg",
070"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Attach%20Icon.jpg",
071"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Applications%20Cascade%20Icon.jpg",
072"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Administrator%20Icon.jpg",
073"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Clients%20Icon.jpg",
074"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Coinstack%20Icon.jpg",
075"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Download%20Icon.jpg",
076"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Help%20Icon.jpg",
077"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Home%20Icon.jpg",
078"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Pen%20Icon.jpg",
079"http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Statistics%20Icon.jpg"
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(newString[]{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代码如下。