ImageLoader深入源码学习探究
来源:互联网 发布:snh48家境 知乎 编辑:程序博客网 时间:2024/05/22 00:31
由于我当前的ImageLoader版本与读者们的版本可能不同,所以下面讲解的地方可能存在一些出入,但大体上的实现基本一致,请读者自己参照自己的imageloader源码来分析
一般在使用ImageLoader的时候都需要进行一些配置 如下//显示图片的配置
DisplayImageOptions options = new DisplayImageOptions.Builder() .showImageOnLoading(R.drawable.default) .showImageOnFail(R.drawable.error) .cacheInMemory(true) .cacheOnDisk(true) .bitmapConfig(Bitmap.Config.RGB_565) .build();然后会调用displayImage方法来加载图片
ImageLoader.getInstance().displayImage(imageUrl, mImageView, options);我们看下一下displayImage的实现
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options, ImageLoadingListener listener) { this.displayImage(uri, (ImageAware)(new ImageViewAware(imageView)), options, listener);}将imageView转化成ImageViewAware,ImageViewAware实现了ImageAware接口,我们来看一下ImageViewAware 中的方法
首先是构造方法
public ImageViewAware(ImageView imageView) { this(imageView, true); } public ImageViewAware(ImageView imageView, boolean checkActualViewSize) { this.imageViewRef = new WeakReference(imageView); this.checkActualViewSize = checkActualViewSize; }可以看到在第一个构造方法中调用了第二个构造方法,在第二个构造方法中使用了WeakReference,即将我们的imageView由强引用转化为弱引用,这样当内存不足的时候,可以更好的回收ImageView对象
接下来看一下displayImage的具体实现代码↓
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageLoadingListener listener) { this.checkConfiguration(); if(imageAware == null) { throw new IllegalArgumentException("Wrong arguments were passed to displayImage() method (ImageView reference must not be null)"); } else { if(listener == null) { listener = this.emptyListener; } if(options == null) { options = this.configuration.defaultDisplayImageOptions; } if(TextUtils.isEmpty(uri)) { this.engine.cancelDisplayTaskFor(imageAware); listener.onLoadingStarted(uri, imageAware.getWrappedView()); if(options.shouldShowImageForEmptyUri()) { imageAware.setImageDrawable(options.getImageForEmptyUri(this.configuration.resources)); } else { imageAware.setImageDrawable((Drawable)null); } listener.onLoadingComplete(uri, imageAware.getWrappedView(), (Bitmap)null); } else { ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, this.configuration.getMaxImageSize()); String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize); this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); listener.onLoadingStarted(uri, imageAware.getWrappedView()); Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey); ImageLoadingInfo imageLoadingInfo; if(bmp != null && !bmp.isRecycled()) { if(this.configuration.writeLogs) { L.d("Load image from memory cache [%s]", new Object[]{memoryCacheKey}); } if(options.shouldPostProcess()) { imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask1.run(); } else { this.engine.submit(displayTask1); } } else { bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); } } else { if(options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources)); } else if(options.isResetViewBeforeLoading()) { imageAware.setImageDrawable((Drawable)null); } imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask.run(); } else { this.engine.submit(displayTask); } } } } }在displayImage方法具体的实现中,第一步调用了checkConfiguration()方法
private void checkConfiguration() { if(this.configuration == null) { throw new IllegalStateException("ImageLoader must be init with configuration before using"); } }当我们的配置是空时,则会抛出异常ImageLoader must be init with configuration before using,这个异常在新手使用时比较容易遇到,这是因为没有init我们的imageloader,下面是初始化的代码(下面这段初始化的代码只提供参考,可根据实际情况自己配置自己需要的参数)
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).threadPriority(Thread.NORM_PRIORITY - 2).denyCacheImageMultipleSizesInMemory().discCacheFileNameGenerator(new Md5FileNameGenerator()).tasksProcessingOrder(QueueProcessingType.LIFO).build();ImageLoader.getInstance().init(config);我们再来分析一下displayImage中的这段代码
if(TextUtils.isEmpty(uri)) { this.engine.cancelDisplayTaskFor(imageAware); listener.onLoadingStarted(uri, imageAware.getWrappedView()); if(options.shouldShowImageForEmptyUri()) { imageAware.setImageDrawable(options.getImageForEmptyUri(this.configuration.resources)); } else { imageAware.setImageDrawable((Drawable)null); } listener.onLoadingComplete(uri, imageAware.getWrappedView(), (Bitmap)null); } else { .... }在if语句中,处理的就是当我们传递进去的url为空的情况,我们看到this.engine.cancelDisplayTaskFor(imageAware);有这么一句,那么这一句是什么意思呢?
engine是一个ImageLoaderEngine对象,ImageLoaderEngine中存在一个HashMap,用来记录正在加载的任务,加载图片的时候会将ImageView的id和图片的url加上尺寸加入到HashMap中,加载完成之后会将其移除,我们可以看cancelDisplayTaskFor的具体试下,他将正在加载中的任务的当前iamgeAware给remove掉了
void cancelDisplayTaskFor(ImageAware imageAware) { this.cacheKeysForImageAwares.remove(Integer.valueOf(imageAware.getId())); }然后将DisplayImageOptions的imageResForEmptyUri的图片设置给ImageView,最后回调给ImageLoadingListener接口告诉它这次任务完成了。
接下来我们就来分析一下在url不为空的情况下,这才是我们应该着重关注的部分
if(TextUtils.isEmpty(uri)) { ... } else { ImageSize targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, this.configuration.getMaxImageSize()); String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize); this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); listener.onLoadingStarted(uri, imageAware.getWrappedView()); Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey); ImageLoadingInfo imageLoadingInfo; if(bmp != null && !bmp.isRecycled()) { if(this.configuration.writeLogs) { L.d("Load image from memory cache [%s]", new Object[]{memoryCacheKey}); } if(options.shouldPostProcess()) { imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask1.run(); } else { this.engine.submit(displayTask1); } } else { bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); } } else { if(options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources)); } else if(options.isResetViewBeforeLoading()) { imageAware.setImageDrawable((Drawable)null); } imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask.run(); } else { this.engine.submit(displayTask); } } }首先它会调用ImageSizeUtils类的defineTargetSizeForView方法 将我们的imageAware封装为一个ImageSize对象 ,defineTargetSizeForView方法实现如下
public static ImageSize defineTargetSizeForView(ImageAware imageAware, ImageSize maxImageSize) { int width = imageAware.getWidth(); if(width <= 0) { width = maxImageSize.getWidth(); } int height = imageAware.getHeight(); if(height <= 0) { height = maxImageSize.getHeight(); } return new ImageSize(width, height); }如果获取ImageView的宽高小于等于0,就会使用手机屏幕的宽高作为ImageView的宽高。
String memoryCacheKey = MemoryCacheUtil.generateKey(uri, targetSize);这一句的作用是生成一个缓存时使用的key,再从缓存中取数据的时候通过该key值来获取generateKey方法如下,非常简单,大家看看就好哈,这里就不说了↓
public static String generateKey(String imageUri, ImageSize targetSize) { return imageUri + "_" + targetSize.getWidth() + "x" + targetSize.getHeight(); }
this.engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);这一句就是将当前任务加入到haspmap中记录起来,cacheKeysForImageAwares就是一个haspMap 如下↓
void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) { this.cacheKeysForImageAwares.put(Integer.valueOf(imageAware.getId()), memoryCacheKey); }
Bitmap bmp = (Bitmap)this.configuration.memoryCache.get(memoryCacheKey);这一句代码从内存缓存中获取Bitmap对象,我们可以再ImageLoaderConfiguration中配置内存缓存逻辑,默认使用的是LruMemoryCache。
我们再来看接下来的这一段代码
if(options.shouldPostProcess()) { imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); ProcessAndDisplayImageTask displayTask1 = new ProcessAndDisplayImageTask(this.engine, bmp, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask1.run(); } else { this.engine.submit(displayTask1); } } else { bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); }这一段代码是在if(bmp != null && !bmp.isRecycled())为true的情况下执行的,就是说是在缓存不为空且没有被回收的条件下执行的。我们如果在DisplayImageOptions中设置了postProcessor就进入true逻辑,不过默认postProcessor是为null的,BitmapProcessor接口主要是对Bitmap进行处理,这个框架并没有给出相对应的实现,如果我们有自己的需求的时候可以自己实现BitmapProcessor接口。
bmp = options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);这两行主要是将Bitmap设置到ImageView上面,这里我们可以在DisplayImageOptions中配置显示需求displayer,默认使用的是SimpleBitmapDisplayer,直接将Bitmap设置到ImageView上面,我们可以配置其他的显示逻辑, 他这里提供了FadeInBitmapDisplayer(透明度从0-1)RoundedBitmapDisplayer(4个角是圆弧)等, 然后回调ImageLoadingListener接口onLoadingComplete 加载完成。
if(options.shouldShowImageOnLoading()) { imageAware.setImageDrawable(options.getImageOnLoading(this.configuration.resources)); } else if(options.isResetViewBeforeLoading()) { imageAware.setImageDrawable((Drawable)null); } imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, this.engine.getLockForUri(uri)); LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(this.engine, imageLoadingInfo, options.getHandler()); if(options.isSyncLoading()) { displayTask.run(); } else { this.engine.submit(displayTask); }
这段代码主要是Bitmap不在内存缓存,从文件中或者网络里面获取bitmap对象,实例化一个LoadAndDisplayImageTask对象,LoadAndDisplayImageTask实现了Runnable,如果配置了isSyncLoading为true, 直接执行LoadAndDisplayImageTask的run方法,表示同步,默认是false,将LoadAndDisplayImageTask提交给线程池对象
我们来看一下LoadAndDisplayImageTask中的run方法如何实现的
public void run() { if(!this.waitIfPaused()) { if(!this.delayIfNeed()) { ... } } }
当waitIfPaused()和delayIfNeed()方法返回true时,会直接结束run方法,我们先来看看这两个方法的实现
waitIfPaused()方法
private boolean waitIfPaused() { AtomicBoolean pause = this.engine.getPause(); synchronized(pause) { if(pause.get()) { this.log("ImageLoader is paused. Waiting... [%s]"); try { pause.wait(); } catch (InterruptedException var5) { L.e("Task was interrupted [%s]", new Object[]{this.memoryCacheKey}); return true; } this.log(".. Resume loading [%s]"); } } return this.checkTaskIsNotActual(); }
这个方法是干嘛用呢,主要是我们在使用ListView,GridView去加载图片的时候,有时候为了滑动更加的流畅,我们会选择手指在滑动或者猛地一滑动的时候不去加载图片,所以才提出了这么一个方法,那么要怎么用呢? 这里用到了PauseOnScrollListener这个类,使用很简单ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling )), pauseOnScroll控制我们缓慢滑动ListView,GridView是否停止加载图片,pauseOnFling 控制猛的滑动ListView,GridView是否停止加载图片
除此之外,这个方法的返回值由isTaskNotActual()决定,我们接着看看checkTaskIsNotActual()的源码
private boolean checkTaskIsNotActual() { return this.checkViewCollected() || this.checkViewReused(); }
heckViewCollected()是判断我们ImageView是否被垃圾回收器回收了,如果回收了,LoadAndDisplayImageTask方法的run()就直接返回了,checkViewReused()判断该ImageView是否被重用,被重用run()方法也直接返回,为什么要用checkViewReused()方法呢?主要是ListView,GridView我们会复用item对象,假如我们先去加载ListView,GridView第一页的图片的时候,第一页图片还没有全部加载完我们就快速的滚动,checkViewReused()方法就会避免这些不可见的item去加载图片,而直接加载当前界面的图片
delayIfNeed()方法与waitIfPaused() 一样,都是由checkTaskIsNotActual()来控制返回值,就不多说这个方法了。
然后我们来看看当这两个都返回false时,执行的代码
ReentrantLock loadFromUriLock = this.imageLoadingInfo.loadFromUriLock; this.log("Start display image task [%s]"); if(loadFromUriLock.isLocked()) { this.log("Image already is loading. Waiting... [%s]"); } loadFromUriLock.lock(); Bitmap bmp; try { if(this.checkTaskIsNotActual()) { return; } bmp = (Bitmap)this.configuration.memoryCache.get(this.memoryCacheKey); if(bmp == null) { bmp = this.tryLoadBitmap(); if(this.imageAwareCollected) { return; } if(bmp == null) { return; } if(this.checkTaskIsNotActual() || this.checkTaskIsInterrupted()) { return; } if(this.options.shouldPreProcess()) { this.log("PreProcess image before caching in memory [%s]"); bmp = this.options.getPreProcessor().process(bmp); if(bmp == null) { L.e("Pre-processor returned null [%s]", new Object[0]); } } if(bmp != null && this.options.isCacheInMemory()) { this.log("Cache image in memory [%s]"); this.configuration.memoryCache.put(this.memoryCacheKey, bmp); } } else { this.loadedFrom = LoadedFrom.MEMORY_CACHE; this.log("...Get cached bitmap from memory after waiting. [%s]"); } if(bmp != null && this.options.shouldPostProcess()) { this.log("PostProcess image before displaying [%s]"); bmp = this.options.getPostProcessor().process(bmp); if(bmp == null) { L.e("Pre-processor returned null [%s]", new Object[]{this.memoryCacheKey}); } } } finally { loadFromUriLock.unlock(); } if(!this.checkTaskIsNotActual() && !this.checkTaskIsInterrupted()) { DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, this.imageLoadingInfo, this.engine, this.loadedFrom); displayBitmapTask.setLoggingEnabled(this.writeLogs); if(this.options.isSyncLoading()) { displayBitmapTask.run(); } else { this.handler.post(displayBitmapTask); } }
我们可以看到在第一行中有一个loadFromUriLock,这个其实就是一个锁,他可以通过ImageLoaderEngine类的getLockForUri()方法来获取
ReentrantLock getLockForUri(String uri) { ReentrantLock lock = uriLocks.get(uri); if (lock == null) { lock = new ReentrantLock(); uriLocks.put(uri, lock); } return lock; }
这个锁对象与图片的url是相互对应的,为什么要这么做?不知道大家有没有考虑过一个场景,假如在一个ListView中,某个item正在获取图片的过程中,而此时我们将这个item滚出界面之后又将其滚进来,滚进来之后如果没有加锁,该item又会去加载一次图片,假设在很短的时间内滚动很频繁,那么就会出现多次去网络上面请求图片,所以这里根据图片的Url去对应一个ReentrantLock对象,让具有相同Url的请求就会在等待,等到这次图片加载完成之后,ReentrantLock就被释放,刚刚那些相同Url的请求才会继续执行下面的代码
接下来又会执行bmp = (Bitmap)this.configuration.memoryCache.get(this.memoryCacheKey);这一句代码,先从内存缓存中获取一遍,如果内存缓存中没有在去执行下面的逻辑,所以ReentrantLock的作用就是避免这种情况下重复的去从网络上面请求图片。
当内存中没有缓存该图片时 会执行一个tryLoadBitmap()方法,
private Bitmap tryLoadBitmap() { File imageFile = this.getImageFileInDiscCache(); Bitmap bitmap = null; try { if(imageFile.exists()) { this.log("Load image from disc cache [%s]"); this.loadedFrom = LoadedFrom.DISC_CACHE; bitmap = this.decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath())); if(this.imageAwareCollected) { return null; } } if(bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { this.log("Load image from network [%s]"); this.loadedFrom = LoadedFrom.NETWORK; String e = this.options.isCacheOnDisc()?this.tryCacheImageOnDisc(imageFile):this.uri; if(!this.checkTaskIsNotActual()) { bitmap = this.decodeImage(e); if(this.imageAwareCollected) { return null; } if(bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { this.fireFailEvent(FailType.DECODING_ERROR, (Throwable)null); } } } } catch (IllegalStateException var4) { this.fireFailEvent(FailType.NETWORK_DENIED, (Throwable)null); } catch (IOException var5) { L.e(var5); this.fireFailEvent(FailType.IO_ERROR, var5); if(imageFile.exists()) { imageFile.delete(); } } catch (OutOfMemoryError var6) { L.e(var6); this.fireFailEvent(FailType.OUT_OF_MEMORY, var6); } catch (Throwable var7) { L.e(var7); this.fireFailEvent(FailType.UNKNOWN, var7); } return bitmap; }
这里面的逻辑是先从文件缓存中获取有没有Bitmap对象,如果没有在去从网络中获取,然后将bitmap保存在文件系统中,我们来看一下它从网络获取图片后是如何进行缓存的
String e = this.options.isCacheOnDisc()?this.tryCacheImageOnDisc(imageFile):this.uri;
先检查是否配置了DisplayImageOptions的isCacheOnDisk,表示是否需要将Bitmap对象保存在文件系统中,一般我们需要配置为true,当为true时就会调用tryCacheImageOnDisc()这个方法了
private boolean tryCacheImageOnDisk() throws TaskCancelledException { L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey); boolean loaded; try { 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); // TODO : process boolean result } } } catch (IOException e) { L.e(e); loaded = false; } return loaded; }
private boolean downloadImage() throws IOException { InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader()); return configuration.diskCache.save(uri, is, this); }
downloadImage()方法是负责下载图片,并将其保持到文件缓存中,将下载保存Bitmap的进度回调到IoUtils.CopyListener接口的onBytesCopied(int current, int total)方法中,所以我们可以设置ImageLoadingProgressListener接口来获取图片下载保存的进度,这里保存在文件系统中的图片是原图
int width = configuration.maxImageWidthForDiskCache; int height = configuration.maxImageHeightForDiskCache;
获取ImageLoaderConfiguration是否设置保存在文件系统中的图片大小,如果设置了maxImageWidthForDiskCache和maxImageHeightForDiskCache,会调用resizeAndSaveImage()方法对图片进行裁剪然后在替换之前的原图,保存裁剪后的图片到文件系统的,所以我们只要在Application中实例化ImageLoaderConfiguration的时候设置maxImageWidthForDiskCache和maxImageHeightForDiskCache就可以保存缩略图了
然后我们再回到run方法中,执行完tryLoadBitmap()后会执行下面这段代码,将图片保存到内存缓存中去
if(bmp != null && this.options.isCacheInMemory()) { this.log("Cache image in memory [%s]"); this.configuration.memoryCache.put(this.memoryCacheKey, bmp); }
最后这一段代码就是一个显示的过程
if(!this.checkTaskIsNotActual() && !this.checkTaskIsInterrupted()) { DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, this.imageLoadingInfo, this.engine, this.loadedFrom); displayBitmapTask.setLoggingEnabled(this.writeLogs); if(this.options.isSyncLoading()) { displayBitmapTask.run(); } else { this.handler.post(displayBitmapTask); } }
我们直接看一下displayBitmapTask.run();方法
public void run() { if(this.imageAware.isCollected()) { if(this.loggingEnabled) { L.d("ImageAware was collected by GC. Task is cancelled. [%s]", new Object[]{this.memoryCacheKey}); } this.listener.onLoadingCancelled(this.imageUri, this.imageAware.getWrappedView()); } else if(this.isViewWasReused()) { if(this.loggingEnabled) { L.d("ImageAware is reused for another image. Task is cancelled. [%s]", new Object[]{this.memoryCacheKey}); } this.listener.onLoadingCancelled(this.imageUri, this.imageAware.getWrappedView()); } else { if(this.loggingEnabled) { L.d("Display image in ImageAware (loaded from %1$s) [%2$s]", new Object[]{this.loadedFrom, this.memoryCacheKey}); } Bitmap displayedBitmap = this.displayer.display(this.bitmap, this.imageAware, this.loadedFrom); this.listener.onLoadingComplete(this.imageUri, this.imageAware.getWrappedView(), displayedBitmap); this.engine.cancelDisplayTaskFor(this.imageAware); } }
假如ImageView被回收了或者被重用了,就回调ImageLoadingListener接口的onLoadingCancelled方法,否则就调用BitmapDisplayer去显示Bitmap。
到此整个加载和缓存的过程就讲完了,里面有很多讲得不好的地方 欢迎大家一起讨论。
- ImageLoader深入源码学习探究
- ImageLoader深入源码学习探究
- EventBus源码学习与探究
- ImageLoader 源码
- ImageLoader学习
- Servlet源码深入学习
- LayoutAnimationController源码深入学习
- Volley学习(三)ImageRequest、ImageLoader、NetworkImageView源码简读
- 《Android源码设计模式》学习笔记之ImageLoader
- Universal ImageLoader源码分析
- Universal-ImageLoader源码解析
- ImageLoader源码解析
- universal imageloader源码分析
- ImageLoader源码分析
- ImageLoader源码详解
- ImageLoader 源码解析
- ImageLoader源码详解
- ImageLoader源码解析(一)
- windows安装多个JDK, JDK切换
- 四种启动模式
- 如何通过一个程序启动另外一个程序
- 构建Android缓存模块(原理分析)
- Android四大图片缓存(Imageloader,Picasso,Glide,Fresco)原理、特性对比
- ImageLoader深入源码学习探究
- webkit源码解读-FileList
- 二维卷积/矩阵卷积
- Android属性动画完全解析(上),初识属性动画的基本用法
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
- Windows服务器通过FileZilla配置FTP的方法
- Android高效加载大图、多图解决方案,有效避免程序OOM
- java:String使用equals和==比较的区别
- 如何提高Android后台进程存活率