Glide看点(三) 内存管理

Glide看点(三) 内存管理

Glide里的缓存是一个精妙的多级缓存,从文档里我们可以知道总的缓存策略如下:

默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:

活动资源 (Active Resources) – 现在是否有另一个 View 正在展示这张图片?
内存缓存 (Memory cache) – 该图片是否最近被加载过并仍存在于内存中?
资源类型(Resource) – 该图片是否之前曾被解码、转换并写入过磁盘缓存?
数据来源 (Data) – 构建这个图片的资源是否之前曾被写入过文件缓存?
前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。

如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)。

本文讲分析Glide对于Bitmap的内存缓存逻辑:

ActiveCache的使用

Glide里有内存里分别有两层缓存,一个是Active Cache一个是普通的Cache。他们本质上都是LinkedHashMap实现的LRUCache,只是调度策略让他们拥有了不同的功能。

从load方法来看策略:

 EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      return null;
    }

    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      return null;
    }
  • 加载时先从Active Cache里检查,如果没有再从普通的Cache里拿去
  • 当从Normal cache里取出来后会把对象加入到Activie里,并且从普通的cache里移除
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
  }  
  private EngineResource<?> getEngineResourceFromCache(Key key) {
    Resource<?> cached = cache.remove(key);

    final EngineResource<?> result;
    if (cached == null) {
      result = null;
    } else if (cached instanceof EngineResource) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource<?>) cached;
    } else {
      result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);
    }
    return result;
  }

  • 当resource release的时候会在active resouce移除对象,在普通cache里加入
 @Override
  public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    Util.assertMainThread();
    activeResources.deactivate(cacheKey);
    if (resource.isCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource);
    }
  }

画个数据流图看得更直观一些

注: resource release时机解释:每个资源有一个自己的使用计数,每当使用的时候会调用acquire方法给计数加1,每次job完成后会计数减1,当计数为零时进行release

Object Pool的使用

我们知道如果较短时间内频繁的新建对象是会造成内存的抖动,可能造成界面的卡顿。

Object Pool就是为了避免这种情况发生的一种常见设计模式。它会初始化一些列的对象,当需要对象的时候不是去创建一个新的对象,二手去复用闲置的对象资源。当没有闲置资源的时候进行扩容。
Glide里大量使用了Object Pool的实现。

Bitmap Pool

Bitmap Pool是保证Bitmap回收使用管理的类,避免了Bitmap被大量创建造成频繁GC的问题。
下面已LRUBitmapPool为安利分析
LRUBitmapPool持有LruPoolStrategy(具体实现是SizeStrategy,SizeConfigStrategy),它是正常持有Bitmap缓存的地方

com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool

private final KeyPool keyPool = new KeyPool();
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
private final NavigableMap<Integer, Integer> sortedSizes = new PrettyPrintTreeMap<>();

Key 根据width和height算出实际的bitmap大小的一个类,作为HashMap的键 (后面会解释为什么Key是bitmap大小)
KeyPool是用宽高生成一个Key的ObjectPool
groupedMap: 缓存Bitmap的对象,是一个修改过的LinkedHashMap作为LRU cache
sortedSizes: 记录每个Key下对应的Bitmap的数量,且已大小排序

为什么Bitmap Pool的Key是Bitmap的尺寸

Android里默认的API每次decode bitmap或则一些对bitmap的操作时默认是新创建一张Bitmap。API 11以后提供了BitmapFactory.Options.inBitmap这个变量来告诉系统去复用原来的已经存在的bitmap而不是重新创建bitmap从而达到节省内存的目的。
但是inBitmap的使用有一些限制,复用的bitmap的大小必须大于等于原来的尺寸。所以需要用bitmap的尺寸作为Key来缓存bitmap。

其他Tips

  • 不同的BitMapConfig不能复用,所以需要用不同的Pool来缓存bitmap。这里也就是Glide里的
    com/bumptech/glide/load/engine/bitmap_recycle/SizeConfigStrategy.java 存在的意义

总结

  • 从Glide里我们可以学习到如何用LruCache建立多级缓存的逻辑。
  • 如何使用inBitmap对bitmap进行缓存
  • LinkedHashMap是如何实现LRU管理的(双向循环链表)
    ## 参考
    Glide v4 Caching
    Re-using Bitmaps

Glide看点(二)巧用Fragment管理生命周期

Motivation/动机

图片的一般是异步的,异步经常面临的问题是内存泄露和异步加载回来view已经销毁导致的空指针问题。而Glide在使用的时候只要求传入当前加载的view或者context,且没有用setLifeCyclelistener什么的方法就实现了生命周期的管理,它是如何做到的呢?

Fragment Trick

我们知道Fragment和View最大区别其实就是多了生命周期,Glide采用的是用创建一个空白Fragment来实现生命周期的管理和监听的。这是如何实现的我们一步步看代码:

Step 1:根据传入的Context得到FragmentManager

Glide.java

     @NonNull
      public static RequestManager with(@NonNull Context context) {
        return getRetriever(context).get(context);
      }

获取到正确的Context
com/bumptech/glide/manager/RequestManagerRetriever.java

      @NonNull
      public RequestManager get(@NonNull Context context) {
        if (context == null) {
          throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
          if (context instanceof FragmentActivity) {
            return get((FragmentActivity) context);
          } else if (context instanceof Activity) {
            return get((Activity) context);
          } else if (context instanceof ContextWrapper) {
            return get(((ContextWrapper) context).getBaseContext());
          }
        }
        return getApplicationManager(context);
      }

根据Context获取到FragmentManager

@NonNull
  public RequestManager get(@NonNull FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      FragmentManager fm = activity.getSupportFragmentManager();
      return supportFragmentGet(activity, fm, null /*parentHint*/);
    }
  }

Step 2:RequestManagerFragment里注册Callback

获取SupportRequestManagerFragment,这个就是监听生命周期的主角Fragment了。下面去看看SupportRequestManagerFragment里有什么吧

  @NonNull
  private RequestManager supportFragmentGet(@NonNull Context context, @NonNull FragmentManager fm,
      @Nullable Fragment parentHint) {
    SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint);
    RequestManager requestManager = current.getRequestManager();
   // 省略..部分
    return requestManager;
  }

其实里面就是一个持有ActivityFragmentLifecycle和RequestManager的空白Fragment。

public class SupportRequestManagerFragment extends Fragment {
  private static final String TAG = "SupportRMFragment";
  private final ActivityFragmentLifecycle lifecycle;
  @Nullable private RequestManager requestManager;
  
  // ..省略..
  @Override
  public void onStart() {
    super.onStart();
    lifecycle.onStart();
  }

  @Override
  public void onStop() {
    super.onStop();
    lifecycle.onStop();
  }

  @Override
  public void onDestroy() {
    super.onDestroy();
    lifecycle.onDestroy();
    unregisterFragmentWithRoot();
  }
}

RequestManager里又实现了ActivityFragmentLifecycle从而可以根据什么周期对请求做一些处理
如:在RequestManager里的onDestroy回调里取消所有图片加载请求,销毁监听器
com.bumptech.glide.RequestManager

  @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);
  }

Bouns拓展

空白Fragment还有什么其他妙用呢?
* 动态申请权限。对于不是必要的权限不同意就退出App是非常糟糕的用户体验,最佳实践应该是当要用到该权限的时候再向用户申请。这样写面临的问题就会是授权代码会散落在各个地方,这样显然是很糟糕的。所以用一个空白的Fragment,把权限检查和申请逻辑全部丢到里面,在需要用到地方只要持有这个Fragment做一下检查就好。
如:

public class CameraMicPermissionHelper extends Fragment {
  private static final int REQUEST_CAMERA_MIC_PERMISSIONS = 10;
  public static final String TAG = "CamMicPerm";

  private CameraMicPermissionCallback mCallback;
  private static boolean sCameraMicPermissionDenied;

  public static CameraMicPermissionHelper newInstance() {
    return new CameraMicPermissionHelper();
  }

  public CameraMicPermissionHelper() {
    // Required empty public constructor
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
  }

  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (activity instanceof CameraMicPermissionCallback) {
      mCallback = (CameraMicPermissionCallback) activity;
    } else {
      throw new IllegalArgumentException("activity must extend BaseActivity and implement LocationHelper.LocationCallback");
    }
  }

  @Override
  public void onDetach() {
    super.onDetach();
    mCallback = null;
  }

  public void checkCameraMicPermissions() {
    if (PermissionUtil.hasSelfPermission(getActivity(), new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO})) {
      mCallback.onCameraMicPermissionGranted();
    } else {
      // UNCOMMENT TO SUPPORT ANDROID M RUNTIME PERMISSIONS
      if (!sCameraMicPermissionDenied) {
        requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO}, REQUEST_CAMERA_MIC_PERMISSIONS);
      }
    }
  }

  public void setCameraMicPermissionDenied(boolean cameraMicPermissionDenied) {
    this.sCameraMicPermissionDenied = cameraMicPermissionDenied;
  }

  public static boolean isCameraMicPermissionDenied() {
    return sCameraMicPermissionDenied;
  }

  /**
   * Callback received when a permissions request has been completed.
   */
  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                         int[] grantResults) {

    if (requestCode == REQUEST_CAMERA_MIC_PERMISSIONS) {
      if (PermissionUtil.verifyPermissions(grantResults)) {
        mCallback.onCameraMicPermissionGranted();
      } else {
        Log.i("BaseActivity", "LOCATION permission was NOT granted.");
        mCallback.onCameraMicPermissionDenied();
      }

    } else {
      super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
  }


  public interface CameraMicPermissionCallback {
    void onCameraMicPermissionGranted();
    void onCameraMicPermissionDenied();
  }

}
  • 不建议的hack手段:
    在ViewModel出现之前,有一种很hack的方式用Fragment保存数据。在设备的configurationChange的时候保存大块数据。
    关键方法, 把Fragment设置为true后可以在configurationChange的时候不被销毁,利用这一点保存数据。现在用ViewModel显然是更好的方法
    /* Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change).*/
    
    setRetainInstance(boolean retain)

  • 声明周期的监听,现在用Android Jetpack里的 Lifecycle-Aware Components来做是一个更简单的做法。

参考

Glide源码看点-图片pre scale

Glide是如何根据View大小对图片进行缩放优化呢 (pre scale)

我们都知道图片实际大小和实际显示的区域一般不能是一样的,所以如果图片尺寸本身很大但是展示区域很小的话就会,如果已完整尺寸对图片进行加载就会效率很低浪费内存,加载速度慢。所以一般我们会根据图片实际需要显示的实际大小对图片进行缩放。
Android内置了inSampleSize方法来对图片进行缩放, Glide抽象出了一个类叫Downsampler.class来根据期望的图片大小和缩放策略进行向下采样。所以关键点变为了Glide如何知道我们的View实际需要的显示区域。下面将通过一个性能问题的debug故事分析glide源码

Wrap Content引发的性能问题“血案”

一个类似聊天信息的界面,如果列表里有4,5张图片后,滑动就会变得异常卡顿。screen_shoot

诊断

打开Android Monitor抓取性能Trace,发现咦咋性能消耗最大的是Glide decode Bitmap。
performance
感觉不应该啊,我图片虽然可能很大但是实际显示区域不大,Glide在显示前应该已经会根据ImageView的实际显示效果进行了缩放采样(DownSample)的优化不应该产生卡顿啊。
**等等****
难道缩放采样出现了问题?

我们根据Glide的调用链找分析。

GlideApp.with(context).load(imgUrl).into(target)

RequestBuilder的into方法其实是调用管理图片加载请求的RequestTrack里的track方法最后调用到了具体Request的begin方法。
Step1:
SingleRequest.run()方法 -> ViewTarget.getSize()

如果开发者有制定大小OverrideSize就会直接采用该尺寸,如果没有就去ViewTarget里去拿size
ViewTarget.getSize -> sizeDeterminer.getSize(cb);

ViewTarget里会根据SizeDeterminer去获取TargetView的具体尺寸。如果能直接从View里读取到具体的尺寸就直接用。

如果读取不到就用PreDrawListener在layout完成后去监听view的实际尺寸:
observer.addOnPreDrawListener(layoutListener);
关键点来了,去看添加的preDrawListener,里面有个特殊的if语句,如果view定义了WRAP_CONTENT, Glide就无法判断到底使用什么大小去preScale图片,所以它取了屏幕的最大宽度作为图片的加载大小。(不取图片的Target.SIZE_ORIGINAL的原因是可能图片会很大,远远大于屏幕尺寸)
SizeDeterminerLayoutListener:
`

  if (!view.isLayoutRequested() && paramSize == LayoutParams.WRAP_CONTENT) {
    if (Log.isLoggable(TAG, Log.INFO)) {
      Log.i(TAG, "Glide treats LayoutParams.WRAP_CONTENT as a request for an image the size of"
          + " this device's screen dimensions. If you want to load the original image and are"
          + " ok with the corresponding memory cost and OOMs (depending on the input size), use"
          + " .override(Target.SIZE_ORIGINAL). Otherwise, use LayoutParams.MATCH_PARENT, set"
          + " layout_width and layout_height to fixed dimension, or use .override() with fixed"
          + " dimensions.");
    }
return getMaxDisplayLength(view.getContext());
  }

`

元凶

看到这里刚才的性能问题迎刃而解,虽然我们界面里的ImageView实际显示区域很小(maxWitdh和maxHeight限制的),但是Glide不知道。一看我们list item的布局果然如此


所以在列表里的每一张图片都是以手机的最大宽度去加载的,所以decode bitmap才花了巨大的时间。

所以Fix这个问题只需要,在加载图片前设置好需要的加载宽度就可以解决啦。

总结

图片加载的一个重要手段是,“按需显示”更具实际需要显示的图片大小进行预缩放(Pre Scale), PreScale要根据图片的显示大小和图片本身实际大小来计算inSampleSize来进行。Glide在处理Wrap Content的ImageView的时候,会不知道如何进行PreScale需要指定好具体显示的尺寸,不然可能会引发性能问题。