一、UIL设置及使用:

1. Include library(官方文档)

Manual:

  • Download JAR
  • Put the JAR in the libs subfolder of your Android project

or

Maven dependency:

<dependency><groupId>com.nostra13.universalimageloader</groupId><artifactId>universal-image-loader</artifactId><version>1.9.5</version>
</dependency>

or

Gradle dependency:

compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'

2. Android Manifest

<manifest><!-- Include following permission if you load images from Internet --><uses-permission android:name="android.permission.INTERNET" /><!-- Include following permission if you want to cache images on SD card --><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />...
</manifest>

3. Application or Activity class (before the first usage of ImageLoader)

public class MyActivity extends Activity {@Overridepublic void onCreate() {super.onCreate();// Create global configuration and initialize ImageLoader with this configImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)....build();ImageLoader.getInstance().init(config);...}
}

4. 详细参数设置(不要全部拷贝,选择需要的)

File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).memoryCacheExtraOptions(480, 800) //设置内存缓存图片的最大宽高 default=device screen dimensions//设置缓存到disk的图片最大宽高和对图片的处理(resizing/compressing),但它会使ImageLoader变慢.diskCacheExtraOptions(480, 800, processorForDiskCache) .taskExecutor(...) //设置自定义的从网络获取图片任务的Executor.taskExecutorForCachedImages(...) //设置自定义的从disk获取图片任务的Executor.threadPoolSize(3) //default 核心线程数(未设置自定义的Executor时有效).threadPriority(Thread.NORM_PRIORITY - 2) // default 线程优先级.tasksProcessingOrder(QueueProcessingType.FIFO) // default 线程池中任务的处理顺序.denyCacheImageMultipleSizesInMemory() //对同一个url是否允许在内存中存储多个尺寸.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //设置内存存储的方式.memoryCacheSize(2 * 1024 * 1024) //设置内存存储的大小.memoryCacheSizePercentage(13) // default 设置内存存储大小的百分比值.diskCache(new UnlimitedDiskCache(cacheDir)) // default设置disk存储的方式.diskCacheSize(50 * 1024 * 1024) //设置disk存储的大小.diskCacheFileCount(100) //设置disk存储的文件数量.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default 设置文件名生成器.imageDownloader(new BaseImageDownloader(context)) // default 设置下载器.imageDecoder(new BaseImageDecoder()) // default 设置图片解码器.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) //default图片显示方式,可自定义.writeDebugLogs() // 输出日志.build();

.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) 创建了默认的,下面看下自定义的:

DisplayImageOptions options = new DisplayImageOptions.Builder().showImageOnLoading(R.drawable.ic_stub) // resource or drawable 当图片正在加载的时候显示的图片.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable 当图片URI为空的时候显示的图片.showImageOnFail(R.drawable.ic_error) // resource or drawable 当图片加载失败的时候显示的图片  .resetViewBeforeLoading(false)  // default 加载前ImageAware是否设为null.delayBeforeLoading(1000) // 延迟加载的时间.cacheInMemory(false) // default 是否内存缓存.cacheOnDisk(false) // default 是否缓存到disk.preProcessor(...) // 内存缓存之前的预处理,不缓存也会处理.postProcessor(...) // 显示之前的再处理,在内存缓存之后.extraForDownloader(...) //设置额外的内容给ImageDownloader  .considerExifParams(false) // default 是否考虑JPEG图像EXIF参数(旋转,翻转)  // 当源图片大小和显示大小不一致时,设置decodingOptions.inSampleSize的计算方式。// 详细请参考源码BaseImageDecoder#prepareDecodingOptions().imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default.bitmapConfig(Bitmap.Config.ARGB_8888) // default 图片的解码类型.decodingOptions(...) //设置图片的解码配置  .displayer(new SimpleBitmapDisplayer()) // default 设置图片的显示方式.handler(new Handler()) // default.build();

Acceptable URIs examples

"http://site.com/image.png" // from Web
"file:///mnt/sdcard/image.png" // from SD card
"file:///mnt/sdcard/video.mp4" // from SD card (video thumbnail)
"content://media/external/images/media/13" // from content provider
"content://media/external/video/media/13" // from content provider (video thumbnail)
"assets://image.png" // from assets
"drawable://" + R.drawable.img // from drawables (non-9patch images)

二、源码分析

(一)、重要模块分析


以上是UIL的全貌,这里我们主要分析红框里相对复杂的模块

1. DiskCache分析

1.1 DishCache:disk缓存的接口定义类,定义了基本的方法(见图)

1.2 BaskDiskCache:这是一个抽象基类,实现了最基本的把inputstream和bitmap存储到disk上

(1). save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener)

用FileNameGenerator来对imageUri生成一个唯一的文件名,把imageStream拷贝到文件里,可用listener来监听拷贝的进度和终止拷贝(当进度大于75%时无法终止)

(2). boolean save(String imageUri, Bitmap bitmap)

用FileNameGenerator来对imageUri生成一个唯一的文件名,然后把bitmap压缩成png格式的图片存储到disk

(3). boolean remove(String imageUri)

根据imageUril来删除指定的图片

1.3 LimitedAgeDiskCache:根据存储时间来删除文件的缓存模式,在获取文件的时候,会检测获取的文件存储的时间是否大于maxFileAge,如果超过了就会删除

(1). public File get(String imageUri):关键函数

@Override
public File get(String imageUri) {  File file = super.get(imageUri); //调用基类生成文件,用的是文件生成器  if (file != null && file.exists()) {  boolean cached;  Long loadingDate = loadingDates.get(file); //获取存入的时间  if (loadingDate == null) {  cached = false;  loadingDate = file.lastModified();  } else {  cached = true;  }  //如果大于传入的maxFileAge则从disk删除  if (System.currentTimeMillis() - loadingDate > maxFileAge) {  file.delete();  loadingDates.remove(file);  } else if (!cached) { //文件存在,但之前没有记录则记录之  loadingDates.put(file, loadingDate);  }  }  return file;
} 

(2). boolean save(...):首先调用基类的save函数,然后把修改时间记录好

1.4 UnlimitedDiskCache extends BaseDiskCache:这个就是BaseDiskCache的默认实现,因为BaseDiskCache本来就是个无限制的存储模式,所以UnlimitedDiskCache里面什么都没做

2. MemoryCache分析

1.1 MemoryCache:这是接口定义类,定义了基本的方法,具体见类图

1.2 LruMemoryCache:这是Least recently used(最近最少使用)的存储模式,就是优先把最近最少使用的删除掉,这里利用了LinkedHashMap的特性来实现的。

LinkedHashMap#get(key):直接根据key返回value,然后把这条数据移动到头部

LinkedHashMap#put(key,value):把key和value插入到头部

插入的时候维护一个size来记录已有的bitmap的大小,当size>maxSize的时候会调用trimToSize去移除最少使用的数据(尾部的数据),直到size<maxSize

1.3 BaseMemoryCache:纯虚类,主要实现了用Map<String, Reference<Bitmap>>对bitmap的软引用存储

1.4 WeakMemoryCache:对BaseMemoryCache的软引用实现类,可以实例化

1.5 LimitedMemoryCache:纯虚类,对bitmap进行强引用存储,新添加的放入list尾部,当超过sizeLimit限制时,会从list头部开始删除,但是基类的弱引用不会删除

(1). put(String key, Bitmap value):关键函数源码

@Override
public boolean put(String key, Bitmap value) {boolean putSuccessfully = false;// Try to add value to hard cacheint valueSize = getSize(value); //计算bitmap大小int sizeLimit = getSizeLimit(); //获取总的大小限制int curCacheSize = cacheSize.get(); //获取当前存储的总大小if (valueSize < sizeLimit) {while (curCacheSize + valueSize > sizeLimit) {//这里的removeNext是虚函数,留给子类去实现(策略模式)Bitmap removedValue = removeNext(); if (hardCache.remove(removedValue)) { //移除并更新存储大小curCacheSize = cacheSize.addAndGet(-getSize(removedValue));}}hardCache.add(value); //记录当前bitmap并更新存储大小cacheSize.addAndGet(valueSize);putSuccessfully = true;}// Add value to soft cachesuper.put(key, value); //调用基类添加软引用return putSuccessfully;
}

(2). abstract Bitmap removeNext():移除下一个bitmap,具体怎么移除留给子类实现(策略模式)

下面我们来看看它的4个子类是怎么实现removeNext的

1.5.1 FIFOLimitedMemoryCache:缓存模式=大小限制+先进先出队列限制。看代码:

@Override
public boolean put(String key, Bitmap value) {if (super.put(key, value)) { //调用基类来限制大小queue.add(value); //直接用队列的特性return true;} else {return false;}
}
@Override
protected Bitmap removeNext() {return queue.remove(0);
}

1.5.2 LRULimitedMemoryCache:缓存模式=大小限制+LRU(最近最少使用)

@Override
public boolean put(String key, Bitmap value) {if (super.put(key, value)) {lruCache.put(key, value); //直接利用LinkedHashMap来保证LRUreturn true;} else {return false;}
}
@Override
protected Bitmap removeNext() {Bitmap mostLongUsedValue = null;synchronized (lruCache) {Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();if (it.hasNext()) {Entry<String, Bitmap> entry = it.next();mostLongUsedValue = entry.getValue();it.remove(); //删除最少使用的}}return mostLongUsedValue;
}

1.5.3 LargestLimitedMemoryCache:缓存模式=大小限制+首先移除最大

@Override
protected Bitmap removeNext() {Integer maxSize = null;Bitmap largestValue = null;Set<Entry<Bitmap, Integer>> entries = valueSizes.entrySet();synchronized (valueSizes) {for (Entry<Bitmap, Integer> entry : entries) {if (largestValue == null) {largestValue = entry.getKey();maxSize = entry.getValue();} else {Integer size = entry.getValue();if (size > maxSize) { //选择size最大的那个maxSize = size;largestValue = entry.getKey();}}}}valueSizes.remove(largestValue); //移除最大的return largestValue;
}

1.5.4 UsingFreqLimitedMemoryCache:缓存模式=大小限制+首先移除使用次数最少的

@Override
public Bitmap get(String key) {Bitmap value = super.get(key);// Increment usage count for value if value is contained in hardCaheif (value != null) {Integer usageCount = usingCounts.get(value);if (usageCount != null) {usingCounts.put(value, usageCount + 1); //每使用一次就记录+1}}return value;
}
@Override
protected Bitmap removeNext() {Integer minUsageCount = null;Bitmap leastUsedValue = null;Set<Entry<Bitmap, Integer>> entries = usingCounts.entrySet();synchronized (usingCounts) {for (Entry<Bitmap, Integer> entry : entries) {if (leastUsedValue == null) {leastUsedValue = entry.getKey();minUsageCount = entry.getValue();} else {Integer lastValueUsage = entry.getValue();if (lastValueUsage < minUsageCount) { //寻找使用次数最少的minUsageCount = lastValueUsage;leastUsedValue = entry.getKey();}}}}usingCounts.remove(leastUsedValue); //移除return leastUsedValue;
}

1.6 FuzzyKeyMemoryCache:这个类的作用是保证一个key存储一次,这里采用了装饰模式,首先看下它的构造函数:

public FuzzyKeyMemoryCache(MemoryCache cache, Comparator<String> keyComparator) {this.cache = cache;this.keyComparator = keyComparator;
}

这里它接收了一个MemoryCache来对它进行装饰,具体体现在put方法:

@Override
public boolean put(String key, Bitmap value) {// Search equal key and remove this entrysynchronized (cache) {String keyToRemove = null;for (String cacheKey : cache.keys()) { //遍历寻找相同的keyif (keyComparator.compare(key, cacheKey) == 0) { //这里的比较方式是传入的keyToRemove = cacheKey;break;}}if (keyToRemove != null) {cache.remove(keyToRemove); //移除,保证同一个key只存储一次}}return cache.put(key, value);
}

这个装饰模式有什么用呢?

简单点说就是在当前传入的MemoryCache基础上做进一步的限制。

比如对于FIFOLimitedMemoryCache来说,如果我前后存入两个相同key值的大图片,显然浪费了存储空间,这时候如果调用FuzzyKeyMemoryCache进行装饰一下,就可以去重了。

1.7 LimitedAgeMemoryCache:基于时间限制的装饰模式,实现方式可参考LimitedAgeDiskCache

3. display显示模块分析

3.1 BitmapDisplayer:接口类,定义了一个显示的函数见类图。有4个实现类

3.2 SimpleBitmapDisplayer:没有多余功能,直接设置bitmap显示

3.3 FadeInBitmapDisplayer:使用动画来渐显bitmap

public static void animate(View imageView, int durationMillis) {if (imageView != null) {AlphaAnimation fadeImage = new AlphaAnimation(0, 1); //逐渐显示出来fadeImage.setDuration(durationMillis);fadeImage.setInterpolator(new DecelerateInterpolator()); //显示的速度越来越慢imageView.startAnimation(fadeImage);}
}

3.4 CircleBitmapDisplayer:对原始图片进行圆形裁剪显示,还可以在圆形图片外面画一个圆圈,圆圈的颜色和线条大小可在构造函数传入。

3.4 RoundedBitmapDisplayer:圆角图片,在构造函数中还可传入圆角半径,margin等,来看看源码:

public static class RoundedDrawable extends Drawable {public RoundedDrawable(Bitmap bitmap, int cornerRadius, int margin) {this.cornerRadius = cornerRadius; //圆角大小this.margin = margin; //边距,这里会占据bitmap的图像//BitmapShader:Bitmap着色器。其实也就是用 Bitmap 的像素来作为图形或文字的填充bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);mBitmapRect = new RectF (margin, margin, bitmap.getWidth() - margin, bitmap.getHeight() - margin);paint = new Paint();paint.setAntiAlias(true);      //设置抗锯齿开关paint.setShader(bitmapShader); //设置着色器paint.setFilterBitmap(true);   //设置是否使用双线性过滤来绘制 Bitmappaint.setDither(true);         //设置图像的抖动}@Overrideprotected void onBoundsChange(Rect bounds) {super.onBoundsChange(bounds);mRect.set(margin, margin, bounds.width() - margin, bounds.height() - margin);// Resize the original bitmap to fit the new boundMatrix shaderMatrix = new Matrix();//当边界变化时,调整bitmap的显示区域shaderMatrix.setRectToRect(mBitmapRect, mRect, Matrix.ScaleToFit.FILL);bitmapShader.setLocalMatrix(shaderMatrix);}@Overridepublic void draw(Canvas canvas) {canvas.drawRoundRect(mRect, cornerRadius, cornerRadius, paint); //画圆角矩形}
}

再来看个效果图:

3.5 RoundedVignetteBitmapDisplayer:除了圆角图片,还会留下装饰图案的效果。对比一下上图:

仔细看会发现下图的四个角有一个灰色的蒙层,但是不太明显,那么我来一个明显的:

这个够明显了,那么我们还是来看下源码,看看究竟是怎么实现的:

protected static class RoundedVignetteDrawable extends RoundedDrawable {RoundedVignetteDrawable(Bitmap bitmap, int cornerRadius, int margin) {super(bitmap, cornerRadius, margin); //圆角半径和边距}@Overrideprotected void onBoundsChange(Rect bounds) {super.onBoundsChange(bounds);//RadialGradient:径向渐变RadialGradient vignette = new RadialGradient(mRect.centerX(), mRect.centerY() * 1.0f / 0.7f, mRect.centerX() * 1f,new int[]{0, 0x7f00ff00, 0xffff0000}, new float[]{0.0f, 0.5f, 1.0f},Shader.TileMode.CLAMP);Matrix oval = new Matrix();oval.setScale(1.0f, 0.7f);   //把y方向缩小vignette.setLocalMatrix(oval);//设置着色器,ComposeShader:混合着色器,所谓混合,就是把两个 Shader 一起使用。paint.setShader(new ComposeShader(bitmapShader, vignette, PorterDuff.Mode.SRC_OVER));}
}

public RadialGradient(float centerX, float centerY, float radius, int colors[], float stops[], TileMode tileMode)

RadialGradient:径向渐变。前面三个字段比较好理解,我们来看下colors[]和stops[]数组。这两个数组的大小必须相同,大小根据需要定,不一定是这里的3,stop中的值(描述的是径向方向的百分比,所以必须是0~1范围)是和color一一对应的。从上面的图你应该就能猜出分别表示:0~50%半径 的径向渐变由 0~0x7f00ff00.

4. decode解码模块分析

4.1 interface ImageDecoder:基础接口类

(1) Bitmap decode(ImageDecodingInfo imageDecodingInfo) throws IOException

只定义了一个解码接口,传入的ImageDecodingInfo是一个包含解码所需要的数据结构类,字段如下:

imageKey:uri+大小组成的key,eg:http://h.hiphotos.baidu.com/2cf5e.jpg_720x400
imageUri Scheme包装过的基于Uri生成的disk文件路径,eg:file:///data/user/0/com.example.test/cache/-87789357
originalImageUri 调用接口是传入的原始uri
targetSize 图片的目标显示尺寸
imageScaleType 图片解码时采样使用的类型
viewScaleType 图片在ImageView内显示的类型(FIT_INSIDE/CROP)
downloader 图片的下载器
extraForDownloader 下载器需要的辅助信息
considerExifParams 是否需要考虑图片 Exif 信息

decodingOptions 图片的解码信息,为 BitmapFactory.Options

4.2 BaseImageDecoder implements ImageDecoder

解码器实现类。源码分析放到后面流程分析里。

5. download图片下载模块

5.1 interface ImageDownloader:下载接口定义类,里面还定义了一个枚举类,枚举UIL所支持的协议类型

public enum Scheme {HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");
}

(1) InputStream getStream(String imageUri, Object extra):获取图片的接口

5.2 BaseImageDownloader implements ImageDownloader

图片下载实现类。对上面Scheme中的各种资源分别加载图片。

6. imageaware图片显示模块

6.1 interface ImageAware:图片接口定义类,定义了获取各种图片属性的方法

6.2 abstract class ViewAware implements ImageAware

它是对View的包装类(装饰模式),定义了对View的弱引用来避免内存泄漏

(1) int getWidth():首先调用view.getWidth(),如果为0再继续调用LayoutParams的width

(2) int getHeight():首先调用view.getHeight(),如果为0再继续调用LayoutParams的height

(3) boolean setImageDrawable(Drawable drawable):在主线程调用虚方法设置图片显示

(4) boolean setImageBitmap(Bitmap bitmap):在主线程调用虚方法设置图片显示

(5) abstract void setImageDrawableInto(Drawable drawable, View view)

(6) abstract void setImageBitmapInto(Bitmap bitmap, View view)

6.3 class ImageViewAware extends ViewAware

主要是针对ImageView进行处理,实现具体的设置图片的方法

6.4 class NonViewAware implements ImageAware

只做处理图片的数据存储,与显示无关

7 listener下载及结果监听模块

7.1 interface ImageLoadingListener:接口定义类,定义的函数如下:

void onLoadingStarted(String imageUri, View view)

void onLoadingFailed(String imageUri, View view, FailReason failReason)

void onLoadingComplete(String imageUri, View view, Bitmap loadedImage)

void onLoadingCancelled(String imageUri, View view);

7.2 class SimpleImageLoadingListener implements ImageLoadingListener

空实现类,作用就是你不用重载上面的4个方法了

7.3 interface ImageLoadingProgressListener:下载进度监听

void onProgressUpdate(String imageUri, View view, int current, int total)

7.4 PauseOnScrollListener implements OnScrollListener

这个主要是用于对ListView,GridView的滑动监听,当滑动的时候会暂停图片的下载线程

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {switch (scrollState) {case OnScrollListener.SCROLL_STATE_IDLE:imageLoader.resume();  //空闲时恢复break;case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:if (pauseOnScroll) {imageLoader.pause(); //滑动时暂停}break;case OnScrollListener.SCROLL_STATE_FLING:if (pauseOnFling) {imageLoader.pause(); //急速滑动时暂停}break;}if (externalListener != null) {externalListener.onScrollStateChanged(view, scrollState);}
}

(二)、图片下载的主流程分析

首先来看一张来自官网的图片,它描述了不同缓存状态下的下载步骤:

在介绍主流程之前,有必要先对核心的几个类进行介绍:

1. ImageLoader:程序的调用入口类,在调用下载图片之前,必须先调用ImageLoader#init()函数来初始化配置。

public static ImageLoader getInstance() {if (instance == null) {synchronized (ImageLoader.class) {if (instance == null) {instance = new ImageLoader();}}}return instance;
}protected ImageLoader() {
}

这里用了双重检验标准的单例模式。ImageLoader注意用于调用下载图片和取消下载,来看下主要的几个函数:

1.1 public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options, ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

核心函数:它根据传入的uri进行图片下载,下载完成根据options和targetSize进行处理后直接显示到imageAware,期间你可以用progressListener跟踪下载进度,listener监听下载结果。源码后面再介绍。

1.2 public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

这个函数和displayImage最大的区别就是少了一个imageAware参数,也就是说需要你用listener监听返回的bitmap,然后自己显示。源码如下:

public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {checkConfiguration(); //检查ImageLoaderConfiguration是否已经设置,未设置则抛出错误if (targetImageSize == null) {//设置一个图片的最大尺寸,如果使用者调用了memoryCacheExtraOptions()进行设置就采用设置的//否则会使用手机屏幕大小作为默认设置targetImageSize = configuration.getMaxImageSize();}if (options == null) { //图片的显示参数,在每次调用加载图片时,都可以根据需求传入不同的options = configuration.defaultDisplayImageOptions;}//NonViewAware:只做存储数据用,和显示无关NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);displayImage(uri, imageAware, options, listener, progressListener); //调用已有的下载函数
}

1.3 public Bitmap loadImageSync(String uri, ImageSize targetImageSize, DisplayImageOptions options)

看这名字就知道,是一个同步图片下载请求,源码如下:

public Bitmap loadImageSync(String uri, ImageSize targetImageSize, DisplayImageOptions options) {if (options == null) {//图片的显示参数,在每次调用加载图片时,都可以根据需求传入不同的options = configuration.defaultDisplayImageOptions;}options = new DisplayImageOptions.Builder().cloneFrom(options).syncLoading(true).build();//设置为同步//同步监听器,当图片下载完成时会存储起来给返回时使用SyncImageLoadingListener listener = new SyncImageLoadingListener();loadImage(uri, targetImageSize, options, listener); //调用已有功能函数,避免重复代码return listener.getLoadedBitmap();
}

1.4 public void cancelDisplayTask(ImageView imageView)

取消下载和显示图片

2. ImageLoaderEngine:引擎类,主要负责任务的分发工作。来看一下它里面的3个线程池:

private Executor taskDistributor;

任务分发线程池。它其实是一个Executors.newCachedThreadPool,这个线程池比较适合 耗时时间比较短的多任务处理

new ThreadPoolExecutor(0, Integer.MAX_VALUE, //核心线程数为0(节省资源),非核心线程数无限60L, TimeUnit.SECONDS,  // 空闲60秒会被回收new SynchronousQueue<Runnable>(), //同步队列表示有任务时立马执行,优先利用空闲线程,没有就创建threadFactory);

private Executor taskExecutor:是一个自定义线程池,创建方法如下:

public static Executor createExecutor(int threadPoolSize, int threadPriority, QueueProcessingType tasksProcessingType) {boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;BlockingQueue<Runnable> taskQueue = //线程池中线程的排列方式lifo ? new LIFOLinkedBlockingDeque<Runnable>() : new LinkedBlockingQueue<Runnable>();return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,createThreadFactory(threadPriority, "uil-pool-")); // threadPoolSize默认是3,也可以自己定义
}

private Executor taskExecutorForCachedImages(适合非耗时任务)

这个线程池的创建方法和taskExecutor一样,那么这里为什么还要创建一个相同的线程池呢?

用途不一样。taskExecutor主要用于需要网络请求的线程,而这个主要是读取disk上面已经缓存好的图片,很明显,不请求网络的情况下,解析一张图片还是比较快的,如果混合使用,显示disk图片的线程会被网络加载(网络不好的情况下)的线程阻挡很长时间,导致缓存在disk上的图片加载速度慢。

3. DisplayBitmapTask implements Runnable

主要用于图片的显示,虽然实现了Runnable接口,但是它不是运行在子线程而是主线程。

4. LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener

主要用于加载网络图片和disk图片

5. ProcessAndDisplayImageTask implements Runnable

主要是对图片的一个自定义处理,基本属于非耗时任务,所以会调用taskExecutorForCachedImages来执行

6. DefaultConfigurationFactory

在设置ImageLoaderConfiguration时,当某些重要参数没有传入的情况下,DefaultConfigurationFactory会构建默认的。比如线程池、diskCache策略、memoryCache策略、downloader、decoder等等

核心的类就介绍到这里了,下面我们进入主流程的源码分析:

ImageLoader#displayImage

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {checkConfiguration(); //如果未配置ImageLoaderConfiguration直接抛异常if (imageAware == null) {throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);}if (listener == null) {listener = defaultListener;  //默认SimpleImageLoadingListener}if (options == null) {options = configuration.defaultDisplayImageOptions;}if (TextUtils.isEmpty(uri)) {engine.cancelDisplayTaskFor(imageAware); //取消下载和显示图片的任务listener.onLoadingStarted(uri, imageAware.getWrappedView()); //回调函数if (options.shouldShowImageForEmptyUri()) { //显示空uri所对应设置的图片imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));} else {imageAware.setImageDrawable(null);}listener.onLoadingComplete(uri, imageAware.getWrappedView(), null); //回调函数return;}if (targetSize == null) { //获取目标size,如果宽高为0,则使用已设置的最大值,如果未设置则使用屏幕宽高targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());}String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize); //在uri后面拼接图片宽x高//以imageAware中的id为key值put memoryCacheKey进缓存engine.prepareDisplayTaskFor(imageAware, memoryCacheKey); listener.onLoadingStarted(uri, imageAware.getWrappedView()); //回调函数Bitmap bmp = configuration.memoryCache.get(memoryCacheKey); //获取缓存中的图片if (bmp != null && !bmp.isRecycled()) { //有而且可用!L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);if (options.shouldPostProcess()) { //用户设置了后处理事件ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey, options, listener, progressListener, engine.getLockForUri(uri)); //组装信息ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo, defineHandler(options));if (options.isSyncLoading()) {displayTask.run(); //同步的话,直接调用run函数} else {engine.submit(displayTask); //异步则提交到线程池taskExecutorForCachedImages(非耗时任务池)}} else {options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE); //没有耗时的操作,直接显示listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp); //回调函数}} else { //内存中未缓存if (options.shouldShowImageOnLoading()) { //显示设置好的加载中的图片imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));} else if (options.isResetViewBeforeLoading()) {imageAware.setImageDrawable(null);}ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,options, listener, progressListener, engine.getLockForUri(uri)); //组装信息LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,defineHandler(options));if (options.isSyncLoading()) {displayTask.run(); //同步的话,直接调用run函数} else {engine.submit(displayTask); //异步则提交到任务分发线程池 taskDistributor}}
}

46行提交ProcessAndDisplayImageTask后就会进入它的run方法。

ProcessAndDisplayImageTask#run

@Override
public void run() {L.d(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey);//获取用户设置好的处理器BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor(); Bitmap processedBitmap = processor.process(bitmap); //直接调用用户定义的处理方法//封装显示的task,然后调用LoadAndDisplayImageTask的静态函数runTaskDisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine, LoadedFrom.MEMORY_CACHE);LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine);
}//LoadAndDisplayImageTask的公共ranTask
static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {if (sync) {r.run(); //同步则直接调用run} else if (handler == null) {engine.fireCallback(r); //handler为空则直接加入taskDistributor线程池} else {handler.post(r); //交给handler去处理(具体在什么线程执行由handler创建的线程决定)}
}

66行把任务交给了taskDistributor,那么我们来看下它是怎么分发任务的。

ImageLoaderEngine#submit

void submit(final LoadAndDisplayImageTask task) {taskDistributor.execute(new Runnable() {@Overridepublic void run() {//首先获取disk上的文件索引,判断是否已经缓存File image = configuration.diskCache.get(task.getLoadingUri());boolean isImageCachedOnDisk = image != null && image.exists();//检查taskExecutor和taskExecutorForCachedImages是否可用,不可用则创建initExecutorsIfNeed(); if (isImageCachedOnDisk) { //存在则调用非耗时线程池处理taskExecutorForCachedImages.execute(task);} else {taskExecutor.execute(task);}}});
}

不过调用的哪个线程池的execute,都会进入到LoadAndDisplayImageTask的run方法,这个方法很重要。翠花~~上源码!

LoadAndDisplayImageTask#run

@Override
public void run() {//如果被暂停了则调用wait等待,比如ListView滑动过程中会暂停加载,resume的时候会调用notifyAllif (waitIfPaused()) return;  if (delayIfNeed()) return;   //如果用户设置的延迟执行则Thread.sleep等待ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);if (loadFromUriLock.isLocked()) {L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);}//如果没有被其它线程获取,获取锁成功后计数器为1,立即返回//如果自己已经获取过了,则计数器+1,立即返回//如果已经被其它线程获取,则当前线程会被禁用并处于休眠状态,直到重新获取loadFromUriLock.lock();Bitmap bmp;try {checkTaskNotActual(); //判断ImageAware是否被回收和重用bmp = configuration.memoryCache.get(memoryCacheKey); //再次判断内存是否存在if (bmp == null || bmp.isRecycled()) {bmp = tryLoadBitmap(); //加载图片,马上进入分析if (bmp == null) return; // tryLoadBitmap已经处理过了,这里就直接返回了checkTaskNotActual();  //判断ImageAware是否被回收和重用checkTaskInterrupted(); //判断当前线程是否被中断if (options.shouldPreProcess()) {L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);bmp = options.getPreProcessor().process(bmp); //调用用户设置的预处理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); 调用用户设置的后处理if (bmp == null) {L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);}}checkTaskNotActual();  //判断ImageAware是否被回收和重用checkTaskInterrupted(); //判断当前线程是否被中断} catch (TaskCancelledException e) {fireCancelEvent(); //调用listener.onLoadingCancelledreturn;} finally {loadFromUriLock.unlock(); //解锁}//获取图片成功,调用显示task直接显示DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);runTask(displayBitmapTask, syncLoading, handler, engine);
}

LoadAndDisplayImageTask#tryLoadBitmap

private Bitmap tryLoadBitmap() throws TaskCancelledException {Bitmap bitmap = null;try {File imageFile = configuration.diskCache.get(uri); //获取disk索引文件if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);loadedFrom = LoadedFrom.DISC_CACHE; //标记从disk获取的checkTaskNotActual(); //判断ImageAware是否被回收和重用//解析disk缓存的图片文件bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));}//如果获取失败(比如文件损坏)则重新从网络获取if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);loadedFrom = LoadedFrom.NETWORK; //标记网络获取String imageUriForDecoding = uri;//如果允许disk缓存则调用tryCacheImageOnDisk获取图片并缓存到diskif (options.isCacheOnDisk() && tryCacheImageOnDisk()) { imageFile = configuration.diskCache.get(uri); //获取刚刚缓存到disk的文件if (imageFile != null) { //用Scheme包装成decodeImage能识别的形式imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());}}checkTaskNotActual();bitmap = decodeImage(imageUriForDecoding);//这个uri可能是网络或者disk的if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {fireFailEvent(FailType.DECODING_ERROR, null); //调用listener.onLoadingFailed通知使用者}}} catch (IllegalStateException e) {fireFailEvent(FailType.NETWORK_DENIED, null);} catch (TaskCancelledException e) {throw e;} catch (IOException e) {L.e(e);fireFailEvent(FailType.IO_ERROR, e);} catch (OutOfMemoryError e) {L.e(e);fireFailEvent(FailType.OUT_OF_MEMORY, e);} catch (Throwable e) {L.e(e);fireFailEvent(FailType.UNKNOWN, e);}return bitmap;
}

第11行和第28行都在调用decodeImage,我们来瞅瞅它:

LoadAndDisplayImageTask#decodeImage

private Bitmap decodeImage(String imageUri) throws IOException {ViewScaleType viewScaleType = imageAware.getScaleType(); //获取View显示的模式ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,getDownloader(), options);return decoder.decode(decodingInfo); //进入BaseImageDecoder的decode函数
}//BaseImageDecoder#decode
@Override
public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {Bitmap decodedBitmap;ImageFileInfo imageInfo;InputStream imageStream = getImageStream(decodingInfo); //见下面if (imageStream == null) {L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());return null;}try {imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo); //确定图片的大小和旋转角度//重置imageStream,如果失败会调用getImageStream重新获取imageStream = resetStream(imageStream, decodingInfo); //根据ImageScaleType来计算图片的decodingOptions.inSampleSize的值//换句话说就是根据imageSize和targetSize来确定图片应该放大还是缩小,计算出scale值Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);//根据计算的decodingOptions来解析decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);} finally {IoUtils.closeSilently(imageStream);}if (decodedBitmap == null) {L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());} else {//prepareDecodingOptions计算的是int类型的粗略的scale值,而这里是计算出精确的float类型的scale值//把decodedBitmap伸缩变换成targetSize大小的bitmapdecodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);}return decodedBitmap;
}//BaseImageDecoder#getImageStream 最终调用了downloader的getStream函数
protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {return decodingInfo.getDownloader().getStream(decodingInfo.getImageUri(), decodingInfo.getExtraForDownloader());
}

上面停留在了downloader的getStream函数调用。再来看看tryLoadBitmap中第20行的tryCacheImageOnDisk函数是如何获取图片并缓存到disk的

LoadAndDisplayImageTask#tryCacheImageOnDisk

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);//获取disk图片,resize成指定的max宽高后重新save进diskresizeAndSaveImage(width, height); // TODO : process boolean result}}} catch (IOException e) {L.e(e);loaded = false;}return loaded;
}private boolean downloadImage() throws IOException {//也调用了downloader的getStream函数InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());if (is == null) {L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);return false;} else {try { //进入BaseDiskCache的save函数return configuration.diskCache.save(uri, is, this); } finally {IoUtils.closeSilently(is);}}
}//BaseDiskCache#save
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {File imageFile = getFile(imageUri); //根据文件名生成器生成文件File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);boolean loaded = false;try {OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);try { //以每次bufferSize的大小将imageStream写入disk,用listener监听写入进度或者终止写入loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);} finally {IoUtils.closeSilently(os);}} finally {if (loaded && !tmpFile.renameTo(imageFile)) { //把临时文件名改为缓存的文件名loaded = false;}if (!loaded) {tmpFile.delete();}}return loaded;
}

以上两个分支最终到走到了BaseImageDownloader的getStream函数,我们来看下它是如何针对不同资源图片进行加载的:

@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {switch (Scheme.ofUri(imageUri)) { //分别调用对应函数加载case HTTP:case HTTPS:return getStreamFromNetwork(imageUri, extra); //网络case FILE:return getStreamFromFile(imageUri, extra); //读取文件case CONTENT:return getStreamFromContent(imageUri, extra); //Contentcase ASSETS:return getStreamFromAssets(imageUri, extra); //Asserts目录文件case DRAWABLE:return getStreamFromDrawable(imageUri, extra); //Drawable资源case UNKNOWN:default:return getStreamFromOtherSource(imageUri, extra);}
}protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {HttpURLConnection conn = createConnection(imageUri, extra); // 创建HttpURLConnection连接int redirectCount = 0; //如果返回的code是3开头的就重试5次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)) {IoUtils.closeSilently(imageStream);throw new IOException("Image request failed with response code " + conn.getResponseCode());}//ContentLengthInputStream是InputStream的装饰类,提供了对InputStream的基本操作return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
}protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {String filePath = Scheme.FILE.crop(imageUri);if (isVideoFileUri(imageUri)) { //如果是视频return getVideoThumbnailStream(filePath); //返回视频的缩略图} else { //根据文件路径读取文件数据BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());}
}protected InputStream getStreamFromContent(String imageUri, Object extra) throws FileNotFoundException {ContentResolver res = context.getContentResolver();Uri uri = Uri.parse(imageUri);if (isVideoContentUri(uri)) { // video thumbnailLong origId = Long.valueOf(uri.getLastPathSegment());Bitmap bitmap = MediaStore.Video.Thumbnails.getThumbnail(res, origId, MediaStore.Images.Thumbnails.MINI_KIND, null);if (bitmap != null) {ByteArrayOutputStream bos = new ByteArrayOutputStream();bitmap.compress(CompressFormat.PNG, 0, bos);return new ByteArrayInputStream(bos.toByteArray());}} else if (imageUri.startsWith(CONTENT_CONTACTS_URI_PREFIX)) { // contacts photoreturn getContactPhotoStream(uri);}return res.openInputStream(uri);
}protected InputStream getStreamFromAssets(String imageUri, Object extra) throws IOException {String filePath = Scheme.ASSETS.crop(imageUri);return context.getAssets().open(filePath);
}protected InputStream getStreamFromDrawable(String imageUri, Object extra) {String drawableIdString = Scheme.DRAWABLE.crop(imageUri);int drawableId = Integer.parseInt(drawableIdString);return context.getResources().openRawResource(drawableId);
}

到这里,流程就走完了,下面用一张流程图来结束吧!

Universal-Image-Loader(UIL) 源码详解相关推荐

  1. 【 数据集加载 DatasetDataLoader 模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人 (7/10)】

    数据集加载 Dataset&DataLoader 模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人 (7/10) 模块介绍 在本节中没有关于数学原理的相关介绍,使用的数据集和类型 ...

  2. Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览

    文章目录 1. 摘要 2. Compaction 概述 3. 实现 3.1 Prepare keys 过程 3.1.1 compaction触发的条件 3.1.2 compaction 的文件筛选过程 ...

  3. 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...

  4. 【Live555】live555源码详解系列笔记

    [Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...

  5. 【Live555】live555源码详解(八):testRTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...

  6. 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...

  7. 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...

  8. 【Live555】live555源码详解(五):MediaSource、MediaSink、MediaSession、MediaSubsession

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的四个类所在的位置: MediaSource.MediaSink.MediaSession.MediaSub ...

  9. 【Live555】live555源码详解(四):Medium媒体基础类

    [Live555]live555源码详解系列笔记 7.Media Medai所依赖关系图 依赖Medai关系图 Media和UsageEnvironment关联图

  10. 【Live555】live555源码详解(二):BasicHashTable、DelayQueue、HandlerSet

    [Live555]live555源码详解系列笔记 3.BasicHashTable 哈希表 协作图: 3.1 BasicHashTable BasicHashTable 继承自 HashTable 重 ...

最新文章

  1. RabbitMQ之比较好的资料
  2. Andorid之BINDSERVICE的使用方法总结
  3. 《开源框架那点事儿14》:教计算机程序解数学题
  4. 软考系统架构师笔记-最后知识点总结(四)
  5. Lucene的索引不跨平台
  6. extjs 网站首页table布局,秀一下
  7. 单调栈求全1(或全0)子矩阵的个数 洛谷P5300与或和 P3400仓鼠窝
  8. 【智力题】拿硬币(数数字)、游戏
  9. 面向对象 —— 类设计(九) —— 类设计的内在一致性
  10. 对比学习(Contrastive Learning)在CV与NLP领域中的研究进展
  11. 搜狗输入法linux版 rpm,opensuse 制作搜狗输入法rpm包
  12. 常用收藏(自己用的)
  13. python经纬度批量定位 绘制高清地图
  14. mysql 1033 frm_修复mysqldump Incorrect information in file frm (1033)
  15. Unity 利用像素点在图片上画线(RawImage)
  16. 应用层读写i2c从设备寄存器
  17. 例25:求1+2!+3!+...+20!的和。
  18. 计网——17差错检测和纠正技术
  19. 百趣代谢组学分享:从SWATH到MRM:一种新型高覆盖度靶向代谢组学技术
  20. Android 屏幕常亮 背景常亮

热门文章

  1. Go 打开文件,写入文件。
  2. 关于omnigraffle存为visio格式乱码的问题
  3. 【python+ROS+路径规划】四、发布路径
  4. 论文翻译-Denoising Implicit Feedback for Recommendation
  5. linux系统禁用usb设备
  6. 双屏不同缩放比例_[WIN10]如何解决鼠标在双屏分辨率不同的情况下移动的问题 顺便说下 U2718Q 的体验...
  7. WINVNC源码阅读(五)
  8. 集散控制系统是利用微型计算机技术,远程西安交通大学17年3月课程考试《化工仪表及自动化(高起专)》作业考核试题...
  9. mac下搭建码云gitee+hexo博客
  10. numpy数组和矢量运算03