美文网首页
Android图片加载框架Glide解析

Android图片加载框架Glide解析

作者: RumbleTsc | 来源:发表于2019-04-03 22:07 被阅读0次

    现在Android上的图片加载框架有很多,比如:Universal Image Loader,Picasso,Fresco,Glide等等,这些框架有各自的优缺点。本文主要介绍的是Glide,它的作者是bumptech,作为一个高效的图片加载缓存框架,Glide被广泛的运用在google的开源项目中。

    本文基于Glide最新的版本4.9.0来进行相关的介绍。

    一、使用示例
    二、整体概述
    三、源码分析
    四、缓存介绍
    五、相关参考
    

    一、使用示例

    1.添加依赖

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

    2.使用

    Glide.with(this)
            .load(URL_IMAGE)
            .placeholder(R.drawable.ic_loading)
            .error(R.drawable.ic_load_error)
            .into(imageView);
    

    上面这段代码即可完成图片加载,其中with()方法可以传入的参数是Context、Activity、Fragment或者FragmentActivity,load()方法可以直接传入图片链接的url或者资源id等等,into()方法传入ImageView实例。

    placeholder是设置占位图,图片加载过程中会显示这张图片。
    error方法是设置图片加载失败的时候显示的图片。

    二、整体概述

    图片加载框架一般由如下几部分构成:

    (1) RequestManager:请求生成和管理模块

    (2) Engine:引擎部分,负责创建任务(获取数据),并调度执行

    (3) GetDataInterface:数据获取接口,负责从各个数据源获取数据。
    比如 MemoryCache 从内存缓存获取数据、DiskCache 从本地缓存获取数据,下载器从网络获取数据等。

    (4) Displayer:资源(图片)显示器,用于显示或操作资源。
    比如 ImageView,这几个图片缓存都不仅仅支持 ImageView,同时支持其他 View 以及虚拟的 Displayer 概念。

    (5) Processor 资源(图片)处理器
    负责处理资源,比如旋转、压缩、截取等。

    上面是Glide的总体设计图。整个库分为RequestManager(请求管理器),Engine(数据获取引擎)、Fetcher(数据获取器)、MemoryCache(内存缓存)、DiskLRUCache、Transformation(图片处理)、Encoder(本地缓存存储)、Registry(图片类型及解析器配置)、Target(目标) 等模块。

    简单的讲就是Glide收到加载及显示资源的任务,创建 Request 并将它交给RequestManager,Request 启动Engine去数据源获取资源(通过 Fetcher),获取到后Transformation处理后交给Target。Glide依赖于DiskLRUCache、GifDecoder等开源库去完成本地缓存和 Gif 图片解码工作。

    Glide优点

    • 图片缓存->媒体缓存

    Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图。甚至是 Video,所以更该当做一个媒体缓存。

    • 与 Activity/Fragment 生命周期一致,支持 trimMemory

    Glide 对每个 context 都保持一个RequestManager,通过FragmentTransaction保持与 Activity/Fragment生命周期一致,并且有对应的trimMemory接口实现可供调用。

    • 支持 okhttp、Volley

    Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。

    • 内存友好

    1)Glide 的内存缓存有个 active 的设计从内存缓存中取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。

    2)内存缓存更小图片Glide 以 url、view_width、view_height、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小

    3)与 Activity/Fragment 生命周期一致,支持 trimMemory

    4)图片默认使用默认 RGB_565 而不是 ARGB_888虽然清晰度差些,但图片更小,也可配置到 ARGB_888。

    三、源码分析

    从上面的示例可以看到Glide加载图片的用法是非常简单的,下面主要是通过阅读源码来看一下Glide是怎么完成这个图片加载过程的。

    1.with()

    Glide的方法with()主要是为了获取到一个RequestManager,它的参数可以是Activity、Fragment或者别的Context参数。

    通过查看with()方法的源码,我们可以看到它最后是调用RequestManagerRetriever的get()方法来获取到RequestManager的。

      @NonNull
      public RequestManager get(@NonNull Activity activity) {
        if (Util.isOnBackgroundThread()) {
          return get(activity.getApplicationContext());
        } else {
          assertNotDestroyed(activity);
          android.app.FragmentManager fm = activity.getFragmentManager();
          return fragmentGet(
              activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
        }
      }
    
      @SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"})
      @Deprecated
      @NonNull
      private RequestManager fragmentGet(@NonNull Context context,
          @NonNull android.app.FragmentManager fm,
          @Nullable android.app.Fragment parentHint,
          boolean isParentVisible) {
        RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
          // TODO(b/27524013): Factor out this Glide.get() call.
          Glide glide = Glide.get(context);
          requestManager =
              factory.build(
                  glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
          current.setRequestManager(requestManager);
        }
        return requestManager;
      }
    

    每一个返回的RequestManager 都会关联一个RequestManagerFragment对象,这个Fragment对象可以监听到加载图片的Activity的生命周期,这样就可以很方便的管理图片加载的过程,当Activity销毁的时候就停止加载。

    2.load()

    方法load()比较简单,它最终返回的是RequestBuilder。

    我们这里传入的是图片的url,从图中可以看到,load方法可以传入很多不同类型的参数,直接的bitmap、文件、字节数组等等,还是很强大的。

    3.placeholder()
    4.error()

    这两个方法是用来设置占位图和加载失败时展示的图片的,点进去我们可以看到这两个都是类BaseRequestOptions中的方法,因为BaseRequestOptions是Glide 4之后新加的一个类,主要是用于配置图片加载的设置。因为RequestBuilder是继承这个类的,所以我们可以直接调用这两个方法。

    5.into()

    最后的into()方法是Glide图片加载的核心部分,它主要是完成图片的加载并显示到ImageView上。

    直接跟进去,最终它会调用到下面这个方法:

      private <Y extends Target<TranscodeType>> Y into(
          @NonNull Y target,
          @Nullable RequestListener<TranscodeType> targetListener,
          BaseRequestOptions<?> options,
          Executor callbackExecutor) {
        Preconditions.checkNotNull(target);
        if (!isModelSet) {
          throw new IllegalArgumentException("You must call #load() before calling #into()");
        }
    
        Request request = buildRequest(target, targetListener, options, callbackExecutor);
    
        Request previous = target.getRequest();
        if (request.isEquivalentTo(previous)
            && !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
          request.recycle();
          // If the request is completed, beginning again will ensure the result is re-delivered,
          // triggering RequestListeners and Targets. If the request is failed, beginning again will
          // restart the request, giving it another chance to complete. If the request is already
          // running, we can let it continue running without interruption.
          if (!Preconditions.checkNotNull(previous).isRunning()) {
            // Use the previous request rather than the new one to allow for optimizations like skipping
            // setting placeholders, tracking and un-tracking Targets, and obtaining View dimensions
            // that are done in the individual Request.
            previous.begin();
          }
          return target;
        }
    
        requestManager.clear(target);
        target.setRequest(request);
        requestManager.track(target, request);
    
        return target;
      }
    

    方法into()里会生成用于请求资源的SingleRequest,在由requestManager调用方法track()、以及RequestTracker调用方法runRequest后,SingleRequest会执行begin()方法:

      public synchronized void begin() {
        assertNotCallingCallbacks();
        stateVerifier.throwIfRecycled();
        startTime = LogTime.getLogTime();
        if (model == null) {
          if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            width = overrideWidth;
            height = overrideHeight;
          }
          // Only log at more verbose log levels if the user has set a fallback drawable, because
          // fallback Drawables indicate the user expects null models occasionally.
          int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
          onLoadFailed(new GlideException("Received null model"), logLevel);
          return;
        }
    
        if (status == Status.RUNNING) {
          throw new IllegalArgumentException("Cannot restart a running request");
        }
    
        // If we're restarted after we're complete (usually via something like a notifyDataSetChanged
        // that starts an identical request into the same Target or View), we can simply use the
        // resource and size we retrieved the last time around and skip obtaining a new size, starting a
        // new load etc. This does mean that users who want to restart a load because they expect that
        // the view size has changed will need to explicitly clear the View or Target before starting
        // the new load.
        if (status == Status.COMPLETE) {
          onResourceReady(resource, DataSource.MEMORY_CACHE);
          return;
        }
    
        // Restarts for requests that are neither complete nor running can be treated as new requests
        // and can run again from the beginning.
    
        status = Status.WAITING_FOR_SIZE;
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
          onSizeReady(overrideWidth, overrideHeight);
        } else {
          target.getSize(this);
        }
    
        if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
            && canNotifyStatusChanged()) {
          target.onLoadStarted(getPlaceholderDrawable());
        }
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
      }
    

    方法begin()里面会对图片的尺寸进行判断,只有在图片的宽高都有效的情况下才会去加载。图片尺寸准备好后会走方法onSizeReady()。

      @Override
      public synchronized void onSizeReady(int width, int height) {
        stateVerifier.throwIfRecycled();
        if (IS_VERBOSE_LOGGABLE) {
          logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {
          return;
        }
        status = Status.RUNNING;
    
        float sizeMultiplier = requestOptions.getSizeMultiplier();
        this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
        this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
    
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadStatus =
            engine.load(
                glideContext,
                model,
                requestOptions.getSignature(),
                this.width,
                this.height,
                requestOptions.getResourceClass(),
                transcodeClass,
                priority,
                requestOptions.getDiskCacheStrategy(),
                requestOptions.getTransformations(),
                requestOptions.isTransformationRequired(),
                requestOptions.isScaleOnlyOrNoTransform(),
                requestOptions.getOptions(),
                requestOptions.isMemoryCacheable(),
                requestOptions.getUseUnlimitedSourceGeneratorsPool(),
                requestOptions.getUseAnimationPool(),
                requestOptions.getOnlyRetrieveFromCache(),
                this,
                callbackExecutor);
    
        // This is a hack that's only useful for testing right now where loads complete synchronously
        // even though under any executor running on any thread but the main thread, the load would
        // have completed asynchronously.
        if (status != Status.RUNNING) {
          loadStatus = null;
        }
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
      }
    

    方法onSizeReady里会调用类Engine的方法load,如果缓存可用的话,那么load方法会直接获取缓存并返回。如果不可用,会走到类DecodeJob的方法runWrapped()里。方法runWrapped()主要是用于启动DataFetcherGenerator,DataFetcherGenerator有三个实现类,网络获取数据一般是SourceGenerator类,SourceGenerator中有DataFetcher,DataFetcher由很多子类:

    从这些实现类的名字可以很容易的区别各自的作用,从网络获取数据是HttpUrlFetcher,查看它内部的方法loadDataWithRedirects可以看到内部是用HttpURLConnection来加载的。

    我们是在SourceGenerator开始数据加载的,加载完成会回调方法onDataReady(),之后会走到DecodeJob的方法onDataFetcherReady()中,类DecodeJob做了很多事情,它的职责主要是解码从数据源获取到的数据。

    接下去,层层调用,走到StreamBitmapDecoder的方法decode中,然后由Downsampler完成从InputStream到Bitmap的转换。

      @SuppressWarnings({"resource", "deprecation"})
      public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
          Options options, DecodeCallbacks callbacks) throws IOException {
        Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
            + " mark()");
    
        byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
        BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
        bitmapFactoryOptions.inTempStorage = bytesForOptions;
    
        DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
        DownsampleStrategy downsampleStrategy = options.get(DownsampleStrategy.OPTION);
        boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
        boolean isHardwareConfigAllowed =
          options.get(ALLOW_HARDWARE_CONFIG) != null && options.get(ALLOW_HARDWARE_CONFIG);
    
        try {
          Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
              downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
              requestedHeight, fixBitmapToRequestedDimensions, callbacks);
          return BitmapResource.obtain(result, bitmapPool);
        } finally {
          releaseOptions(bitmapFactoryOptions);
          byteArrayPool.put(bytesForOptions);
        }
      }
    

    DecodeJob完成解码后会调用方法notifyComplete(),然后EngineJob接收到通知后再通知结果出去:

      void notifyCallbacksOfResult() {
        ResourceCallbacksAndExecutors copy;
        Key localKey;
        EngineResource<?> localResource;
        synchronized (this) {
          stateVerifier.throwIfRecycled();
          if (isCancelled) {
            // TODO: Seems like we might as well put this in the memory cache instead of just recycling
            // it since we've gotten this far...
            resource.recycle();
            release();
            return;
          } else if (cbs.isEmpty()) {
            throw new IllegalStateException("Received a resource without any callbacks to notify");
          } else if (hasResource) {
            throw new IllegalStateException("Already have resource");
          }
          engineResource = engineResourceFactory.build(resource, isCacheable);
          // Hold on to resource for duration of our callbacks below so we don't recycle it in the
          // middle of notifying if it synchronously released by one of the callbacks. Acquire it under
          // a lock here so that any newly added callback that executes before the next locked section
          // below can't recycle the resource before we call the callbacks.
          hasResource = true;
          copy = cbs.copy();
          incrementPendingCallbacks(copy.size() + 1);
    
          localKey = key;
          localResource = engineResource;
        }
    
        listener.onEngineJobComplete(this, localKey, localResource);
    
        for (final ResourceCallbackAndExecutor entry : copy) {
          //通知结果
          entry.executor.execute(new CallResourceReady(entry.cb));
        }
        decrementPendingCallbacks();
      }
    

    SingleRequest接收到结果后显示出来:

      private synchronized void onResourceReady(Resource<R> resource, R result, DataSource dataSource) {
        // We must call isFirstReadyResource before setting status.
        boolean isFirstResource = isFirstReadyResource();
        status = Status.COMPLETE;
        this.resource = resource;
    
        if (glideContext.getLogLevel() <= Log.DEBUG) {
          Log.d(GLIDE_TAG, "Finished loading " + result.getClass().getSimpleName() + " from "
              + dataSource + " for " + model + " with size [" + width + "x" + height + "] in "
              + LogTime.getElapsedMillis(startTime) + " ms");
        }
    
        isCallingCallbacks = true;
        try {
          boolean anyListenerHandledUpdatingTarget = false;
          if (requestListeners != null) {
            for (RequestListener<R> listener : requestListeners) {
              anyListenerHandledUpdatingTarget |=
                  listener.onResourceReady(result, model, target, dataSource, isFirstResource);
            }
          }
          anyListenerHandledUpdatingTarget |=
              targetListener != null
                  && targetListener.onResourceReady(result, model, target, dataSource, isFirstResource);
    
          if (!anyListenerHandledUpdatingTarget) {
            Transition<? super R> animation =
                animationFactory.build(dataSource, isFirstResource);
            target.onResourceReady(result, animation);
          }
        } finally {
          isCallingCallbacks = false;
        }
    
        notifyLoadSuccess();
      }
    

    Target接收到结果,完成图片设置:

      @Override
      protected void setResource(Bitmap resource) {
        view.setImageBitmap(resource);
      }
    

    这样,一个大致的整体流程就走完了。

    四、缓存介绍

    一般来说,图片缓存主要有内存缓存和磁盘缓存两种,二者使用的都是LRU算法,即最近最少使用算法。Glide中除了这两个缓存之外还有一种用弱引用实现的ActiveResources缓存。

    具体在使用的过程中,图片资源获取成功后会写入磁盘缓存,ActiveResources缓存保存的是正在使用中的图片资源,当ActiveResources被移除时,资源会被放进内存缓存。获取缓存时,优先从ActiveResources中获取,没有的话从内存缓存中获取,最后从磁盘中读取。

    下面是相关的代码:

    1.缓存读取

      public synchronized <R> LoadStatus load(...) {
    
        long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
    
        EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
            resourceClass, transcodeClass, options);
    
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
          cb.onResourceReady(active, DataSource.MEMORY_CACHE);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Loaded resource from active resources", startTime, key);
          }
          return null;
        }
    
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
          cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Loaded resource from cache", startTime, key);
          }
          return null;
        }
    
        EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
        if (current != null) {
          current.addCallback(cb, callbackExecutor);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Added to existing load", startTime, key);
          }
          return new LoadStatus(cb, current);
        }
    
        EngineJob<R> engineJob =
            engineJobFactory.build(
                key,
                isMemoryCacheable,
                useUnlimitedSourceExecutorPool,
                useAnimationPool,
                onlyRetrieveFromCache);
    
        DecodeJob<R> decodeJob = decodeJobFactory.build(...);
    
        jobs.put(key, engineJob);
    
        engineJob.addCallback(cb, callbackExecutor);
        engineJob.start(decodeJob);
    
        if (VERBOSE_IS_LOGGABLE) {
          logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
      }
    

    2.写入缓存

    磁盘缓存(SourceGenerator)

      private void cacheData(Object dataToCache) {
        long startTime = LogTime.getLogTime();
        try {
          Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
          DataCacheWriter<Object> writer =
              new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
          originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
          helper.getDiskCache().put(originalKey, writer);
          if (Log.isLoggable(TAG, Log.VERBOSE)) {
            Log.v(TAG, "Finished encoding source to cache"
                + ", key: " + originalKey
                + ", data: " + dataToCache
                + ", encoder: " + encoder
                + ", duration: " + LogTime.getElapsedMillis(startTime));
          }
        } finally {
          loadData.fetcher.cleanup();
        }
    
        sourceCacheGenerator =
            new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
      }
    

    内存缓存()

    EngineJob获取到结果后调用方法notifyCallbacksOfResult,然后调用Engine的方法onEngineJobComplete,这里会把图片资源放在ActiveResources中。

      @SuppressWarnings("unchecked")
      @Override
      public synchronized void onEngineJobComplete(
          EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
        // A null resource indicates that the load failed, usually due to an exception.
        if (resource != null) {
          resource.setResourceListener(key, this);
    
          if (resource.isCacheable()) {
            activeResources.activate(key, resource);
          }
        }
    
        jobs.removeIfCurrent(key, engineJob);
      }
    

    当ActiveResources被移除时,类Engine收到通知调用方法onResourceReleased()把资源放到内存缓存中。

      @Override
      public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
        activeResources.deactivate(cacheKey);
        if (resource.isCacheable()) {
          cache.put(cacheKey, resource);
        } else {
          resourceRecycler.recycle(resource);
        }
      }
    

    五、相关参考

    Glide最全解析

    相关文章

      网友评论

          本文标题:Android图片加载框架Glide解析

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