自定义图片加载框架
来源:互联网 发布:苹果软件等待中删不掉 编辑:程序博客网 时间:2024/06/09 19:53
从最初的ImageLoader到现在的Glide Picasso等各种框架不胜枚举,其核心的设计架构大同小异。其过程大致如下:
- 拿到URI和imageView对象(getView)生产一个请求(BitmapRequest)
- 将这些请求放到一个队列(PriorityBlockingQueue)里面去
- 转发器(RequestDispatcher)负责去消费这些请求,将这些请求通过加载器(Loader)去加载
- 设置配置信息,如加载策略、缓存策略、开启的线程数等
- 配置ImageLoaderConfig
- BitmapCache 缓存策略
- MemoryCache
- DiskCache
- DoubleCache
- LoadPolicy 加载策略
- RequestQueue 请求队列
- RequestDispatcher 请求转发器
- Loader加载器
- AbstarctLoader
- LoaderManager
- UrlLoader和BitmapDecoder
- LocalLoader
- 暴露给外部使用的类SimpleImageLoader
- BitmapRequest
流程图:
类图如下:
配置ImageLoaderConfig
建造者模式
- 线程个数:根据CPU的内核数去确定开启多少个请求转发器
- 加载中所显示的图片,加载错误时显示的图片
初始化一些配置信息
public class ImageLoaderConfig { //缓存策略 private BitmapCache bitmapCache= new MemoryCache(); //加载策略 private LoadPolicy loadPolicy=new ReversePolicy(); //默认线程数 private int threadCount=Runtime.getRuntime().availableProcessors(); //显示的配置 private DisplayConfig displayConfig=new DisplayConfig(); private ImageLoaderConfig(){ } /** * 建造者模式 * 和AlterDialog建造过程类似 */ public static class Builder{ private ImageLoaderConfig config; public Builder(){ config=new ImageLoaderConfig(); } /** * 设置缓存策略 * @param bitmapCache * @return */ public Builder setCachePolicy(BitmapCache bitmapCache){ config.bitmapCache=bitmapCache; return this; } /**& * 设置加载策略 * @param loadPolicy * @return */ public Builder setLoadPolicy(LoadPolicy loadPolicy){ config.loadPolicy=loadPolicy; return this; } /** * 设置线程个数 * @param count * @return */ public Builder setThreadCount(int count){ config.threadCount=count; return this; } /** * 设置加载过程中的图片 * @param resID * @return */ public Builder setLoadingImage(int resID){ config.displayConfig.loadingImage=resID; return this; } /** * 设置加载过程中的图片 * @param resID * @return */ public Builder setFaildImage(int resID){ config.displayConfig.faildImage=resID; return this; } public ImageLoaderConfig build(){ return config; } } public BitmapCache getBitmapCache() { return bitmapCache; } public LoadPolicy getLoadPolicy() { return loadPolicy; } public int getThreadCount() { return threadCount; } public DisplayConfig getDisplayConfig() { return displayConfig; }}
BitmapCache 缓存策略
定义好接口后,需要实现硬盘缓存DiskCache和内存缓存MemoryCache的两种策略
public interface BitmapCache { /** * 缓存bitmap */ void put(BitmapRequest request, Bitmap bitmap); /** * 通过请求取Bitmap */ Bitmap get(BitmapRequest request); /** * 移除缓存 */ void remove(BitmapRequest request);}
MemoryCache
内存缓存,这里面主要使用的是LruCache1.一般情况下设置的LruCache的大小为系统内存的1/8,只需重写sizeOf方法测量每个图片所占用的内存,图片占用的内存为一行的字节数*高即为每张图片的大小
public class MemoryCache implements BitmapCache { private LruCache<String,Bitmap> mLruCache; public MemoryCache() { int maxSize= (int) (Runtime.getRuntime().freeMemory()/1024/8); mLruCache=new LruCache<String,Bitmap>(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight(); } }; } @Override public void put(BitmapRequest request, Bitmap bitmap) { mLruCache.put(request.getImageUriMD5(),bitmap); } @Override public Bitmap get(BitmapRequest request) { return mLruCache.get(request.getImageUriMD5()); } @Override public void remove(BitmapRequest request) { mLruCache.remove(request.getImageUriMD5()); }}
DiskCache
这里面主要使用了DiskLruCache这个类,可以给这个类设置一个最大值,依然是lru算法,代码第32行实例化这个类,最后一个参数为设置的最大值,put方法:代码50~52行 拿到edtor和输出流,代码65行将图片写到输出流中。get方法81行道84行通过snapshot拿到流给BitmapFactory.decodeStream(inputStream)。
public class DiskCache implements BitmapCache { private static DiskCache mDiskCache; //缓存路径 private String mCacheDir = "Image"; //MB private static final int MB = 1024 * 1024; //jackwharton的杰作 private DiskLruCache mDiskLruCache; private DiskCache(Context context) { iniDiskCache(context); } public static DiskCache getInstance(Context context) { if (mDiskCache == null) { synchronized (DiskCache.class) { if (mDiskCache == null) { mDiskCache = new DiskCache(context); } } } return mDiskCache; } private void iniDiskCache(Context context) { //得到缓存的目录 android/data/data/com.dongnao.imageloderFrowork/cache/Image File directory = getDiskCache(mCacheDir, context); if (!directory.exists()) { directory.mkdir(); } try { //最后一个参数 指定缓存容量 mDiskLruCache = DiskLruCache.open(directory, 1, 1, 50 * MB); } catch (IOException e) { e.printStackTrace(); } } private File getDiskCache(String mCacheDir, Context context) { String cachePath; //默认缓存路径 return new File(Environment.getExternalStorageDirectory(), mCacheDir); } @Override public void put(BitmapRequest request, Bitmap bitmap) { DiskLruCache.Editor edtor = null; OutputStream os = null; try { //路径必须是合法字符 edtor = mDiskLruCache.edit(request.getImageUriMD5()); os = edtor.newOutputStream(0); if (persistBitmap2Disk(bitmap, os)) { edtor.commit(); } else { edtor.abort(); } } catch (IOException e) { e.printStackTrace(); } } private boolean persistBitmap2Disk(Bitmap bitmap, OutputStream os) { BufferedOutputStream bos = new BufferedOutputStream(os); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos); try { bos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { IOUtil.closeQuietly(bos); } return true; } @Override public Bitmap get(BitmapRequest request) { try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(request.getImageUriMD5()); if (snapshot != null) { InputStream inputStream = snapshot.getInputStream(0); return BitmapFactory.decodeStream(inputStream); } } catch (IOException e) { e.printStackTrace(); } return null; } @Override public void remove(BitmapRequest request) { try { mDiskLruCache.remove(request.getImageUriMD5()); } catch (IOException e) { e.printStackTrace(); } }}
DoubleCache
一般情况下都是使用的双缓存,分别调用上述两种缓存方式
public class DoubleCache implements BitmapCache { //内存缓存 private MemoryCache mMemoryCache = new MemoryCache(); //硬盘缓存 private DiskCache mDiskCache; public DoubleCache(Context context) { mDiskCache = DiskCache.getInstance(context); } @Override public void put(BitmapRequest request, Bitmap bitmap) { mMemoryCache.put(request, bitmap); mDiskCache.put(request, bitmap); } @Override public Bitmap get(BitmapRequest request) { Bitmap bitmap = mMemoryCache.get(request); if(bitmap == null){ bitmap = mDiskCache.get(request); if(bitmap != null){ //放入内存,方便再获取 mMemoryCache.put(request, bitmap); } } return bitmap; } @Override public void remove(BitmapRequest request) { mMemoryCache.remove(request); mDiskCache.remove(request); }}
LoadPolicy 加载策略
加载策略分为两种:先进先出和先进后出。例如,当ListView滑动到最底部时,用户最想看到的是划出来的条目。
/** * 加载策略 */public interface LoadPolicy { /** * 两个BitmapRequest进行比较 * @param request1 * @param request2 * @return 小于0,request1 < request2,大于0,request1 > request2,等于 */ public int compareTo(BitmapRequest request1, BitmapRequest request2);}/** * 先进先加载 */public class SerialPolicy implements LoadPolicy { @Override public int compareTo(BitmapRequest request1, BitmapRequest request2) { return request1.getSerialNO() - request2.getSerialNO(); }}/** * 后进先加载 */public class ReversePolicy implements LoadPolicy { @Override public int compareTo(BitmapRequest request1, BitmapRequest request2) { return request2.getSerialNO() - request1.getSerialNO(); }}
RequestQueue 请求队列
PriorityBlockingQueue这个阻塞式队列
RequestDispatcher请求转发器
代码50行中会开启请求转发器,每个请求转发器就是一个线程,代码71~79行中创建一个请求转发器的数组,for循环创建出threadCount个转发器(BlockingQueue作为参数传给转发器),将其放入数组中,依次开启。
public class RequestQueue { //队列 //多线程的数据共享 //阻塞队列 //生成效率和消费效率相差甚远,处理好兼顾效率和线程安全问题 //concurrent出现了 //优先级的阻塞队列 //1.当队列中没有产品时,消费者被阻塞 //2.使用优先级,优先级高的产品会被优先消费 //前提:每个产品的都有一个编号(实例化出来的先后顺序) //优先级的确定,受产品编号的影响,但是由加载策略所决定 private BlockingQueue<BitmapRequest> mRequestQueue = new PriorityBlockingQueue<BitmapRequest>(); //转发器的数量 private int threadCount; //一组转发器 private RequestDispatcher[] mDispachers; //i++ ++i线程不安全 //线程安全 private AtomicInteger ai = new AtomicInteger(0); /** * 构造函数 * @param threadCount 指定线程的数量(转发器的数量) */ public RequestQueue(int threadCount) { this.threadCount = threadCount; } /** * 添加请求 * @param request */ public void addRequest(BitmapRequest request){ if(!mRequestQueue.contains(request)){ //给请求编号 request.setSerialNO(ai.incrementAndGet()); mRequestQueue.add(request); Log.d("jason", "添加请求"+request.getSerialNO()); }else{ Log.d("jason", "请求已经存在"+request.getSerialNO()); } } /** * 开始 */ public void start(){ //先停止,再启动 stop(); startDispatchers(); } private void startDispatchers() { mDispachers = new RequestDispatcher[threadCount]; //初始化所有的转发器 for (int i = 0; i < threadCount; i++) { RequestDispatcher p = new RequestDispatcher(mRequestQueue); mDispachers[i] = p; //启动线程 mDispachers[i].start(); } } /** * 停止 */ public void stop(){ if(mDispachers != null && mDispachers.length > 0){ for (int i = 0; i < mDispachers.length; i++) { //打断 mDispachers[i].interrupt(); } } }}
RequestDispatcher 请求转发器
每个请求转发器就是一个线程,通过构造方法拿到队列的引用,从队列中取出BitmapRequest后丢给加载器Loader,为了区分加载类别这里面使用LoaderManager去管理Loader,转发器直接与LoaderManager对接。
代码第49行拿到“://”前面的部分,到代码第37行会拿到相应类别的Loader去进行加载。
public class RequestDispatcher extends Thread{ //请求队列 private BlockingQueue<BitmapRequest> mRequestQueue; public RequestDispatcher(BlockingQueue<BitmapRequest> mRequestQueue) { this.mRequestQueue = mRequestQueue; } @Override public void run() { while (!isInterrupted()) { try { //阻塞式函数 BitmapRequest request=mRequestQueue.take(); /** * 处理请求对象 */ String schema=parseSchema(request.getImageUrl()); //获取加载器 Loader loader= LoaderManager.getInstance().getLoader(schema); loader.loadImage(request); } catch (InterruptedException e) { e.printStackTrace(); } } } private String parseSchema(String imageUrl) { if(imageUrl.contains("://")) { return imageUrl.split("://")[0]; } else { Log.i(TAG,"不支持此类型"); } return null; }}
Loader加载器
加载器会根据缓存策略判断从网络或本地拿到图片,AbstarctLoader为具体的实现类,通过LoaderManager去选择那种方式加载
AbstarctLoader
每次请求图片时会先尝试从缓存中拿图片如果没有再去加载代码10行,代码第82行,设置默认显示图片即未加载完成所显示,imageView.post让主线程去显示图片。代码82行具体如何去哪里加载就留给子类去执行,拿到图片后进行缓存代码64行。具体如何去缓存就交给接口,synchronized使线程安全。第28行和第42行的两个方法就是对已经加载好的图片进行显示,在ListView中ImageView是会进行反复复用的,为防止显示错误的问题,需要给View设置TGA,在进行地址的比对。54行的监听回掉处理一些特殊的需求。
public abstract class AbstarctLoader implements Loader { //拿到用户自定义配置的缓存策略 private BitmapCache bitmapCache=SimpleImageLoader.getInstance().getConfig().getBitmapCache(); //拿到显示配置 private DisplayConfig displayConfig=SimpleImageLoader.getInstance().getConfig().getDisplayConfig(); @Override public void loadImage(BitmapRequest request) { //从缓存取到Bitmap Bitmap bitmap=bitmapCache.get(request); if(bitmap==null) { //显示默认加载图片 showLoadingImage(request); //开始真正加载图片 bitmap=onLoad(request); //缓存图片 cacheBitmap(request,bitmap); } deliveryToUIThread(request,bitmap); } /** * 交给主线程显示 * @param request * @param bitmap */ protected void deliveryToUIThread(final BitmapRequest request, final Bitmap bitmap) { ImageView imageView = request.getImageView(); if(imageView!=null) { imageView.post(new Runnable() { @Override public void run() { updateImageView(request, bitmap); } }); } } private void updateImageView(final BitmapRequest request, final Bitmap bitmap) { ImageView imageView = request.getImageView(); //加载正常 防止图片错位 if(bitmap != null && imageView.getTag().equals(request.getImageUrl())){ imageView.setImageBitmap(bitmap); } //有可能加载失败 if(bitmap == null && displayConfig!=null&&displayConfig.faildImage!=-1){ imageView.setImageResource(displayConfig.faildImage); } //监听 //回调 给圆角图片 特殊图片进行扩展 if(request.imageListener != null){ request.imageListener.onComplete(imageView, bitmap, request.getImageUrl()); } } /** * 缓存图片 * @param request * @param bitmap */ private void cacheBitmap(BitmapRequest request, Bitmap bitmap) { if(request!=null&&bitmap!=null) { synchronized (AbstarctLoader.class) { bitmapCache.put(request,bitmap); } } } //抽象加载策略 因为加载网络图片和本地图片有差异 protected abstract Bitmap onLoad(BitmapRequest request); /** * 加载前显示的图片 * @param request */ protected void showLoadingImage(BitmapRequest request) { //指定了,显示配置 if(hasLoadingPlaceHolder()){ final ImageView imageView = request.getImageView(); if(imageView!=null) { imageView.post(new Runnable() { @Override public void run() { imageView.setImageResource(displayConfig.loadingImage); } }); } } } protected boolean hasLoadingPlaceHolder(){ return (displayConfig != null && displayConfig.loadingImage > 0); }}
LoaderManager
NullLoader 只是为了方便这么写,直接返回null
暂时只支持三种方式http、https、file。(这种在register的思想在系统中常常用到,如注册蓝牙服务 闹钟服务等)
public class LoaderManager { //缓存所有支持的Loader类型 private Map<String ,Loader> mLoaderMap=new HashMap<>(); private static LoaderManager mInstance=new LoaderManager(); private LoaderManager() { register("http",new UrlLoader()); register("https",new UrlLoader()); register("file",new LocalLoader()); } public static LoaderManager getInstance() { return mInstance; } private void register(String schema, Loader loader) { mLoaderMap.put(schema,loader); } public Loader getLoader(String schema) { if(mLoaderMap.containsKey(schema)) { return mLoaderMap.get(schema); } return new NullLoader(); }}
UrlLoader和BitmapDecoder
直接从网络加载图片,先下载后读取,其中主要使用BitmapDecoder这个类去对图片进行适配,从第16行的代码开始下载图片通过输出流写到file中,程序运行到代码12行传入控件的宽高,这里面使用了工具类工具类ImageViewHelper2.
,69行第一次调用子类实现的抽象方法代码第9行。程序继续运行到第69行对图片进行缩放适配,if(图片的宽 > 控件的宽 || 图片的高 > 控件的高)需要进行缩放,代码第89行拿到缩放的比例,第94行~102行对options里面的缩放比例、图片类型、读取全部图片等属性进行设置。
public class UrlLoader extends AbstarctLoader { @Override protected Bitmap onLoad(final BitmapRequest request) { //先下载 后读取 downloadImgByUrl(request.getImageUrl(), getCache(request.getImageUriMD5())); BitmapDecoder decoder = new BitmapDecoder() { @Override public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) { return BitmapFactory.decodeFile(getCache(request.getImageUriMD5()).getAbsolutePath(), options); } }; return decoder.decodeBitmap(ImageViewHelper.getImageViewWidth(request.getImageView()) , ImageViewHelper.getImageViewHeight(request.getImageView())); } public static boolean downloadImgByUrl(String urlStr, File file) { FileOutputStream fos = null; InputStream is = null; try { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); is = conn.getInputStream(); fos = new FileOutputStream(file); byte[] buf = new byte[512]; int len = 0; while ((len = is.read(buf)) != -1) { fos.write(buf, 0, len); } fos.flush(); return true; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } try { if (fos != null) fos.close(); } catch (IOException e) { } } return false; } private File getCache(String unipue) { File file = new File(Environment.getExternalStorageDirectory(), "ImageLoader"); if (!file.exists()) { file.mkdir(); } return new File(file, unipue); }}public abstract class BitmapDecoder { public Bitmap decodeBitmap(int reqWidth, int reqHeight) { //初始化options BitmapFactory.Options options = new BitmapFactory.Options(); //只需要读取图片宽高信息,无需将整张图片加载到内存 inJustDecodeBounds设置true options.inJustDecodeBounds = true; //根据options加载Bitmap 抽象 //decodeBitmapWithOption(options); //计算图片缩放比例 calculateSampleSizeWithOption(options, reqWidth, reqHeight); return decodeBitmapWithOption(options); } /** * 计算图片缩放的比例 */ private void calculateSampleSizeWithOption(BitmapFactory.Options options, int reqWidth, int reqHeight) { //计算缩放的比例 //图片的原始宽高 int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; // reqWidth ImageView的 宽 if (width > reqWidth || height > reqHeight) { //宽高的缩放比例 int heightRatio = Math.round((float) height / (float) reqHeight); int widthRatio = Math.round((float) width / (float) reqWidth); //有的图是长图、有的是宽图 inSampleSize = Math.max(heightRatio, widthRatio); } //全景图 //当inSampleSize为2,图片的宽与高变成原来的1/2 //options.inSampleSize = 2 options.inSampleSize = inSampleSize; //每个像素2个字节 options.inPreferredConfig = Bitmap.Config.RGB_565; //Bitmap占用内存 true options.inJustDecodeBounds = false; //当系统内存不足时可以回收Bitmap options.inPurgeable = true; options.inInputShareable = true; } public abstract Bitmap decodeBitmapWithOption(BitmapFactory.Options options);}
LocalLoader
从本地直接读取的代码就很简单了,过程同上
public class LocalLoader extends AbstarctLoader { @Override protected Bitmap onLoad(BitmapRequest request) { //得到本地图片的路径 final String path= Uri.parse(request.getImageUrl()).getPath(); File file=new File(path); if(!file.exists()) { return null; } BitmapDecoder decoder=new BitmapDecoder() { @Override public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) { return BitmapFactory.decodeFile(path,options); } }; return decoder.decodeBitmap(ImageViewHelper.getImageViewWidth(request.getImageView()) ,ImageViewHelper.getImageViewHeight(request.getImageView())); }}
暴露给外部使用的类:SimpleImageLoader
DCL单例模式
- 初始化队列
- 两次获取单例模式,第一次需要进行初始化的设置。
- 两个displayImage()的重载,90行代码中接口的意义是当图片加载完成后需要进行一些处理的时候调用,如设置圆角等。
- displayImage的作用就是把参数封装成一个BitmapRequest,并将这个请求放到队列里面去。
public class SimpleImageLoader { //配置 private ImageLoaderConfig config; //请求队列 private RequestQueue mRequestQueue; //单例对象 private static volatile SimpleImageLoader mInstance; private SimpleImageLoader() { } private SimpleImageLoader(ImageLoaderConfig imageLoaderConfig) { this.config=imageLoaderConfig; mRequestQueue=new RequestQueue(config.getThreadCount()); //开启请求队列 mRequestQueue.start(); } /** * 获取单例方法 * 第一次调用 * @param config * @return */ public static SimpleImageLoader getInstance(ImageLoaderConfig config) { if(mInstance==null) { synchronized (SimpleImageLoader.class) { if(mInstance==null) { mInstance=new SimpleImageLoader(config); } } } return mInstance; } /** * 第二次获取单例 * @return */ public static SimpleImageLoader getInstance() { if(mInstance==null) { throw new UnsupportedOperationException("没有初始化"); } return mInstance; } /** *暴露获取图片 * @param imageView * @param uri http: file 开头 */ public void displayImage(ImageView imageView,String uri) { displayImage(imageView,uri,null,null); } /** * 重载 * @param imageView * @param uri * @param displayConfig * @param imageListener */ public void displayImage(ImageView imageView, String uri , DisplayConfig displayConfig,ImageListener imageListener) { //实例化一个请求 BitmapRequest bitmapRequest=new BitmapRequest(imageView,uri,displayConfig,imageListener); //添加到队列里面 mRequestQueue.addRequest(bitmapRequest); } public static interface ImageListener{ /** * * @param imageView * @param bitmap * @param uri */ void onComplete(ImageView imageView, Bitmap bitmap,String uri); } /** * 拿到全局配置 * @return */ public ImageLoaderConfig getConfig() { return config; }}
BitmapRequest
- 实现了Comparable接口,因为在队列里面需要根据优先级去加载,而在实际重写的compareTo方法中,把这些比较的工作都丢给LoadPolicy去完成。(需要注意的是Comparable在lang包下的而不是util包的)
- 在插入到队列的过程中防止重复插入,只需比较其serialNo,重新hashCode和equals方法
- 请求会被大量创建为优化内存问题这里用软引用
public class BitmapRequest implements Comparable<BitmapRequest> { //持有imageview的软引用 private SoftReference<ImageView> imageViewSoft; //图片路径 private String imageUrl; //MD5的图片路径 private String imageUriMD5; //下载完成监听 public SimpleImageLoader.ImageListener imageListener; private DisplayConfig displayConfig; public BitmapRequest(ImageView imageView,String imageUrl,DisplayConfig displayConfig, SimpleImageLoader.ImageListener imageListener) { this.imageViewSoft=new SoftReference<ImageView>(imageView); //设置可见的Image的Tag,要下载的图片路径 imageView.setTag(imageUrl); this.imageUrl=imageUrl; this.imageUriMD5= MD5Utils.toMD5(imageUrl); if(displayConfig!=null) { this.displayConfig=displayConfig; } this.imageListener = imageListener; } //加载策略 private LoadPolicy loadPolicy= SimpleImageLoader.getInstance().getConfig().getLoadPolicy(); /** * 编号 */ private int serialNo; public int getSerialNo() { return serialNo; } public void setSerialNo(int serialNo) { this.serialNo = serialNo; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BitmapRequest that = (BitmapRequest) o; if (serialNo != that.serialNo) return false; return loadPolicy != null ? loadPolicy.equals(that.loadPolicy) : that.loadPolicy == null; } @Override public int hashCode() { int result = loadPolicy != null ? loadPolicy.hashCode() : 0; result = 31 * result + serialNo; return result; } public ImageView getImageView() { return imageViewSoft.get(); } public String getImageUrl() { return imageUrl; } public String getImageUriMD5() { return imageUriMD5; } public DisplayConfig getDisplayConfig() { return displayConfig; } public LoadPolicy getLoadPolicy() { return loadPolicy; } @Override public int compareTo(BitmapRequest o) { return loadPolicy.compareto(o,this); }}
- ImageViewHelper
这里写代码片
. ↩ - ImageViewHelper
这里写代码片
. ↩
- 自定义图片加载框架
- Android-自定义图片加载框架
- Android图片加载框架Glide之Glide的自定义模块
- 自定义框架-加载配置文件
- 自定义ProgressDialog加载图片
- 自定义图片加载器
- 图片加载框架
- 图片加载框架大合集
- 图片加载框架Glide
- Android图片加载框架
- facebook 图片加载框架
- 图片加载框架
- Imageloader图片加载框架
- Fresco图片加载框架
- 图片加载框架
- 简述图片加载框架
- 打造图片加载框架
- 图片加载框架-Fresco
- python链表常用方法
- 6. ZigZag Conversion
- 蟠桃记
- enote笔记语言(4)(ver0.2)——“5w1h2k”分析法
- springMVC + ajaxfileupload异步上传图片预览,裁剪并保存图片
- 自定义图片加载框架
- java菜鸟的回炉之旅之四----整型数据类型和浮点数数据类型
- 数学黑洞
- 机器学习---k-近邻算法
- C++与JAVA的创建对象
- Three.js进阶篇之7
- 【那些年我们一起看过的论文】之《Fully Convolutional Networks for Semantic Segmentation》
- 们--加强菲波那切数列
- loadrunner中的web_url和web_submit_data的使用