一张图掌握Glide内存管理

前言

Glide源码异常庞大, 其中的代码是非常复杂的, 所以笔者不会逐行的分析源码,避免读者陷入代码黑洞中无法自拔,而是通过分析关键的代码和流程来让读者快速的理解和掌握Glide内存管理策略的核心思想 。

Glide缓存可以分为三块

  1. ActiveResources
  2. MemoryCache
  3. BitmapPool

结论

  • 缓存类型和说明
类型 名称 说明
ActiveResources 活跃内存缓存 正在被页面引用展示的缓存资源
MemoryCache 普通内存缓存 无任何页面引用缓存资源,但是Bitmap是有效的, 通过key获取后可以直接展示
BitmapPool Bitmap回收池缓存 不可以用来直接展示,只是具有一块内存空间,可以理解为是一个dirty的Bitamp,需要再次绑定图片数据后展示
  • 缓存获取优先级顺序 : ActiveResources -> MemoryCache -> BitmapPool 复用Bitmap 从磁盘或者网络中decoded数据到 `Bitamp中。
功能

ActiveResources 也被称为活跃缓存,它的主要作用就是缓存正在被页面引用的资源。

特性

缓存中的某个资源是否被移除唯一条件是没有页面在引用该资源,否则该缓存中的资源是不会被移除的,即使应用可用内存很低或OOM也不会被回收。

资源是否应该被回收算法: 引用计数法

Glide是通过引用计数法来判断ActiveResources中的缓存资源是否应该被移除,具体的逻辑并不在ActiveResources中,而是在ActiveResources持有的缓存资源EngineResource类中,当EngineResource被引用时引用数+1,取消引用时-1 , 当引用数为0的时候,则从ActiveResources移除。

arduino 复制代码
class EngineResource<Z> implements Resource<Z> {
 
 
    synchronized void acquire() {
        if (isRecycled) {
            throw new IllegalStateException("Cannot acquire a recycled resource");
        }
        ++acquired;
    }

  
    void release() {
        boolean release = false;
        synchronized (this) {
            if (acquired <= 0) {
                throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
            }
            // 引用数为0 ,标记为可释放
            if (--acquired == 0) {
                release = true;
            }
        }
        if (release) {
             // 回调该资源可释放 listener == Engine
             listener.onResourceReleased(key, this);
        }
    }
}

listener 是一个接口,该接口的唯一实现是 Engine 类,带活跃缓存的资源被标记可回收时,则会通过调用 listener.onResourceReleased函数通知 EngineonResourceReleased函数表示资源可以被回收。源代码实现如下:

typescript 复制代码
 public class Engine
    implements EngineJobListener,
    MemoryCache.ResourceRemovedListener,
    EngineResource.ResourceListener {   
    
    @Override
    public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
        activeResources.deactivate(cacheKey);
        // 如果设置了内存缓存,就将资源放入内存缓存
        if (resource.isMemoryCacheable()) {
            cache.put(cacheKey, resource);
        } else {
        // 否则放入到 BitmapPool 回收池
            resourceRecycler.recycle(resource, /* forceNextFrame= */ false);
        }
    }
  }

可以发现当活跃缓存的资源被回收时,并不是被直接销毁,而是尝试放入普通缓存中,如果资源被设置了内存不可缓存 isMemoryCacheable() = false,则会被放入到BitmapPool池中。

MemoryCache

功能

MemoryCache 也被称为普通缓存,它的主要作用有两个:

  1. 从上面的分析代码中可以看到,从活跃缓存中被释放的资源,即无任何页面引用的资源,会被放入到 MemoryCache内。
  2. 当从缓存中加载一个图片时如果ActiveResources活动缓存中不存在, 则尝试从 MemoryCache 中获取,如果获取成功,则资源会被放入到 ActiveResources活动。
scss 复制代码
 // Engine
 @Nullable
  private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    // 设置了禁用缓存参数,直接返回 null
    if (!isMemoryCacheable) {
      return null;
    }
    // 尝试从一级活跃缓存中获取资源
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }
    // 尝试从二级普通内存缓存中获取资源
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }
  
  
  private EngineResource<?> loadFromCache(Key key) {
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      // 引用计数 + 1
      cached.acquire();
      // 放入活动缓存中
      activeResources.activate(key, cached);
    }
    return cached;
  }
源码实现
java 复制代码
public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
  private ResourceRemovedListener listener;

  public LruResourceCache(long size) {
    super(size);
  }

  ...
  
    @Override
  protected void onItemEvicted(@NonNull Key key, @Nullable Resource<?> item) {
    if (listener != null && item != null) {
      // Engine    
      listener.onResourceRemoved(item);
    }
  }

  @SuppressLint("InlinedApi")
  @Override
  public void trimMemory(int level) {
    if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
      // 清除缓存
      clearMemory();
    } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
        || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
      // 减少缓存
      trimToSize(getMaxSize() / 2);
    }
  }
}

MemoryCache是一个接口,具体的实现类 LruResourceCache,从LruResourceCachetrimMemory函数可以看出,当内存较低时,LruResourceCache会根据不同的内存低level级别主动的释放缓存。LruResourceCache继承了LruCache类, LruCache 的最大特性就是可以根据优先级移除优先级较低的资源,当资源被移除时在 onItemEvicted 函数内调用 listener.onResourceRemoved(item) 函数,listener的具体实现类也是Engine,同样也会被放入到 BitmapPool 缓存中。

less 复制代码
  @Override
  public void onResourceRemoved(@NonNull final Resource<?> resource) {
    // Avoid deadlock with RequestManagers when recycling triggers recursive clear() calls.
    // See b/145519760.
    resourceRecycler.recycle(resource, /* forceNextFrame= */ true);
  }

BitmapPool

功能

我们知道 Bitmap 是可以复用的 ,从ActiveResourcesMemoryCache 释放的资源会调用ResourceRecycler.recycle(...)函数,最后都会调用BitmapResource.recycle(...) 被放入到 BitmapPool 中去,当有新的Bitmap需要加载时,会从BitmapPool缓存池中取出一个合适的Bitmap进行复用,将资源数据绑定到复用的Bitmap内。

java 复制代码
class ResourceRecycler {
 
  ...
 
  synchronized void recycle(Resource<?> resource, boolean forceNextFrame) {
    if (isRecycling || forceNextFrame) {
        ...
    } else {
      isRecycling = true;
      resource.recycle();
      isRecycling = false;
    }
  }
 
  ...
}



public class BitmapResource implements Resource<Bitmap>, Initializable {
  private final Bitmap bitmap;
  private final BitmapPool bitmapPool;
  
  ...
  
  // bitmap 回收放入到 BitmapPool
  @Override
  public void recycle() {
    bitmapPool.put(bitmap);
  }

  ...
  
}
源码实现

BitmapPool 具体的实现是 LruBitmapPool, 从名字可以看出也是一个具有Lru特性的实现类,和MemoryCache一样实现了当接口到系统内存较低的回调时,自动回收清理内存。

typescript 复制代码
public class LruBitmapPool implements BitmapPool {
  
  @Override
  public void clearMemory() {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "clearMemory");
    }
    trimToSize(0);
  }

  @SuppressWarnings("checkstyle:UnnecessaryParentheses") // Readability
  @SuppressLint("InlinedApi")
  @Override
  public void trimMemory(int level) {
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "trimMemory, level=" + level);
    }
    if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
        || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
            && level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)) {
      clearMemory();
    } else if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
        || level == ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
      trimToSize(getMaxSize() / 2);
    }
  }

}

Glide使用注意点

  1. 避免使用 ApplicationContext作为Glide.with(Context) 函数的context参数 , 使用该方式加载的图片资源将会长期在 ActiveResources 缓存中,导致内存无法被释放, 严重情况下可能会导致OOM
csharp 复制代码
Glide.with(applicationContext)
    .load("https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF")
    .into(findViewById(R.id.imageView))
  1. 避免在子线程中执行 Glide.with(Activity/Fragment/Context/View) , 如果在子线程中执行该方法,无论with()函数参数是是任何类型,均会被Glide替换为 ApplicationContext ,也会出现和场景1一样的问题,源码如下:
less 复制代码
@NonNull
  public RequestManager get(@NonNull FragmentActivity activity) {
    // 如果在子线程,默认使用 ApplicationContext
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    }
    assertNotDestroyed(activity);
    frameWaiter.registerSelf(activity);
    boolean isActivityVisible = isActivityVisible(activity);
    Glide glide = Glide.get(activity.getApplicationContext());
    return lifecycleRequestManagerRetriever.getOrCreate(
        activity,
        glide,
        activity.getLifecycle(),
        activity.getSupportFragmentManager(),
        isActivityVisible);
  }
  1. 如果使用 android.app.Activity、android.app.Fragment 作为 Glide.with()参数,均会被Glide替换为 ApplicationContext 。 可以看到官方也将其标记为了 @Deprecated 方法,不再推荐使用。
less 复制代码
@Deprecated
@NonNull
public RequestManager get(@NonNull Activity activity) {
  return get(activity.getApplicationContext());
}


@Deprecated
@NonNull
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public RequestManager get(@NonNull android.app.Fragment fragment) {
  if (fragment.getActivity() == null) {
    throw new IllegalArgumentException(
        "You cannot start a load on a fragment before it is attached");
  }
  return get(fragment.getActivity().getApplicationContext());
}
  1. 如果在一些场景下必须要使用 ApplicationContext或者在子线程中 Glide.with(context) ,需要注意在合理的时机释放缓存
scss 复制代码
val target = Glide.with(applicationContext)
                .load("https://t7.baidu.com/it/u=1595072465,3644073269&fm=193&f=GIF")
                //  添加这个方法,会在view detach的时候,自动清除图片请求
                .into(findViewById(R.id.imageView))
// 方式一 自动释放                
target.clearOnDetach()
// 方式二 手动释放                
Glide.with(applicationContext).clear(target)
相关推荐
m0_548514772 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯2 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯2 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐3 小时前
Handle
android
m0_748232925 小时前
Android Https和WebView
android·网络协议·https
m0_748251725 小时前
Android webview 打开本地H5项目(Cocos游戏以及Unity游戏)
android·游戏·unity
m0_748254667 小时前
go官方日志库带色彩格式化
android·开发语言·golang
zhangphil7 小时前
Android使用PorterDuffXfermode模式PorterDuff.Mode.SRC_OUT橡皮擦实现“刮刮乐”效果,Kotlin(2)
android·kotlin
爱学测试的李木子7 小时前
从0到1搭建 Android 自动化 python+appium 环境
android·软件测试·python·测试工具·自动化
咸芝麻鱼7 小时前
Android Studio | 连接手机设备后,启动App时出现:Waiting For DebuggerApplication (App名)...
android·adb·智能手机·android studio