乐趣区

关于android:面试官简历上最好不要写Glide不是问源码那么简单

这次来面试的是一个有着 5 年工作教训的小伙,截取了一段对话如下:

面试官:我看你写到 Glide,为什么用 Glide,而不抉择其它图片加载框架?
小伙:Glide 应用简略,链式调用,很不便,始终用这个。
面试官:有看过它的源码吗?跟其它图片框架相比有哪些劣势?
小伙:没有,只是在我的项目中应用而已~
面试官:如果当初不让你用开源库,须要你本人写一个图片加载框架,你会思考哪些方面的问题,说说大略的思路。
小伙:额~,压缩吧。
面试官:还有吗?
小伙:额~,这个没写过。

说到图片加载框架,大家最相熟的莫过于 Glide 了,但我却不举荐简历上写相熟 Glide,除非你熟读它的源码,或者参加 Glide 的开发和保护。

在个别面试中,遇到图片加载问题的频率个别不会太低,只是问法会有一些差别,例如:

  • 简历上写 Glide,那么会问一下 Glide 的设计,以及跟其它同类框架的比照;
  • 如果让你写一个图片加载框架,说说思路;
  • 给一个图片加载的场景,比方网络加载一张或多张大图,你会怎么做;

带着问题进入注释~

一、谈谈 Glide

1.1 Glide 应用有多简略?

Glide 因为其口碑好,很多开发者间接在我的项目中应用,应用办法相当简略

1、增加依赖:

implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'

2、增加网络权限

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

3、一句代码加载图片到 ImageView

Glide.with(this).load(imgUrl).into(mIv1);

进阶一点的用法,参数设置

RequestOptions options = new RequestOptions()
            .placeholder(R.drawable.ic_launcher_background)
            .error(R.mipmap.ic_launcher)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .override(200, 100);
    
Glide.with(this)
            .load(imgUrl)
            .apply(options)
            .into(mIv2);

应用 Glide 加载图片如此简略,这让很多开发者省下本人解决图片的工夫,图片加载工作全副交给 Glide 来就完事,同时,很容易就把图片解决的相干知识点忘掉。

1.2 为什么用 Glide?

从前段时间面试的状况,我发现了这个景象:简历上写相熟 Glide 的,根本都是相熟应用办法,很多 3 年 - 6 年工作教训,除了说 Glide 使用方便,不分明 Glide 跟其余图片框架如 Fresco 的比照有哪些优缺点。

首先,当下风行的图片加载框架有那么几个,能够拿 Glide 跟 Fresco 比照,例如这些:

Glide:

  • 多种图片格式的缓存,实用于更多的内容表现形式(如 Gif、WebP、缩略图、Video)
  • 生命周期集成(依据 Activity 或者 Fragment 的生命周期治理图片加载申请)
  • 高效解决 Bitmap(bitmap 的复用和被动回收,缩小零碎回收压力)
  • 高效的缓存策略,灵便(Picasso 只会缓存原始尺寸的图片,Glide 缓存的是多种规格),加载速度快且内存开销小(默认 Bitmap 格局的不同,使得内存开销是 Picasso 的一半)

Fresco:

  • 最大的劣势在于 5.0 以下 (最低 2.3) 的 bitmap 加载。在 5.0 以下零碎,Fresco 将图片放到一个特地的内存区域(Ashmem 区)
  • 大大减少 OOM(在更底层的 Native 层对 OOM 进行解决,图片将不再占用 App 的内存)
  • 实用于须要高性能加载大量图片的场景

对于个别 App 来说,Glide 齐全够用,而对于图片需要比拟大的 App,为了避免加载大量图片导致 OOM,Fresco 会更适合一些。并不是说用 Glide 会导致 OOM,Glide 默认用的内存缓存是 LruCache,内存不会始终往上涨。

二、如果让你本人写个图片加载框架,你会思考哪些问题?

首先,梳理一下必要的图片加载框架的需要:

  • 异步加载:线程池
  • 切换线程:Handler,没有争议吧
  • 缓存:LruCache、DiskLruCache
  • 避免 OOM:软援用、LruCache、图片压缩、Bitmap 像素存储地位
  • 内存泄露:留神 ImageView 的正确援用,生命周期治理
  • 列表滑动加载的问题:加载错乱、队满工作过多问题

当然,还有一些不是必要的需要,例如加载动画等。

2.1 异步加载:

线程池,多少个?

缓存个别有三级,内存缓存、硬盘、网络。

因为网络会阻塞,所以读内存和硬盘能够放在一个线程池,网络须要另外一个线程池,网络也能够采纳 Okhttp 内置的线程池。

读硬盘和读网络须要放在不同的线程池中解决,所以用两个线程池比拟适合。

Glide 必然也须要多个线程池,看下源码是不是这样

public final class GlideBuilder {
  ...
  private GlideExecutor sourceExecutor; // 加载源文件的线程池,包含网络加载
  private GlideExecutor diskCacheExecutor; // 加载硬盘缓存的线程池
  ...
  private GlideExecutor animationExecutor; // 动画线程池

Glide 应用了三个线程池,不思考动画的话就是两个。

2.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());

问 RxJava 是齐全用 Java 语言写的,那怎么实现从子线程切换到 Android 主线程的?仍然有很多 3 - 6 年的开发答不上来这个很根底的问题,而且只有是这个问题答复不进去的,接下来有对于原理的问题,根本都答不上来。

有不少工作了很多年的 Android 开发不晓得 鸿洋、郭霖、玉刚说,不晓得掘金是个啥玩意,心田预计会想是不是还有叫掘银掘铁的(我不晓得有没有)。

我想表白的是,干这一行,真的是须要有对技术的激情,一直学习,不怕他人比你优良,就怕比你优良的人比你还致力,而你却不晓得

2.3 缓存

咱们常说的图片三级缓存:内存缓存、硬盘缓存、网络。

2.3.1 内存缓存

个别都是用LruCache

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

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

既然说到 LruCache,必须要理解一下 LruCache 的特点和源码:

为什么用 LruCache?

LruCache 采纳 最近起码应用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,防止图片占用内存过大导致 OOM。

LruCache 源码剖析
    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);
    }
    ...

LruCache 构造方法里创立一个LinkedHashMap,accessOrder 参数传 true,示意依照拜访程序排序,数据存储基于 LinkedHashMap。

先看看 LinkedHashMap 的原理吧

LinkedHashMap 继承 HashMap,在 HashMap 的根底上进行扩大,put 办法并没有重写,阐明LinkedHashMap 遵循 HashMap 的数组加链表的构造

LinkedHashMap 重写了 createEntry 办法。

看下 HashMap 的 createEntry 办法

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

HashMap 的数组外面放的是HashMapEntry 对象

看下 LinkedHashMap 的 createEntry 办法

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

LinkedHashMap 的数组外面放的是 LinkedHashMapEntry 对象

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 变量,所以是一个双向链表构造,还增加了 addBeforeremove 办法,用于新增和删除链表节点。

LinkedHashMapEntry#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 构造能够用这种图示意

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 数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保障内存不超过设定的最大值。

2.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;
    ...
}

2.4 避免 OOM

加载图片十分重要的一点是须要避免 OOM,下面的 LruCache 缓存大小设置,能够无效避免 OOM,然而当图片需要比拟大,可能须要设置一个比拟大的缓存,这样的话产生 OOM 的概率就进步了,那应该摸索其它避免 OOM 的办法。

办法 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 就形同虚设,相当于内存缓存生效了,必然呈现效率问题。

办法 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();}
办法 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;
    }

Bitmap 的创立是通过 native 办法 nativeCreate

对应源码 8.0.0\_r4/xref/frameworks/base/core/jni/android/graphics/Bitmap.cpp

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

次要两个步骤:

  1. 申请内存,创立 native 层 Bitmap,看下 allocateHeapBitmap 办法
//
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 堆中。

  1. 创立 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

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 就是通过这个 finalize 办法来开释 native 层对象的。 6.0 Bitmap.java

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 以下都用匿名共享内存不好吗?期待你在评论区跟大家分享~

2.5 ImageView 内存泄露

已经在 Vivo 驻场开发,带有头像性能的页面被测出内存透露,起因是 SDK 中有个加载网络头像的办法,持有 ImageView 援用导致的。

当然,批改也比较简单粗犷,将 ImageView 用 WeakReference 润饰 就完事了。

事实上,这种形式尽管解决了内存泄露问题,然而并不完满,例如在界面退出的时候,咱们除了心愿 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 销毁的时候,勾销图片加载工作,细节大家能够本人去看源码。

2.6 列表加载问题

图片错乱

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

惯例的做法是给 ImageView 设置 tag,tag 个别是图片地址,更新 ImageView 之前判断 tag 是否跟 url 统一。

当然,能够在 item 从列表隐没的时候,勾销对应的图片加载工作。要思考放在图片加载框架做还是放在 UI 做比拟适合。

线程池工作过多

列表滑动,会有很多图片申请,如果是第一次进入,没有缓存,那么队列会有很多工作在期待。所以在申请网络图片之前,须要判断队列中是否曾经存在该工作,存在则不加到队列去。

总结

本文通过 Glide 开题,剖析一个图片加载框架必要的需要,以及各个需要波及到哪些技术和原理。

  • 异步加载:起码两个线程池
  • 切换到主线程:Handler
  • 缓存:LruCache、DiskLruCache,波及到 LinkHashMap 原理
  • 避免 OOM:软援用、LruCache、图片压缩没开展讲、Bitmap 像素存储地位源码剖析、Fresco 局部源码剖析
  • 内存泄露:留神 ImageView 的正确援用,生命周期治理
  • 列表滑动加载的问题:加载错乱用 tag、队满工作存在则不增加
退出移动版