美文网首页
Android面试Android进阶(二十)-Glide相关

Android面试Android进阶(二十)-Glide相关

作者: 肖义熙 | 来源:发表于2021-05-08 15:23 被阅读0次

问:几种主流的图片加载框架的了解有哪些?

答:目前较为主流的图片加载框架有Glide、Picasso、Fresco以及目前不再更新的ImageLoader

  • Picasso:是 Square 开源的项目,Picasso将网络请求的缓存部分交给了okhttp实现,也因此Picasso的缓存只能缓存原图
  • Glide: Google 的亲儿子,模仿了Picasso的API,在其基础上添加了扩展(比如gif支持、视频解析等)。Glide默认的Bitmap格式是RGB_565,比Picasso默认的ARGB_8888格式的内存开销要小一半;Picasso缓存的是全尺寸的图像(只缓存一种),而Glide缓存的是跟ImageView尺寸相同的图像。
  • Fresco: 是 Facebook 上开源的图片缓存框架。在5.0以下系统,Fresco将图片放到一个Ashmem区(匿名共享内存区域),这部分内存类似于Native内存区,不占用Java堆内存,这样Bitmap对象的创建和释放将不会引发GC,更少的GC会使你的App运行得更加流畅,同时在图片不显示的时候,占用的内存会自动被释放,减少因图片内存占用而引发的OOM。在5.0以后系统默认就是存储在Ashmem区了。

如果是大量图片应用可以选择Fresco(体积较大),图片少的且对图片质量要求不需要很高的可以选择使用Glide(体积较小),毕竟Glide是google的亲儿子,又学习了Picasso的优点~

问:如果要你自己设计一个图片加载框架,你要怎么做?

其实,没有哪个公司是真的要你自己设计一个图片加载框架,这只是想看看你对第三方框架的了解程度吧。所以,别慌!
答:如果自己设计一个图片加载框架,要考虑的问题就比较多,例如:

  • 图片加载肯定是耗时任务,所以需要用线程去加载,那就需要设计线程池。
  • 既然用到线程,在Android中更新页面需要在主线程中执行,所以就需要考虑切换线程的问题,不管是RxJava还是EventBus等等,都需要用到Handler来切换线程。
  • Android中对图片Bitmap的操作极易引发OOM,所以需要考虑到如何防止OOM的问题,一般就使用软引用、缓存策略、图片压缩等等。
  • 考虑到资源加载效率的问题,就需要考虑缓存策略了,例如LruCache、DiskLruCache以及缓存合适图片大小等
  • 页面关闭时还需要考虑内存泄漏的问题,框架要显示图片,肯定需要引用页面的View,这里就要考虑到生命周期同步管理的方式了,例如使用无页面的Fragment等,让这个框架有能力感知页面生命周期。

接下来就按照这几个思路去分析一下Glide使用及源码设计
Glide的简单使用:

        // 加载本地图片
        val file = File(externalCacheDir.toString() + "/image.jpg")
        Glide.with(this).load(file).into(imageView)

        // 加载应用资源
        val resource: Int = R.drawable.image
        Glide.with(this).load(resource).into(imageView)

        // 加载二进制流
        val image: ByteArray = getImageBytes()
        Glide.with(this).load(image).into(imageView)

        // 加载Uri对象
        val imageUri: Uri = getImageUri()
        Glide.with(this).load(imageUri).into(imageView)

问:Glide的线程池有哪些,如何设计的?

答:Glide线程池有三个:
1、sourceExecutor——加载源文件的线程池,包括网络加载
2、diskCacheExecutor——加载硬盘缓存的线程池
3、animationExecutor——动画线程池,不重要,不看了

从Glide.with(this)源码一路跟踪到GlideBuilder类中的Glide build()方法中:

  //GlideBuilder.java
  Glide build(@NonNull Context context) {
    if (sourceExecutor == null) {
      sourceExecutor = GlideExecutor.newSourceExecutor();    //加载源文件的线程池,包括网络加载
    }

    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();    //加载硬盘缓存的线程池
    }

    if (animationExecutor == null) {
      animationExecutor = GlideExecutor.newAnimationExecutor();    //动画线程池,不重要
    }
    //省略一些其他初始化代码,先看线程池 
  }
  //newSourceExecutor()方法,设置线程池线程数量及名称 
  public static GlideExecutor.Builder newSourceBuilder() {
    return new GlideExecutor.Builder(/*preventNetworkOperations=*/ false)
        .setThreadCount(calculateBestThreadCount())
        .setName(DEFAULT_SOURCE_EXECUTOR_NAME);
  }
  //calculateBestThreadCount()获取最佳线程数量,最多为4个线程(MAXIMUM_AUTOMATIC_THREAD_COUNT)
  public static int calculateBestThreadCount() {
    if (bestThreadCount == 0) {
      bestThreadCount =
          Math.min(MAXIMUM_AUTOMATIC_THREAD_COUNT, RuntimeCompat.availableProcessors());
    }
    return bestThreadCount;
  }
  
  //设置核心线程数及线程总数
  public Builder setThreadCount(@IntRange(from = 1) int threadCount) {
      corePoolSize = threadCount;
      maximumPoolSize = threadCount;
      return this;
    }
  //磁盘缓存线程池,核心线程及线程总数为1
  public static GlideExecutor.Builder newDiskCacheBuilder() {
    return new GlideExecutor.Builder(/*preventNetworkOperations=*/ true)
        .setThreadCount(DEFAULT_DISK_CACHE_EXECUTOR_THREADS)
        .setName(DEFAULT_DISK_CACHE_EXECUTOR_NAME);
  }

通过上面源码可见:Glide线程池有三个
sourceExecutor——资源加载线程池最大线程数量为4个或系统CPU支持的最大线程数量,如果低于4则取CPU最大支持线程数。
diskCacheExecutor——加载磁盘缓存线程池核心线程数为1且最大线程数量为1的线程池。
同时,线程空闲时间为0(任务执行完成即可立即回收),使用的是PriorityBlockingQueue工作队列,PriorityBlockingQueue是一个支持优先级的无界阻塞队列,直到系统资源耗尽

问:Glide是如何进行线程切换的

答:Glide线程切换还是利用Handler来进行的,在GlideBuilder类中的Glide build()方法中初始化了RequestManagerRetriever请求管理器,在RequestManagerRetriever类中初始化了一个hanlder,用于最终请求完成的callback中进行线程切换。

  Glide build(@NonNull Context context) {
    //省略其他代码...
    RequestManagerRetriever requestManagerRetriever =
        new RequestManagerRetriever(requestManagerFactory);
     //省略其他代码...
  }

  public RequestManagerRetriever(@Nullable RequestManagerFactory factory) {
    this.factory = factory != null ? factory : DEFAULT_FACTORY;
    //在请求执行器管理器中初始化一个handler,关联主线程Looper
    handler = new Handler(Looper.getMainLooper(), this /* Callback */);
  }

这里明确了Glide最终的线程切换还是使用的Handler。

问:Glide是如何防止OOM以及Glide的缓存的实现?

答:Glide使用 BitmapPool 的池化概念,使得bitmap得以复用。Glide默认加载RGB_565,比ARGB_8888内存减少一半,并且Glide引入 内存缓存、磁盘缓存 两种缓存方式,内存缓存中又分为 活动内存缓存非活动内存缓存。用bitmap池、压缩及缓存的方式来降低内存的使用,减少OOM的发生。

Glide加载引擎是Engine来实现的,看看Engine的load方法:

  public <R> LoadStatus load(
  //省略一堆参数...
  ) {
    //生成缓存key
    EngineKey key =
        keyFactory.buildKey(
          //省略一堆参数
          );
    EngineResource<?> memoryResource;
    synchronized (this) {
      //从内存缓存中获取缓存
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
      //这里先不管
      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
           //省略一堆参数...  
         );
      }
    }
    //获取到缓存直接返回
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
    return null;
  }

看看Glide是如何获取内存缓存的:

  private EngineResource<?> loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) {
    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<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }
  //获取非活动缓存
  private EngineResource<?> loadFromCache(Key key) {
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      //将非活动缓存存入活动缓存
      activeResources.activate(key, cached);
    }
    return cached;
  }
  
  private EngineResource<?> getEngineResourceFromCache(Key key) {
    //cache实际上就是LruResourceCache
    //这里执行remove在LruCache算法中只是将链表指针修改为最前端,表示最近最新使用
    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, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }

从上面可以看出Glide的内存缓存分为两种,活动内存缓存(实际上是弱引用缓存)、非活动内存缓存(LruResourceCache)。当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后在将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉。
总结一下内存缓存的过程就是:先从活动缓存中取,如果取不到则从非活动缓存中取(LruResourceCache),将非活动缓存中要取出的缓存去除,然后加入到活动缓存中。

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

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 继承 HashMap,在 HashMap 的基础上进行扩展,put 方法并没有重写,说明LinkedHashMap遵循HashMap的数组加链表的结构,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++;
}

HashMap中存储的是 HashMapEntry,LinkedHashMap中存储的是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 方法,用于新增和删除链表节点。
existingEntry 传的都是链表头header,将一个节点添加到header节点前面,只需要移动链表指针即可,添加新数据都是放在链表头header 的before位置,链表头节点header的before是最新访问的数据,header的after则是最旧的数据。链表节点的移除比较简单,改变指针指向即可。
LinkedHashMap的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;
}

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

LruCache算法总结一下就是:

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

以上是有内存缓存的情况下的,没有内存缓存的情况下就要执行

 if (memoryResource == null) {
        return waitForExistingOrStartNewJob(
           //省略一堆参数...  
           engineJob.start(decodeJob);
         );
      }
  //创建了 EngineJob 和 DecodeJob 并调用了 EngineJob 的 start(decodeJob) 方法:
  public synchronized void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor =
        decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
    executor.execute(decodeJob);
  }
  //之后执行run方法,再执行到:
  private void runWrapped() {
    switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
  }

getNextGenerator() 得到的是 DataCacheGenerator,并执行了 startNext 方法:

  public boolean startNext() {
    while (modelLoaders == null || !hasNextModelLoader()) {
        ...
      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
      cacheFile = helper.getDiskCache().get(originalKey);
      if (cacheFile != null) {
        this.sourceKey = sourceId;
        modelLoaders = helper.getModelLoaders(cacheFile);
        modelLoaderIndex = 0;
      }
    }
 
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
      loadData =
          modelLoader.buildLoadData(
              cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
      if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
  }

调用了 cacheFile = helper.getDiskCache().get(originalKey) 获取 cacheFile 对象,此处的 helper 是 DecodeHelper,getDiskCache() 方法:

  DiskCache getDiskCache() {
    return diskCacheProvider.getDiskCache();
  }

获取到的是 DiskCache,是从 diskCacheProvider 中获取,而 diskCacheProvider 的实现在 Engine 类中:

  private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider {
 
    private final DiskCache.Factory factory;
    private volatile DiskCache diskCache;
 
    LazyDiskCacheProvider(DiskCache.Factory factory) {
      this.factory = factory;
    }
 
    @VisibleForTesting
    synchronized void clearDiskCacheIfCreated() {
      if (diskCache == null) {
        return;
      }
      diskCache.clear();
    }
 
    @Override
    public DiskCache getDiskCache() {
      if (diskCache == null) {
        synchronized (this) {
          if (diskCache == null) {
            diskCache = factory.build();
          }
          if (diskCache == null) {
            diskCache = new DiskCacheAdapter();
          }
        }
      }
      return diskCache;
    }
  }

而 DiskCache 是从 DiskCache.Factory 中获取,DiskCache.Factory 的实现类是 DiskLruCacheFactory,通过调用 factory.build() 方法:

  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 的 create 方法去创建 DiskLruCacheWrapper,而上面的 getDiskCache 的实现就是在此:

  private synchronized DiskLruCache getDiskCache() throws IOException {
    if (diskLruCache == null) {
      diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
    }
    return diskLruCache;
  }

这里可以看到,最终Glide的磁盘缓存通过DiskLruCache来实现。

问:Glide是如果管理内存泄漏问题的?

答:Glide在使用时调用了into方法,传入一个View(ImageView),在Activity/Fragment关闭时,页面已经不存在,但Glide的加载线程任然可能持有ImageView,就可能造成内存泄漏。Glide在with方法传入一个上下文,通过上下文来获取或生成一个无页面的Fragment来实现Glide与使用者的生命周期的同步。

  //在Activity中使用 
  public RequestManager get(@NonNull FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {    //判断是否是在主线程,如果不是主线程中使用,则传入的是application的Context
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      FragmentManager fm = activity.getSupportFragmentManager();
      return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
    }
  }
  //后面就详细去看了,最终是将生命周期的状态监听交给RequestManager来管理的

相关文章

网友评论

      本文标题:Android面试Android进阶(二十)-Glide相关

      本文链接:https://www.haomeiwen.com/subject/dbwrdltx.html