项目中经常使用xutils框架的bitmaputils工具类来加载网络图片,用法极为方便,内置线程池开启多个线程来加载图片,内置lru算法防止内存溢出,也可以缓存本地,等等功能。
现在看看其中内部如何实现的。
bitmaputils类中大量的构造方法,配置缓存路径,缓存大小,加载时显示图片等等,不再赘述,直接从设置图片的方法入手。
- public <T extends View> void display(T container, String uri, BitmapDisplayConfig displayConfig, BitmapLoadCallBack<T> callBack) {
- if (container == null) {
- return;
- }
- container.clearAnimation();
- //如果回调是空则创建默认
- if (callBack == null) {
- callBack = new SimpleBitmapLoadCallBack<T>();
- }
- //同上
- if (displayConfig == null || displayConfig == defaultDisplayConfig) {
- displayConfig = defaultDisplayConfig.cloneNew();
- }
- // Optimize Max Size,确定设置图片的大小
- BitmapSize size = displayConfig.getBitmapMaxSize();
- displayConfig.setBitmapMaxSize(BitmapCommonUtils.optimizeMaxSizeByView(container, size.getWidth(), size.getHeight()));
- callBack.onPreLoad(container, uri, displayConfig);
- //如果uri是空,回调加载失败方法
- if (TextUtils.isEmpty(uri)) {
- callBack.onLoadFailed(container, uri, displayConfig.getLoadFailedDrawable());
- return;
- }
- //从内存缓存中抓取该这个bitmap
- Bitmap bitmap = globalConfig.getBitmapCache().getBitmapFromMemCache(uri, displayConfig);
- if (bitmap != null) {
- //显示图片,并且回调
- callBack.onLoadStarted(container, uri, displayConfig);
- callBack.onLoadCompleted(
- container,
- uri,
- bitmap,
- displayConfig,
- BitmapLoadFrom.MEMORY_CACHE);
- } else if (!bitmapLoadTaskExist(container, uri, callBack)) {
- final BitmapLoadTask<T> loadTask = new BitmapLoadTask<T>(container, uri, displayConfig, callBack);
- // set loading image
- final AsyncDrawable<T> asyncDrawable = new AsyncDrawable<T>(
- displayConfig.getLoadingDrawable(),
- loadTask);
- callBack.setDrawable(container, asyncDrawable);
- // load bitmap from uri or diskCache
- loadTask.executeOnExecutor(globalConfig.getBitmapLoadExecutor());
- }
- }
display方法很多个重载,几经辗转最终调用这个方法。
17行开始,获取需要设置图片的大小,跟踪这个方法。
- public BitmapSize getBitmapMaxSize() {
- return bitmapMaxSize == null ? BitmapSize.ZERO : bitmapMaxSize;
- }
- public static BitmapSize optimizeMaxSizeByView(View view, int maxImageWidth, int maxImageHeight) {
- int width = maxImageWidth;
- int height = maxImageHeight;
- if (width > 0 && height > 0) {
- return new BitmapSize(width, height);
- }
- //若params参数有值,即view的大小是固定,不是warp,match。
- final ViewGroup.LayoutParams params = view.getLayoutParams();
- if (params != null) {
- if (params.width > 0) {
- width = params.width;
- } else if (params.width != ViewGroup.LayoutParams.WRAP_CONTENT) {
- width = view.getWidth();
- }
- if (params.height > 0) {
- height = params.height;
- } else if (params.height != ViewGroup.LayoutParams.WRAP_CONTENT) {
- height = view.getHeight();
- }
- }
- // 如果要设置图片的view是imageview,通过反射取到大小
- if (width <= 0) width = getImageViewFieldValue(view, "mMaxWidth");
- if (height <= 0) height = getImageViewFieldValue(view, "mMaxHeight");
- //若都没有,则屏幕大小
- BitmapSize screenSize = getScreenSize(view.getContext());
- if (width <= 0) width = screenSize.getWidth();
- if (height <= 0) height = screenSize.getHeight();
- return new BitmapSize(width, height);
- }
根据这个bitmapsize大小和view的大小得出最终设置图片的大小并且设置到配置对象中
- callBack.onPreLoad(container, uri, displayConfig);
- Bitmap bitmap = globalConfig.getBitmapCache().getBitmapFromMemCache(uri, displayConfig);
这句很重要,取内存缓存中的bitmap对象,若有,则直接显示即可,如无,则需要加载。追踪这句代码
- public Bitmap getBitmapFromMemCache(String uri, BitmapDisplayConfig config) {
- if (mMemoryCache != null && globalConfig.isMemoryCacheEnabled()) {
- MemoryCacheKey key = new MemoryCacheKey(uri, config == null ? null : config.toString());
- return mMemoryCache.get(key);
- }
- return null;
- }
这里根据uri生成key,从lru队列中取出这个bitmap对象
- mMemoryCache = new LruMemoryCache<MemoryCacheKey, Bitmap>(globalConfig.getMemoryCacheSize()) {
- /**
- * Measure item size in bytes rather than units which is more practical
- * for a bitmap cache
- * 测量一个bitmap对象所占的内存大小
- */
- @Override
- protected int sizeOf(MemoryCacheKey key, Bitmap bitmap) {
- if (bitmap == null) return 0;
- return bitmap.getRowBytes() * bitmap.getHeight();
- }
- };
- public class MemoryCacheKey {
- private String uri;
- private String subKey;
- private MemoryCacheKey(String uri, String subKey) {
- this.uri = uri;
- this.subKey = subKey;
- }
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof MemoryCacheKey)) return false;
- MemoryCacheKey that = (MemoryCacheKey) o;
- if (!uri.equals(that.uri)) return false;
- if (subKey != null && that.subKey != null) {
- return subKey.equals(that.subKey);
- }
- return true;
- }
- @Override
- public int hashCode() {
- return uri.hashCode();
- }
- }
接下去看,读缓存图片就不说了,看bitmap为空该如何。
- else if (!bitmapLoadTaskExist(container, uri, callBack)) {
- final BitmapLoadTask<T> loadTask = new BitmapLoadTask<T>(container, uri, displayConfig, callBack);
- // set loading image
- final AsyncDrawable<T> asyncDrawable = new AsyncDrawable<T>(
- displayConfig.getLoadingDrawable(),
- loadTask);
- callBack.setDrawable(container, asyncDrawable);
- // load bitmap from uri or diskCache
- loadTask.executeOnExecutor(globalConfig.getBitmapLoadExecutor());
- }
- private static <T extends View> boolean bitmapLoadTaskExist(T container, String uri, BitmapLoadCallBack<T> callBack) {
- final BitmapLoadTask<T> oldLoadTask = getBitmapTaskFromContainer(container, callBack);
- if (oldLoadTask != null) {
- final String oldUrl = oldLoadTask.uri;
- if (TextUtils.isEmpty(oldUrl) || !oldUrl.equals(uri)) {
- oldLoadTask.cancel(true);
- } else {
- return true;
- }
- }
- return false;
- }
- private static <T extends View> BitmapLoadTask<T> getBitmapTaskFromContainer(T container, BitmapLoadCallBack<T> callBack) {
- if (container != null) {
- final Drawable drawable = callBack.getDrawable(container);
- if (drawable instanceof AsyncDrawable) {
- final AsyncDrawable<T> asyncDrawable = (AsyncDrawable<T>) drawable;
- return asyncDrawable.getBitmapWorkerTask();
- }
- }
- return null;
- }
在这里很好奇的是,AsyncDrawable是个什么东西,看一下代码
- public class AsyncDrawable<T extends View> extends Drawable {
- private final WeakReference<BitmapUtils.BitmapLoadTask<T>> bitmapLoadTaskReference;
- private final Drawable baseDrawable;
- public AsyncDrawable(Drawable drawable, BitmapUtils.BitmapLoadTask<T> bitmapWorkerTask) {
- if (drawable == null) {
- throw new IllegalArgumentException("drawable may not be null");
- }
- if (bitmapWorkerTask == null) {
- throw new IllegalArgumentException("bitmapWorkerTask may not be null");
- }
- baseDrawable = drawable;
- bitmapLoadTaskReference = new WeakReference<BitmapUtils.BitmapLoadTask<T>>(bitmapWorkerTask);
- }
- public BitmapUtils.BitmapLoadTask<T> getBitmapWorkerTask() {
- return bitmapLoadTaskReference.get();
- }
- //省略大量代码,都是和drawable绘制有关
AsyncDrawable继承了Drawable,在里面使用弱引用bitmapLoadTaskReference把加载任务装进去,构造方法传入加载任务,提供get方法取出加载任务
好,回到主线,若加载任务为空,则创建 BitmapLoadTask,创建AsyncDrawable对象,构造方法传入加载时显示的图片和加载任务,给view设置上drawable。
调用终极方法
- loadTask.executeOnExecutor(globalConfig.getBitmapLoadExecutor());
不过调用方法和往常不大一样,平时用异步任务异步都是调用execute(),而这个却是executeOnExecutor,还要传入一个奇怪的东西。
(再往后面涉及大量Java多线程知识,我这方面也很弱,如有讲错,轻喷、)
不了解系统异步任务的可以看看Android AsyncTask源码解析,洋洋写的很牛逼,大致说明了Android3.0之后系统AsyncTask的execute采用单线程队列形式处理并发请求,同时一百个异步任务开启,也只会按照队列的形式一个接一个执行。
现在使用executeOnExecutor(),使用线程池的方式来并发执行任务,来看看传进什么去了
- public ExecutorService getBitmapLoadExecutor() {
- if (_dirty_params_bitmapLoadExecutor || bitmapLoadExecutor == null) {
- bitmapLoadExecutor = Executors.newFixedThreadPool(getThreadPoolSize(), sThreadFactory);
- _dirty_params_bitmapLoadExecutor = false;
- }
- return bitmapLoadExecutor;
- }
创建一个带固定数量的线程池。构造方法传入一个线程工厂sThreadFactory,看看这个什么做什么用的
- private static final ThreadFactory sThreadFactory = new ThreadFactory() {
- private final AtomicInteger mCount = new AtomicInteger(1);
- @Override
- public Thread newThread(Runnable r) {
- Thread thread = new Thread(r, "BitmapUtils #" + mCount.getAndIncrement());
- thread.setPriority(Thread.NORM_PRIORITY - 1);
- return thread;
- }
- };
接着重写newThread方法,定义用此线程池创建线程的规范,如线程名,线程优先级。
- public class BitmapLoadTask<T extends View> extends CompatibleAsyncTask<Object, Object, Bitmap> {
- private final String uri;
- //这个是弱引用
- private final WeakReference<T> containerReference;
- private final BitmapLoadCallBack<T> callBack;
- private final BitmapDisplayConfig displayConfig;
- private BitmapLoadFrom from = BitmapLoadFrom.DISK_CACHE;
- public BitmapLoadTask(T container, String uri, BitmapDisplayConfig config, BitmapLoadCallBack<T> callBack) {
- if (container == null || uri == null || config == null || callBack == null) {
- throw new IllegalArgumentException("args may not be null");
- }
- this.containerReference = new WeakReference<T>(container);
- this.callBack = callBack;
- this.uri = uri;
- this.displayConfig = config;
- }
- protected Bitmap doInBackground(Object... params) {
- synchronized (pauseTaskLock) {
- while (pauseTask && !this.isCancelled()) {
- try {
- pauseTaskLock.wait();
- } catch (Throwable e) {
- }
- }
- }
- Bitmap bitmap = null;
- // get cache from disk cache
- if (!this.isCancelled() && this.getTargetContainer() != null) {
- this.publishProgress(PROGRESS_LOAD_STARTED);
- bitmap = globalConfig.getBitmapCache().getBitmapFromDiskCache(uri, displayConfig);
- }
- // download image
- if (bitmap == null && !this.isCancelled() && this.getTargetContainer() != null) {
- bitmap = globalConfig.getBitmapCache().downloadBitmap(uri, displayConfig, this);
- from = BitmapLoadFrom.URI;
- }
- return bitmap;
- }
前几行是线程挂起的常用手段,定义一个object对象来锁线程,当需要线程暂停时,改变pauseTask的值,调用wait方法,线程休眠并且释放对象锁让外部线程访问,知道外面线程调用notify方法,将唤醒线程。在listview滑动暂停加载时,就是调用了这里方法。
接着从本地缓存中读取图片
- public Bitmap getBitmapFromDiskCache(String uri, BitmapDisplayConfig config) {
- if (uri == null || !globalConfig.isDiskCacheEnabled()) return null;
- synchronized (mDiskCacheLock) {
- while (!isDiskCacheReadied) {
- try {
- mDiskCacheLock.wait();
- } catch (Throwable e) {
- }
- }
- if (mDiskLruCache != null) {
- LruDiskCache.Snapshot snapshot = null;
- try {
- snapshot = mDiskLruCache.get(uri);
- if (snapshot != null) {
- Bitmap bitmap = null;
- if (config == null || config.isShowOriginal()) {
- bitmap = BitmapDecoder.decodeFileDescriptor(
- snapshot.getInputStream(DISK_CACHE_INDEX).getFD());
- } else {
- bitmap = BitmapDecoder.decodeSampledBitmapFromDescriptor(
- snapshot.getInputStream(DISK_CACHE_INDEX).getFD(),
- config.getBitmapMaxSize(),
- config.getBitmapConfig());
- }
- bitmap = rotateBitmapIfNeeded(uri, config, bitmap);
- addBitmapToMemoryCache(uri, config, bitmap, mDiskLruCache.getExpiryTimestamp(uri));
-
- return bitmap;
- }
- } catch (Throwable e) {
- LogUtils.e(e.getMessage(), e);
- } finally {
- IOUtils.closeQuietly(snapshot);
- }
- }
- return null;
- }
- }
这里涉及了disklrucache,这块也挺复杂的,依旧推荐洋洋的DiskLruCache源码解析
这块基本操作就是从本地缓存中取出图片,值得注意的
bitmap = rotateBitmapIfNeeded(uri, config, bitmap); addBitmapToMemoryCache(uri, config, bitmap, mDiskLruCache.getExpiryTimestamp(uri));
第一句是旋转bitmap图片,第二句是将这个bitmap添加到内存缓存中。
private Bitmap rotateBitmapIfNeeded(String uri, BitmapDisplayConfig config, Bitmap bitmap) { Bitmap result = bitmap; if (config != null && config.isAutoRotation()) { File bitmapFile = this.getBitmapFileFromDiskCache(uri); if (bitmapFile != null && bitmapFile.exists()) { ExifInterface exif = null; try { exif = new ExifInterface(bitmapFile.getPath()); } catch (Throwable e) { return result; } int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED); int angle = 0; switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: angle = 90; break; case ExifInterface.ORIENTATION_ROTATE_180: angle = 180; break; case ExifInterface.ORIENTATION_ROTATE_270: angle = 270; break; default: angle = 0; break; } if (angle != 0) { Matrix m = new Matrix(); m.postRotate(angle); result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, true); bitmap.recycle(); bitmap = null; } } } return result; }方法开始读取图片,关键点从这里开始
exif = new ExifInterface(bitmapFile.getPath());ExifInterface是个拍照api,这个接口提供了图片文件的旋转,gps,时间等信息。Exifinterface详解
这里读取出图片的旋转信息,因为有时候拍照会横屏拍,这时候可能会出现图片是横着的情况,这里把图片旋转回去了。
private void addBitmapToMemoryCache(String uri, BitmapDisplayConfig config, Bitmap bitmap, long expiryTimestamp) throws IOException { if (uri != null && bitmap != null && globalConfig.isMemoryCacheEnabled() && mMemoryCache != null) { MemoryCacheKey key = new MemoryCacheKey(uri, config == null ? null : config.toString()); mMemoryCache.put(key, bitmap, expiryTimestamp); } }这里创建key,把这个bitmap缓存到内存缓存中。
回到主线,如果本地缓存没有,那么就需要从网络上面获取下来。download方法挺大的,大致逻辑:先判断是否启用磁盘缓存,若有,启用disklrucache缓存图片,若无,直接下载到内存中。最后自动转图片+添加到内存缓存中。
public Bitmap downloadBitmap(String uri, BitmapDisplayConfig config, final BitmapUtils.BitmapLoadTask<?> task) { BitmapMeta bitmapMeta = new BitmapMeta(); OutputStream outputStream = null; LruDiskCache.Snapshot snapshot = null; try { Bitmap bitmap = null; // try download to disk if (globalConfig.isDiskCacheEnabled()) { synchronized (mDiskCacheLock) { // Wait for disk cache to initialize while (!isDiskCacheReadied) { try { mDiskCacheLock.wait(); } catch (Throwable e) { } } if (mDiskLruCache != null) { try { snapshot = mDiskLruCache.get(uri); if (snapshot == null) { LruDiskCache.Editor editor = mDiskLruCache.edit(uri); if (editor != null) { outputStream = editor.newOutputStream(DISK_CACHE_INDEX); bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task); if (bitmapMeta.expiryTimestamp < 0) { editor.abort(); return null; } else { editor.setEntryExpiryTimestamp(bitmapMeta.expiryTimestamp); editor.commit(); } snapshot = mDiskLruCache.get(uri); } } if (snapshot != null) { bitmapMeta.inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); bitmap = decodeBitmapMeta(bitmapMeta, config); if (bitmap == null) { bitmapMeta.inputStream = null; mDiskLruCache.remove(uri); } } } catch (Throwable e) { LogUtils.e(e.getMessage(), e); } } } } // try download to memory stream,说明手机sd卡不能用,下载到内存中 if (bitmap == null) { outputStream = new ByteArrayOutputStream(); bitmapMeta.expiryTimestamp = globalConfig.getDownloader().downloadToStream(uri, outputStream, task); if (bitmapMeta.expiryTimestamp < 0) { return null; } else { bitmapMeta.data = ((ByteArrayOutputStream) outputStream).toByteArray(); bitmap = decodeBitmapMeta(bitmapMeta, config); } } if (bitmap != null) {//你懂得 bitmap = rotateBitmapIfNeeded(uri, config, bitmap); addBitmapToMemoryCache(uri, config, bitmap, bitmapMeta.expiryTimestamp); } return bitmap; } catch (Throwable e) { LogUtils.e(e.getMessage(), e); } finally { IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(snapshot); } return null; }
24行开始,是disklrucache存图片的标准用法,需要一个edit对象,利用这个对象创建输出流,传进downloadToStream方法。跟踪这个方法,Downloader类是一个抽象类,实现在SimpleDownloader。
public long downloadToStream(String uri, OutputStream outputStream, final BitmapUtils.BitmapLoadTask<?> task) { if (task == null || task.isCancelled() || task.getTargetContainer() == null) return -1; URLConnection urlConnection = null; BufferedInputStream bis = null; OtherUtils.trustAllSSLForHttpsURLConnection(); long result = -1; long fileLen = 0; long currCount = 0; try { if (uri.startsWith("/")) { FileInputStream fileInputStream = new FileInputStream(uri); fileLen = fileInputStream.available(); bis = new BufferedInputStream(fileInputStream); result = System.currentTimeMillis() + this.getDefaultExpiry(); } else if (uri.startsWith("assets/")) { InputStream inputStream = this.getContext().getAssets().open(uri.substring(7, uri.length())); fileLen = inputStream.available(); bis = new BufferedInputStream(inputStream); result = Long.MAX_VALUE; } else { final URL url = new URL(uri); urlConnection = url.openConnection(); urlConnection.setConnectTimeout(this.getDefaultConnectTimeout()); urlConnection.setReadTimeout(this.getDefaultReadTimeout()); bis = new BufferedInputStream(urlConnection.getInputStream()); result = urlConnection.getExpiration(); result = result < System.currentTimeMillis() ? System.currentTimeMillis() + this.getDefaultExpiry() : result; fileLen = urlConnection.getContentLength(); } if (task.isCancelled() || task.getTargetContainer() == null) return -1; byte[] buffer = new byte[4096]; int len = 0; while ((len = bis.read(buffer)) != -1) { outputStream.write(buffer, 0, len); currCount += len; if (task.isCancelled() || task.getTargetContainer() == null) return -1; task.updateProgress(fileLen, currCount); } outputStream.flush(); } catch (Throwable e) { result = -1; LogUtils.e(e.getMessage(), e); } finally { IOUtils.closeQuietly(bis); } return result; }
这里终于看到了,他是如何识别要加载的是本地图片还是网络图片。本地图片好办,传进路径建立起输入流,这里网络图片用的是urlConnection里建立http连接,一般安卓里面都是用httpclient,这是原声Java的api,这里可以了解一下:urlconnection
拿到输入流就好办,边读边写,把流写到本地缓存文件中。
到此,基本结束。(虽然里面还要很多很多方法,但确实能力有限)
处女作,轻喷~