从功能模块设计的角度分析Glide源码(二)

概述

本文的内容为Glide的多线程模块和图像资源自动缩放到ViewTarget的宽高大小然后被加载到内存中的原理

多线程模块

在图片的第一次加载过程中必然会有网络请求和对IO的操作,这些耗时的操作会放在另外的线程执行这样就不会阻塞UI主线程,同时也会存在并发的需求,所以Glide中也会有多线程的模块来处理这些问题。

上篇文章我们了解到Glide会对请求进行适配于Android组件的生命周期的管理,其中RequestTracker类中会维护两个request的集合。

class RequestTracker

private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());
// A set of requests that have not completed and are queued to be run again. We use this list to maintain hard
// references to these requests to ensure that they are not garbage collected before they start running or
// while they are paused. See #346.
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
private final List<Request> pendingRequests = new ArrayList<Request>();
 

第一个集合的数据结构为Set,维护的是当前activity或者fragment或者application中的所有Glide的request实例,其中的实例为弱引用,在资源紧张的时候其中的实例可能会被回收掉。
第二个集合的数据结构为ArrayList,维护的是还未完成的request实例,采用强引用,保证这些请求不会被丢失,且仍可以被重新执行。

当activity或者fragment onStop时第一个集合中未完成的请求会被加入到第二个集合中,这样在onResume的时候会重新restart这些未完成的请求。

RequestTracker这个类主要作用就是追踪,取消,重启当前activity或者fragment或者application中request实例。

查看它的runRequest的调用可以发现它出现在Glide调用链函数中的into(T target)函数体中:

class GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType>

public <Y extends Target<TranscodeType>> Y into(Y target) {
        Util.assertMainThread();
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }
 
        Request previous = target.getRequest();
 
        if (previous != null) {
            previous.clear();
            requestTracker.removeRequest(previous);
            previous.recycle();
        }
 
        Request request = buildRequest(target);
        target.setRequest(request);
        lifecycle.addListener(target);
        requestTracker.runRequest(request);
 
        return target;
    }
 

可以看到在第21行调用了requestTracker.runRequest(request)方法将request实例添加到requestTracker中并begin该request.

那么该GenericRequestBuilder中的requestTracker是哪里来的呢?
在Glide调用链函数load(T model)中会将requestTracker作为GenericRequestBuilder构造函数的参数传入进来:

class RequestManager

private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
        ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
        ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
                Glide.buildFileDescriptorModelLoader(modelClass, context);
        if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
            throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
                    + " which there is a registered ModelLoader, if you are using a custom model, you must first call"
                    + " Glide#register with a ModelLoaderFactory for your custom model class");
        }
 
        return optionsApplier.apply(
                new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                        glide, requestTracker, lifecycle, optionsApplier));
    }
 

可以看到在最后一行代码中将requestTracker传入了DrawableTypeRequest中,而DrawableTypeRequest是GenericRequestBuilder的子类。

我们继续看requestTracker.runRequest执行Request的beginRequest中做了什么,Request接口有2个实现,以其中GenericRequest最为常用,可以看到在其begin方法中就开启了Glide的真正的资源加载流程(其中就使用到了多线程技术):
class GenericRequest<A, T, Z, R>

public void begin() {
        startTime = LogTime.getLogTime();
        if (model == null) {
            onException(null);
            return;
        }
 
        status = Status.WAITING_FOR_SIZE;
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            onSizeReady(overrideWidth, overrideHeight);
        } else {
            target.getSize(this);
        }
 
        if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
            target.onLoadStarted(getPlaceholderDrawable());
        }
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
    }
 

该函数里面并没有什么内容,主要是占位符的显示,和获取到合格的overrideWidth, overrideHeight并执行对应的onSizeReady回调函数,这其中有Glide获取目标View宽高大小的部分逻辑,我们会在后面分析,继续看onSieReady中做了什么
class GenericRequest<A, T, Z, R>

public void onSizeReady(int width, int height) {
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {
            return;
        }
        status = Status.RUNNING;
 
        width = Math.round(sizeMultiplier * width);
        height = Math.round(sizeMultiplier * height);
 
        ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
        final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
 
        if (dataFetcher == null) {
            onException(new Exception("Failed to load model: \'" + model + "\'"));
            return;
        }
        ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadedFromMemoryCache = true;
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);
        loadedFromMemoryCache = resource != null;
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
    }
 

可以看到这个函数前半部分干的时就是获取后面engine.load方法所需要的参数对象,在第25行会执行engine.load()方法,engine是怎么来的呢?那就找他的Builder类了,可以看到就是在前面的GenericRequestBuilder的into()方法的第18行代码buildRequest中构造的,继续查看engine.load()方法:
class Engine

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        Util.assertMainThread();
        long startTime = LogTime.getLogTime();
 
        final String id = fetcher.getId();
        EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                transcoder, loadProvider.getSourceEncoder());
 
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }
 
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }
 
        EngineJob current = jobs.get(key);
        if (current != null) {
            current.addCallback(cb);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Added to existing load", startTime, key);
            }
            return new LoadStatus(cb, current);
        }
 
        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);
 
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
    }
 

Engine类是Glide中非常重要的一个类,从命名引擎可以看出这个类主要负责图片加载中资源加载,缓存的核心流程。

其中load函数又是其核心逻辑块,主要流程逻辑是:

先从内存缓存中取该资源、再从活动资源中取资源、最后从硬盘或者网络上面取资源。

从硬盘和网络取资源必然就涉及到IO操作和网络请求操作,那么这块就会用到多线程技术,这也就到了本文第一个部分需要分析的Glide的多线程模块。

在最后几行会创建EngineJob,DecodeJob,EngineRunnable实例,而EngineRunnable是一个Runnable线程可执行类,根据以往的套路可以相信这个EngineRunnable里面就会执行从disk或者从网络中加载资源的任务。

继续查看engineJob.start(runnable):

class EngineJob

public void start(EngineRunnable engineRunnable) {
        this.engineRunnable = engineRunnable;
        future = diskCacheService.submit(engineRunnable);
    }
 

ok,到这里我们就看到了执行这个engineRunnable的目标了,diskCacheService是一个FifoPriorityThreadPoolExecutor线程池实例,用来管理加载磁盘资源的EngineRunnable异步线程,那么加载网络请求的线程池资源在哪执行加载网络请求的Runnable呢?
回到刚才diskCacheService.submit(engineRunnable)中,执行完这一步就没了,那么触发网络加载的逻辑肯定在engineRunnable的执行逻辑中,查看engineRunnable的run函数可以知道当磁盘中没有目标资源时会调用onLoadFailed方法,其中又会执行manager.submitForSource(this)方法,而这个manager就是刚才的EngineJob的实例:

class EngineJob

public void submitForSource(EngineRunnable runnable) {
    future = sourceService.submit(runnable);
}  
 

到这也就看到了另一个线程池 sourceService

那么这两个线程池是什么时候创建的呢?

在Glide的初始化过程中会构建一些默认的配置资源,以简化我们使用者的调用,如果需要自定义这些配置可以继承GlideModule来操作。

class Glide

public static void setup(GlideBuilder builder) {
    if (isSetup()) {
        throw new IllegalArgumentException("Glide is already setup, check with isSetup() first");
    }
 
    glide = builder.createGlide();
}
 

class GlideBuilder

Glide createGlide() {
        if (sourceService == null) {
            final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
            sourceService = new FifoPriorityThreadPoolExecutor(cores);
        }
        if (diskCacheService == null) {
            diskCacheService = new FifoPriorityThreadPoolExecutor(1);
        }
 
        MemorySizeCalculator calculator = new MemorySizeCalculator(context);
        if (bitmapPool == null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                int size = calculator.getBitmapPoolSize();
                bitmapPool = new LruBitmapPool(size);
            } else {
                bitmapPool = new BitmapPoolAdapter();
            }
        }
 
        if (memoryCache == null) {
            memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
        }
 
        if (diskCacheFactory == null) {
            diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }
 
        if (engine == null) {
            engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
        }
 
        if (decodeFormat == null) {
            decodeFormat = DecodeFormat.DEFAULT;
        }
 
        return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
    }    
 

GlideBuilder类中会构建一些Glide全局的资源实例:

private Engine engine;  引擎
private BitmapPool bitmapPool; Bitmap复用资源池
private MemoryCache memoryCache; 内存缓存
private ExecutorService sourceService; 源资源加载线程池
private ExecutorService diskCacheService; 磁盘资源加载线程池
private DecodeFormat decodeFormat; 加码格式 如RGB8888或者RGB565
private DiskCache.Factory diskCacheFactory; 磁盘缓存工厂类 
 

Glide使用了自定义的线程池类FifoPriorityThreadPoolExecutor
根据命名可以看出这是一个先入先执行和有优先级概念的线程池,查看它的源码可以看到它具体的先后顺序执行策略是:

先根据优先级执行线程,即优先级高的先执行,当优先级相同时再根据加入到这个线程池的时间的先后来执行,即先加入先执行(Fifo)

class FifoPriorityThreadPoolExecutor.LoadTask

public int compareTo(LoadTask<?> loadTask) {
            int result = priority - loadTask.priority;
            if (result == 0) {
                result = order - loadTask.order;
            }
            return result;
        }   
 

那么这个优先级是怎么体现在我们的使用中的呢?

我们顺着这个异步线程EngineRunnable的构造函数取找优先级这个参数可以在哪被设置进来

EngineRunnable -> Engine.load() -> GenericRequest -> GenericRequestBuilder.priority

而这个priority变量可以通过priority(Priority priority)方法被设置进来:

class GenericRequestBuilder

public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> priority(
            Priority priority) {
        this.priority = priority;
 
        return this;
    }      
 

在Request被创建的时候默认的优先级是Priority.NORMAL:
class GenericRequestBuilder

private Request buildRequest(Target<TranscodeType> target) {
        if (priority == null) {
            priority = Priority.NORMAL;
        }
        return buildRequestRecursive(target, null);
    }
 

而Priority的优先级有5种:

public enum Priority {
    IMMEDIATE,
    HIGH,
    NORMAL,
    LOW, 
    priority,
}   
 

所以我们在使用Glide的时候可以通过调用priority(Priority priority)方法来指定优先级,优先级高的加载请求会被优先执行(当然只限定于从磁盘和网络加载的过程)

主要类:

RequestTracker
Engine
EngineRunnable
EngineJob
GenericRequestBuilder
GenericRequest
FifoPriorityThreadPoolExecutor

图像资源自动缩放到ViewTarget的宽高大小然后被加载到内存中的原理

设备的单位面积内显示的像素点是固定的,所以对于设备上的有着准确宽高的ViewTarget的总像素点也是固定的,如果加载一个大于该像素点总数的图片资源那么必然是浪费内存资源的,所以在图片资源被加载到内存中前,我们会对其进行一定的处理以达到对内存的合理使用。

对于Glide不光是有上面的这个需求需要提前知道具体的宽高,还有一个地方也需要知道具体的宽高:

磁盘缓存

Glide提供了可缓存处理后的图片资源到disk中的功能,所以当缓存策略为Result模式时需要知道具体的目标宽高。

因此Glide的整个加载流程是先去获取目标ViewTarget的宽高,再从不同地方加载对应的资源

对于Glide框架而言,它提供了override(int width, int height)这个api来满足我们使用者的个性化需求,如果我们调用了这个api,那么Glide就会根据我们提供的宽高的像素值老老实实的将原始资源压缩成这个值。如果我们没有调用这个api,Glide也有一套默认的智能化处理策略。

默认处理策略

在ImageView或者其它展示图片的自定义View中 除了宽高为wrap_content需要具体的图片资源加载回来后才知道精确尺寸,其它类似于具体尺寸大小xdp和match_parent这样的参数,都可以在图片加载回来前精确知道其尺寸大小,那么前面我们所说的需要知道ViewTarget的宽高来对图片资源进行对应的压缩来达到合理使用内存的目的,对于wrap_content这种无法提前知道宽高的情况那又应该怎么处理呢,是放任原图大小,有多大加载多大还是有另外更好的处理方式呢?
答案都在ViewTarget的getSize函数中:
class ViewTarget

public void getSize(SizeReadyCallback cb) {
        sizeDeterminer.getSize(cb);
    }
 

class ViewTarget.SizeDeterminer

public void getSize(SizeReadyCallback cb) {
            int currentWidth = getViewWidthOrParam();
            int currentHeight = getViewHeightOrParam();
            if (isSizeValid(currentWidth) && isSizeValid(currentHeight)){
                cb.onSizeReady(currentWidth, currentHeight);
            } else {
                // We want to notify callbacks in the order they were added and we only expect one or two callbacks to
                // be added a time, so a List is a reasonable choice.
                if (!cbs.contains(cb)) {
                    cbs.add(cb);
                }
                if (layoutListener == null) {
                    final ViewTreeObserver observer = view.getViewTreeObserver();
                    layoutListener = new SizeDeterminerLayoutListener(this);
                    observer.addOnPreDrawListener(layoutListener);
                }
            }
        }  
 
    private int getViewWidthOrParam() {
        final LayoutParams layoutParams = view.getLayoutParams();
        if (isSizeValid(view.getWidth())) {
            return view.getWidth();
        } else if (layoutParams != null) {
            return getSizeForParam(layoutParams.width, false /*isHeight*/);
        } else {
            return PENDING_SIZE;
        }
    }
 
    private int getSizeForParam(int param, boolean isHeight) {
        if (param == LayoutParams.WRAP_CONTENT) {
            Point displayDimens = getDisplayDimens();
            return isHeight ? displayDimens.y : displayDimens.x;
        } else {
            return param;
        }
    }
 
    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
    @SuppressWarnings("deprecation")
    private Point getDisplayDimens() {
        if (displayDimens != null) {
            return displayDimens;
        }
        WindowManager windowManager = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
        Display display = windowManager.getDefaultDisplay();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
            displayDimens = new Point();
            display.getSize(displayDimens);
        } else {
            displayDimens = new Point(display.getWidth(), display.getHeight());
        }
        return displayDimens;
    }
 
    private boolean isSizeValid(int size) {
        return size > 0 || size == LayoutParams.WRAP_CONTENT;
    }  
 

这里的整个逻辑流程是这样的:如果通过getWidth和getHeight获取的宽高一个或者都是0(即还没完成View的测量过程)那么通过getLayoutParams布局参数来确定,如果布局参数是wrap_content那么就取当前view所在窗口对应的宽或者高的大小,如果没有布局参数就取0,其它的情况就添加一个监听器,当该view真正测量完成后再去获取它的大小。

在这里我们就了解到了Glide对获取目标View宽高大小的具体策略, 所以当我们在使用Glide的时候能确定ImageView大小的时候就给一个准确的值,尽量少用wrap_content,因为采用wrap_content后有可能会加载一个当前手机屏幕分辨率大小的图到内存中,如果是一个列表视图就有可能引发崩溃,我觉得对于wrap_content这种处理方式,glide还可以进一步的优化,因为wrap_content的情况下最大是不会超过父控件大小的,如取获取父控件的宽高,父控件如果也是wrap_content再继续去取上层父控件的宽高…

主要类:

ViewTarget

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/59994edbf265da248a7a6d9b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞