第十二章-实现一个ImageLoader

一、ImageLoader的实现功能介绍
前面了解了Bitmap的高效加载方式、LruCache以及DiskLruCache,现在我们来实现一个优秀的ImageLoader。

一个优秀的ImageLoader应该具备如下功能:

  • 图片的同步加载
  • 图片的异步加载
  • 图片压缩
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

图片的同步加载是指能够以同步的方式向调用者所提供加载的图片,这个图片可能是从内存缓存中读取的,也可以是从磁盘缓存中读取的,还可以是从网络拉取的。

图片的异步加载是一个很有用的功能,很多时候调用者不想在单独的线程中以同步的方式来获取图片,这个时候ImageLoader内部需要自己在线程中加载图片并将图片设置给需要的ImageView。图片压缩的作用毋庸置疑了,这是降低OOM概率的有效手段,ImageLoader必须合适地处理图片的压缩问题。

内存缓存和磁盘缓存是ImageLoader的核心,也是ImageLoader的意义所在,通过这两级缓存极大的提高了程序的效率并且降低了用户所造成的流量消耗,只是当这二级缓存都不可用时才需要从网络拉取图片。

除此之外,ImageLoader还需要处理一些特殊的情况,比如ListView或者GridView中,View复用即是它们的优点也是它们的缺点,优点想必读者很清楚了,那缺点可能还不太清楚。考虑一种情况,在ListView或者GridView中,假设一个item A正在从网络加载图片,它对应的ImageView A,这个时候用户快速向下滑动列表,很有可能item B复用了ImageView A,然后等了一会之前的图片下载完毕了。如果直接给ImageView A设置图片,由于这个时候ImageViewA被itemB所复用,但是item B要显示的图片显然不是item A刚刚下载好的图片,这个时候就会出现item B中显示了item A的图片,这就是常见的列表错位问题,ImageLoader需要正确地处理这些特殊情况。

二、图片压缩功能的实现
图片压缩在上一章节已经分析过了,为了有良好的设计风格,这里单独抽象了一个类用于完成图片的压缩功能,这个类叫ImageResizer,实现如下。

package com.example.bitmap.imagerLoader;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import java.io.FileDescriptor;

/**
 * 图片压缩工具类
 */
public class ImageResizer {
	private static final String TAG = "ImageResizer";

	public ImageResizer() {
	}

	/**
	 * 从资源文件中加载压缩后的Bitmap图片
	 * @param res eg:getResources()
	 * @param resId eg:R.mipmap.ic_launcher
	 * @param reqWidth 目标宽度
	 * @param reqHeight 目标高度
	 * @return
	 */
	public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
		BitmapFactory.Options options = new BitmapFactory.Options();

		//设置只请求大小不加载标记
		options.inJustDecodeBounds = true;

		//获取到图片的宽高
		BitmapFactory.decodeResource(res, resId, options);

		//计算采样率
		options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

		//重置标记,加载图片
		options.inJustDecodeBounds = false;
		return BitmapFactory.decodeResource(res, resId, options);
	}

	/** 通过FileDescriptor加载Bitmap图片 */
	public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
		BitmapFactory.Options options = new BitmapFactory.Options();

		//设置只请求大小不加载标记
		options.inJustDecodeBounds = true;

		//获取到图片的宽高
		BitmapFactory.decodeFileDescriptor(fd,null,options);

		//计算采样率
		options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

		//重置标记,加载图片
		options.inJustDecodeBounds = false;
		return BitmapFactory.decodeFileDescriptor(fd,null,options);
	}

	/**计算采样率,逻辑就是采样后的宽和高都大于请求的宽高,采样率*2。直到满足条件返回 */
	public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
		int width = options.outWidth;
		int height = options.outHeight;
		int inSampleSize = 1;

		if (height > reqHeight || width > reqWidth) {
			int halfHeight = height / 2;
			int halfWidth = width / 2;
			while (halfHeight / inSampleSize >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
				inSampleSize *= 2;
			}
		}
		Log.d(TAG,"采样率 = " + inSampleSize);
		return inSampleSize;
	}

}

三、内存缓存和磁盘缓存的实现
这里选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作。在ImageLoader初始化时,会创建LruCache和DiskLruCache,如下所示。

private LruCache<String, Bitmap> mMemoryLruCache;
private DiskLruCache mDiskLruCache;
private Context mContext;
private long DISK_CACHE_SIZE = 1024 * 1024 *50;
private boolean mIsDiskLruCacheCreate;

public ImageLoader(Context context) {
	Log.d(TAG,"ImageLoader");
	mContext = context.getApplicationContext();
	int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//KB
	int cacheSize = maxMemory / 8;
	Log.d(TAG,"memory cacheSize = " + cacheSize);

	mMemoryLruCache = new LruCache<String, Bitmap>(cacheSize) {
		@Override
		protected int sizeOf(String key, Bitmap value) {
			return value.getRowBytes() * value.getHeight() / 1024;//KB
		}
	};

	File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
	if(!diskCacheDir.exists()){
		diskCacheDir.mkdirs();
	}

	try {
		mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
		mIsDiskLruCacheCreate = true;
	} catch (IOException e) {
		e.printStackTrace();
	}

}

private File getDiskCacheDir(Context context,String folderName){
	String cacheDirPath = context.getCacheDir().getAbsolutePath();
	File diskCacheDir = new File(cacheDirPath + File.separator + "bitmap");
	//File diskCacheDir = new File("/sdcard" + File.separator + "bitmap");//sdcard路径
	return diskCacheDir;
}

在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需要大小,一般是指用户的手机空间已经不足了,因为没有办法创建磁盘缓存,这个时候磁盘缓存就会失效,在上面的代码中,ImageLoader的内存缓存的容量为当前进程可用内存的1/8,磁盘缓存的容量为50MB

内存缓存和磁盘缓存创建完毕后,还需要提供方法来完成缓存的添加和获取功能。先看内存缓存,它的添加过程比较简单,如下所示。

    /** 内存缓存的添加 */
    private void addBitmapToMemoryCache(String key,Bitmap bitmap){
        if(getBitmapFromMemoryCache(key) == null){
            mMemoryLruCache.put(key,bitmap);
        }
    }

    /** 内存缓存的获取 */
    private Bitmap getBitmapFromMemoryCache(String key){
        return mMemoryLruCache.get(key);
    }

而磁盘缓存的添加和读取功能稍微复杂一些,上一章节介绍过了,这里再简单说明一下,磁盘缓存的添加需要通过Editor来完成,Editor提供了commit和abort方法来提交和撤销对文件系统的写操作,具体实现请参考下面的loadBitmapFromHttp方法。磁盘缓存的读取需要通过Snapshot来完成,通过SnapShot可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷地进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存中,具体实现参看下面的loadBitmapFromDiskCache方法。

    /** 从网络加载Bitmap */
    private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if(editor != null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if(downloadUrlToStream(url,outputStream)){
                editor.commit();//下载成功了提交
            }else {
                editor.abort();//下载失败了回退
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);//获取从网络下载到disk的缓存图片
    }
	
    /** 从磁盘上获取缓存文件 */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }

四、同步加载和异步加载接口的设计
首先看同步加载,同步加载接口需要外部在线程中调用,这是因为同步加载很可能比较耗时,实现如下。

    /** 同步加载 */
    private Bitmap loadBitmap(String url,int reqWidth,int reqHeight){
        /** 尝试从内存中加载图片 */
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if(bitmap != null){
            Log.d(TAG,"loadBitmapFromMemCache,url = " + url);
            return bitmap;
        }

        /** 尝试从磁盘中加载图片 */
        try {
            bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
            if(bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);//loadBitmap这个方法不能在UI线程中调用,这个里面做了判断
        } catch (IOException e) {
            e.printStackTrace();
        }

        /** 磁盘中写不进去,读取不到,从网络下载图片 */
        if(bitmap == null && !mIsDiskLruCacheCreate){
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

从loadBitmap的实现可以看出,其工作过程遵循如下几个步:首先尝试从内存缓存中读取图片,接着尝试从磁盘缓存中读取图片,最后才会从网络拉取图片。另外,这个方法不能在主线程中调用,否则就抛出异常。这个检查是在loadBitmapFromHttp中实现的:

	if(Looper.myLooper() == Looper.getMainLooper()){
		throw new RuntimeException("cat not visit network from UI thread");
	}

接着看下异步加载接口的设计

    /** 异步加载 */
    public void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
        imageView.setTag(TAG_KEY_URI,uri);
        Bitmap bitmap = getBitmapFromMemoryCache(uri);
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap =  loadBitmap(uri,reqWidth,reqHeight);
                if(bitmap != null){
                    LoaderResult result = new LoaderResult(imageView,uri,bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

从bindBitmap的实现来看,bindBitmap方法会尝试从内存缓存中读取图片,如果读取成功就直接返回结果,否则会在线程池中调用loadBitmap方法,当图片加载成功后再将图片,图片的地址以及需要绑定的imageView封装成一个LoaderResult对象,然后再通过mMainHandler向主线程发送一个消息,这样就可以在主线程中给imageView设置图片了,之所以通过Handler来中转是因为子线程无法访问UI。
下面是线程池THREAD_POOL_EXECUTOR的实现。

    /** 线程池初始化的一些参数 */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CODE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;
	
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread( Runnable runnable) {
            return new Thread(runnable,"ImageLoader#"+ mCount.getAndIncrement());
        }
    };
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CODE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),
            sThreadFactory);

之所以采用线程池是有原因的,首先肯定不能采用普通的线程去做这个事。如果直接采用普通的线程去加载图片,随着列表的滑动这可能会产生大量的线程,这样并不利于整体效率的提升。而且这个是高并发场景,所以使用线程池就合适。

下面看Handler的实现。ImageLoader直接采用主线程的Looper来构造Handler对象,这就使得ImageLoader可以在非主线程中构造了。另外为了解决由于View的复用所导致的列表错位这一问题,在给ImageView设置图片之前都会检查它的url有没有发生改变,如果发生改变就不再给它设置图片,这样就解决了列表错位问题。

    private Handler mMainHandler = new Handler(Looper.myLooper()){

        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            /** 拿到imageView的tag对应的uri,如果和这个result中的uri相等,表示这个图片就是这个imageView需要的,
             可能出现网络下载慢,用户滑动后,其它的imageView复用了这个。使用tag很好的解决了列表错位的问题。
             */
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if(uri.equals(result.uri)){
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

到此为止,ImageLoader的细节已经做了全部的分析。

五、ImageLoader的完整代码
以下代码在真机上实测通过

package com.example.bitmap.imagerLoader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import com.example.bitmap.disklrucache.DiskLruCache;
import com.example.test.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ImageLoader {
    private static final String TAG = "ImageLoader";

    public static final int MESSAGE_POST_RESULT = 1;

    /** 线程池初始化的一些参数 */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CODE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    private static final int TAG_KEY_URI = R.id.imageloader_uri;//这个id必须是应用唯一的id,可以在values文件夹中创建一个ids.xml文件定义
    private static final long DISK_CACHE_SIZE = 1024 * 1024 *50;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreate = false;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread( Runnable runnable) {
            return new Thread(runnable,"ImageLoader#"+ mCount.getAndIncrement());
        }
    };
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CODE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),
            sThreadFactory);

    private Handler mMainHandler = new Handler(Looper.myLooper()){

        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            /** 拿到imageView的tag对应的uri,如果和这个result中的uri相等,表示这个图片就是这个imageView需要的,
             可能出现网络下载慢,用户滑动后,其它的imageView复用了这个。使用tag很好的解决了列表错位的问题。
             */
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if(uri.equals(result.uri)){
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryLruCache;
    private DiskLruCache mDiskLruCache;


    private ImageLoader(Context context) {
        Log.d(TAG,"ImageLoader");
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//KB
        int cacheSize = maxMemory / 8;
        Log.d(TAG,"memory cacheSize = " + cacheSize);

        mMemoryLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;//KB
            }
        };

        File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
        if(!diskCacheDir.exists()){
            diskCacheDir.mkdirs();
        }

        try {
            mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
            mIsDiskLruCacheCreate = true;
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static ImageLoader build(Context context){
        return new ImageLoader(context);
    }


    /** 内存缓存的添加 */
    private void addBitmapToMemoryCache(String key,Bitmap bitmap){
        if(getBitmapFromMemoryCache(key) == null){
            mMemoryLruCache.put(key,bitmap);
        }
    }
    /** 内存缓存的获取 */
    public Bitmap getBitmapFromMemoryCache(String key){
        return mMemoryLruCache.get(key);
    }

    public void bindBitmap(final String uri, final ImageView imageView){
        bindBitmap(uri,imageView,0,0);
    }

    /** 异步加载 */
    private void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
        imageView.setTag(TAG_KEY_URI,uri);
        Bitmap bitmap = getBitmapFromMemoryCache(uri);
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap =  loadBitmap(uri,reqWidth,reqHeight);
                if(bitmap != null){
                    LoaderResult result = new LoaderResult(imageView,uri,bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }


    /** 同步加载 */
    public Bitmap loadBitmap(String url,int reqWidth,int reqHeight){
        /** 尝试从内存中加载图片 */
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if(bitmap != null){
            Log.d(TAG,"loadBitmapFromMemCache,url = " + url);
            return bitmap;
        }

        /** 尝试从磁盘中加载图片 */
        try {
            bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
            if(bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);//loadBitmap这个方法不能在UI线程中调用,这个里面做了判断
        } catch (IOException e) {
            e.printStackTrace();
        }

        /** 磁盘中写不进去,读取不到,从网络下载图片 */
        if(bitmap == null && !mIsDiskLruCacheCreate){
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }


    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemoryCache(key);
        return bitmap;
    }

    /** 从网络加载Bitmap */
    private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(url);
        DiskLruCache.Editor editor = mDiskLruCache.edit(key);
        if(editor != null){
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if(downloadUrlToStream(url,outputStream)){
                editor.commit();//下载成功了提交
            }else {
                editor.abort();//下载失败了回退
            }
            mDiskLruCache.flush();
        }
        return loadBitmapFromDiskCache(url,reqWidth,reqHeight);//获取从网络下载到disk的缓存图片
    }


    /** 从磁盘上获取缓存文件 */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }


    /** 网络下载图片并且写入到磁盘 */
    private boolean downloadUrlToStream(String urlString,OutputStream outputStream){
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
            int b ;
            while ((b = in.read()) != -1){
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.d(TAG,"下载网络图片失败");
        }finally {
            if(urlConnection != null){
                urlConnection.disconnect();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /** 网络下载图片,返回bitmap对象 */
    private Bitmap downloadBitmapFromUrl(String urlString){
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (IOException e) {
            Log.d(TAG,"下载网络图片失败");
        }finally {
            if(urlConnection != null){
                urlConnection.disconnect();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }


    private String hashKeyFormUrl(String url){
        String cacheKey;
        try {
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] digest) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < digest.length; i++) {
            String hex = Integer.toHexString(0xFF&digest[i]);
            if(hex.length() == 1){
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    private File getDiskCacheDir(Context context,String folderName){
        String cacheDirPath = context.getCacheDir().getAbsolutePath();
        File diskCacheDir = new File(cacheDirPath + File.separator + "bitmap");
//        File diskCacheDir = new File("/sdcard" + File.separator + "bitmap");//sdcard路径
        return diskCacheDir;
    }


    private static class LoaderResult{
        public ImageView imageView;
        public String uri;
        public Bitmap bitmap;

        public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
            this.imageView = imageView;
            this.uri = uri;
            this.bitmap = bitmap;
        }
    }

}

特别注意:private static final int TAG_KEY_URI = R.id.imageloader_uri;
这个id是在res/values/ids.xml中定义的

	<?xml version="1.0" encoding="utf-8"?>
	<resources>
		<item name="imageloader_uri" type="id"/>
	</resources>

六、ImageLoader的使用
测试的activity (TestBitmapActivity.java)

package com.example.bitmap;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.example.bitmap.imagerLoader.ImageLoader;
import com.example.bitmap.imagerLoader.MyUtils;
import com.example.test.R;

import java.util.ArrayList;

public class TestBitmapActivity extends AppCompatActivity implements AbsListView.OnScrollListener {
    private static final String TAG = "G_TestBitmapActivity";

    private ArrayList<String> mUrList = new ArrayList<String>();
    private ImageLoader mImageLoader;
    private GridView mImageGridView;
    private BaseAdapter mImageAdapter;

    private boolean mIsGridViewIdle = true;
    private int mImageWidth = 0;
    private boolean mIsWifi = false;
    private boolean mCanGetBitmapFromNetWork = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_bitmap);

        initData();
        initView();
        mImageLoader = ImageLoader.build(this);//初始化一个ImageLoader
    }

    String[] imageUrls = {
            "http://img3.imgtn.bdimg.com/it/u=3719870775,3435259810&fm=26&gp=0.jpg",
            "http://img5.imgtn.bdimg.com/it/u=1177374310,3414687935&fm=26&gp=0.jpg",
            "http://img0.imgtn.bdimg.com/it/u=1737410255,2903983043&fm=26&gp=0.jpg",
            "http://img3.imgtn.bdimg.com/it/u=101044436,1645701061&fm=26&gp=0.jpg",
            "http://img2.imgtn.bdimg.com/it/u=3195176921,3515190403&fm=26&gp=0.jpg",
            "http://img5.imgtn.bdimg.com/it/u=1665477401,3216469060&fm=26&gp=0.jpg"
    };
    private void initData() {
        for (String url : imageUrls) {
            mUrList.add(url);
        }
        int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        int space = (int)MyUtils.dp2px(this, 20f);
        mImageWidth = (screenWidth - space) / 3;//计算image的宽度
        mIsWifi = MyUtils.isWifi(this);
        if (mIsWifi) {
            mCanGetBitmapFromNetWork = true;
        }
    }

    private void initView() {
        mImageGridView = findViewById(R.id.gv);
        mImageAdapter = new ImageAdapter(this);
        mImageGridView.setAdapter(mImageAdapter);
        mImageGridView.setOnScrollListener(this);

        if (!mIsWifi) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("初次使用会从网络下载大概5MB的图片,确认要下载吗?");
            builder.setTitle("注意");
            builder.setPositiveButton("是", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mCanGetBitmapFromNetWork = true;
                    mImageAdapter.notifyDataSetChanged();
                }
            });
            builder.setNegativeButton("否", null);
            builder.show();
        }
    }

    public void eventLoadMore(View view) {
        for (String url : imageUrls) {
            mUrList.add(url);
        }
        mImageAdapter.notifyDataSetChanged();
    }


    private class ImageAdapter extends BaseAdapter {
        private LayoutInflater mInflater;
        private Drawable mDefaultBitmapDrawable;

        private ImageAdapter(Context context) {
            mInflater = LayoutInflater.from(context);
            mDefaultBitmapDrawable = context.getResources().getDrawable(R.mipmap.ic_launcher);
        }

        @Override
        public int getCount() {
            return mUrList.size();
        }

        @Override
        public String getItem(int position) {
            return mUrList.get(position);
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.image_list_item,parent, false);
                holder = new ViewHolder();
                holder.imageView = convertView.findViewById(R.id.image);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            ImageView imageView = holder.imageView;
            final String tag = (String)imageView.getTag();
            final String uri = getItem(position);
            if (!uri.equals(tag)) {
                imageView.setImageDrawable(mDefaultBitmapDrawable);
            }

            /** mIsGridViewIdle 没有在滑动,并且允许下载就开始异步加载bitmap图片
               看到京东客户端在滑动过程中也会加载,淘宝客户端滑动中就不会。是否需要mIsGridViewIdle这个标记看业务和性能之间的取舍 */
            if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
                imageView.setTag(uri);
                mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);//这个大小主要控制压缩图片的质量
            }
            return convertView;
        }

    }

    private static class ViewHolder {
        public ImageView imageView;
    }

    /** 判断是否在滑动,如果在滑动就设置标记位,停止加载*/
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            mIsGridViewIdle = true;
            mImageAdapter.notifyDataSetChanged();
        } else {
            mIsGridViewIdle = false;
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {
    }

}

activity布局文件(activity_test_bitmap.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.bitmap.TestBitmapActivity"
    android:orientation="vertical"
    >

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="加载更多"
        android:onClick="eventLoadMore"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="5dp" >

        <GridView
            android:id="@+id/gv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:horizontalSpacing="5dp"
            android:verticalSpacing="5dp"
            android:listSelector="@android:color/transparent"
            android:numColumns="3"
            android:stretchMode="columnWidth" >
        </GridView>
    </LinearLayout>

</LinearLayout>

工具类MyUtils.java

package com.example.bitmap.imagerLoader;

import android.app.ActivityManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;

public class MyUtils {
    public static String getProcessName(Context cxt, int pid) {
        ActivityManager am = (ActivityManager) cxt
                .getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
            if (procInfo.pid == pid) {
                return procInfo.processName;
            }
        }
        return null;
    }

    public static void close(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static DisplayMetrics getScreenMetrics(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        return dm;
    }

    public static float dp2px(Context context, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics());
    }

    public static boolean isWifi(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
        if (activeNetInfo != null
                && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            return true;
        }
        return false;
    }

    public static void executeInThread(Runnable runnable) {
        new Thread(runnable).start();
    }

}

自定义ImageView,用于设置相等宽高,如下所示。

package com.example.bitmap.imagerLoader;

import android.content.Context;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;

public class SquareImageView extends AppCompatImageView {

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);//相等宽高
    }
}

ImageAdapter中的item布局(image_list_item),如下所示。

<?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:gravity="center"
    android:orientation="vertical" >

    <com.example.bitmap.imagerLoader.SquareImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@mipmap/ic_launcher" />

</LinearLayout>

AndroidManifest.xml配置的activity,开启硬件加速

	<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    
	<activity android:name="com.example.bitmap.TestBitmapActivity"
		android:hardwareAccelerated="true">
	</activity>

上面的代码解决了imageView复用导致的列表错位问题。滑动过程中不加载,避免出现大量的线程占用资源(看业务取舍,如果用户非常快的滑动,就没必要从网络中加载)。

很使用的工具类封装,可以做成和淘宝、京东一样的刷新效果。

运行的效果图如下所示。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值