Android——自定义简化版ImageLoader

来源:互联网 发布:genesis软件 编辑:程序博客网 时间:2024/06/06 05:42

在上一篇我们已经先后介绍了 Bitmap 的高效加载方式、LruCache 以及 DiskLruCache,那么我们就动手来写一个简化版的 ImageLoader 吧!!!

一般来说,一个优秀的 ImageLoader 应该具备如下功能:

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

声明一下,这里实现的 ImageLoader 并不是为了写一个框架,而是纯粹的加深下三级缓存以及图片的高效加载而写的一个小Demo。

1、图片压缩功能的实现

图片压缩在上一篇博客中已经做了介绍,这里就不在废话了,为了有良好的设计风格,这里单独抽象了一个类似于完成图片的压缩功能,这个类叫 ImageResizer,它的实现如下:

public class ImageResizer {    private static final String TAG = "ImageResizer";    public ImageResizer() {    }    /**     * 根据指定的资源文件以及指定的宽/高进行等比例缩放     * @param res     * @param resId     * @param reqWidth     * @param reqHeight     * @return     */    public static Bitmap decodeSampledBitmapFromResource(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);    }    /**     * 根据文件流的文件描述符以及指定的宽/高进行等比例缩放     * @param fileDescriptor     * @param reqWidth     * @param reqHeight     * @return     *     */    public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {        BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);        Log.e("00000000", reqWidth + "=====" + reqHeight + "======" + options.inSampleSize);        options.inJustDecodeBounds = false;        return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);    }    /**     * 根据指定的宽/高进行 2 的指数缩放     *     * @param options     * @param reqWidth     * @param reqHeight     * @return     */    private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {        final int width = options.outWidth;        int height = options.outHeight;        int inSampleSize = 1;        if (reqHeight > 0 || reqHeight > 0) {            if (width > reqHeight && height > reqHeight) {                final int halfWidth = width / 2;                final int halfHeight = height / 2;                while (halfWidth / inSampleSize >= reqWidth                        && halfHeight / inSampleSize >= reqHeight) {                    inSampleSize *= 2;                }            }        }        return inSampleSize;    }}

2、内存缓存的实现

public class MemoryCache {    private static MemoryCache instance;    private LruCache<String, Bitmap> mMemoryCache;    private MemoryCache() {        //获取当前进程的可用内存        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);        int cacheSize = maxMemory / 8;        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {            @Override            protected int sizeOf(String key, Bitmap bitmap) {                //完成bitmap对象大小的计算                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;            }        };    }    public static MemoryCache getInstance() {        if (instance == null) {            synchronized (MemoryCache.class) {                if (instance == null) {                    instance = new MemoryCache();                }            }        }        return instance;    }    public void addBitmapToMemoryCache(String uri, Bitmap bitmap) {        String key = MD5.hashKeyFormUrl(uri);        mMemoryCache.put(key, bitmap);    }    public Bitmap getBitmapFromMemoryCache(String key) {        return mMemoryCache.get(key);    }}

没什么好说的,略过…

3、磁盘缓存的实现

public class DiskCache {    private static DiskCache instance;    private final long DISK_CACHE_SIZE = 1024 * 1024 * 50;  //可缓存的大小    private DiskLruCache mDiskLruCache;    public static final int DISK_CACHE_INDEX = 0;    private final int IO_BUFFER_SIZE = 8 * 1024;    //缓冲流的大小    private DiskCache(Context context) {        File diskCacheDir = FilePath.getDiskCacheDir(context, "bitmap");        if (!diskCacheDir.exists()) {            diskCacheDir.mkdirs();        }        //判断当前sdk缓存目录可用大小是否满足我们设置的缓存大小        if (FilePath.getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {            try {                mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);            } catch (IOException e) {                e.printStackTrace();            }        }    }    public static DiskCache getInstance(Context context) {        if (instance == null) {            synchronized (DiskCache.class) {                if (instance == null) {                    instance = new DiskCache(context);                }            }        }        return instance;    }    public Bitmap get(String uri) throws IOException {        FileDescriptor fileDescriptor = getFileDescriptor(uri);        if (fileDescriptor != null) {            return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, 0, 0);        } else {            return null;        }    }    public Bitmap get(String uri, int reqWidth, int reqHeight) throws IOException {        FileDescriptor fileDescriptor = getFileDescriptor(uri);        if (fileDescriptor != null) {            return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);        } else {            return null;        }    }    public void edit(String uri) throws IOException {        String key = MD5.hashKeyFormUrl(uri);        DiskLruCache.Editor editor = null;        //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null        if (mDiskLruCache != null) {            editor = mDiskLruCache.edit(key);        }        if (editor != null) {            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);            if (downloadUrlToStream(uri, outputStream)) {                editor.commit();            } else {                editor.abort();            }            mDiskLruCache.flush();        }    }    public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {        HttpURLConnection urlConnection = null;        BufferedOutputStream out = null;        BufferedInputStream in = null;        try {            final URL url = new URL(urlString);            urlConnection = (HttpURLConnection) url.openConnection();            urlConnection.connect();            InputStream inputStream = urlConnection.getInputStream();            in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);            int b = 0;            while ((b = in.read()) != -1) {                out.write(b);            }            return true;        } catch (Exception e) {            e.printStackTrace();        } finally {            if (urlConnection != null) {                urlConnection.disconnect();            }            if (in != null) {                try {                    in.close();                } catch (IOException e) {                    e.printStackTrace();                }            }            if (out != null) {                try {                    out.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }        return false;    }    private FileDescriptor getFileDescriptor(String uri) throws IOException {        //mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null        if (mDiskLruCache != null) {            String key = MD5.hashKeyFormUrl(uri);            Bitmap bitmap = null;            FileDescriptor fileDescriptor = null;            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);            if (snapshot != null) {                FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DiskCache.DISK_CACHE_INDEX);                fileDescriptor = fileInputStream.getFD();            }            return fileDescriptor;        } else {            return null;        }    }}

在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需要的大小,一般是指用户的手机空间不足了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。

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

4、同步加载

public Bitmap loadBitmap(String uri, ImageView imageView) {        Bitmap bitmap = loadBitmapFromMemCache(uri);        if (bitmap != null) {            return bitmap;        }        bitmap = loadBitmapFromDiskCache(uri, imageView);        if (bitmap != null) {            return bitmap;        }        bitmap = loadBitmapFromHttp(uri, imageView);        if (bitmap != null) {            return bitmap;        }        return downloadBitmapFromUrl(uri);    }

从 loadBitmap 的实现可以看出,其工作过程遵循如下几步:首先尝试从内存缓存中读取图片,接着尝试冲磁盘缓存中读取图片,最后才从网络中拉取图片。另外这个方法不能在主线程中调用,否则就抛出异常。这个执行环境的检测是在各种缓存获取方法中实现的,通过检测当前线程的 Looper 是否为主线程的 Looper 来判断当前线程是否是主线程,如果是主线程就直接抛出异常终止程序。如下所示:

        if (Looper.myLooper() == Looper.getMainLooper()) {            throw new RuntimeException("can not visit network form UI Thread.");        }

5、异步加载

    public void bindBitmap(final String uri, final ImageView imageView) {        imageView.setTag(TAG_KEY_URI, uri);        Bitmap bitmap = loadBitmapFromMemCache(uri);        if (bitmap != null) {            imageView.setImageBitmap(bitmap);            return;        }        Runnable loadBitmapTask = new Runnable() {            @Override            public void run() {                Bitmap bitmap = loadBitmap(uri, imageView);                if (bitmap != null) {                    LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap);                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget();                }            }        };        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);    }

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

bindBitm 中用到了线程池和 Handler,这里看一下它们的实现,首先看线程池 THREAD_POOL_EXECUTOR 的实现,如下所示。可以看出它的核心线程数为当前设备的 CPU 核心数 +1,最大容量为 CPU 核心数的 2 倍 +1,线程闲置超时时长为 10 秒,关于线程池的详细介绍我们在 Android——线程和线程池 已经详细介绍过。

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;    //核心线程数量    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //最大线程数量    private static final int KEEP_ALIVE = 10;   //存活时间    private static final ThreadFactory sThreadFactory = new ThreadFactory() {        private final AtomicInteger mCount = new AtomicInteger(1);        public Thread newThread(Runnable r) {            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());        }    };    private static final BlockingQueue<Runnable> sPoolWorkQueue =            new LinkedBlockingQueue<Runnable>();    //等待队列    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(            CORE_POOL_SIZE,            MAXIMUM_POOL_SIZE,            KEEP_ALIVE,            TimeUnit.SECONDS,            sPoolWorkQueue,            sThreadFactory);

之所以采用线程池是有原因的,首先肯定不能采用普通的线程去做这个事,线程池的好处上篇博客已经说明。如果直接采用普通的线程去加载图片,随着列表的滑动这可能会产生大量的线程,这样并不利于整体效率的提升。另外一点,这里也没有选择采用 AsyncTask,AsyncTask 封装了线程池和 Handler,按道理它应该适合 ImageLoader 的场景。从 Android——线程和线程池 我们对 AsyncTask 的分析可以知道,AsyncTask 在 3.0 的低版本和高版本上具有不同的表现,在 3.0 以上的版本 AsyncTask 无法实现并发的效果,这显然是不能接受的,因为 ImageLoader 就是需要并发特性,虽然可以通过改造 AsyncTask 或者使用 AsyncTask 的 executeOnExecutor 方法的形式来执行异步任务,但是这总归是不太自然的实现f方式。鉴于以上两点原因,这里选择线程池和 Handler 来提供 ImageLoader 的并发能力和访问 UI 的能力。

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

    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {        @Override        public void handleMessage(Message msg) {            LoaderResult result = (LoaderResult) msg.obj;            ImageView imageView = result.imageView;            String uri = (String) imageView.getTag(TAG_KEY_URI);            if (uri.equals(result.uri)) {                imageView.setImageBitmap(result.bitmap);            } else {                Log.w(TAG, "set image bitmap,but url has changed, ignored!");            }        }    };

到此为止,ImageLoader 的细节都已经做了全面的分析,下面是 ImageLoader的完整代码:

public class ImageLoader {    private static ImageLoader instance;    private Context mContext;    private final int TAG_KEY_URI = R.id.imageloader_uri;    private final String TAG = "ImageLoader";    private int MESSAGE_POST_RESULT = 1;    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;    //核心线程数量    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1; //最大线程数量    private static final int KEEP_ALIVE = 10;   //存活时间    private static final ThreadFactory sThreadFactory = new ThreadFactory() {        private final AtomicInteger mCount = new AtomicInteger(1);        public Thread newThread(Runnable r) {            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());        }    };    private static final BlockingQueue<Runnable> sPoolWorkQueue =            new LinkedBlockingQueue<Runnable>();    //等待队列    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(            CORE_POOL_SIZE,            MAXIMUM_POOL_SIZE,            KEEP_ALIVE,            TimeUnit.SECONDS,            sPoolWorkQueue,            sThreadFactory);    //更新 ImageView    private Handler mMainHandler = new Handler(Looper.getMainLooper()) {        @Override        public void handleMessage(Message msg) {            LoaderResult result = (LoaderResult) msg.obj;            ImageView imageView = result.imageView;            String uri = (String) imageView.getTag(TAG_KEY_URI);            if (uri.equals(result.uri)) {                imageView.setImageBitmap(result.bitmap);            } else {                Log.w(TAG, "set image bitmap,but url has changed, ignored!");            }        }    };    private ImageLoader() {    }    public static ImageLoader getInstance() {        if (instance == null) {            synchronized (ImageLoader.class) {                if (instance == null) {                    instance = new ImageLoader();                }            }        }        return instance;    }    public void init(Context context) {        this.mContext = context;    }    /**     * 异步加载     * @param uri     * @param imageView     */    public void bindBitmap(final String uri, final ImageView imageView) {        imageView.setTag(TAG_KEY_URI, uri);        Bitmap bitmap = loadBitmapFromMemCache(uri);        if (bitmap != null) {            imageView.setImageBitmap(bitmap);            return;        }        Runnable loadBitmapTask = new Runnable() {            @Override            public void run() {                Bitmap bitmap = loadBitmap(uri, imageView);                if (bitmap != null) {                    LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap);                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget();                }            }        };        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);    }    /**     * 同步加载     * @param uri     * @param imageView     * @return     */    public Bitmap loadBitmap(String uri, ImageView imageView) {        Bitmap bitmap = loadBitmapFromMemCache(uri);        if (bitmap != null) {            return bitmap;        }        bitmap = loadBitmapFromDiskCache(uri, imageView);        if (bitmap != null) {            return bitmap;        }        bitmap = loadBitmapFromHttp(uri, imageView);        if (bitmap != null) {            return bitmap;        }        return downloadBitmapFromUrl(uri);    }    /**     * 从网络获取bitmap     * @param urlString     * @return     */    private Bitmap downloadBitmapFromUrl(String urlString) {        if (Looper.myLooper() == Looper.getMainLooper()) {            throw new RuntimeException("can not visit network form UI Thread.");        }        return DownLoad.downLoadBitmapFromUrl(urlString);    }    /**     * 从网络获取bitmap,并添加到磁盘缓存     * @param uri     * @param imageView     * @return     */    private Bitmap loadBitmapFromHttp(String uri, ImageView imageView) {        if (Looper.myLooper() == Looper.getMainLooper()) {            throw new RuntimeException("can not visit network form UI Thread.");        }        try {            DiskCache.getInstance(mContext).edit(uri);            return loadBitmapFromDiskCache(uri, imageView);        } catch (IOException e) {            e.printStackTrace();        }        return null;    }    /**     * 从磁盘缓存中获取bitmap     * @param uri     * @param imageView     * @return     */    private Bitmap loadBitmapFromDiskCache(String uri, ImageView imageView) {        if (Looper.myLooper() == Looper.getMainLooper()) {            throw new RuntimeException("can not visit network form UI Thread.");        }        Bitmap bitmap = null;        try {            bitmap = DiskCache.getInstance(mContext).get(uri, imageView.getWidth(), imageView.getHeight());            if (bitmap != null) {                MemoryCache.getInstance().addBitmapToMemoryCache(uri, bitmap);            }        } catch (IOException e) {            e.printStackTrace();        }        return bitmap;    }    /**     * 从内存缓存中获取bitmap     * @param uri     * @return     */    private Bitmap loadBitmapFromMemCache(String uri) {        final String key = MD5.hashKeyFormUrl(uri);        return MemoryCache.getInstance().getBitmapFromMemoryCache(key);    }    /**     * 返回结果的封装     */    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;        }    }}

其他相关类:

//MD5public class MD5 {    public static String hashKeyFormUrl(String url){        String cacheKey = url;        try {            MessageDigest mDigest = MessageDigest.getInstance("MD5");            mDigest.update(url.getBytes());            cacheKey = bytesToHexString(mDigest.digest());        } catch (NoSuchAlgorithmException e) {            e.printStackTrace();        }        return cacheKey;    }    private static String bytesToHexString(byte[] bytes) {        StringBuilder sb = new StringBuilder();        for (int i = 0; i < bytes.length; i++) {            String hex = Integer.toHexString(0xff & bytes[i]);            if (hex.length() == 1) {                sb.append('0');            }            sb.append(hex);        }        return sb.toString();    }}
//路径获取相关类 FilePathpublic class FilePath {    public static File getDiskCacheDir(Context context, String uniqueName) {        boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);        final String cachePath;        if (externalStorageAvailable) {            cachePath = context.getExternalCacheDir().getParent();        } else {            cachePath = context.getCacheDir().getPath();        }        return new File(cachePath + File.separator + uniqueName);    }    public static long getUsableSpace(File path){        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){            return path.getUsableSpace();        }        final StatFs statFs = new StatFs(path.getPath());        return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();    }}

源码下载地址:

http://download.csdn.net/detail/akaic/9659585

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 oppo媒体声音小怎么办 手机视频音量小怎么办 苹果6内存不够怎么办 wiwox7忘记密码了怎么办 wiwo手机声音小怎么办 海信手机声音小怎么办 ppos手机声音小怎么办 手机铃声太小怎么办 手机声音没有了怎么办 vivo外放没声音怎么办 fgo关服充的钱怎么办 淘宝提前收货了怎么办 淘宝直播间没人怎么办 花呗地区限制怎么办 支付宝花呗账单错误怎么办 台球厅生意惨淡怎么办 退货少了东西怎么办 微店注册id怎么办 卖红酒没有客源怎么办 来姨妈10天没走怎么办 京东违反广告法怎么办 支付宝定位错误怎么办 银行账户未年审怎么办 淘宝集运禁运品 怎么办 物流显示禁运品怎么办 淘宝禁运品怎么办呢 货物退回日本了怎么办 淘宝卖家寄多了衣服怎么办 集运地址选错怎么办 淘宝卖韩国化妆品退货怎么办 去韩国留学手机怎么办 韩国办无线网怎么办 淘宝卖家被骗怎么办 淘宝买软件被骗怎么办 被淘宝店诈骗怎么办? 支付宝被骗2000怎么办 给私人打款后不发货怎么办 毕业证寄丢了怎么办 微商下单返现被骗一千四怎么办 淘宝买东西卡里多扣钱怎么办 付款了卖家不发货怎么办