吃水不忘挖井人:
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
本文是在上述连接内容的基础上进行了整理总结。
一、问题的来源:
应用的新需求,要动态获取服务器给出的信息完成应用界面。其中就包括每个功能按钮的名字,连接,当然也包括图标的下载链接。所以,就涉及到图片的下载和本地缓存。
又了解到使用软引用可以更合理的利用内存资源,就涉及到了软引用的部分。
二、软引用介绍:
软引用(soft reference)在强度上弱于强引用,通过SoftReference类来表示。
它的作用是告诉垃圾回收器,程序中的哪些对象并不那么重要,当内存不足的时候是可以被暂时回收的。当JVM中的内存不足的时候,垃圾回收器会释放那些只被软引用所指向的对象。如果全部释放完这些对象之后,内存还不足,才会抛出Out Of Memory错误。
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。比如考虑一个图像编辑器的程序,该程序会把图像文件的全部内容都读取到内存中,以方便进行处理,而用户也可以同时打开多个文件。当同时打开的文件过多的时候,就可能造成内存不足。如果使用软引用来指向图像文件内容的话,垃圾回收器就可以在必要的时候回收掉这些内存。
那么,如何使用软引用?
SoftReference 的特点是它的一个实例保存着一个 Java 对象的软引用,该软引用的存在不妨碍垃圾收集器线程对该 Java 对象的回收。也就是说,一旦SoftReference 保存着一个 Java 对象的软引用之后,在垃圾收集器线程对这个 Java 对象回收之前, SoftReference 类所提供的 get() 方法都会返回这个Java 对象的强引用。另外,一旦垃圾线程回收该 Java 对象之后, get() 方法将返回 null 。
软引用的使用方法如下面的Java代码所示:
MyObject aRef = new MyObject();//创建一个对象
SoftReference aSoftRef = new SoftReference(aRef );//创建对象的软引用
上面的代码执行后,对于MyObject 对象,有两个引用路径,一个是来自 aSoftRef对象的软引用,一个来自变量 aRef 的强引用,所以 MyObject对象是强可及对象。紧跟着,可以使用下面的java的代码结束 aReference 对 MyObject 实例的强引用:
aRef = null ;//断开对象的强引用
此后, MyObject 对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个 SoftReference 对该对象的引用而始终保留该对象。 Java 虚拟机的垃圾收集线程对软可及对象和其他一般 Java 对象进行了区别对待,软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出 OutOfMemoryError 之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可及对象会被虚拟机尽可能保留。如果想获取软引用中包含的对象,可以使用下面的Java代码:
MyObject anotherRef =(MyObject) aSoftRef.get();//通过软引用获取对象
在回收这些对象之前,可以通过上面的代码重新获得对该实例的强引用。而回收之后,当调用软引用的get() 方法时,返回的是 null 。
三、功能实现:
1、ImageDownloaderTask.java文件
(用于图片的下载和本地缓存。在要下载图片的位置调用此类的构造函数传入实现了下面文件3接口的对象,用于回调。
注意此处的
private static final String WHOLESALE_CONV = "/data/data/<pakagename>/files/caches";其中的<pakagename>应该对应你的工程包名)
import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Environment; import android.os.StatFs; import android.util.Log; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Comparator; public class ImageDownloaderTask extends AsyncTask<String, Void, Bitmap> { private static String TAG = ImageDownloaderTask.class.getSimpleName(); private static final int IO_BUFFER_SIZE = 4 * 1024; private static final int MB = 1024 * 1024; private static final int CACHE_SIZE = 1024 * 1024; private static final int mTimeDiff = 5 * 24 * 60 * 60 * 1000; private static final int FREE_SD_SPACE_NEEDED_TO_CACHE = 30; private static final String WHOLESALE_CONV = "/data/data/<pakagename>/files/caches"; private String url; private final WeakReference<RemoteImageCallback> activityReference; public ImageDownloaderTask(RemoteImageCallback activity) { activityReference = new WeakReference<RemoteImageCallback>(activity); } @Override protected Bitmap doInBackground(String... params) { //先到本地文件中去查找,如果存在就取出并返回,如果不存在就从网上下载 url = params[0]; String filename = convertUrlToFileName(url); String dir = getDirectory(filename); File file = new File(dir + "/" + filename); if (file.exists()) { removeExpiredCache(dir, filename); updateFileTime(dir, filename); Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath()); if (bitmap != null) return bitmap; } final DefaultHttpClient client = new DefaultHttpClient(); final HttpGet getRequest = new HttpGet(url); try { HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { Log.w(TAG, "从" + url + "中下载图片时出错!,错误码:" + statusCode); return null; } final HttpEntity entity = response.getEntity(); if (entity != null) { InputStream inputStream = null; OutputStream outputStream = null; try { inputStream = entity.getContent(); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); copy(inputStream, outputStream); outputStream.flush(); final byte[] data = dataStream.toByteArray(); final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); saveBmpToSd(bitmap, url); return bitmap; } finally { closeStream(inputStream); closeStream(outputStream); entity.consumeContent(); } } } catch (IOException e) { getRequest.abort(); Log.w(TAG, "I/O error while retrieving bitmap from " + url, e); } catch (IllegalStateException e) { getRequest.abort(); Log.w(TAG, "Incorrect URL:" + url); } catch (Exception e) { getRequest.abort(); Log.w(TAG, "Error while retrieving bitmap from " + url, e); } return null; } @Override protected void onPostExecute(Bitmap result) { super.onPostExecute(result); RemoteImageCallback act = activityReference.get(); if (act != null && result != null) { act.onComplete(url, result); } } /** * Copy the content of the input stream into the output stream, using a temporary * byte array buffer whose size is defined by {@link #IO_BUFFER_SIZE}. * * @param in The input stream to copy from. * @param out The output stream to copy to. * * @throws java.io.IOException If any error occurs during the copy. */ public static void copy(InputStream in, OutputStream out) throws IOException { byte[] b = new byte[IO_BUFFER_SIZE]; int read; while ((read = in.read(b)) != -1) { out.write(b, 0, read); } } /** * Closes the specified stream. * * @param stream The stream to close. */ public static void closeStream(Closeable stream) { if (stream != null) { try { stream.close(); } catch (IOException e) { android.util.Log.e(TAG, "Could not close stream", e); } } } private void saveBmpToSd(Bitmap bm, String url) { //这里存取文件要注意,file.createNewFile();只能创建对应文件名的文件,如果存放的目录不存在,就会抛出异常。所以要先创建目录 Log.w(TAG, " start to save____________________________________________________"); Log.w(TAG, " start to save____________________________________________________"); Log.w(TAG, " start to save____________________________________________________"); if (bm == null) { Log.w(TAG, " trying to savenull bitmap"); return; } // 判断sdcard上的空间 if (FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { Log.w(TAG, "Low free space onsd, do not cache"); removeCache(WHOLESALE_CONV); return; } String filename = convertUrlToFileName(url); String dir = getDirectory(filename); File cacheDir=new File(dir); Log.d("cacheDir", "_cacheDir_________________————————————————————————————————————————————————————" + cacheDir); Log.d("cacheDir", "_cacheDir_________________————————————————————————————————————————————————————" + cacheDir); if (!cacheDir.exists()) { Log.d("cacheDir", "_cacheDir_________________————————————————————————————————————————————————————not exist"); Log.d("cacheDir", "_cacheDir_________________————————————————————————————————————————————————————not exist"); Log.d("cacheDir", "_cacheDir_________________————————————————————————————————————————————————————not exist"); cacheDir.mkdirs();} File file = new File(dir + "/" + filename); try { file.createNewFile(); OutputStream outStream = new FileOutputStream(file); bm.compress(Bitmap.CompressFormat.JPEG, 100, outStream); outStream.flush(); outStream.close(); Log.i(TAG, "Image saved tosd"); } catch (FileNotFoundException e) { Log.w(TAG, "FileNotFoundException"); } catch (IOException e) { Log.w(TAG, "IOException"); } } private String convertUrlToFileName(String url) { int lastIndex = url.lastIndexOf('/'); return url.substring(lastIndex + 1); } private String getDirectory(String filename) { return WHOLESALE_CONV; } /** * 计算sdcard上的剩余空间 * * @return */ private int freeSpaceOnSd() { StatFs stat = new StatFs(Environment.getExternalStorageDirectory() .getPath()); double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat .getBlockSize()) / MB; return (int) sdFreeMB; } /** * 修改文件的最后修改时间 * * @param dir * @param fileName */ private void updateFileTime(String dir, String fileName) { File file = new File(dir, fileName); long newModifiedTime = System.currentTimeMillis(); file.setLastModified(newModifiedTime); } /** *计算存储目录下的文件大小, * 当文件总大小大于规定的CACHE_SIZE或者sdcard剩余空间小于FREE_SD_SPACE_NEEDED_TO_CACHE的规定 * 那么删除40%最近没有被使用的文件 * * //@param dirPath * //@param filename */ private void removeCache(String dirPath) { File dir = new File(dirPath); File[] files = dir.listFiles(); if (files == null) { return; } int dirSize = 0; for (int i = 0; i < files.length; i++) { if (files[i].getName().contains(WHOLESALE_CONV)) { dirSize += files[i].length(); } } if (dirSize > CACHE_SIZE * MB || FREE_SD_SPACE_NEEDED_TO_CACHE > freeSpaceOnSd()) { int removeFactor = (int) ((0.4 * files.length) + 1); Arrays.sort(files, new FileLastModifSort()); Log.i(TAG, "Clear some expiredcache files "); for (int i = 0; i < removeFactor; i++) { if (files[i].getName().contains(WHOLESALE_CONV)) { files[i].delete(); } } } } /** * TODO 根据文件的最后修改时间进行排序 * */ class FileLastModifSort implements Comparator<File> { public int compare(File arg0, File arg1) { if (arg0.lastModified() > arg1.lastModified()) { return 1; } else if (arg0.lastModified() == arg1.lastModified()) { return 0; } else { return -1; } } } /** * 删除过期文件 * * @param dirPath * @param filename */ private void removeExpiredCache(String dirPath, String filename) { File file = new File(dirPath, filename); if (System.currentTimeMillis() - file.lastModified() > mTimeDiff) { Log.i(TAG, "Clear some expiredcache files "); file.delete(); } } }
2、
Get_ICON.java文件。
(此文件实现图片的获取和软引用。在要获取图片的位置调用:getBitmapFromCache(String url)并传入图片对应的rul即可。
注意,这个文件获得的图片是已经在内存中的图片)
import android.graphics.Bitmap; import android.util.Log; import java.lang.ref.SoftReference; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.concurrent.ConcurrentHashMap; public class Get_ICON { private static final String TAG = AsyncListImage.class.getSimpleName(); private static final int HARD_CACHE_CAPACITY = 10; private final HashMap<String, Bitmap> mHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) { //可以重写removeEldestEntry方法返回true值指定插入元素时移除最老的元素。 @Override protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) { if (size() > HARD_CACHE_CAPACITY) { //当map的size大于10时,把最近不常用的key放到mSoftBitmapCache中,从而保证mHardBitmapCache的效率 mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue())); return true; } else return false; } }; /** *当mHardBitmapCache的key大于10的时候,会根据LRU算法把最近没有被使用的key放入到这个缓存中。 * Bitmap使用了SoftReference,当内存空间不足时,此cache中的bitmap会被垃圾回收掉 */ private final static ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>( HARD_CACHE_CAPACITY / 2); /** * 从缓存中获取图片 */ public Bitmap getBitmapFromCache(String url) { // 先从mHardBitmapCache缓存中获取 synchronized (mHardBitmapCache) { //这里对同步代码块加锁,是因为mHardBitmapCache是LinkedHashmap,会在容量达到一定额时删除元素 final Bitmap bitmap = mHardBitmapCache.get(url); if (bitmap != null) { //如果找到的话,把元素移到linkedhashmap的最前面,从而保证在LRU算法中是最后被删除 mHardBitmapCache.remove(url); Log.d(TAG, "move bitmap to the head of linkedhashmap:" + url); mHardBitmapCache.put(url,bitmap); return bitmap; } } //如果mHardBitmapCache中找不到,到mSoftBitmapCache中找 SoftReference<Bitmap> bitmapReference = mSoftBitmapCache.get(url); if (bitmapReference != null) { final Bitmap bitmap = bitmapReference.get(); if (bitmap != null) { Log.d(TAG, "get bitmap from mSoftBitmapCache with key:" + url); return bitmap; } else { mSoftBitmapCache.remove(url); Log.d(TAG, "remove bitmap with key:" + url); } } return null; } public void put_HardBitmapCache(String url,Bitmap bitmap){ mHardBitmapCache.put(url, bitmap); } }
3、
RemoteImageCallback.java文件
(这是一个接口文件。记得在要下载图片的java类中实现这个接口,并实现其中的onComplete函数。此函数在文件1中被调用,所以,它其实是一个回调函数。用于当图片下载成功后刷新对应位置显示新图片,并将图片保存到内存中,以便之后的快速访问)
import android.graphics.Bitmap; public interface RemoteImageCallback { void onComplete(String url, Bitmap bitmap); }
四、使用上面实现的例子:
(其中的initCacheDir()函数也比较重要。用于创建文件夹。最后在下载图片之前调用。)
import android.app.Activity; import android.graphics.Bitmap; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.ListView; import com.asiaweiluy.live.v1.R; import java.io.File; @SuppressWarnings("serial") public class AsyncListImage extends Activity implements RemoteImageCallback { private ListView list; private static final String TAG = AsyncListImage.class.getSimpleName(); private Get_ICON mGet_icon=new Get_ICON(); @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_async_list_image); list = (ListView) findViewById(R.id.list); initCacheDir(); MyListAdapter adapter = new MyListAdapter(); list.setAdapter(adapter); } private class MyListAdapter extends BaseAdapter { private String[] urls = new String[] { "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Add%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Adobe%20Illustator%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Attach%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Applications%20Cascade%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Administrator%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Clients%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Coinstack%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Download%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Help%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Home%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Pen%20Icon.jpg", "http://www.icosky.com/icon/thumbnails/System/Sleek%20XP%20Basic/Statistics%20Icon.jpg" }; @Override public int getCount() { return urls.length; } @Override public String getItem(int position) { return urls[position]; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = new ImageView(AsyncListImage.this); } ImageView iv = (ImageView)convertView; iv.setScaleType(ScaleType.FIT_CENTER); Bitmap bitmap = mGet_icon.getBitmapFromCache(getItem(position)); if (bitmap == null) { //如果缓存中不存在当前图片,就使用默认图片填充。并开启线程下载图片 /* iv.setImageResource(R.drawable.ic_launcher); */ iv.setTag(getItem(position)); new ImageDownloaderTask(AsyncListImage.this).execute(new String[]{getItem(position)}); } else { iv.setImageBitmap(bitmap); } iv = null; return convertView; } } @Override public void onComplete(String url, Bitmap bitmap) { //此函数用于在ImageDownloaderTask中进行回调,以保证图片下载完成后进行更新,并存在mHardBitmapCache中 Log.d(TAG, "onComplete after got bitmap from remote with key:" + url); ImageView iv = (ImageView)list.findViewWithTag(url); if (iv != null) { iv.setImageBitmap(bitmap); mGet_icon.put_HardBitmapCache(url, bitmap); /* mHardBitmapCache.put(url, bitmap); */ } } private void initCacheDir() { String cacheDir = "/data/data/info.cmptech.icon_download_test/files/caches"; File f = new File(cacheDir); if (!f.exists()) { f.mkdirs(); } } }