android Picasso使用详解
来源:互联网 发布:淘宝店铺修改店名 编辑:程序博客网 时间:2024/06/05 14:45
Picasso是Square公司出品的一款非常优秀的开源图片加载库,是目前Android开发中超级流行的图片加载库之一,今天我们就来分析下他的使用及实现流程。
使用简介
首先在项目中引入picasso(以gradle为例)
compile 'com.squareup.picasso:picasso:2.5.2'
传统的ImageVIew设置图片
Picasso.with(context).load(url).placeholder(R.drawable.tab_item_bg).into(imageView);
自定义的布局设置图片,target是指实现了Target接口的自定义View
Picasso.with(context).load(url).placeholder(R.drawable.tab_item_bg).into(target);
adapter中的使用
//View复用会自动察觉并且取消之前的下载任务@Override public void getView(int position, View convertView, ViewGroup parent) { SquaredImageView view = (SquaredImageView) convertView; if (view == null) { view = new SquaredImageView(context); } String url = getItem(position); Picasso.with(context).load(url).into(view); }
自动设置图片宽高像素的大小
Picasso .with(context) .load(url) .resize(50, 50) .centerCrop() .into(imageView)
流程分析
接下来我们就以上面的链式调用来分析Picasso的实现流程,首先来认识几个类
RequestHandler
//抽象类,由不同的子类来实现不同来源的图片的获取与加载,比如://AssetRequestHandler:加载asset里的图片//FileRequestHandler:加载硬盘里的图片//ResourceRequestHandler:加载资源图片//NetworkRequestHandler:加载网络图片
BitmapHunter
//是一个Runnable的子类,用来进行Bitmap的获取(网络,硬盘,内存等),处理(角度,大小等),//然后执行分发器(dispatcher)的回调处理
PicassoDrawable
//实现了引入图片渐变功能和debug状态的标识的Drawable,用来在最后bitmap转换成PicassoDrawable//然后设置给ImageView,根据图片的来源可以在图片的左上角显示一个不同颜色的三角形色块MEMORY(Color.GREEN) //内存加载 DISK(Color.BLUE) //本地加载NETWORK(Color.RED) //网络加载
DeferredRequestCreator
//ViewTreeObserver.OnPreDrawListener的实现类,即将绘制视图树时执行的回调函数。//这时所有的视图都测量完成并确定了框架。 客户端可以使用该方法来调整滚动边框,//甚至可以在绘制之前请求新的布局,这里用来实现当开发者修改图片尺寸时的逻辑
Action
//Action代表了一个具体的加载任务,主要用于图片加载后的结果回调,有两个抽象方法,complete和error,//并保存了每个请求的各种信息(),具体的实现类如下//GetAction:同步执行请求时使用。//FetchAction:当不需要ImageView来安置bitmap时的异步请求,通常用来预热缓存//RemoteViewsAction:用来更新远程图片(notification等)的抽象类。//TargetAction:一般在View(不只是ImageView)或者ViewHolder中用来加载图片,需要实现Target接口//ImageViewAction:最常用的Action,主要用来给ImageView加载图片
流程分析
1. Picasso对象的创建
//通过Picasso的一个静态内部类Builder来创建本身的对象(单例)//这种构建方式在很多的流行开源库中都在使用,可以借鉴public static Picasso with(Context context) { if (singleton == null) { synchronized (Picasso.class) { if (singleton == null) { singleton = new Builder(context).build(); } } } return singleton;}public Picasso build() { Context context = this.context; //下载器 if (downloader == null) { downloader = Utils.createDefaultDownloader(context); } //Lru缓存 if (cache == null) { cache = new LruCache(context); } //线程池,核心线程数为3,使用优先队列 if (service == null) { service = new PicassoExecutorService(); } //请求的前置处理,在请求发出去之前执行,类似于拦截器 if (transformer == null) { transformer = RequestTransformer.IDENTITY; } //状态控制类,用来发送各种消息,例如查找图片缓存的结果(击中/未击中),下载完成等 Stats stats = new Stats(cache); //分发器,用来分发任务 Dispatcher dispatcher = new Dispatcher( context, service, HANDLER, downloader, cache, stats); //返回一个Picasso对象return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled, loggingEnabled); }}
2. 加载url,创建并返回一个图片下载请求的构建器RequestCreator
//不仅仅用来存储图片的URL,public RequestCreator load(String path) { if (path == null) { return new RequestCreator(this, null, 0); } if (path.trim().length() == 0) { throw new IllegalArgumentException("Path must not be empty."); } return load(Uri.parse(path));}//请求构建器,用来存储该图片加载任务的一切信息,比如://目标的宽高,图片的显示样式(centerCrop,centerInside),旋转角度和旋转点坐标,以及图片RequestCreator(Picasso picasso, Uri uri, int resourceId) { if (picasso.shutdown) { throw new IllegalStateException("Picasso instance already shut down. Cannot submit new requests."); } this.picasso = picasso; this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig); }//对图片设置矛盾之处的校验,校验通过则返回Request对象,该方法在最初方法链的into方法中(createRequest)调用public Request build() { if (centerInside && centerCrop) { throw new IllegalStateException("Center crop and center inside can not be used together."); } if (centerCrop && (targetWidth == 0 && targetHeight == 0)) { throw new IllegalStateException("Center crop requires calling resize with positive width and height."); } if (centerInside && (targetWidth == 0 && targetHeight == 0)) { throw new IllegalStateException("Center inside requires calling resize with positive width and height."); } //默认的优先级为普通if (priority == null) { priority = Priority.NORMAL; } return new Request(uri, resourceId, stableKey, transformations, targetWidth, targetHeight, centerCrop, centerInside, onlyScaleDown, rotationDegrees, rotationPivotX, rotationPivotY, hasRotationPivot, purgeable, config, priority); }}
3. 设置默认图片及出错图片
public RequestCreator placeholder(int placeholderResId) { //是否允许设置默认图片 if (!setPlaceholder) { throw new IllegalStateException("Already explicitly declared as no placeholder."); } //默认图片资源不合法 if (placeholderResId == 0) { throw new IllegalArgumentException("Placeholder image resource invalid."); } //已经设置了默认图片 if (placeholderDrawable != null) { throw new IllegalStateException("Placeholder image already set."); } this.placeholderResId = placeholderResId; return this; }//出错图片的处理逻辑与之类似
4. 修改图片的尺寸,填充图片进ImageView
public void into(ImageView target, Callback callback) { long started = System.nanoTime(); //线程检查 checkMain(); if (target == null) { throw new IllegalArgumentException("Target must not be null."); } //没设置url以及resId则取消请求 if (!data.hasImage()) { picasso.cancelRequest(target); if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable()); } return; } //仅有fit()方法会修改该flag为true,但是该方法只能由开发者显式调用,因此下面的代码默认是不会执行的 if (deferred) {//当尝试调整图片的大小让其正好适合ImageView时defered为true(fit()方法) if (data.hasSize()) { //判断ImageView大小,即如果ImageView已经渲染完成,则无法改变大小 throw new IllegalStateException("Fit cannot be used with resize."); } int width = target.getWidth(); int height = target.getHeight(); if (width == 0 || height == 0) { //设置默认图 if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable()); } //TODO: picasso.defer(target, new DeferredRequestCreator(this, target, callback)); return; } data.resize(width, height); } //Request的拦截器://简单介绍下。他会对原始的Request做一个拦截,看是否需要做处理。//比如说使用CDN的时候修改主机名以达到更快的访问速度等操作。Request request = createRequest(started); //根据请求的URL,图片的处理等参数创建一个字符串KeyString requestKey = createKey(request); //缓存策略,是否应该从内存中读取 if (shouldReadFromMemoryCache(memoryPolicy)) { //内存的快速检查, Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey); if (bitmap != null) { //如果缓存中已存在则取消请求并直接设置给ImageView picasso.cancelRequest(target); setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled); ...//日志 if (callback != null) { callback.onSuccess(); } return; } }if (setPlaceholder) { setPlaceholder(target, getPlaceholderDrawable());}//这里创建的是ImageViewAction对象,后面会用到Action action = new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,errorDrawable, requestKey, tag, callback, noFade);//提交请求,Picasso内部维护了一个map,key是imageView,value是Action//提交时做判断,如果当前imageView已经在任务队列里了。判断当前任务与之前的任务是否相同,//如果不相同则取消之前的任务并将新的key-value加入到mappicasso.enqueueAndSubmit(action);}
5. Dispatcher(任务分发器)会通过Handler来提交任务,然后交由Dispatcher的performSubmit方法来执行
void performSubmit(Action action, boolean dismissFailed) { if (pausedTags.contains(action.getTag())) { pausedActions.put(action.getTarget(), action); ... return; } //查看是否有对应url的缓存的hunter //BitmapHunter的简介请看文章开始的介绍 BitmapHunter hunter = hunterMap.get(action.getKey()); if (hunter != null) { hunter.attach(action); return; } if (service.isShutdown()) { ... return; } hunter = forRequest(action.getPicasso(), this, cache, stats, action); //提交任务到线程池 hunter.future = service.submit(hunter); //将runnable缓存在map集合中 hunterMap.put(action.getKey(), hunter); if (dismissFailed) { failedActions.remove(action.getTarget()); } ... }
6.根据不同的加载路径,选择合适的RequestHandler来创建BitmapHunter
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) { Request request = action.getRequest(); //Picasso中默认保存了一个集合,里面存储了每一类图片的加载,判断和处理逻辑 ,比如网络图片,资源图片,硬盘图片,照片等 List<RequestHandler> requestHandlers = picasso.getRequestHandlers(); // Index-based loop to avoid allocating an iterator. //noinspection ForLoopReplaceableByForEach for (int i = 0, count = requestHandlers.size(); i < count; i++) { RequestHandler requestHandler = requestHandlers.get(i); //它会通过request中url的scheme来判断该使用哪一个RequestHandler if (requestHandler.canHandleRequest(request)) { return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler); } } return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);}
7. BitmapHunter被提交到线程池之后,接下来就该run方法执行了
@Override public void run() { try { updateThreadName(data); ... } //重点!!! result = hunt(); //得到bitmap后,执行Dispater中的方法 if (result == null) { dispatcher.dispatchFailed(this); } else { dispatcher.dispatchComplete(this); } } catch (Downloader.ResponseException e) { if (!e.localCacheOnly || e.responseCode != 504) { ... } dispatcher.dispatchFailed(this); } catch (IOException e) {//重试 ... dispatcher.dispatchRetry(this); } catch (OutOfMemoryError e) {//OOM异常的处理 StringWriter writer = new StringWriter(); stats.createSnapshot().dump(new PrintWriter(writer)); exception = new RuntimeException(writer.toString(), e); dispatcher.dispatchFailed(this); } catch (Exception e) { ... dispatcher.dispatchFailed(this); } finally { ... } }
8. 重点:bitmap的获取(包括获取途径(内存,硬盘,网络)的判断以及加载)
Bitmap hunt() throws IOException { Bitmap bitmap = null; //是否从内存读取 if (shouldReadFromMemoryCache(memoryPolicy)) { bitmap = cache.get(key); if (bitmap != null) { //发送一个内存缓存中查找成功的消息 stats.dispatchCacheHit(); loadedFrom = MEMORY; ... return bitmap; } } //NO_CACHE:跳过检查硬盘缓存,强制从网络获取 //NO_STORE:不存储到本地硬盘 //OFFLINE:只从本地硬盘获取,不走网络 //如果重试次数为0则走本地硬盘,否则从网络获取 data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy; //根据重试的次数和不同的requestHandler的子类来实现不同来源图片的加载,资源文件,硬盘图片又或者网络图片 //硬盘及网络资源的加载逻辑,具体实现在下面 RequestHandler.Result result = requestHandler.load(data, networkPolicy); if (result != null) { //加载途径(硬盘,内存,网络) loadedFrom = result.getLoadedFrom(); // exifOrientation = result.getExifOrientation(); bitmap = result.getBitmap(); // 返回的resuslt中包括bitmap和inputstream,如果bitmap为null则需要自己从stream中转换 if (bitmap == null) { InputStream is = result.getStream(); try { bitmap = decodeStream(is, data); } finally { Utils.closeQuietly(is); } } } if (bitmap != null) { ... //修改stats中记录的图片的个数和占用的内存总大小以及平均内存占用量 stats.dispatchBitmapDecoded(bitmap); //图片是否需要旋转或者其他的操作处理 if (data.needsTransformation() || exifOrientation != 0) { synchronized (DECODE_LOCK) { if (data.needsMatrixTransform() || exifOrientation != 0) { bitmap = transformResult(data, bitmap, exifOrientation); ... } //自定义的图片处理 if (data.hasCustomTransformations()) { bitmap = applyCustomTransformations(data.transformations, bitmap); ... } } //记录下图片处理的次数以及处理后的占用的总内存大小以及每张图片的平均内存占用量 if (bitmap != null) { stats.dispatchBitmapTransformed(bitmap); } } } return bitmap; }
9.以网络图片为例介绍下从硬盘和网络加载图片的流程
@Override public Response load(Uri uri, int networkPolicy) throws IOException { CacheControl cacheControl = null; if (networkPolicy != 0) { //只走本地缓存 if (NetworkPolicy.isOfflineOnly(networkPolicy)) { cacheControl = CacheControl.FORCE_CACHE; } else { //自定义缓存策略 CacheControl.Builder builder = new CacheControl.Builder(); //不从硬盘读 if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) { builder.noCache(); } //不写入硬盘 if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) { builder.noStore(); } cacheControl = builder.build(); } } Request.Builder builder = new Request.Builder().url(uri.toString()); if (cacheControl != null) { builder.cacheControl(cacheControl); } //重点:调用okhttp或者UrlConnection来执行bitmap的下载任务(拦截器)。 okhttp3.Response response = client.newCall(builder.build()).execute(); int responseCode = response.code(); if (responseCode >= 300) { response.body().close(); throw new ResponseException(responseCode + " " + response.message(), networkPolicy, responseCode); } boolean fromCache = response.cacheResponse() != null; ResponseBody responseBody = response.body(); return new Response(responseBody.byteStream(), fromCache, responseBody.contentLength()); }
10. 当从硬盘或者服务器获取Bitmap之后,就可以通过Action来执行各种自定义的callback了
//dispatcher.dispatchComplete()用来处理bitmap成功获取后的逻辑,//经过一系列的转发,最终逻辑是在Picasso类里的complete方法中执行的。void complete(BitmapHunter hunter) { Action single = hunter.getAction(); List<Action> joined = hunter.getActions(); boolean hasMultiple = joined != null && !joined.isEmpty(); boolean shouldDeliver = single != null || hasMultiple; if (!shouldDeliver) { return; } Uri uri = hunter.getData().uri; Exception exception = hunter.getException(); Bitmap result = hunter.getResult(); LoadedFrom from = hunter.getLoadedFrom(); if (single != null) { deliverAction(result, from, single); } if (hasMultiple) { //noinspection ForLoopReplaceableByForEach for (int i = 0, n = joined.size(); i < n; i++) { Action join = joined.get(i); deliverAction(result, from, join); } } if (listener != null && exception != null) { listener.onImageLoadFailed(this, uri, exception); } }private void deliverAction(Bitmap result, LoadedFrom from, Action action) { if (action.isCancelled()) { return; } if (!action.willReplay()) { targetToAction.remove(action.getTarget()); } if (result != null) { if (from == null) { throw new AssertionError("LoadedFrom cannot be null."); } //这里的action就是前面提到的ImageViewAction,其他还有RemoteViewsAction,TargetAction等 action.complete(result, from); ... } else { action.error(); ... } }
以ImageViewAction为例看下complete中的实现
@Override public void complete(Bitmap result, Picasso.LoadedFrom from) { if (result == null) { throw new AssertionError(String.format("Attempted to complete action with no result!\n%s", this)); } ImageView target = this.target.get(); if (target == null) { return; } Context context = picasso.context; //是否展示来源的标签,默认不展示。 boolean indicatorsEnabled = picasso.indicatorsEnabled; PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled); if (callback != null) { callback.onSuccess(); }}//PicassoDrawable中setBitmap方法的实现static void setBitmap(ImageView target, Context context, Bitmap bitmap, Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) { Drawable placeholder = target.getDrawable(); if (placeholder instanceof AnimationDrawable) { ((AnimationDrawable) placeholder).stop(); } //最终扔到ImageView上现实的的是PicassoDrawable PicassoDrawable drawable = new PicassoDrawable(context, bitmap, placeholder, loadedFrom, noFade, debugging); target.setImageDrawable(drawable);}
到这里,Picasso加载图片的逻辑就分析完了。下面我们看下Square还留给我们什么其他可以学习的东西。
Picasso的引用清理策略
Picasso的缓存是对请求的缓存,通过WeakReference与ReferenceQueue的联合使用来缓存请求,然后
关于ReferenceQueue请看下文的详细介绍
WeakReference与ReferenceQueue联合使用构建java高速缓存
//1.首先通过RequestWeakReference来弱化Action的引用强度static class RequestWeakReference<M> extends WeakReference<M> { final Action action; public RequestWeakReference(Action action, M referent, ReferenceQueue<? super M> q) { super(referent, q); this.action = action; }}//2.其次通过ReferenceQueue来存放引用Action(Picasso picasso, T target, Request request, int memoryPolicy, int networkPolicy, int errorResId, Drawable errorDrawable, String key, Object tag, boolean noFade) { ... this.target = target == null ? null : new RequestWeakReference<T>(this, target, picasso.referenceQueue); ... }//3.通过Picasso内部的CleanupThread来清理ReferenceQueue中的引用。@Override public void run() { Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); while (true) { try { // Prior to Android 5.0, even when there is no local variable, the result from // remove() & obtainMessage() is kept as a stack local variable. // We're forcing this reference to be cleared and replaced by looping every second // when there is nothing to do. // This behavior has been tested and reproduced with heap dumps. RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove(THREAD_LEAK_CLEANING_MS); Message message = handler.obtainMessage(); if (remove != null) { message.what = REQUEST_GCED; message.obj = remove.action; handler.sendMessage(message); } else { message.recycle(); } } catch (InterruptedException e) { break; } catch (final Exception e) { handler.post(new Runnable() { @Override public void run() { throw new RuntimeException(e); } }); break; } } }
Picasso优先级策略
请求的优先级
public enum Priority { LOW, // 只有当通过fetch方法(不需要ImageView,仅需要下载Bitmap并执行回调)请求图片时才会给request设置该优先级 NORMAL,// 正常的请求优先级 HIGH// }
图片下载任务的优先级
真实的项目开发过程中,只请求图片而不设置给ImageView毕竟是少数,所以大多数的任务依然会是NORMAL级别的,同级别的任务数太多,那么优先级策略就没有太大的效果,所以,Picasso在执行图片下载任务时,又做了第二次优先级划分
1. 依赖于优先级队列的线程池
PicassoExecutorService() { super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());}
2. 实现了Comparable接口的FutureTask
private static final class PicassoFutureTask extends FutureTask<BitmapHunter> implements Comparable<PicassoFutureTask> { private final BitmapHunter hunter; public PicassoFutureTask(BitmapHunter hunter) { super(hunter, null); this.hunter = hunter; } @Override public int compareTo(PicassoFutureTask other) { Picasso.Priority p1 = hunter.getPriority(); Picasso.Priority p2 = other.hunter.getPriority(); //高优先级的请求放到队列前面 //同等级的请求按照顺序先后执行 return (p1 == p2 ? hunter.sequence - other.hunter.sequence : p2.ordinal() - p1.ordinal()); } }
PicassoExecutorService(Picasso自己封装的线程池,对移动网络做了处理,并提供了支持优先级比较的FutureTask)
//这段代码我们在将来的开发中可能会用得上,包含了国内每种移动网络switch (info.getType()) { case ConnectivityManager.TYPE_WIFI: case ConnectivityManager.TYPE_WIMAX: case ConnectivityManager.TYPE_ETHERNET: setThreadCount(4); break; case ConnectivityManager.TYPE_MOBILE: switch (info.getSubtype()) { case TelephonyManager.NETWORK_TYPE_LTE: // 4G case TelephonyManager.NETWORK_TYPE_HSPAP: case TelephonyManager.NETWORK_TYPE_EHRPD: setThreadCount(3); break; case TelephonyManager.NETWORK_TYPE_UMTS: // 3G case TelephonyManager.NETWORK_TYPE_CDMA: case TelephonyManager.NETWORK_TYPE_EVDO_0: case TelephonyManager.NETWORK_TYPE_EVDO_A: case TelephonyManager.NETWORK_TYPE_EVDO_B: setThreadCount(2); break; case TelephonyManager.NETWORK_TYPE_GPRS: // 2G case TelephonyManager.NETWORK_TYPE_EDGE: setThreadCount(1); break; default: setThreadCount(DEFAULT_THREAD_COUNT); } break; default: setThreadCount(DEFAULT_THREAD_COUNT); } }
OKHttp的拦截器链设计模式的应用(简介)
当执行如下代码时,如果底层是通过OKHttp来请求图片,会先执行OKHttp自带的拦截器中的方法,拦截器中的逻辑执行完之后才会执行真正的图片请求
okhttp3.Response response = client.newCall(builder.build()).execute();
看一下Call接口的实现类RealCall中execute方法的具体实现。
@Override public Response execute() throws IOException { ... try { //开始执行拦截器链 Response result = getResponseWithInterceptorChain(false); ... return result; } finally { client.dispatcher().finished(this); } }private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException { Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket); return chain.proceed(originalRequest); }class ApplicationInterceptorChain implements Interceptor.Chain { private final int index; //拦截器的计数器,表示当前需要执行第几个拦截器中的方法。 ... ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) { this.index = index; ... } ... @Override public Response proceed(Request request) throws IOException { //判断是否超出拦截器集合的下标 if (index < client.interceptors().size()) { //在Picasso中,维护了一个拦截器的集合,这里通过对集合内拦截器的下标索引 //来依次获取开发者所定义的拦截器 Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket); Interceptor interceptor = client.interceptors().get(index); //然后依次调用拦截方法 Response interceptedResponse = interceptor.intercept(chain); if (interceptedResponse == null) { throw new NullPointerException("application interceptor " + interceptor + " returned null"); } return interceptedResponse; } // 拦截器执行完了。就开始执行真正的网络请求了。 return getResponse(request, forWebSocket); } }
到这里,Picasso的使用介绍及流程分析就全部介绍完了。
0 0
- android Picasso使用详解
- Android 之 Picasso使用
- Android-Picasso库使用详解-从入门到源码剖析
- Android图片下载框架Picasso 使用
- Android Picasso的简单使用
- Android Picasso的基本使用
- Android Picasso的基本使用
- Android图片加载库:Picasso详解
- Android图片框架Picasso LRU缓存详解
- Picasso的使用及原理详解
- Picasso使用详解及源码解析
- 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!
- 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!
- Android框架Picasso的使用简介
- Android中使用Picasso加载图片
- Android Picasso图片缓存框架的使用
- Android图形缓存库Picasso的使用
- Android Picasso
- leetcode:53. Maximum Subarray
- 使用jsoup爬虫抓取页面
- App开发智能车载之SDK篇
- android.os.handler相关知识整理
- 【DOCKER】走进DOCKER,神奇的环境隔离
- android Picasso使用详解
- css阻塞,js阻塞
- bzoj1077: [SCOI2008]天平
- 团体程序设计天梯赛-练习集 L1-009. N个数求和 解题报告
- 理解RESTful架构
- Mysql主从配置,实现读写分离
- 前端开发-移动端(2)- 自适应屏幕
- Java Web1
- 安装SQL SERVER 2012失败,出现"."(十六进制0x00)是无效的字符,解决办法