Picasso(2) - 自定义配置

it2022-05-20  151

Builder 建造者模式 正常情况引入第三方框架, 直接使用的不多。很多时候要根据项目来对第三方框架做额外的配置 。  Picasso 采用建造者模式进行额外的配置。  Picasso 的 Builder : 

Context Picasso 建造者模式必须指定 Context 。  但是你还记得旧版本 Picasso 是怎么调用的吗 ? 旧版本的 Picasso 每次调用都需要指定 Context 对象。

Picasso.with(this)

但是新版本不需要指定

Picasso.get()

这是为什么呢 ? 进源码看下, 原来 Context 使用的是 PicassoProvider.context 。

  public static Picasso get() {     if (singleton == null) {       synchronized (Picasso.class) {         if (singleton == null) {           if (PicassoProvider.context == null) {             throw new IllegalStateException("context == null");           }           singleton = new Builder(PicassoProvider.context).build();         }       }     }     return singleton;   }

PicassoProvider 是什么呢 ? 原来是 ContentProvider 。

public final class PicassoProvider extends ContentProvider {   @SuppressLint("StaticFieldLeak") static Context context;   @Override public boolean onCreate() {     context = getContext();     return true;   } .......

PicassoProvider 继承 ContentProvider , onCreate 的时候声明了 Context 对象。  PicassoProvider 的 onCreate() 方法回调是要早于 Application 的 onCreate() 。  如果你是手动导入 Picasso 到工程中,记得在 AndroidManifest.xml 中声明 PicassoProvider , 否则当你调用 Picasso.get() 就会 Crash 。

Downloader 顾名思义,下载器。从磁盘或网络加载图片, 首先来看接口 。

public interface Downloader {   Response load(okhttp3.Request request) throws IOException;   void shutdown(); }

注意到接口中声明了两个方法。  第一个方法参数为 Request , 方法返回值为 Reponse 。  很明显如果我们要自己实现 Downloader , 需要使用同步方法。  第二个方法 shutdown 。  正常开发中,几乎不使用这个方法。这个方法是用来关闭磁盘缓存和其他资源。  正常情况下我们使用单例获取 Picasso 对象。 某些特殊情况下,你创建了多个 Picasso 的实例,当你不再需要使用其他 Picasso实例的时候,你可以调用此方法。  举个例子 :  看下 Picasso 中的 shutdown() 方法 :  首先就判断了当前对象是否和单例是同一对象。如果了同一对象则抛出异常  也就是说这个方法是用来回收其他 Picasso 实例资源的。例如回收内存缓存、取消请求、关闭线程池等。

   public void shutdown() {     if (this == singleton) {       throw new UnsupportedOperationException("Default singleton instance cannot be shutdown.");     }     if (shutdown) {       return;     }     cache.clear();     cleanupThread.shutdown();     stats.shutdown();     dispatcher.shutdown();     for (DeferredRequestCreator deferredRequestCreator : targetToDeferredRequestCreator.values()) {       deferredRequestCreator.cancel();     }     targetToDeferredRequestCreator.clear();     shutdown = true;   }

如果你对 shutdown 还有疑问,可以访问这个链接 issue 。  Picasso 中Downloader 的唯一实现类为 OkHttp3Downloader 。网络请求和磁盘缓存都交由 OkHttp 处理。

public final class OkHttp3Downloader implements Downloader {   @VisibleForTesting final Call.Factory client;   private final Cache cache;   private boolean sharedClient = true;   public OkHttp3Downloader(final Context context) {     this(Utils.createDefaultCacheDir(context));   }   public OkHttp3Downloader(final File cacheDir) {     this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));   }   public OkHttp3Downloader(final Context context, final long maxSize) {     this(Utils.createDefaultCacheDir(context), maxSize);   }   public OkHttp3Downloader(final File cacheDir, final long maxSize) {     this(new OkHttpClient.Builder().cache(new Cache(cacheDir, maxSize)).build());     sharedClient = false;   }   public OkHttp3Downloader(OkHttpClient client) {     this.client = client;     this.cache = client.cache();   }       public OkHttp3Downloader(Call.Factory client) {     this.client = client;     this.cache = null;   }   @NonNull @Override public Response load(@NonNull Request request) throws IOException {     return client.newCall(request).execute();   }   @Override public void shutdown() {     if (!sharedClient && cache != null) {       try {         cache.close();       } catch (IOException ignored) {       }     }   } }

我们看到 OkHttp3Downloader 这个类做了三件事。  1. 初始化 OkHttpClient, 指定磁盘缓存的目录。  2. 实现了 load 同步方法。  3. 实现了 shutdown 方法,关闭磁盘缓存。

如果项目中需要对图片加载实现细粒度的监控,例如当加载失败的时候,记录访问失败的 IP 地址、统计加载失败、加载成功的次数等。  可以使用 OkHttp 的 Interceptor 来拦截请求。  当然 Picasso 也有提供图片加载失败的回调 Listener,但是可定制程度不高,下面会介绍到 。  大家都知道磁盘缓存和网络请求是耗时任务,需要异步加载。但是这个接口 load() 方法是同步方法。  那么 Picasso 当中怎么处理异步请求的呢 ?  Picasso 自定义了线程池来处理异步任务,官方解释这样做的一个好处是开发者可以提供自己的实例来替代 Picasso 提供的 PicassoExecutorService 。  仔细想想,这就是优秀的框架写法,高扩展性。满足开发者不同的业务需求。

ExecutorService ExecutorService 在 Picasso 中的实现类为 PicassoExecutorService 。乍一看代码有点多,脑壳有点疼。  仔细看下,刨除 adjustThreadCount() 方法后,不到 20 行代码。一下子就豁然开朗、神清气爽 !  而 adjustThreadCount() 方法很简单,根据当前网络状态, 设置线程池维护线程数。  虽然国内基本都是 4G 和 wifi ,不过优秀的框架就是严谨,各种情况都考虑到了,追求最优的性能。  我们可以看到线程池默认维护了三个核心线程,但是根据网络状态,又进行调整。wifi 下维护了四个线程, 4G网络状态维护了三个线程。

class PicassoExecutorService extends ThreadPoolExecutor {   private static final int DEFAULT_THREAD_COUNT = 3;   PicassoExecutorService() {     super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,         new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());   }   void adjustThreadCount(NetworkInfo info) {     if (info == null || !info.isConnectedOrConnecting()) {       setThreadCount(DEFAULT_THREAD_COUNT);       return;     }     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);     }   }   private void setThreadCount(int threadCount) {     setCorePoolSize(threadCount);     setMaximumPoolSize(threadCount);   }   @Override   public Future<?> submit(Runnable task) {     PicassoFutureTask ftask = new PicassoFutureTask((BitmapHunter) task);     execute(ftask);     return ftask;   }   private static final class PicassoFutureTask extends FutureTask<BitmapHunter>       implements Comparable<PicassoFutureTask> {     private final BitmapHunter hunter;     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();       // High-priority requests are "lesser" so they are sorted to the front.       // Equal priorities are sorted by sequence number to provide FIFO ordering.       return (p1 == p2 ? hunter.sequence - other.hunter.sequence : p2.ordinal() - p1.ordinal());     }   } }

PicassoExecutorService 在写法上还有一点可以借鉴,按优先级来执行任务。当指定 ThreadFactory 的时候,我们注意到使用了 PicassoThreadFactory 。  点进去看一下,值得注意的是 PicassoThread 。 在调用 run 方法的时候设置了线程优先级。  等等 ! 似乎和我平常设置线程优先级的方法不同。

  PicassoExecutorService() {     super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,         new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());   } ....   static class PicassoThreadFactory implements ThreadFactory {     @SuppressWarnings("NullableProblems")     public Thread newThread(Runnable r) {       return new PicassoThread(r);     }   }   private static class PicassoThread extends Thread {     PicassoThread(Runnable r) {       super(r);     }     @Override public void run() {       Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);       super.run();     }   }

我之前设置线程优先级用的 Java 提供的 API 。

   Thread.currentThread().setPriority(NORM_PRIORITY + 1);

但是 Picasso 通过 Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND) 来设置线程优先级,这个是 Android 提供的 API 。  那么问题来了 ? 两者之间具体的区别是什么呢 ?  Process.setThreadPriority() 为 Android 提供 API,基于 linux 优先级设置线程优先级,从最高优先级 -20 到 最低优先级 19 。(看起来调度粒度更细,效果更好。)  Thread.currentThread().setPriority() 为 Java 提供API,从最高优先级 10 到最低优先级 1 。  Process.setThreadPriority() 并不影响 Thread.currentThread().getPriority() 的值。  同时还注意到了一点,不要轻易在主线程设置 Process.setThreadPriority(), 这个哥们遇到了个坑。

Cache Picasso 中涉及到缓存的只有内存缓存和磁盘缓存。  磁盘缓存 : DiskLruCache 由 OkHttp 来管理。  内存缓存 : LruCahe 。  Picasso 中 LruCache 实现了 Cache 接口 。需要注意的是,如果你自定义实现了 Cache 接口,需要保证其方法是线程安全的。

public interface Cache {   Bitmap get(String key);   void set(String key, Bitmap bitmap);   int size();   int maxSize();   void clear();   void clearKeyUri(String keyPrefix); }

LruCache 内部实际上使用 android 的 LruCache 。 LruCache 原理这里就不提了,因为涉及的东西比较多。  以后还是单独开个文章来写, 对应的学习顺序为 HashMap -> LinkedHashMap->LruCache-> DiskLruCache 。

public final class LruCache implements Cache {   final android.util.LruCache<String, BitmapAndSize> cache;   /** Create a cache using an appropriate portion of the available RAM as the maximum size. */   public LruCache(@NonNull Context context) {     this(Utils.calculateMemoryCacheSize(context));   }   /** Create a cache with a given maximum size in bytes. */   public LruCache(int maxByteCount) {     cache = new android.util.LruCache<String, BitmapAndSize>(maxByteCount) {       @Override protected int sizeOf(String key, BitmapAndSize value) {         return value.byteCount;       }     };   }   @Nullable @Override public Bitmap get(@NonNull String key) {     BitmapAndSize bitmapAndSize = cache.get(key);     return bitmapAndSize != null ? bitmapAndSize.bitmap : null;   }   @Override public void set(@NonNull String key, @NonNull Bitmap bitmap) {     if (key == null || bitmap == null) {       throw new NullPointerException("key == null || bitmap == null");     }     int byteCount = Utils.getBitmapBytes(bitmap);     // If the bitmap is too big for the cache, don't even attempt to store it. Doing so will cause     // the cache to be cleared. Instead just evict an existing element with the same key if it     // exists.     if (byteCount > maxSize()) {       cache.remove(key);       return;     }     cache.put(key, new BitmapAndSize(bitmap, byteCount));   }   @Override public int size() {     return cache.size();   }   @Override public int maxSize() {     return cache.maxSize();   }   @Override public void clear() {     cache.evictAll();   }   @Override public void clearKeyUri(String uri) {     // Keys are prefixed with a URI followed by '\n'.     for (String key : cache.snapshot().keySet()) {       if (key.startsWith(uri)           && key.length() > uri.length()           && key.charAt(uri.length()) == KEY_SEPARATOR) {         cache.remove(key);       }     }   }   /** Returns the number of times {@link #get} returned a value. */   public int hitCount() {     return cache.hitCount();   }   /** Returns the number of times {@link #get} returned {@code null}. */   public int missCount() {     return cache.missCount();   }   /** Returns the number of times {@link #set(String, Bitmap)} was called. */   public int putCount() {     return cache.putCount();   }   /** Returns the number of values that have been evicted. */   public int evictionCount() {     return cache.evictionCount();   }   static final class BitmapAndSize {     final Bitmap bitmap;     final int byteCount;     BitmapAndSize(Bitmap bitmap, int byteCount) {       this.bitmap = bitmap;       this.byteCount = byteCount;     }   } }

通过这个类还可以学到获取最大可用内存的方法。Picasso 中获取了最大可用内存的七分之一来作为 LruCache 最大容量。这个方法果断收藏到自己的类库中。

  static int calculateMemoryCacheSize(Context context) {     ActivityManager am = getService(context, ACTIVITY_SERVICE);     boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;     int memoryClass = largeHeap ? am.getLargeMemoryClass() : am.getMemoryClass();     // Target ~15% of the available heap.     return (int) (1024L * 1024L * memoryClass / 7);   }

Listener Listener 所有图片加载失败都会走这个回调,注释也写的比较清楚,主要为了统计、分析使用。

  public interface Listener {     /**      * Invoked when an image has failed to load. This is useful for reporting image failures to a      * remote analytics service, for example.      */     void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception);   }

如果是想要监听单个图片加载失败后的调用。

Picasso.get().load(url).into(imageView,new Callback());

我上面的代码对吗 ? 写的不对 ! 匿名内存类持有外部类的引用,导致 Activity 和 Fragment 不能被回收。  怎么解决呢 ? 在 Activity 或 Fragment onDestroy 的时候取消请求。(事实上对 OkHttp 回调处理方式也相同。)  解决方案 Picasso 在方法上明确注释了, 所以阅读源码的时候,多看看注释还是有好处的。

Picasso.get().cancelRequest(imageView);  或者 Picasso.get().cancelTag(tag);

有的时候我们不需要加载图片到 ImageView 当中, 仅仅需要获取 Bitmap 对象。你可以这样写,不要着急请往下看,  此处有坑 !

    Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(new Target() {             @Override             public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {             }             @Override             public void onBitmapFailed(Exception e, Drawable errorDrawable) {             }             @Override             public void onPrepareLoad(Drawable placeHolderDrawable) {             }         });

我上面的写法对吗 ? 也不对哦 ! 哪里不对呢 ! 赶紧点进去源码看注释,注释写了这个方法持有 Target 实例的弱引用。如果你不持有强引用的话,会被垃圾回收器回收。我里个龟龟哦 ! 这句话告诉我们,这样写可能永远都不会有回调。  那么办呢 ? 不要使用匿名内部类,使用成员变量吧。事实上我们传入的 ImageView 也是持有的弱引用。

private Target target = new Target() {       @Override       public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {       }       @Override       public void onBitmapFailed(Drawable errorDrawable) {       }       @Override       public void onPrepareLoad(Drawable placeHolderDrawable) {       } } private void someMethod() {    Picasso.with(this).load("url").into(target); } @Override  public void onDestroy() {  // could be in onPause or onStop    Picasso.with(this).cancelRequest(target);    super.onDestroy(); }

官方推荐在自定义 View 或者 ViewHolder 中实现 Target 接口。 例如 :

  public class ProfileView extends FrameLayout implements Target {       {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {        setBackgroundDrawable(new BitmapDrawable(bitmap));      }       {@literal @}Override public void onBitmapFailed(Exception e, Drawable errorDrawable) {         setBackgroundDrawable(errorDrawable);       }       {@literal @}Override public void onPrepareLoad(Drawable placeHolderDrawable) {         setBackgroundDrawable(placeHolderDrawable);       }     }    public class ViewHolder implements Target {       public FrameLayout frame;       public TextView name;       {@literal @}Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {         frame.setBackgroundDrawable(new BitmapDrawable(bitmap));      }       {@literal @}Override public void onBitmapFailed(Exception e, Drawable errorDrawable) {         frame.setBackgroundDrawable(errorDrawable);       }       {@literal @}Override public void onPrepareLoad(Drawable placeHolderDrawable) {         frame.setBackgroundDrawable(placeHolderDrawable);      }

上述的例子,点进源码看上边的注释就能看到。

RequestTransformer 在每个请求提交之前,可以对请求进行额外的处理。  注意这里的 Request 是 Picasso 中的 Request , 不是 OkHttp3 中的 Request 。  官方对这个方法的解释是 : 为了更快的下载速度,你可以更改图片的 hostname 为离用户最近的 CDN 的 hostname。

  public interface RequestTransformer {     Request transformRequest(Request request);   }

但我们是否能利用这个接口来做些其他的事呢 我们看下这个接口什么时候调用。  是在 RequestCreator 的 createRequest() 方法中调用。而 createRequest() 基本上在  RequestCreator 的 into() 方法中调用, 该方法调用是相当早的。从这个角度来说,除了更改 hostname 以外,我们并不能做太多的事。因此注释也说了, 这个功能是个测试的功能,之后可能会改变。所以大家不需要花太多精力在这个 API 上。

  private Request createRequest(long started) {     int id = nextId.getAndIncrement();     Request request = data.build();     request.id = id;     request.started = started;     boolean loggingEnabled = picasso.loggingEnabled;     if (loggingEnabled) {       log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());     }     Request transformed = picasso.transformRequest(request);     if (transformed != request) {       // If the request was changed, copy over the id and timestamp from the original.       transformed.id = id;       transformed.started = started;       if (loggingEnabled) {         log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed);       }     }     return transformed;   }

RequestHandler RequestHandler 请求处理器, Picasso 中 非常重要的抽象类。  代码如下 :

public abstract class RequestHandler {   public abstract boolean canHandleRequest(Request data);   @Nullable public abstract Result load(Request request, int networkPolicy) throws IOException;   int getRetryCount() {     return 0;   }   boolean shouldRetry(boolean airplaneMode, NetworkInfo info) {     return false;   }   boolean supportsReplay() {     return false;   } }

我们注意到 RequestHandler 有两个很重要的抽象的方法。

public abstract boolean canHandleRequest(Request data); public abstract Result load(Request request, int networkPolicy) throws IOException;

canHandleRequest() : 是否能够处理给定的 Request 。  load() : 如果 canHandleRequest() 返回值为 true 。那么之后就会执行 load () 方法 。

我们知道 Picasso.get().load() 方法可以传入 File 、Uri、ResourceId 。  不同的参数是怎么处理的呢 ? 就是让对应的 RequestHandler 子类来实现 。  看下继承关系 :

可以看出扩展性好,结构清晰。  那么简单介绍下,这些实现类分别是具体解决哪些问题 。  AssetRequestHandler : 从 assets 目录下加载图片。   有的同学说,我怎么没有这个目录 ? 你可以创建一个 asset 文件,然后放张图片进去。 

具体加载的代码是这样的 : 其中 file:///android_asset/ 为固定格式 。。

Picasso.get().load("file:///android_asset/icon_flower.jpg").into(ivSrc);

有同学可能会说 ,我直接用系统 API 也能加载 ,干嘛用 Picasso 。嗯 , 你说的没错  使用 Picasso 的好处呢有两点 :  1. 内存缓存,加载速度很快。  2. 一行代码,链式调用 !

ResourceRequestHandler 加载 res下的图片,通过资源 Id 或者 资源 Id 的 URI 来加载图片 。

Picasso.get().load(R.drawable.test_img).into(ivSrc); //两种写法效果相同,第一种方法用的最多。 Picasso.get().load("android.resource://" + getPackageName() + "/" + R.drawable.test_img).into(ivSrc);

ContactsPhotoRequestHandler 加载联系人头像,我在开发过程中很少用到。  测试起来太麻烦了,我就偷懒不写代码了。 

NetworkRequestHandler 重点来了, 从网络中加载图片 。我们简单来看下 load ()方法 。  首先将 Picasso 中的 Request 对象转换为 OKHttp 中的 Request 对象 。  然后使用 OkHttp3Downloader 同步下载图片。  然后判断请求是否成功 , 文件 content-length 是否为 0 。  整个过程很简单。

 @Override public Result load(Request request, int networkPolicy) throws IOException {     okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);     Response response = downloader.load(downloaderRequest);     ResponseBody body = response.body();     if (!response.isSuccessful()) {       body.close();       throw new ResponseException(response.code(), request.networkPolicy);     }     // Cache response is only null when the response comes fully from the network. Both completely     // cached and conditionally cached responses will have a non-null cache response.     Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;     // Sometimes response content length is zero when  requests are being replayed. Haven't found     // root cause to this but retrying the request seems safe to do so.     if (loadedFrom == DISK && body.contentLength() == 0) {       body.close();       throw new ContentLengthException("Received response with 0 content-length header.");     }     if (loadedFrom == NETWORK && body.contentLength() > 0) {       stats.dispatchDownloadFinished(body.contentLength());     }     return new Result(body.source(), loadedFrom);   }

ContentStreamRequestHandler 这个不好翻译,有人翻译成从 ContentProvider 中加载图片,我觉得不太准确 。 虽然 ContentStreamRequestHandler 处理的 URI Scheme 为 content ,但是在设计上 FileRequestHandler 却继承自 ContentStreamRequestHandler , 处理的 URI Scheme 为 file 。  ContentStreamRequestHandler 及其子类 可以处理 File 、相册图片、视频缩略图。

Bitmap.Config 图片压缩质量参数  ARGB_8888 每个像素占用四个字节。  ARGB_4444 每个像素占两个字节 。已被废弃, API 19 以上 将被替换为 ARGB_8888 。  RGB_565 每个像素占两个字节。  ALPHA_8 每个像素占用1字节(8位),存储的是透明度信息。  Glide 默认使用 RGB_565 ,因此有些人说 Glide 性能优化好 !!! 其实你只要设置 Picasso 的 Bitmap.Config , 同时也设置 Picasso 的 resize () 。  从性能上来看,未必比 Glide 差。

indicatorsEnabled 是否显示图片从哪里加载。  红色 : 网络加载。  蓝色 : 磁盘加载 。  绿色 : 内存中加载。  开发过程中,可以开启。便于我们 debug ,正式环境记得关闭。

loggingEnabled 显示 Picasso 加载图片的日志 ,便于我们 debug,正式环境记得关闭。


最新回复(0)