Android 图片加载及缓存技术解析

写的一个图片缓存的demo,包括内存缓存和硬盘缓存,加载大量图片的时候感觉效果还是挺好的。

直接上代码吧:

package com.hongri.recyclerview.fragment;
 
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
import com.hongri.recyclerview.R;
import com.hongri.recyclerview.adapter.DetailLoadPicsTestAdapter;
import com.hongri.recyclerview.common.APPConstants;
import com.hongri.recyclerview.utils.DataUtil;
 
/**
 * @author:zhongyao on 2016/7/22 10:52
 * @description:网络加载图片性能测试
 */
public class DetailLoadPicsTestFragment extends Fragment{
    private static DetailLoadPicsTestFragment loadPicsTestFragment;
    private RecyclerView rv;
    private DetailLoadPicsTestAdapter mAdapter;
    private DetailLoadPicsTestFragment(){}
 
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_detail_loadpicstest,container,false);
        initView(view);
 
        return view;
    }
 
    private void initView(View view) {
        rv = (RecyclerView) view.findViewById(R.id.rv);
        rv.setLayoutManager(new GridLayoutManager(getActivity(), APPConstants.Column_Nums));
        mAdapter = new DetailLoadPicsTestAdapter(getActivity(), DataUtil.getImageThumbUrls());
        rv.setAdapter(mAdapter);
    }
 
 
    public static Fragment newInstance() {
        if (loadPicsTestFragment == null){
            synchronized (DetailLoadPicsTestFragment.class){
                if (loadPicsTestFragment == null){
                    loadPicsTestFragment = new DetailLoadPicsTestFragment();
                }
            }
        }
        return loadPicsTestFragment;
    }
}
package com.hongri.recyclerview.adapter;
 
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
 
import com.hongri.recyclerview.R;
import com.hongri.recyclerview.cache.BitmapCache;
import com.hongri.recyclerview.cache.BitmapDiskCache;
import com.hongri.recyclerview.cache.ImageWorker;
import com.hongri.recyclerview.https.HttpRequest;
import com.hongri.recyclerview.https.IHttpRequest;
import com.hongri.recyclerview.utils.Logger;
 
import java.util.ArrayList;
 
/**
 * @author:zhongyao on 2016/7/22 11:09
 * @description:
 */
public class DetailLoadPicsTestAdapter extends RecyclerView.Adapter<DetailLoadPicsTestAdapter.LoadPicsViewHolder> {
    private Context context;
    private ArrayList<String> imageUrls;
    private LayoutInflater inflater;
    private Handler mHandler = new Handler();
 
    public DetailLoadPicsTestAdapter(Context context, ArrayList<String> imageUrls) {
        this.context = context;
        this.imageUrls = imageUrls;
        this.inflater = LayoutInflater.from(context);
    }
 
    @Override
    public LoadPicsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.fragment_detail_loadpicstest_item, parent, false);
        return new LoadPicsViewHolder(view);
    }
 
    @Override
    public void onBindViewHolder(final LoadPicsViewHolder holder, final int position) {
        /**
         * 使用自己写的内存缓存+SD卡缓存(貌似效果甚至比使用DiskLruCache的效果好,这就比较蛋疼了,没看出DiskLruCache的优点)
         */
        ImageWorker.loadImage(holder.iv,imageUrls.get(position) ,mHandler);
    }
 
    @Override
    public int getItemCount() {
        return imageUrls.size();
    }
 
    public class LoadPicsViewHolder extends ViewHolder {
        private ImageView iv;
 
        public LoadPicsViewHolder(View itemView) {
            super(itemView);
            iv = (ImageView) itemView.findViewById(R.id.iv);
        }
    }

}

以上就是使用RecyclerView实现的GridView,真正加载缓存图片的是:ImageWorker.loadImage(holder.iv,imageUrls.get(position),mHandler);这行代码。

下面我们着重介绍ImageWorker等类,内存缓存用的是LruCache类,sd卡缓存是自己写的,测试效果貌似不比用DiskLruCache差。(最后我会给出demo,demo中也用到了DiskLruCache做对比,有兴趣的可以自己测试下性能,对比下,欢迎交流)。

package com.hongri.recyclerview.cache;
 
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.os.Handler;
import android.support.v4.util.LruCache;
import android.widget.ImageView;
 
import com.hongri.recyclerview.https.DownloadImageFromNetwork;
 
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * 图片缓存类ImageWorker:
 * 与ImageTask类相似
 */
public class ImageWorker {
	private static final int lruSizeMaxSize = 10*1024*1024;// 图片内存缓存最大空间10M
	public final static LruCache<String,Bitmap> mLruCache = new LruCache<String,Bitmap>(lruSizeMaxSize);
	private static ExecutorService mExecutorService = null;// 下载图片的线程管理类
	private static final int threadPoolMaxSize = 6;// 同时下载图片的 最大线程数
	private ImageWorker() {
	}
	private static ImageWorker mImageWorker = null;
	private static String bitmapPath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/zhong/";// 图片在硬盘中的存储地址
	/**
	 * 初始化,使用单例获得ImageWorker1的对象
	 */
	public static synchronized ImageWorker getImageWorkerInstance(){
		if(mImageWorker == null){
			mImageWorker = new ImageWorker();
			mExecutorService = Executors.newFixedThreadPool(threadPoolMaxSize);
		}
		return mImageWorker;
	}
 
	/**
	 * (简单来说)加载图片:
	 *  1 先从内存缓存中查找,如果没有则执行下一步。
	 *  2 从磁盘中查找,如果没有执行下一步。
	 *  3从网络下载
	 */
	/**
	 * 1、首先判断内存缓存是否为空(不为空直接显示)
	 * 2、内存缓存为空时,再从磁盘中读取后,将图片添加到内存缓存中(然后显示)
	 * 3、磁盘为空时,只能重新网络加载图片(下载下来的图片,会在内存缓存、磁盘中各存储一份,然后显示)
	 * @param mImageView
	 * @param imgUrl
	 * @param mHandler
	 */
	public static void loadImage(ImageView mImageView, String imgUrl,
								 Handler mHandler) {
		getImageWorkerInstance();
		Bitmap bitmapCache = getBitmapFromCache(imgUrl);
		if(bitmapCache!=null){
			mImageView.setImageBitmap(bitmapCache);
		}else {
			Bitmap bitmapDisk = getBitmapFromDisk(imgUrl);
			if(bitmapDisk!=null){
				addBitmapToLruCache(imgUrl,bitmapDisk);
				mImageView.setImageBitmap(bitmapDisk);
			}else{
				loadImageFromNetwork(mImageView,imgUrl,mHandler,mLruCache,bitmapPath);
			}
		}
	}
 
 
	private static void loadImageFromNetwork(ImageView mImageView, String imgUrl, Handler mHandler, LruCache<String, Bitmap> mLruCache, String bitmapPath) {
		mExecutorService.submit(new DownloadImageFromNetwork(mImageView,imgUrl,mHandler,mLruCache,bitmapPath));
	}
 
 
 
	private static void addBitmapToLruCache(String imgUrl, Bitmap bitmapDisk) {
		mLruCache.put(imgUrl,bitmapDisk);
	}
 
 
 
	private static Bitmap getBitmapFromDisk(String imgUrl) {
		InputStream inputStream = null;
		String[] fileNames = imgUrl.split("/");
		String fileName = fileNames[fileNames.length-1];
		try {
			inputStream = new FileInputStream(bitmapPath+fileName);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
		return bitmap;
	}
 
 
 
	/**
	 * 第一步:尝试从缓存中获取图片
	 * @param imgUrl
	 * @return
	 */
	private static Bitmap getBitmapFromCache(String imgUrl) {
		Bitmap bitmap = mLruCache.get(imgUrl);
		return bitmap;
	}
}

说的很清晰了,还是那一套:先内存找,没有则sd,sd没有,则网络加载。

这里用到了ExecutorService线程管理类,可以设置启动最大的线程数,线程对象可重复利用,避免过多的重复创建线程对象,浪费内存空间。

package com.hongri.recyclerview.https;
 
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.support.v4.util.LruCache;
import android.widget.ImageView;
 
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
 
/**
 * @author:zhongyao on 2016/7/11 10:39
 * @description:从网络下载图片,并将下载好的图片存储到内存缓存、SD卡缓存中
 */
public class DownloadImageFromNetwork implements Runnable {
    private ImageView mImageView;
    private String imgUrl;
    private Handler mHandler;
    private LruCache<String, Bitmap> mLruCache;
    private String bitmapPath;
 
    public DownloadImageFromNetwork(ImageView mImageView, String imgUrl,
                                    Handler mHandler, LruCache<String, Bitmap> mLruCache, String bitmapPath) {
        this.mImageView = mImageView;
        this.imgUrl = imgUrl;
        this.mHandler = mHandler;
        this.mLruCache = mLruCache;
        this.bitmapPath = bitmapPath;
    }
 
    @Override
    public void run() {
        try {
            InputStream inputStream;
            URL url = new URL(imgUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setReadTimeout(10 * 1000);
            conn.setConnectTimeout(10 * 1000);
            conn.setDoInput(true);
            conn.connect();// Starts the query
            int responseCode = conn.getResponseCode();
            if (responseCode == 200) {
                inputStream = conn.getInputStream();
                if (inputStream != null) {
                    final Bitmap bitmapNetwork = BitmapFactory.decodeStream(inputStream);
                    mHandler.post(new Runnable() {
 
                        @Override
                        public void run() {
                            mImageView.setImageBitmap(bitmapNetwork);
                        }
                    });
                    /**
                     * 此时输入流inputStream不存在了(输入流使用一次后就没有了),
                     * 所以以下两行传送的代码是Bitmap对象
                     */
                    addBitmapToCache(imgUrl, bitmapNetwork);
                    addBitmapToDisk(imgUrl, bitmapNetwork);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    private void addBitmapToDisk(String imgUrl, Bitmap bitmapNetwork) {
        OutputStream outputStream = null;
        BufferedOutputStream bos = null;
        InputStream inputStream = null;
        File file = new File(bitmapPath);
        if (!file.exists()) {
            boolean result = file.mkdirs();
        }
        try {
            String[] fileNames = imgUrl.split("/");
            String fileName = fileNames[fileNames.length - 1];
            outputStream = new FileOutputStream(bitmapPath + fileName);
            bos = new BufferedOutputStream(outputStream);
            /**
             * 以下三行的代码表示:
             * 将Bitmap对象转换为inputStream
             */
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            bitmapNetwork.compress(Bitmap.CompressFormat.JPEG, 100, baos);
            inputStream = new ByteArrayInputStream(baos.toByteArray());
 
            byte[] data = new byte[1024];
            while (inputStream.read(data) != -1) {
                bos.write(data);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                bos.close();
                outputStream.close();
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
 
    private void addBitmapToCache(String imgUrl, Bitmap bitmapNetwork) {
        mLruCache.put(imgUrl, bitmapNetwork);
    }
}

该类需要注意的是,下载完图片并展示的时候,需要将其加载到内存和sd卡缓存中。以后再次加载的时候直接get就可以了。

效果图:

v2-9bbfce4e9994643e68201b5d0c22a695_720w
Android 图片加载及缓存技术解析
滚动到顶部