圖片加載庫Picasso和Glide對比

Android上圖片加載庫現在出現了有挺多了,比較出名的有ImageLoader、Picasso、Glide、Fresco,這裏來對比一下兩個比較相像的庫,Picasso和Glide。

這兩個庫在API使用上非常的類似,都是很簡單的鏈式調用法,但是實現上卻有較大的不同,下載源碼就能發現Glide複雜很多,Picasso相對就簡單不少,下面就分幾個方面來簡單對比一下這兩個庫的不同之處和實現的關鍵點。
在這裏插入圖片描述

1.API使用

API使用上就不多做介紹,簡單貼一下samle源碼:
Picasso:

Picasso.get()
    .load(url)
    .placeholder(R.drawable.user_placeholder)
    .error(R.drawable.user_placeholder_error)
    .into(imageView);

Glide:

Glide.with(fragment)
   .load(myUrl)
   .placeholder(placeholder)
   .fitCenter()
   .into(imageView);

2.緩存實現

Picasso緩存比較簡單,有兩級,內存緩存實現是LruCache,源碼在com.squareup.picasso3.PlatformLruCache類,裏面有個LruCache<String, BitmapAndSize> cache;變量,在調到into(imageView)時是這樣的:

// RequestCreator.java
if (shouldReadFromMemoryCache(request.memoryPolicy)) {
  Bitmap bitmap = picasso.quickMemoryCacheCheck(request.key);
  if (bitmap != null) {
    picasso.cancelRequest(target);
    RequestHandler.Result result = new RequestHandler.Result(bitmap, MEMORY);
    setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled);
    if (picasso.loggingEnabled) {
      log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
    }
    if (callback != null) {
      callback.onSuccess();
    }
  // Picasso.java  
  @Nullable Bitmap quickMemoryCacheCheck(String key) {
  Bitmap cached = cache.get(key);
  if (cached != null) {
    stats.dispatchCacheHit();
  } else {
    stats.dispatchCacheMiss();
  }
  return cached;
}

磁盤緩存Picasso直接使用了OkHttp緩存機制,沒有單獨的磁盤緩存設計,關鍵代碼在:

// NetworkRequestHandler.java
private static okhttp3.Request createRequest(Request request) {
  CacheControl cacheControl = null;
  int networkPolicy = request.networkPolicy;
  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();
    }
  }

 // ......
  return builder.build();
}

而Glide則比較複雜,首先內存緩存分活動緩存即正在使用的和另一個內存緩存,關鍵代碼:

// com.bumptech.glide.load.engine.Engine類的load方法
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
    resourceClass, transcodeClass, options);

EngineResource active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
  cb.onResourceReady(active, DataSource.MEMORY_CACHE);
  if (VERBOSE_IS_LOGGABLE) {
    logWithTimeAndKey("Loaded resource from active resources", startTime, key);
  }
  return null;
}

EngineResource cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
  cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
  if (VERBOSE_IS_LOGGABLE) {
    logWithTimeAndKey("Loaded resource from cache", startTime, key);
  }
  return null;
}
// 默認是LruResourceCache,關鍵代碼在GlideBuilder.java的build方法中可以看到
if (memoryCache == null) {
  memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}

if (diskCacheFactory == null) {
  diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}

而磁盤緩存是DiskLruCache,關鍵代碼:

// DiskLruCacheFactory
@Override
public DiskCache build() {
  File cacheDir = cacheDirectoryGetter.getCacheDirectory();

  if (cacheDir == null) {
    return null;
  }

  if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
    return null;
  }

  return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
}
// DiskLruCacheWrapper.java
private synchronized DiskLruCache getDiskCache() throws IOException {
  if (diskLruCache == null) {
    diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
  }
  return diskLruCache;
}

而在decode圖片時,Glide還獨特做了優化,它使用BitmapPool緩存了option相同的圖片,以減少內存開銷,減少內存回收機率,從而提高性能,關鍵代碼:

// Downsampler.java
private static void setInBitmap(
    BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
  @Nullable Bitmap.Config expectedConfig = null;
 // ...
  // BitmapFactory will clear out the Bitmap before writing to it, so getDirty is safe.
  options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}

// Downsampler.java decodeStream方法
try {
  result = BitmapFactory.decodeStream(is, null, options);
} catch (IllegalArgumentException e) {
  IOException bitmapAssertionException =
      newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options);
  if (Log.isLoggable(TAG, Log.DEBUG)) {
    Log.d(TAG, "Failed to decode with inBitmap, trying again without Bitmap re-use",
        bitmapAssertionException);
  }
  if (options.inBitmap != null) {
    try {
      is.reset();
      bitmapPool.put(options.inBitmap);
      options.inBitmap = null;
      return decodeStream(is, options, callbacks, bitmapPool);
    } catch (IOException resetException) {
      throw bitmapAssertionException;
    }
  }
  throw bitmapAssertionException;
} finally {
  TransformationUtils.getBitmapDrawableLock().unlock();
}

3.生命週期

Picasso沒有生命週期綁定的功能,而Glide實現了,Glide是在Activity裏插入一個沒有界面的Fragment來達到綁定的,關鍵代碼:

// RequestmanagerRetriever.java
private RequestManager fragmentGet(@NonNull Context context,
    @NonNull android.app.FragmentManager fm,
    @Nullable android.app.Fragment parentHint,
    boolean isParentVisible) {
  RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
  RequestManager requestManager = current.getRequestManager();
  if (requestManager == null) {
    // TODO(b/27524013): Factor out this Glide.get() call.
    Glide glide = Glide.get(context);
    requestManager =
        factory.build(
            glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
    current.setRequestManager(requestManager);
  }
  return requestManager;
}
// RequestManagerFragment.java
@NonNull
ActivityFragmentLifecycle getGlideLifecycle() {
  return lifecycle;
}
@Override
public void onStop() {
  super.onStop();
  lifecycle.onStop();
}

Glide將RequestMangerFragment的生命週期通過Lifecycle綁定到RequestManager上,原理是RequestMangerFragment.setRequestManager然後在RequestManager構造的時候調用lifecycle.addListener

if (Util.isOnBackgroundThread()) {
  mainHandler.post(addSelfToLifecycle);
} else {
  lifecycle.addListener(this);
}

不過要綁定Activity的生命週期需要使用Activity作爲Context傳入Glide中

Glide.width(Activity).load(url).into(target);

熟悉內存泄漏的你看到Activity傳到單實例肯定擔心,不過在RequestManger在收到生命週期Destory時會自動釋放引用:

// RequestManager.java
@Override
public void onDestroy() {
  targetTracker.onDestroy();
  for (Target❔ target : targetTracker.getAll()) {
    clear(target);
  }
  targetTracker.clear();
  requestTracker.clearRequests();
  lifecycle.removeListener(this);
  lifecycle.removeListener(connectivityMonitor);
  mainHandler.removeCallbacks(addSelfToLifecycle);
  glide.unregisterRequestManager(this);
}

4.內存佔用

Picasso內存佔用較高的原因是默認decode的時候沒有用實際的控件大小,關鍵代碼:

// BitmapUtils.java
private static Bitmap decodeStreamPreP(Request request, BufferedSource bufferedSource)
    throws IOException {
  boolean isWebPFile = Utils.isWebPFile(bufferedSource);
  boolean isPurgeable = request.purgeable && SDK_INT < Build.VERSION_CODES.LOLLIPOP;
  BitmapFactory.Options options = createBitmapOptions(request);
  boolean calculateSize = requiresInSampleSize(options);

  Bitmap bitmap;
  // We decode from a byte array because, a) when decoding a WebP network stream, BitmapFactory
  // throws a JNI Exception, so we workaround by decoding a byte array, or b) user requested
  // purgeable, which only affects bitmaps decoded from byte arrays.
  if (isWebPFile || isPurgeable) {
    byte[] bytes = bufferedSource.readByteArray();
    if (calculateSize) {
      BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
      calculateInSampleSize(request.targetWidth, request.targetHeight,
          checkNotNull(options, "options == null"), request);
    }
    bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
  } else {
    if (calculateSize) {
      BitmapFactory.decodeStream(bufferedSource.peek().inputStream(), null, options);
      calculateInSampleSize(request.targetWidth, request.targetHeight,
          checkNotNull(options, "options == null"), request);
    }
    bitmap = BitmapFactory.decodeStream(bufferedSource.inputStream(), null, options);
  }
  if (bitmap == null) {
    // Treat null as an IO exception, we will eventually retry.
    throw new IOException("Failed to decode bitmap.");
  }
  return bitmap;
}
// BitmapUtils.java
@Nullable static BitmapFactory.Options createBitmapOptions(Request data) {
  final boolean justBounds = data.hasSize();
  BitmapFactory.Options options = null;
  if (justBounds || data.config != null || data.purgeable) {
    options = new BitmapFactory.Options();
    options.inJustDecodeBounds = justBounds;
    options.inInputShareable = data.purgeable;
    options.inPurgeable = data.purgeable;
    if (data.config != null) {
      options.inPreferredConfig = data.config;
    }
  }
  return options;
}
// Request.java
public boolean hasSize() {
  return targetWidth != 0 || targetHeight != 0;
}

可以看到Picasso在解碼時需要判斷Request的targetWidth和targetHeight是不是爲0,需要在調用時調用resize指定大小纔會觸發到這裏。而Glide默認就做了這個事,關鍵代碼:

// SingleRequest.java begin方法
status = Status.WAITING_FOR_SIZE;
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
  onSizeReady(overrideWidth, overrideHeight);
} else {
  target.getSize(this);
}
// ViewTarget.java
void getSize(@NonNull SizeReadyCallback cb) {
  int currentWidth = getTargetWidth();
  int currentHeight = getTargetHeight();
  if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
    cb.onSizeReady(currentWidth, currentHeight);
    return;
  }

  // We want to notify callbacks in the order they were added and we only expect one or two
  // callbacks to be added a time, so a List is a reasonable choice.
  if (!cbs.contains(cb)) {
    cbs.add(cb);
  }
  if (layoutListener == null) {
    ViewTreeObserver observer = view.getViewTreeObserver();
    layoutListener = new SizeDeterminerLayoutListener(this);
    observer.addOnPreDrawListener(layoutListener);
  }
}

//ViewTarget.java
private static final class SizeDeterminerLayoutListener
    implements ViewTreeObserver.OnPreDrawListener {
//......
  @Override
  public boolean onPreDraw() {
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "OnGlobalLayoutListener called attachStateListener=" + this);
    }
    SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
    if (sizeDeterminer != null) {
      sizeDeterminer.checkCurrentDimens();
    }
    return true;
  }
}
// SizeDeterminer類
private void notifyCbs(int width, int height) {
  for (SizeReadyCallback cb : new ArrayList<>(cbs)) {
    cb.onSizeReady(width, height);
  }
}

可以看到在into的時候如果size不正確會調用ViewTarget的getSize,然後通過OnPrewDrawListener的回調獲得大小後再回到onSizeReady到SingleRequest執行engine的load進行加載。

5.擴展性

擴展性方面Picasso比較差,比如網絡只能使用OkHttp,關鍵代碼:

// Picasso.java
@NonNull
public Builder client(@NonNull OkHttpClient client) {
  checkNotNull(client, "client == null");
  callFactory = client;
  return this;
}
// NetworkRequestHandler.java
private static okhttp3.Request createRequest(Request request) {
  //......
  Uri uri = checkNotNull(request.uri, "request.uri == null");
  okhttp3.Request.Builder builder = new okhttp3.Request.Builder().url(uri.toString());
  if (cacheControl != null) {
    builder.cacheControl(cacheControl);
  }
  return builder.build();
}

而Glide則可以使用OkHttp或Volley,擴展比較多,具體代碼這裏就不分析

6.圖片格式/圖形變換

Picasso默認不支持gif,而glide支持,只要這樣使用:

Glide.width(context).asGif().load(url).into(target);

圖形變換兩個庫都支持,但Picasso沒有默認實現,只有Transformation接口,而Glide有默認實現,如圓角圖片:

RequestOptions mRequestOptions = RequestOptions.circleCropTransform()
.diskCacheStrategy(DiskCacheStrategy.NONE)//不做磁盤緩存
.skipMemoryCache(true);//不做內存緩存
 
Glide.with(mContext).load(userInfo.getImage()).apply(mRequestOptions).into(mUserIcon);

總結

Picasso和Glide兩個庫各有優缺點,需要根據不同的需求進行選擇,不過較爲複雜的場景下,Glide還是更爲推薦使用。Glide庫裏面的緩存實現等技術,非常值得學習,看了源碼也會發現,其實最根本的還是要掌握android的基本功,比如BitmapPool技術其實就是用了decode圖片時的內存複用技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章