Universal-Image-Loader(UIL) 源码详解
一、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) 源码详解相关推荐
- 【 数据集加载 DatasetDataLoader 模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人 (7/10)】
数据集加载 Dataset&DataLoader 模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人 (7/10) 模块介绍 在本节中没有关于数学原理的相关介绍,使用的数据集和类型 ...
- Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览
文章目录 1. 摘要 2. Compaction 概述 3. 实现 3.1 Prepare keys 过程 3.1.1 compaction触发的条件 3.1.2 compaction 的文件筛选过程 ...
- 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...
- 【Live555】live555源码详解系列笔记
[Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...
- 【Live555】live555源码详解(八):testRTSPClient
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...
- 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...
- 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...
- 【Live555】live555源码详解(五):MediaSource、MediaSink、MediaSession、MediaSubsession
[Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的四个类所在的位置: MediaSource.MediaSink.MediaSession.MediaSub ...
- 【Live555】live555源码详解(四):Medium媒体基础类
[Live555]live555源码详解系列笔记 7.Media Medai所依赖关系图 依赖Medai关系图 Media和UsageEnvironment关联图
- 【Live555】live555源码详解(二):BasicHashTable、DelayQueue、HandlerSet
[Live555]live555源码详解系列笔记 3.BasicHashTable 哈希表 协作图: 3.1 BasicHashTable BasicHashTable 继承自 HashTable 重 ...
最新文章
- RabbitMQ之比较好的资料
- Andorid之BINDSERVICE的使用方法总结
- 《开源框架那点事儿14》:教计算机程序解数学题
- 软考系统架构师笔记-最后知识点总结(四)
- Lucene的索引不跨平台
- extjs 网站首页table布局,秀一下
- 单调栈求全1(或全0)子矩阵的个数 洛谷P5300与或和 P3400仓鼠窝
- 【智力题】拿硬币(数数字)、游戏
- 面向对象 —— 类设计(九) —— 类设计的内在一致性
- 对比学习(Contrastive Learning)在CV与NLP领域中的研究进展
- 搜狗输入法linux版 rpm,opensuse 制作搜狗输入法rpm包
- 常用收藏(自己用的)
- python经纬度批量定位 绘制高清地图
- mysql 1033 frm_修复mysqldump Incorrect information in file frm (1033)
- Unity 利用像素点在图片上画线(RawImage)
- 应用层读写i2c从设备寄存器
- 例25:求1+2!+3!+...+20!的和。
- 计网——17差错检测和纠正技术
- 百趣代谢组学分享:从SWATH到MRM:一种新型高覆盖度靶向代谢组学技术
- Android 屏幕常亮 背景常亮
热门文章
- Go 打开文件,写入文件。
- 关于omnigraffle存为visio格式乱码的问题
- 【python+ROS+路径规划】四、发布路径
- 论文翻译-Denoising Implicit Feedback for Recommendation
- linux系统禁用usb设备
- 双屏不同缩放比例_[WIN10]如何解决鼠标在双屏分辨率不同的情况下移动的问题 顺便说下 U2718Q 的体验...
- WINVNC源码阅读(五)
- 集散控制系统是利用微型计算机技术,远程西安交通大学17年3月课程考试《化工仪表及自动化(高起专)》作业考核试题...
- mac下搭建码云gitee+hexo博客
- numpy数组和矢量运算03