现在网上对此Imageloader图片加载的开源框架的解析有好多文章,有好多只是简单分析它的实现,此篇文章是通过自己对其源码的分析,对它的实现方式进行分析,针对它用到的重点知识点进行重点介绍,以及自己对于此框架的理解。下面的分析从以下两个方面进行分析。
Imageloader的初始化
Imageloader加载图片的实现方式分析
1.Imageloader的初始化
Imageloader是在Application的onCreate()方法中进行初始化的,在官方的demo 中的初始化方式如下:
ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context); config.threadPriority(Thread.NORM_PRIORITY - 2); config.denyCacheImageMultipleSizesInMemory(); config.diskCacheFileNameGenerator(new Md5FileNameGenerator()); config.diskCacheSize(50 * 1024 * 1024); // 50 MiB config.tasksProcessingOrder(QueueProcessingType.LIFO); config.writeDebugLogs(); // Remove for release app // Initialize ImageLoader with configuration. ImageLoader.getInstance().init(config.build());从上面我们能看出,官方的demo对指定的配置选项进行了配置,当然,没有设置的选项,系统也会采用默认的方式进行实现。可以通过config.build()方法中实现的,会调用initEmptyFieldsWithDefaultValues()方法会将那些必要的并且没有设置的配置参数进行初始化,比如downloader(执行下载的对象),decoder(执行解码的对象)等等,如果想了解,可以自己查看更多默认参数,下面分析在加载的图片的流程中,用到这些对象会对其进行详细的分析。
2.Imageloader加载图片的实现方式分析
本文主要对displayImage()方法进行分析,其实其他方式原理都一样,就不过多描述了。实现看displayImage()参数列表如下:
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)其实其中,需要了解的也就是参数三的DisplayImageOptions了,这是对显示的图片的配置信息,我们来看官方demo的实现方式如下:
options = new DisplayImageOptions.Builder() .showImageOnLoading(R.drawable.ic_stub) .showImageForEmptyUri(R.drawable.ic_empty) .showImageOnFail(R.drawable.ic_error) .cacheInMemory(true) .cacheOnDisk(true) .considerExifParams(true) .displayer(new CircleBitmapDisplayer(Color.WHITE, 5)) .build();其中重点需要说一下的就是设置displayer(),这里实现的是显示圆形图片,系统还提供了其他几种Displayer,都是继承Bitmap Displayer进行扩展实现的。内部的实现是通过Drawable实现,而不是对ImageView进行操作了,这样性能会更好。实现原理就是将Bitmap画到Drawable,然后直接将Drawable设置给ImageVIew即可。我记得好像之前看到鸿阳有一篇文件就是写的这方面,想要了解的可以看一下。这里代码的具体实现:
public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) { <span style="white-space:pre"> </span>if (!(imageAware instanceof ImageViewAware)) { throw new IllegalArgumentException("ImageAware should wrap ImageView. ImageViewAware is expected."); } imageAware.setImageDrawable(new CircleDrawable(bitmap, strokeColor, strokeWidth)); }内部有一个CircleDrawable的静态内部泪,它继承自Drawable,具体实现,以及原理,大家可以自己查看,以及查阅相关资料,这个不是这里的重点。
继续上面displayImage()的分析,其他最后它真正调用的方法是:
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
下面分析其内部代码的实现(只截取重要部分):
if (targetSize == null) { //根据 ImageView的宽高 和 设置的最大图片宽高 计算出 目标图片的尺寸(如果图片宽(高)等于0,那么会取configuration中的设置的最大图片的尺寸(默认去屏幕尺寸)) targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize()); } //根据 uri 和 图片尺寸计算出 缓存的key String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);//存储起来(Map存储,key:imageView的hashcode;value:计算出来的 cachekey) listener.onLoadingStarted(uri, imageAware.getWrappedView());//回调函数的回调 //第一层缓存(内存缓存) Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);//普通的基于lru算法的缓存(使用LinkedHashMap实现) if (bmp != null && !bmp.isRecycled()) {//缓存中有,就直接取出来,不去通过网络获取了 L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey); if (options.shouldPostProcess()) {//是否设置了BitmapProcessor(Bitmap处理器,对 Bitmap获取到的Bitmap做相应的处理) ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, defineHandler(options));//这是一个Runnable任务,run主体方法中,会调用Options中设置的BitmapProcessor处理图片,然后加入到engine的线程池中执行对应显示图片的任务 if (options.isSyncLoading()) {//同步执行,直接调用 displayTask.run(); } else {//异步执行,添加到线程池中,等待异步执行 engine.submit(displayTask); } } else { options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);//根据设置Displayer直接为ImageView设置Drawable(根据要求,通过Bitmap创建对应的Drawable) listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回调方法的调用 } } else {// 内存缓存没有,走 第二层磁盘缓存 if (options.shouldShowImageOnLoading()) {//设置正在加载的图片 imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources)); } else if (options.isResetViewBeforeLoading()) {//如果设置加载之前,重置图片,那么就将ImageView的 图像设置为null imageAware.setImageDrawable(null); } ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri));//最后一个参数:创建url对应的线程锁 LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo, defineHandler(options));//创建下载任务 if (options.isSyncLoading()) {//同步 displayTask.run();//直接调用 Runable的 run()方法 } else {//异步执行(默认)(运行在子线程中,将其添加到线程池中) engine.submit(displayTask); } }代码都已经加上注释,其实它也是采用的两级级缓存,通过之前的配置选项也可以看出来,内存,本地磁盘这两级缓存来优化体验。每个代码的内部具体实现,自己查阅内部代码实现吧,我就不详细分析其内部实现,下面重点分析LoadAndDisplayImageTask这个任务中的具体实现,其实它内部就是先从磁盘缓存中查找是否含有对应的缓存,有的话直接取出来,进行处理解码设置给ImageView,否者调用对应的downloader去下载对应的资源。主要看它run()方法的实现:
loadFromUriLock.lock();//线程安全 Bitmap bmp; try { checkTaskNotActual();//判断当前ImageView是否回首掉了,是否正在被其他线程使用 bmp = configuration.memoryCache.get(memoryCacheKey);//从缓存中获取数据 if (bmp == null || bmp.isRecycled()) {//没有从缓存中获取数据,或者bitmap已经回收掉了 bmp = tryLoadBitmap();//先从缓存中获取数据,如果没有,再从网络下载,然后存储在到本地(如果设置缓存在本地) if (bmp == null) return; // listener callback already was fired checkTaskNotActual(); checkTaskInterrupted(); if (options.shouldPreProcess()) { L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey); bmp = options.getPreProcessor().process(bmp);//提前处理器,对Bitmap进行相应的处理(这个需要自己配置,并且需要自己实现对应的PreProcessor) if (bmp == null) { L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey); } } //如果设置缓存到内存中,将数据缓存到内存中.(存储到内存中的是已经压缩处理后的图片) if (bmp != null && options.isCacheInMemory()) { L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey); configuration.memoryCache.put(memoryCacheKey, bmp); } } else { loadedFrom = LoadedFrom.MEMORY_CACHE; L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey); } if (bmp != null && options.shouldPostProcess()) { L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey); bmp = options.getPostProcessor().process(bmp);//如果配置,执行对Bitmap的处理操作 if (bmp == null) { L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey); } } checkTaskNotActual(); checkTaskInterrupted(); } catch (TaskCancelledException e) { fireCancelEvent(); return; } finally { loadFromUriLock.unlock(); } DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);//为Imageview设置图片的task,内部是调用displayer实现的 runTask(displayBitmapTask, syncLoading, handler, engine);//如果是异步操作,并且handler不为null,用handler执行此task,也就是设置图片的操作要运行在主线程中下面主要对tryLoadBitmap()方法内部进行分析,其内部会先从内存中获取图片,如果有处理之后返回,如果没有,使用downloader去下载图片。
File imageFile = configuration.diskCache.get(uri);//下载之前从缓存中获取(可能已经存在,其他线程现在了相同的图片) if (imageFile != null && imageFile.exists() && imageFile.length() > 0) { L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey); loadedFrom = LoadedFrom.DISC_CACHE; checkTaskNotActual(); bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));//decodeImage()操作:读取数据流,然后调整图片的应该缩小的大小(使用BitmapFactory.Options实现, // 里面使用BufferInputStream来存储数据流,重复使用) } if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey); loadedFrom = LoadedFrom.NETWORK; String imageUriForDecoding = uri; //第一个判断:是否缓存到本地,只有第一个判断为true,才会执行第二个判断方法 if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//tryCacheImageOnDisk()下载图片,并缓存本地 imageFile = configuration.diskCache.get(uri); if (imageFile != null) { imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath()); } } checkTaskNotActual(); //如果没有设置缓存本地,那么执行下面的方法的时候,imageUriForDecoding 还是 http,还是会调用 Downloader的 getStream()方法, //此方法会判断uri是什么开头的,可以处理http,file,content,assets等 bitmap = decodeImage(imageUriForDecoding); if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { fireFailEvent(FailType.DECODING_ERROR, null); } }首先对decodeImage()方法进行分析,此方法就是调用decoder的decode()方法进行实现,所以直接看官方demo使用的BaseImageDecoder的decode()方法的实现:
InputStream imageStream = getImageStream(decodingInfo);//如果没有下载,下载图片,否则获取到缓存到本地的file的流 if (imageStream == null) { L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey()); return null; } try { imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//创建一个file info(计算出图片流的图片的实际尺寸) //这里很重要;因为上面已经decodeStream()已经将上面的流给读取了,当前流指定的位置已经改变了,所以下面的流要重置一下(两种方式,如果支持mark(),那么就reset()之前的位置) //如果不支持,那么就重新下载这个流 imageStream = resetStream(imageStream, decodingInfo);//重置图片流,如果图片流可以重置到之前读取的位置,那么就reset(),如果不能,就重新下载这个流 //创建缩放的比例,然后赋值创建Options返回 Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);//参数1:图片流的大小, decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);//根据流创建对应的Bitmap(如果这里的图片源太大,此方法会报异常,亲测结果) } finally { IoUtils.closeSilently(imageStream);//不要忘记:将数据流关闭 } if (decodedBitmap == null) { L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey()); } else { decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal); } return decodedBitmap;有两个地方需要注意一下,第一个:getImageStream()获取的输入流是缓冲输入流,支持markSupport()和reset()和mark()方法,如果一个流都到末尾之后,可以调用reset()将读取位置重置到头部,这个流就可以继续读取数据。在BitmapFactory.decodeStream()第一次获取图片的尺寸的时候,已经将流读到末尾了;第二次BitmapFactory.decodeStream()的之前必须调用这个输入流的reset()方法重置这个流,才能继续使用这个流。第二个:BitmapFactory.decodeStream()方法报异常,由于图片太大,没找到解决方法(希望哪位大神了解的,告知小弟)。
下面分析,磁盘缓存没有对应的缓存的时候,执行的方法:tryCacheImageOnDisk().
loaded = downloadImage();//下载图片,并将图片缓存到本地缓存中 if (loaded) { int width = configuration.maxImageWidthForDiskCache; int height = configuration.maxImageHeightForDiskCache; if (width > 0 || height > 0) { L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey); resizeAndSaveImage(width, height);// 调整图片大小,并将调整后的图片的Bitmap缓存在本地缓存中<span style="font-family: Arial, Helvetica, sans-serif;">// TODO : process boolean result</span> } }首先看downloadImage()方法的实现:
InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader()); if (is == null) { L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey); return false; } else { try { return configuration.diskCache.save(uri, is, this);//下载完成之后,缓存在本地(没有经过处理和压缩的图片流) } finally { IoUtils.closeSilently(is);//释放数据流 } }紧接着看getDownloader.getStream()的实现,官方demo采用的是BaseImageDownloader 的 getStream()的实现:
switch (Scheme.ofUri(imageUri)) { case HTTP: case HTTPS://网络获取 return getStreamFromNetwork(imageUri, extra); case FILE://本地文件中获取 <span style="white-space:pre"> </span> return getStreamFromFile(imageUri, extra); case CONTENT://ContentProvider return getStreamFromContent(imageUri, extra); case ASSETS://assets文件夹中获取 return getStreamFromAssets(imageUri, extra); case DRAWABLE://从资源文件中获取 return getStreamFromDrawable(imageUri, extra); case UNKNOWN: default: return getStreamFromOtherSource(imageUri, extra); }主要看从网络获取图片实现:
<span style="white-space:pre"> </span>HttpURLConnection conn = createConnection(imageUri, extra); int redirectCount = 0; while (conn.getResponseCode() / 100 == 3 && redirectCount < MAX_REDIRECT_COUNT) { conn = createConnection(conn.getHeaderField("Location"), extra); redirectCount++; } InputStream imageStream; try { imageStream = conn.getInputStream(); } catch (IOException e) { // Read all data to allow reuse connection (http://bit.ly/1ad35PY) IoUtils.readAndCloseStream(conn.getErrorStream()); throw e; } if (!shouldBeProcessed(conn)) {//状态码!=200 IoUtils.closeSilently(imageStream); throw new IOException("Image request failed with response code " + conn.getResponseCode()); } //注意它将外层包裹了 BufferedInputStream(使用缓冲输入流,它可以markSupport()返回true,支持mark()和reset()操作,可以重复读取流数据) //解决问题:在BitmapFactory.decode()执行之后,读取的位置到达了流的结尾,如果是这个对象,就可以调用reset()方法之后继续使用这个流 return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());下面看resizeAndSaveImage()方法的实现,内部调整存储在本地的图片流的大小,生成对应的Bitmap存储到磁盘缓存中。
<span style="white-space:pre"> </span>File targetFile = configuration.diskCache.get(uri); if (targetFile != null && targetFile.exists()) { ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight); DisplayImageOptions specialOptions = new DisplayImageOptions.Builder().cloneFrom(options) .imageScaleType(ImageScaleType.IN_SAMPLE_INT).build(); ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, Scheme.FILE.wrap(targetFile.getAbsolutePath()), uri, targetImageSize, ViewScaleType.FIT_INSIDE, getDownloader(), specialOptions); Bitmap bmp = decoder.decode(decodingInfo); if (bmp != null && configuration.processorForDiskCache != null) { L.d(LOG_PROCESS_IMAGE_BEFORE_CACHE_ON_DISK, memoryCacheKey); bmp = configuration.processorForDiskCache.process(bmp); if (bmp == null) { L.e(ERROR_PROCESSOR_FOR_DISK_CACHE_NULL, memoryCacheKey); } } if (bmp != null) { saved = configuration.diskCache.save(uri, bmp);//将处理完的Bitmap存储到本地缓存 bmp.recycle(); } }