• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

深入解析Glide

互联网 diligentman 2周前 (04-08) 9次浏览

在深入了解Glide之前,我们要先来认识一下Glide到底是什么,不要只知道他是图片处理框架,只会其简单的使用,只知道这些是不够的,因为学东西就是要刨根问底,学以致用。今天我们就带着这样几个问题来学习Glide。Glide是什么?Glide的实现原理是什么?Glide对比其他图片加载框架有什么优缺点(也就是为什么要使用Glide)? 自己如何设计一款图片加载框架?

一:Glide是什么?

1.Glide是什么?

Glide是Google在2014的IO大会发布一款图片处理框架,是目前android领域比较成熟的一款图片处理框架,也是Google官方推荐的图片处理框架,它主要支持网络图片、二进制流、drawable资源、本地图片显示,还支持本地视频显示。

2. Glide 基本用法

2.1 在app级别下面配置gradle引用

添加引用

dependencies {
  implementation 'com.github.bumptech.glide:glide:4.11.0' //图片加载框架
   annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'    //图片加载框架注解处理器
}

申请权限

<uses-permission android:name="android.permission.INTERNET" />

2.2 Glide类简单的链式调用

Glide.with(getApplicationContext()).load(imageurl).into(imageview)

上面的简单的链式调用调用我想大家都用过。链式调用的参数大概有如下几个

  • with()

    在使用过程中尽量要传入Applicaiton、Activity 、Fragment等类型的参数,因为glide加载图片的请求会与该参数的调用者的生命周期绑定在一起,如果onPaush时候,Glide就会暂停加载,重新onResume之后,又会继续加载。

  • load()
    支持网络图片网址、二进制流、drawable资源、本地图片的传入。

  • crossFade
    是否开启显示淡入淡出动画。

  • override
    如果获取的网络图片过大,我们通过它进行一个大小的裁剪,传入width和height参数进行宽高裁剪。

  • diskCacheStrategy
    磁盘缓存的设置,默认Glide会开启的。磁盘缓存又包括如下几种:

DiskCacheStrategy.NONE 什么都不缓存

DiskCacheStrategy.SOURCE 只缓存全尺寸图

DiskCacheStrategy.RESULT 只缓存最终的加载图

DiskCacheStrategy.ALL 缓存所有版本图(默认)

Glide 不仅缓存了全尺寸的图,还会根据 ImageView 大小所生成的图也会缓存起来。比如,请求一个 800×600 的图加载到一个 400×300 的 ImageView 中,Glide默认会将这原图还有加载到 ImageView 中的 400×300 的图也会缓存起来。

  • error
    这里的设置是当加载图片出现错误时,显示的图片。
  • placeholder
    图片加载完成之前显示的占位图。也是预加占位图。
  • into()
    一般传 ImageView。就是要显示的控件view。

2.3 Glide的缓存机制

上面我们介绍过diskCacheStrategy 有四种缓存:

  1. DiskCacheStrategy.NONE 什么都不缓存
  2. DiskCacheStrategy.SOURCE 只缓存全尺寸图
  3. DiskCacheStrategy.RESULT 只缓存最终的加载图
  4. DiskCacheStrategy.ALL 缓存所有版本图(默认)

缓存一般通过键值对的形式,所以存在缓存键。缓存的键包括图片的宽、高、signature等参数

EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

2.4 Glide的图片加载策略

深入解析Glide

  1. 首先从ActivateResource获取,是个值为弱引用的Map
  2. MemoryCache和DiskCache是LruCache

MemoryCache和ActivateResource关系如下:
深入解析Glide
图片加载时会从MemoryCache移到ActivateResouce,生命周期结束后会缓存至MemoryCache,所以内存中至多有一份缓存。

2.5 Glide的主要线程池

public Glide build(@NonNull Context context) {
    if (sourceExecutor == null) {
        sourceExecutor = GlideExecutor.newSourceExecutor();//创建网络加载线程池对象
    }

    if (diskCacheExecutor == null) {
        diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();//创建磁盘加载线程池对象
    }

    if (animationExecutor == null) {
        animationExecutor = GlideExecutor.newAnimationExecutor();//创建动画加载线程池对象
    }
    
}

二:Glide的实现原理是什么?

我们先解读一下源码

  1. Glide.class和RequestManagerRetriever.class,主要用来获得RequestManager
//with返回一个RequestManager
public static RequestManager with(Activity activity) {
    return getRetriever(activity).get(activity);
}
//无论调用的是哪个with重载方法,最后都会到这里
public RequestManager get(Activity activity) {
    if (Util.isOnBackgroundThread()) {
        return get(activity.getApplicationContext());
    } else {
        assertNotDestroyed(activity);
        android.app.FragmentManager fm = activity.getFragmentManager();
        return fragmentGet(activity, fm, null);
    }
}

//这里新建了一个没有视图的RequestManagerFragment 
private RequestManager fragmentGet(Context context,
                                   android.app.FragmentManager fm,
                                   android.app.Fragment parentHint) {
    RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
        Glide glide = Glide.get(context);
     //绑定requestManager和Fragment的Lifecycle
        requestManager =
                factory.build(
                        glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
        current.setRequestManager(requestManager);
    }
    return requestManager;
}
  1. RequestManagerFragment.class中持有一个lifecycle,在Fragment进入关键生命周期时会主动通知lifecycle执行相关方法
public class RequestManagerFragment extends Fragment {
  ...
  private final ActivityFragmentLifecycle lifecycle;
  ...
 @Override
  public void onStart() {
    super.onStart();
    lifecycle.onStart();
  }

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

  @Override
  public void onDestroy() {
    super.onDestroy();
    lifecycle.onDestroy();
  } 
}
  1. ActivityFragmentLifecycle.class中持有一个lifecycleListeners,在Fragment进入关键生命周期时Lifecycle会通知他的所有Listener
class ActivityFragmentLifecycle implements Lifecycle {
 ...
  private final Set<LifecycleListener> lifecycleListeners;void onStart() {
    isStarted = true;
    for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
      lifecycleListener.onStart();
    }
  }

  void onStop() {
    isStarted = false;
    for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
      lifecycleListener.onStop();
    }
  }

  void onDestroy() {
    isDestroyed = true;
    for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
      lifecycleListener.onDestroy();
    }
  }
  ...
}
  1. RequestManger.class关键生命周期中处理加载任务
@Override
public void onStart() {
    resumeRequests();
    targetTracker.onStart();
}

@Override
public void onStop() {
    pauseRequests();
    targetTracker.onStop();
}

@Override
public void onDestroy() {
    targetTracker.onDestroy();
    for (Target<?> target : targetTracker.getAll()) {
        clear(target);
    }
    targetTracker.clear();
    requestTracker.clearRequests();
}

根据源码,你会发现,withGlide类的一个静态方法,重载方法很多可以接收Activity,Fragment,Contextwith方法里面,首先会调用RequestManagerRetriever的静态get方法得到RequestManagerRetriver对象。然后再调用该对象的get方法获取RequestManager对象。静态get方法中也有很多重载方法,主要分为传入Application参数和非Application参数,传入Application参数是最简单的情况,Glide只要持保和整个应用生命周期同步。

Application参数不管是Activity,Fragment,最终都会向当前Activity传入一个隐藏的Fragment,因为Glide需要监控Activity的生命周期,Fragment依赖Activity生命周期并且是同步的,通过这个隐藏的Fragment就监听到Activity生命周期。

load方法,with方法返回的是一个RequestManager对象,所以load方法在RequestManager类中,load方法也有很多重载,支持本地图片,内存图片,网络图片,只看加载urlload方法。首先调用了fromString方法,再调用load方法,传入图片urlfromString方法里调用了loadGeneric方法,这个方法创建并返回了DrawableTypeRequest对象。

DrawableTypeRequest并没有load方法,loadDrawableTypeRequest的父类DrawableTypeRequestBuildle中。大部分操作都在这个类中,比如placeholder占位符,error,discacheStrategy等。

into方法是Glide图片加载流程中逻辑最为复杂的方法。into方法在DrawableTypeRequestBuilder类中,里面调用了super.into方法,真正的实现在DrawableTypeRequestBuilder的父类GenericRequestBuilder中,这个类包括了网络请求,图片解析,图片解码,bitmap生成,缓存处理,图片压缩等大量逻辑操作,最后的最后才将图片展示出来。

分析之后,大致可以总结如下:

1:Glide在加载绑定了Activity的生命周期。
:2:在Activity内新建一个无UI的Fragment,这个特殊的Fragment持有一个Lifecycle。通过Lifecycle在Fragment关键生命周期通知RequestManger进行相关的操作。
3:在生命周期onStart时继续加载,onStop时暂停加载,onDestory是停止加载任务和清除操作。

三:为什么要使用Glide

现在主流的图片加载框架有很多,比如我们大家熟悉的Fresco,我把Glide和Fresco做了对比,他们优缺点大致如下:

Glide的优势:

  1. 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
  2. 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
  3. 高效处理Bitmapbitmap的复用和主动回收,减少系统回收压力)
  4. 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)

Fresco的优势:

  1. 最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
  2. 大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
  3. 适用于需要高性能加载大量图片的场景

总结:
对于一般App来说,Glide完全够用,而对于图片需求比较大的App,为了防止加载大量图片导致OOM,Fresco 会更合适一些。并不是说用Glide会导致OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。

四:自己如何设计一款图片加载框架?

借鉴了Glide的设计原理和现实使用场景,如果要我们自己设计一款图片加载框架我觉得可能要从如下几个角度考虑。

  1. 异步加载:线程池管理
  2. 切换线程:Handler(当然也可以使用kotlin的协程)
  3. 缓存:LruCache、DiskLruCache
  4. 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
  5. 内存泄露:注意ImageView的正确引用,生命周期管理
  6. 列表滑动加载的问题:加载错乱、队满任务过多问题
  7. 适当加入加载动画特性,毕竟大家都是爱美的物种

好了,我就对着上面的几个角度来实现一下。

1:异步加载:线程池管理

线程池,多少个?我们知道,缓存一般有三级,内存缓存、硬盘、网络。

由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。

读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。

Glide 必然也需要多个线程池,上面我们看过源码,发现Glide具备如下几个线程池:

public Glide build(@NonNull Context context) {
    if (sourceExecutor == null) {
        sourceExecutor = GlideExecutor.newSourceExecutor();//创建网络加载线程池对象
    }

    if (diskCacheExecutor == null) {
        diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();//创建磁盘加载线程池对象
    }

    if (animationExecutor == null) {
        animationExecutor = GlideExecutor.newAnimationExecutor();//创建动画加载线程池对象
    }
    
}

如果不考虑动画,实际上就有俩个,一个是读内存和硬盘可以放在一个线程池diskCacheExecutor,另外一个是网络线程池sourceExecutor ,由于网络可能都塞,就把网络放在另外一个线程池。

2:线程切换

我们知道耗时操作是不能放在主线程的,不然会阻塞主线程,导致陈序异常退出,所以在图片异步加载成功前,是需要放在子线程的,图片异步加载成功后,需要在主线程去更新ImageView。因此必须具备线程切换调度的功能。

无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。看下Glide 相关源码

    class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
      private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
      //创建Handler
      private static final Handler MAIN_THREAD_HANDLER =
          new Handler(Looper.getMainLooper(), new MainThreadCallback());

3:缓存

我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。所以必须具备以上缓存。

3.1 内存缓存

3.1.1 LruCache缓存

一般都是用LruCache。Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。

// -> GlideBuilder#build
if (memoryCache == null) {
  memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}

既然说到LruCache ,那么就了解一下LruCache的特点和源码

LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。

LruCache 构造方法里创建一个LinkedHashMap,accessOrder 参数传true,表示按照访问顺序排序,数据存储基于LinkedHashMap。看一下源码就知道了。

    public class LruCache<K, V> {
    // 数据最终存在 LinkedHashMap
    private final LinkedHashMap<K, V> map;
    ...
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        // 创建一个LinkedHashMap,accessOrder 传true
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    ...

我们还是看一下LinkedHashMap 的实现原理吧,LinkedHashMap 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构。
深入解析Glide
LinkedHashMap重写了 createEntry 方法。在看下HashMap 的 createEntry 方法,HashMap的数组里面放的是HashMapEntry 对象.

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
    size++;
}

看下LinkedHashMap 的 createEntry方法,LinkedHashMap的数组里面放的是LinkedHashMapEntry对象.

void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMapEntry<K,V> old = table[bucketIndex];
    LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
    table[bucketIndex] = e; //数组的添加
    e.addBefore(header);  //处理链表
    size++;
}

LinkedHashMapEntry

private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
    // These fields comprise the doubly linked list used for iteration.
    LinkedHashMapEntry<K,V> before, after; //双向链表

    private void remove() {
        before.after = after;
        after.before = before;
    }

    private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

LinkedHashMapEntry继承 HashMapEntry,添加before和after变量,所以是一个双向链表结构,还添加了addBefore和remove 方法,用于新增和删除链表节点。

addBefore,将一个数据添加到Header的前面.

private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
}

existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。

下面在看看LinkedHashMapEntry的remove方法。

private void remove() {
        before.after = after;
        after.before = before;
    }

链表节点的移除比较简单,改变指针指向即可。

再看下LinkHashMap的put 方法。

public final V put(K key, V value) {

    V previous;
    synchronized (this) {
        putCount++;
        //size增加
        size += safeSizeOf(key, value);
        // 1、linkHashMap的put方法
        previous = map.put(key, value);
        if (previous != null) {
            //如果有旧的值,会覆盖,所以大小要减掉
            size -= safeSizeOf(key, previous);
        }
    }

    trimToSize(maxSize);
    return previous;
}

LinkedHashMap 结构可以用这种图表示:
深入解析Glide
LinkHashMap 的 put方法和get方法最后会调用trimToSize方法,LruCache 重写trimToSize方法,判断内存如果超过一定大小,则移除最老的数据。

LruCache#trimToSize,移除最老的数据

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {

            //大小没有超出,不处理
            if (size <= maxSize) {
                break;
            }

            //超出大小,移除最老的数据
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }

            key = toEvict.getKey();
            value = toEvict.getValue();
            map.remove(key);
            //这个大小的计算,safeSizeOf 默认返回1;
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        entryRemoved(true, key, value, null);
    }
}

对LinkHashMap 还不是很理解的话可以参考:图解LinkedHashMap原理。写的是非常好。

LruCache小结:

  • LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
  • LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。

3.2 磁盘缓存 DiskLruCache

使用前先添加如下依赖:

implementation 'com.jakewharton:disklrucache:2.0.2'

DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作:

    // DiskLruCache 内部也是用LinkedHashMap
    private final LinkedHashMap<String, Entry> lruEntries =
        new LinkedHashMap<String, Entry>(0, 0.75f, true);
    ...

    public synchronized boolean remove(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (entry == null || entry.currentEditor != null) {
          return false;
        }

            //一个key可能对应多个value,hash冲突的情况
        for (int i = 0; i < valueCount; i++) {
          File file = entry.getCleanFile(i);
            //通过 file.delete() 删除缓存文件,删除失败则抛异常
          if (file.exists() && !file.delete()) {
            throw new IOException("failed to delete " + file);
          }
          size -= entry.lengths[i];
          entry.lengths[i] = 0;
        }
        ...
        return true;
  }

可以看到 DiskLruCache 同样是利用LinkHashMap的特点,只不过数组里面存的 Entry 有点变化,Editor 用于操作文件。

private final class Entry {
    private final String key;

    private final long[] lengths;

    private boolean readable;

    private Editor currentEditor;

    private long sequenceNumber;
    ...
}

4:防止OOM

加载图片非常重要的一点是需要防止OOM,上面的LruCache缓存大小设置,可以有效防止OOM,但是当图片需求比较大,可能需要设置一个比较大的缓存,这样的话发生OOM的概率就提高了,那应该探索其它防止OOM的方法。

4.1软引用

回顾一下Java的四大引用:

强引用: 普通变量都属于强引用,比如 private Context context;
软应用: SoftReference,在发生OOM之前,垃圾回收器会回收SoftReference引用的对象。
弱引用: WeakReference,发生GC的时候,垃圾回收器会回收WeakReference中的对象。
虚引用: 随时会被回收,没有使用场景。

简单理解就是:

强引用对象的回收时机依赖垃圾回收算法,我们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里可以大胆猜想,Activity对象的创建是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,肯定是持有Activity引用的,那么这个GCRoot可以认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,所以会被垃圾回收器标记为可回收对象。

软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,可以通过 SoftReference 修饰,防止大对象造成OOM,看下这段代码。

    private static LruCache<String, SoftReference<Bitmap>> mLruCache = new LruCache<String, SoftReference<Bitmap>>(10 * 1024){
        @Override
        protected int sizeOf(String key, SoftReference<Bitmap> value) {
            //默认返回1,这里应该返回Bitmap占用的内存大小,单位:K

            //Bitmap被回收了,大小是0
            if (value.get() == null){
                return 0;
            }
            return value.get().getByteCount() /1024;
        }
    };

LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说通过SoftReference修饰的Bitmap就不会导致OOM。

当然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该重新计算,可以写个方法,当Bitmap取出来是空的时候,LruCache清理一下,重新计算剩余内存;

还有另一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,相当于内存缓存失效了,必然出现效率问题。

4.2. onLowMemory

我们知道,当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。

//Glide
public void onLowMemory() {
    clearMemory();
}

public void clearMemory() {
    // Engine asserts this anyway when removing resources, fail faster and consistently
    Util.assertMainThread();
    // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
    memoryCache.clearMemory();
    bitmapPool.clearMemory();
    arrayPool.clearMemory();
  }

4.3 从Bitmap 像素存储位置考虑

我们知道,系统为每个进程,也就是每个虚拟机分配的内存是有限的,早期的16M、32M,现在100+M。

虚拟机的内存划分主要有5部分:

虚拟机栈
本地方法栈
程序计数器
方法区

而对象的分配一般都是在堆中,堆是JVM中最大的一块内存,OOM一般都是发生在堆中。

Bitmap 之所以占内存大不是因为对象本身大,而是因为Bitmap的像素数据, Bitmap的像素数据大小 = 宽 * 高 * 1像素占用的内存。

1像素占用的内存是多少?不同格式的Bitmap对应的像素占用内存是不同的,具体是多少呢?在Fresco中看到如下定义代码。

  /**
   * Bytes per pixel definitions
   */
  public static final int ALPHA_8_BYTES_PER_PIXEL = 1;
  public static final int ARGB_4444_BYTES_PER_PIXEL = 2;
  public static final int ARGB_8888_BYTES_PER_PIXEL = 4;
  public static final int RGB_565_BYTES_PER_PIXEL = 2;
  public static final int RGBA_F16_BYTES_PER_PIXEL = 8;

如果Bitmap使用 RGB_565 格式,则1像素占用 2 byte,ARGB_8888 格式则占4 byte。
在选择图片加载框架的时候,可以将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的概率越低。 Glide内存开销是Picasso的一半,就是因为默认Bitmap格式不同。

至于宽高,是指Bitmap的宽高,怎么计算的呢?看BitmapFactory.Options 的 outWidth。

/**
     * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is
     * set to false, this will be width of the output bitmap after any
     * scaling is applied. If true, it will be the width of the input image
     * without any accounting for scaling.
     *
     * <p>outWidth will be set to -1 if there is an error trying to decode.</p>
     */
    public int outWidth;

看注释的意思,如果 BitmapFactory.Options 中指定 inJustDecodeBounds 为true,则为原图宽高,如果是false,则是缩放后的宽高。所以我们一般可以通过压缩来减小Bitmap像素占用内存。

扯远了,上面分析了Bitmap像素数据大小的计算,只是说明Bitmap像素数据为什么那么大。那是否可以让像素数据不放在java堆中,而是放在native堆中呢?据说Android 3.0到8.0 之间Bitmap像素数据存在Java堆,而8.0之后像素数据存到native堆中,是不是真的?看下源码就知道了~

8.0 Bitmap解析

  • java层创建Bitmap方法
    public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
        ...
        Bitmap bm;
        ...
        if (config != Config.ARGB_8888 || colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
            //最终都是通过native方法创建
            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true, null, null);
        } else {
            bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                    d50.getTransform(), parameters);
        }

        ...
        return bm;
    }

  • JNI层
    Bitmap 的创建是通过native方法 nativeCreate。如下所示:
//Bitmap.cpp
static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
...

而JNI动态注册,nativeCreate 方法 对应 Bitmap_creator:

//Bitmap.cpp
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jfloatArray xyzD50, jobject transferParameters) {
    ...
    //1. 申请堆内存,创建native层Bitmap
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap, NULL);
    if (!nativeBitmap) {
        return NULL;
    }

    ...
    //2.创建java层Bitmap
    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}

主要两个步骤:

  • 申请内存,创建native层Bitmap,看下allocateHeapBitmap方法
//Bitmap.cpp
static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes,
        SkColorTable* ctable) {
    // calloc 是c++ 的申请内存函数
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes, ctable));
}

可以看到通过c++的 calloc 函数申请了一块内存空间,然后创建native层Bitmap对象,把内存地址传过去,也就是native层的Bitmap数据(像素数据)是存在native堆中。

  • 创建java 层Bitmap
//Bitmap.cpp
jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    ...
    BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
     //通过JNI回调Java层,调用java层的Bitmap构造方法
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
            isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);

   ...
    return obj;
}

env->NewObject,通过JNI创建Java层Bitmap对象,gBitmap_class,gBitmap_constructorMethodID这些变量是什么意思,看下面这个方法,对应java层的Bitmap的类名和构造方法。

//Bitmap.cpp
int register_android_graphics_Bitmap(JNIEnv* env)
{
    gBitmap_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap"));
    gBitmap_nativePtr = GetFieldIDOrDie(env, gBitmap_class, "mNativePtr", "J");
    gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JIIIZZ[BLandroid/graphics/NinePatch$InsetStruct;)V");
    gBitmap_reinitMethodID = GetMethodIDOrDie(env, gBitmap_class, "reinit", "(IIZ)V");
    gBitmap_getAllocationByteCountMethodID = GetMethodIDOrDie(env, gBitmap_class, "getAllocationByteCount", "()I");
    return android::RegisterMethodsOrDie(env, "android/graphics/Bitmap", gBitmapMethods,
                                         NELEM(gBitmapMethods));
}

8.0 的Bitmap创建就两个点:

  1. 创建native层Bitmap,在native堆申请内存。
  2. 通过JNI创建java层Bitmap对象,这个对象在java堆中分配内存。
    像素数据是存在native层Bitmap,也就是证明8.0的Bitmap像素数据存在native堆中。

7.0 Bitmap
直接看native层的方法:

//JNI动态注册
static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
...

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable) {
    ... 
    //1.通过这个方法来创建native层Bitmap
    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    ...

    return GraphicsJNI::createBitmap(env, nativeBitmap,
            getPremulBitmapCreateFlags(isMutable));
}

native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp

//JNI动态注册
static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
...

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable) {
    ... 
    //1.通过这个方法来创建native层Bitmap
    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    ...

    return GraphicsJNI::createBitmap(env, nativeBitmap,
            getPremulBitmapCreateFlags(isMutable));
}

native层Bitmap 创建是通过GraphicsJNI::allocateJavaPixelRef,看看里面是怎么分配的, GraphicsJNI 的实现类是Graphics.cpp:

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
    const SkImageInfo& info = bitmap->info();

    size_t size;
    //计算需要的空间大小
    if (!computeAllocationSize(*bitmap, &size)) {
        return NULL;
    }

    // we must respect the rowBytes value already set on the bitmap instead of
    // attempting to compute our own.
    const size_t rowBytes = bitmap->rowBytes();
    // 1. 创建一个数组,通过JNI在java层创建的
    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);
    ...
    // 2. 获取创建的数组的地址
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    ...
    //3. 创建Bitmap,传这个地址
    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);
    wrapper->getSkBitmap(bitmap);
    // since we're already allocated, we lockPixels right away
    // HeapAllocator behaves this way too
    bitmap->lockPixels();

    return wrapper;
}

可以看到,7.0 像素内存的分配是这样的:

  1. 通过JNI调用java层创建一个数组
  2. 然后创建native层Bitmap,把数组的地址传进去。

由此说明,7.0 的Bitmap像素数据是放在java堆的。当然,3.0 以下Bitmap像素内存据说也是放在native堆的,但是需要手动释放native层的Bitmap,也就是需要手动调用recycle方法,native层内存才会被回收。这个大家可以自己去看源码验证。

native层Bitmap 回收问题
Java层的Bitmap对象由垃圾回收器自动回收,而native层Bitmap印象中我们是不需要手动回收的,源码中如何处理的呢?
先回忆一下下面三个关键字之间的关系。

说说final、finally、finalize 的关系。三者除了长得像,其实没有半毛钱关系,final、finally大家都用的比较多,而 finalize 用的少,或者没用过,finalize 是 Object 类的一个方法,注释是这样的:

/**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object.
     * A subclass overrides the {@code finalize} method to dispose of
     * system resources or to perform other cleanup.
     * <p>
     ...**/
  protected void finalize() throws Throwable { }

意思是说,垃圾回收器确认这个对象没有其它地方引用到它的时候,会调用这个对象的finalize方法,子类可以重写这个方法,做一些释放资源的操作。

6.0 以前的Bitmap
在6.0以前,Bitmap 就是通过这个finalize 方法来释放native层对象的。

Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
        ...
        mNativePtr = nativeBitmap;
        //1.创建 BitmapFinalizer
        mFinalizer = new BitmapFinalizer(nativeBitmap);
        int nativeAllocationByteCount = (buffer == null ? getByteCount() : 0);
        mFinalizer.setNativeAllocationByteCount(nativeAllocationByteCount);
}

 private static class BitmapFinalizer {
        private long mNativeBitmap;

        // Native memory allocated for the duration of the Bitmap,
        // if pixel data allocated into native memory, instead of java byte[]
        private int mNativeAllocationByteCount;

        BitmapFinalizer(long nativeBitmap) {
            mNativeBitmap = nativeBitmap;
        }

        public void setNativeAllocationByteCount(int nativeByteCount) {
            if (mNativeAllocationByteCount != 0) {
                VMRuntime.getRuntime().registerNativeFree(mNativeAllocationByteCount);
            }
            mNativeAllocationByteCount = nativeByteCount;
            if (mNativeAllocationByteCount != 0) {
                VMRuntime.getRuntime().registerNativeAllocation(mNativeAllocationByteCount);
            }
        }

        @Override
        public void finalize() {
            try {
                super.finalize();
            } catch (Throwable t) {
                // Ignore
            } finally {
                //2.就是这里了,
                setNativeAllocationByteCount(0);
                nativeDestructor(mNativeBitmap);
                mNativeBitmap = 0;
            }
        }
    }

在Bitmap构造方法创建了一个 BitmapFinalizer类,重写finalize 方法,在java层Bitmap被回收的时候,BitmapFinalizer 对象也会被回收,finalize 方法肯定会被调用,在里面释放native层Bitmap对象。

6.0 之后做了一些变化,BitmapFinalizer 没有了,被NativeAllocationRegistry取代。

例如 8.0 Bitmap构造方法。

    Bitmap(long nativeBitmap, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {

        ...
        mNativePtr = nativeBitmap;
        long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
        //  创建NativeAllocationRegistry这个类,调用registerNativeAllocation 方法
        NativeAllocationRegistry registry = new NativeAllocationRegistry(
            Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
        registry.registerNativeAllocation(this, nativeBitmap);
    }

NativeAllocationRegistry 就不分析了, 不管是BitmapFinalizer 还是NativeAllocationRegistry,目的都是在java层Bitmap被回收的时候,将native层Bitmap对象也回收掉。 一般情况下我们无需手动调用recycle方法,由GC去盘它即可。

上面分析了Bitmap像素存储位置,我们知道,Android 8.0 之后Bitmap像素内存放在native堆,Bitmap导致OOM的问题基本不会在8.0以上设备出现了(没有内存泄漏的情况下),那8.0 以下设备怎么办?赶紧升级或换手机吧

我们换手机当然没问题,但是并不是所有人都能跟上Android系统更新的步伐,所以,问题还是要解决。

Fresco 之所以能跟Glide 正面交锋,必然有其独特之处,文中开头列出 Fresco 的优点是:“在5.0以下(最低2.3)系统,Fresco将图片放到一个特别的内存区域(Ashmem区)” 这个Ashmem区是一块匿名共享内存,Fresco 将Bitmap像素放到共享内存去了,共享内存是属于native堆内存。

Fresco 关键源码在 PlatformDecoderFactory 这个类:

public class PlatformDecoderFactory {

  /**
   * Provide the implementation of the PlatformDecoder for the current platform using the provided
   * PoolFactory
   *
   * @param poolFactory The PoolFactory
   * @return The PlatformDecoder implementation
   */
  public static PlatformDecoder buildPlatformDecoder(
      PoolFactory poolFactory, boolean gingerbreadDecoderEnabled) {
    //8.0 以上用 OreoDecoder 这个解码器
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
      return new OreoDecoder(
          poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      //大于5.0小于8.0用 ArtDecoder 解码器
      int maxNumThreads = poolFactory.getFlexByteArrayPoolMaxNumThreads();
      return new ArtDecoder(
          poolFactory.getBitmapPool(), maxNumThreads, new Pools.SynchronizedPool<>(maxNumThreads));
    } else {
      if (gingerbreadDecoderEnabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
        //小于4.4 用 GingerbreadPurgeableDecoder 解码器
        return new GingerbreadPurgeableDecoder();
      } else {
        //这个就是4.4到5.0 用的解码器了
        return new KitKatPurgeableDecoder(poolFactory.getFlexByteArrayPool());
      }
    }
  }
}

8.0 先不看了,看一下 4.4 以下是怎么得到Bitmap的,看下GingerbreadPurgeableDecoder这个类有个获取Bitmap的方法:

//GingerbreadPurgeableDecoder
private Bitmap decodeFileDescriptorAsPurgeable(
      CloseableReference<PooledByteBuffer> bytesRef,
      int inputLength,
      byte[] suffix,
      BitmapFactory.Options options) {
    //  MemoryFile :匿名共享内存
    MemoryFile memoryFile = null;
    try {
      //将图片数据拷贝到匿名共享内存
      memoryFile = copyToMemoryFile(bytesRef, inputLength, suffix);
      FileDescriptor fd = getMemoryFileDescriptor(memoryFile);
      if (mWebpBitmapFactory != null) {
        // 创建Bitmap,Fresco自己写了一套创建Bitmap方法
        Bitmap bitmap = mWebpBitmapFactory.decodeFileDescriptor(fd, null, options);
        return Preconditions.checkNotNull(bitmap, "BitmapFactory returned null");
      } else {
        throw new IllegalStateException("WebpBitmapFactory is null");
      }
    } 
  }

总结一下,你会发现,4.4以下,Fresco 使用匿名共享内存来保存Bitmap数据,首先将图片数据拷贝到匿名共享内存中,然后使用Fresco自己写的加载Bitmap的方法。

Fresco对不同Android版本使用不同的方式去加载Bitmap,至于4.4-5.0,5.0-8.0,8.0 以上,对应另外三个解码器,大家可以从PlatformDecoderFactory 这个类入手,自己去分析,思考为什么不同平台要分这么多个解码器,8.0 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享?

5. 内存泄露

ImageView导致的内存泄露
Glide的做法是监听生命周期回调,看 RequestManager 这个类。

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

在Activity/fragment 销毁的时候,取消图片加载任务,细节大家可以自己去看源码。

6.列表加载问题

6.1图片错乱

由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。

常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。

当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。

6.2 线程池任务过多

列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。

五:总结

通过对Glide分析,分析出一个图片加载框架必要的需求,以及各个需求涉及到哪些技术和原理。

  1. 异步加载:最少两个线程池
  2. 切换到主线程:Handler
  3. 缓存:LruCache、DiskLruCache,涉及到LinkHashMap原理
  4. 防止OOM:软引用、LruCache、图片压缩没展开讲、Bitmap像素存储位置源码分析、Fresco部分源码分析
  5. 内存泄露:注意ImageView的正确引用,生命周期管理
  6. 列表滑动加载的问题:加载错乱用tag、队满任务存在则不添加

程序员灯塔
转载请注明原文链接:深入解析Glide
喜欢 (0)