【Android - 进阶】之图片三级缓存的原理及实现

  在Android开发中,如果图片过多,而我们又没有对图片进行有效的缓存,就很容易导致OOM(Out Of Memory)错误。因此,图片的缓存是非常重要的,尤其是对图片非常多的应用。现在很多框架都做了很好的图片缓存处理,如【Fresco】【Glide】等。

  本帖主要介绍以下Android中图片的三级缓存机制的原理及其应用。本帖中的代码都是使用Android原生的代码编写的。

1、原理

  Android图片三级缓存的原理如下图所示:

  可见,Android中图片的三级缓存主要是强引用、软银用和文件系统。

  Android原生为我们提供了一个LruCache,其中维护着一个LinkedHashMap。LruCache可以用来存储各种类型的数据,但最常见的是存储图片(Bitmap)。LruCache创建LruCache时,我们需要设置它的大小,一般是系统最大存储空间的八分之一。LruCache的机制是存储最近、最后使用的图片,如果LruCache中的图片大小超过了其默认大小,则会将最老、最远使用的图片移除出去。

  当图片被LruCache移除的时候,我们需要手动将这张图片添加到软引用(SoftReference)中。我们需要在项目中维护一个由SoftReference组成的集合,其中存储被LruCache移除出来的图片。软引用的一个好处是当系统空间紧张的时候,软引用可以随时销毁,因此软引用是不会影响系统运行的,换句话说,如果系统因为某个原因OOM了,那么这个原因肯定不是软引用引起的。

  下面叙述一下三级缓存的流程:

  当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,如果LruCache中没有,则去SoftReference中寻找,如果SoftReference中有,则从SoftReference中取出图片使用,同时将图片重新放回到LruCache中,如果SoftReference中也没有图片,则去文件系统中寻找,如果有则取出来使用,同时将图片添加到LruCache中,如果没有,则连接网络从网上下载图片。图片下载完成后,将图片保存到文件系统中,然后放到LruCache中。

 

2、实现

(1)网络访问工具类HttpUtil:
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 访问Http的工具类
 */
public class HttpUtil {
    private static HttpUtil instance;

    private HttpUtil() {
    }

    public static HttpUtil getInstance() {
        if (instance == null) {
            synchronized (HttpUtil.class) {
                if (instance == null) {
                    instance = new HttpUtil();
                }
            }
        }
        return instance;
    }

    /**
     * 通过path(URL)访问网络获取返回的字节数组
     */
    public byte[] getByteArrayFromWeb(String path) {
        byte[] b = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            URL url = new URL(path);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setDoInput(true);
            connection.setConnectTimeout(5000);
            connection.connect();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                baos = new ByteArrayOutputStream();
                is = connection.getInputStream();
                byte[] tmp = new byte[1024];
                int length = 0;
                while ((length = is.read(tmp)) != -1) {
                    baos.write(tmp, 0, length);
                }
            }
            b = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
                if (baos != null) {
                    baos.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return b;
    }
}
(2)操作文件系统的工具类FileUtil:
import android.content.Context;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * 操作内存文件的工具类
 */
public class FileUtil {
    private static FileUtil instance;

    private Context context;

    private FileUtil(Context context) {
        this.context = context;
    }

    public static FileUtil getInstance(Context context) {
        if (instance == null) {
            synchronized (FileUtil.class) {
                if (instance == null) {
                    instance = new FileUtil(context);
                }
            }
        }
        return instance;
    }

    /**
     * 将文件存储到内存中
     */
    public void writeFileToStorage(String fileName, byte[] b) {
        FileOutputStream fos = null;
        try {
            File file = new File(context.getFilesDir(), fileName);
            fos = new FileOutputStream(file);
            fos.write(b, 0, b.length);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从内存中读取文件的字节码
     */
    public byte[] readBytesFromStorage(String fileName) {
        byte[] b = null;
        FileInputStream fis = null;
        ByteArrayOutputStream baos = null;
        try {
            fis = context.openFileInput(fileName);
            baos = new ByteArrayOutputStream();
            byte[] tmp = new byte[1024];
            int len = 0;
            while ((len = fis.read(tmp)) != -1) {
                baos.write(tmp, 0, len);
            }
            b = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (baos != null) {
                    baos.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return b;
    }
}
(3)LruCache的子类ImageCache:
import android.graphics.Bitmap;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.LruCache;

import java.lang.ref.SoftReference;
import java.util.Map;

/**
 * 图片缓存
 */
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1)
public class ImageCache extends LruCache<String, Bitmap> {
    private Map<String, SoftReference<Bitmap>> cacheMap;

    public ImageCache(Map<String, SoftReference<Bitmap>> cacheMap) {
        super((int) (Runtime.getRuntime().maxMemory() / 8));
        this.cacheMap = cacheMap;
    }

    @Override // 获取图片大小
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override // 当有图片从LruCache中移除时,将其放进软引用集合中
    protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
        if (oldValue != null) {
            SoftReference<Bitmap> softReference = new SoftReference<Bitmap>(oldValue);
            cacheMap.put(key, softReference);
        }
    }

    public Map<String, SoftReference<Bitmap>> getCacheMap() {
        return cacheMap;
    }
}
(4)三级缓存的工具类CacheUtil:
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.widget.ImageView;

import java.io.File;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

/**
 * 缓存工具类
 */
public class CacheUtil {
    private static CacheUtil instance;

    private Context context;
    private ImageCache imageCache;

    private CacheUtil(Context context) {
        this.context = context;
        Map<String, SoftReference<Bitmap>> cacheMap = new HashMap<>();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { // SDK版本判断
            this.imageCache = new ImageCache(cacheMap);
        }
    }

    public static CacheUtil getInstance(Context context) {
        if (instance == null) {
            synchronized (CacheUtil.class) {
                if (instance == null) {
                    instance = new CacheUtil(context);
                }
            }
        }
        return instance;
    }

    /**
     * 将图片添加到缓存中
     */
    private void putBitmapIntoCache(String fileName, byte[] data) {
        // 将图片的字节数组写入到内存中
        FileUtil.getInstance(context).writeFileToStorage(fileName, data);
        // 将图片存入强引用(LruCache)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            imageCache.put(fileName, BitmapFactory.decodeByteArray(data, 0, data.length));
        }
    }

    /**
     * 从缓存中取出图片
     */
    private Bitmap getBitmapFromCache(String fileName) {
        // 从强引用(LruCache)中取出图片
        Bitmap bm = null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { // SDK版本判断
            bm = imageCache.get(fileName);
            if (bm == null) {
                // 如果图片不存在强引用中,则去软引用(SoftReference)中查找
                Map<String, SoftReference<Bitmap>> cacheMap = imageCache.getCacheMap();
                SoftReference<Bitmap> softReference = cacheMap.get(fileName);
                if (softReference != null) {
                    bm = softReference.get();
                    imageCache.put(fileName, bm);
                } else {
                    // 如果图片不存在软引用中,则去内存中找
                    byte[] data = FileUtil.getInstance(context).readBytesFromStorage(fileName);
                    if (data != null && data.length > 0) {
                        bm = BitmapFactory.decodeByteArray(data, 0, data.length);
                        imageCache.put(fileName, bm);
                    }
                }
            }
        }
        return bm;
    }

    /**
     * 使用三级缓存为ImageView设置图片
     */
    public void setImageToView(final String path, final ImageView view) {
        final String fileName = path.substring(path.lastIndexOf(File.separator) + 1);
        Bitmap bm = getBitmapFromCache(fileName);
        if (bm != null) {
            view.setImageBitmap(bm);
        } else {
            // 从网络获取图片
            new Thread(new Runnable() {
                @Override
                public void run() {
                    byte[] b = HttpUtil.getInstance().getByteArrayFromWeb(path);
                    if (b != null && b.length > 0) {
                        // 将图片字节数组写入到缓存中
                        putBitmapIntoCache(fileName, b);
                        final Bitmap bm = BitmapFactory.decodeByteArray(b, 0, b.length);
                        // 将从网络获取到的图片设置给ImageView
                        view.post(new Runnable() {
                            @Override
                            public void run() {
                                view.setImageBitmap(bm);
                            }
                        });
                    }
                }
            }).start();
        }
    }
}
 

3、调用

(1)MainActivity的布局文件activity_main.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/id_main_lv_lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:divider="#DDDDDD"
        android:dividerHeight="1.0dip" />

</RelativeLayout>
(2)MainActivity中的代码:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import com.example.itgungnir.testimagecache.R;
import com.example.itgungnir.testimagecache.SharedData;
import com.example.itgungnir.testimagecache.adapter.ImageAdapter;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private ListView lv;

    private List<String> urlList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = (ListView) findViewById(R.id.id_main_lv_lv);
        initData();
    }

    // 初始化数据
    private void initData() {
        // 初始化图片URL列表
        urlList = Arrays.asList(SharedData.IMAGE_URLS);
    }

    @Override
    protected void onResume() {
        super.onResume();
        initView();
    }

    // 初始化视图
    private void initView() {
        // 为ListView适配数据
        ImageAdapter adapter = new ImageAdapter(MainActivity.this, urlList);
        lv.setAdapter(adapter);
    }
}
(3)ListView的适配器类ImageAdapter中的代码:
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;

import com.example.itgungnir.testimagecache.R;
import com.example.itgungnir.testimagecache.SharedData;
import com.example.itgungnir.testimagecache.adapter.ImageAdapter;

import java.util.Arrays;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private ListView lv;

    private List<String> urlList;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = (ListView) findViewById(R.id.id_main_lv_lv);
        initData();
    }

    // 初始化数据
    private void initData() {
        // 初始化图片URL列表
        urlList = Arrays.asList(SharedData.IMAGE_URLS);
    }

    @Override
    protected void onResume() {
        super.onResume();
        initView();
    }

    // 初始化视图
    private void initView() {
        // 为ListView适配数据
        ImageAdapter adapter = new ImageAdapter(MainActivity.this, urlList);
        lv.setAdapter(adapter);
    }
}
(4)ListView的Item的布局文件listitem_image.xml中的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="10.0dip">

    <ImageView
        android:id="@+id/id_imageitem_image"
        android:layout_width="100.0dip"
        android:layout_height="100.0dip"
        android:layout_gravity="center_horizontal"
        android:contentDescription="@string/app_name"
        android:scaleType="fitXY" />

</LinearLayout>
最终运行结果如下图所示:

posted on 2016-12-22 14:03  ITGungnir  阅读(15022)  评论(3编辑  收藏  举报

导航