Glide源码分析(二) 如何动态测量获取ImageView大小

介绍Glide如何动态测量ImageView大小

我们都知道,Glide是在Picasso的基础上进行的改善,相比于Picasso,Glide会根据ImageView的大小来生成图片大小,这样可以减少图片占用的内存大小,我们来看看Glide是怎么动态测量获取ImageView的大小,并且设置根据大小设置图片的大小

我们先来简单回顾一下Activity中加载视图的过程:
我们在onCreate(...)方法中调用了setContentView(int layoutId)来设置了布局文件,其实这里并没有马上进行视图的测量,布局,绘制,一切都是等到onResume之后才进行界面视图的渲染。至于如何渲染,这里有两篇文章,如果你不熟悉或者还是小白,可以先参考下:
Android View的绘制流程
Android应用程序启动过程源代码分析

那么在这个生命周期过程中,如果使用了Glide的加载图片方法,那么其实这时候图片是无法确定大小和位置的(因为这时候整个页面视图还没有进行测量布局绘制,哪来的确定大小和位置),这时候Glide该怎么办呢?

还是以我们上文Glide源码分析(一) 图片加载的生命周期中的例子来分析一下整个过程,重点讲解一下动态获取ImageView大小

例子

Glide.with(StartActivity.this).load(R.mipmap.pizza).into(mIvShow);

分析

本例中,Glide前面的方法最终要生成Bitmap对象写入ImageView对象中显示出来,我们看下into()方法
GenericRequestBuilder.java

// 设置了将要加载图片到哪个视图,取消已经加载到ImageView中的资源,并且释放资源用于后面可能的复用
 public Target<TranscodeType> into(ImageView view) {
        Util.assertMainThread();
        if (view == null) {
            throw new IllegalArgumentException("You must pass in a non null View");
        }
        // 如果之前没有定义过Transformation,并且ImageView设置了scaleType
        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {
                case CENTER_CROP:
                   // 生成一个CENTER_CROP的Transformation,用于后面生成图片时转换
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                   // 生成一个FIT_END的Transformation对象,用于后面生成图片时转换
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }
        // 统一构建ImageViewTarget
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }

这里最终会构建一个ImageViewTarget对象,接下来看下是如何构建这个类
关于自定义Transformation的例子,具体看我的例子工程GlideSampleTransformationActivity.java

Glide.java

    <R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
        return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
    }

ImageViewTargetFactory.java

    @SuppressWarnings("unchecked")
    public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            ......
        }
        ......
    }

这里我们最终会返回GlideDrawableImageViewTarget对象,接下来就是request任务
GenericRequestBuilder.java

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

        // 这个target之前是否有存在Request任务
        Request previous = target.getRequest();

        if (previous != null) {
            // 如果存在,那么清除之前的Request
            previous.clear();
            requestTracker.removeRequest(previous);
            previous.recycle();
        }
        // 构建新的Request,下面会讲解到
        Request request = buildRequest(target);
        target.setRequest(request);
        lifecycle.addListener(target);
        // 运行Request,本文重点在这里
        requestTracker.runRequest(request);

        return target;
    }

上面代码我们主要关注下buildRequest(target)方法和requestTracker.runRequest(request)方法(本文重点)
GenericRequestBuilder.java

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

    private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
        if (thumbnailRequestBuilder != null) {  // 是否有指定自定义缩略图的请求,本例中没有
            ......
        } else if (thumbSizeMultiplier != null) {  // 是否有等比例缩放的请求,本例中没有
           ......
        } else {
            // 没有缩略图,构建一个GenericRequest对象
            return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
        }
    }

关于缩略图,具体看我的例子工程GlideSampleThumbnailActivity.java类,里面有关于缩略图的用法

好了,前面铺垫了这么多,其实还没有涉及到如何获取ImageView的大小,别急,马上就来~~
我们看下requestTracker.runRequest(target)方法
RequestTracker.java

public void runRequest(Request request) {
        requests.add(request);
        if (!isPaused) {
            request.begin();
        } else {
            pendingRequests.add(request);
        }
    }

这个方法运行了封装好的GenericRequest.begin()类方法
GenericRequest.java

public void begin() {
       // 记录Request运行起始时间
        startTime = LogTime.getLogTime();
        if (model == null) {
            onException(null);
            return;
        }
        // 设置为等待测量ImageView大小
        status = Status.WAITING_FOR_SIZE;
       // overrideWidth和overrideHeight是用户可能通过Glide.override(x,y)
       // 在显示图片前重新剪裁图片大小
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            用户已经指定图片大小,无需测量ImageView大小
            onSizeReady(overrideWidth, overrideHeight);
        } else {
            // 这里的target是我们上文中得到的GlideDrawableImageViewTarget,
            // 通过target类设置了一个监听进去,来监听ImageView的图片固定
            target.getSize(this);
        }

        if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
            // 开始加载占位符图片(如果用户有设置占位符的情况下)
            target.onLoadStarted(getPlaceholderDrawable());
        }
        // 记录本次run的时间
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
    }

重点在这里target.getSize(this),这个类设置了
GlideDrawableImageViewTarget.java

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

GlideDrawableImageViewTarget.java

public void getSize(SizeReadyCallback cb) {
            // 获取ImageView的准确宽度或者LayoutParams的常量值(例如:LayoutParams.WRAP_CONTENT)
            int currentWidth = getViewWidthOrParam();
            // 同上面获取宽度
            int currentHeight = getViewHeightOrParam();
            // 如果宽度是准确值,或者是LayoutParams.WRAP_CONTENT属性
            // 如果高度是准确值,或者是LayoutParams.WRAP_CONTENT属性
            if (isSizeValid(currentWidth) && isSizeValid(currentHeight)) {
                // 直接回调已获取到当前的宽高
                cb.onSizeReady(currentWidth, currentHeight);
            } else {
                // 加入到回调队列中
                if (!cbs.contains(cb)) {
                    cbs.add(cb);
                }
                if (layoutListener == null) {
                    // 获取ImageView的视图树监听
                    final ViewTreeObserver observer = view.getViewTreeObserver();
                    layoutListener = new SizeDeterminerLayoutListener(this);
                    /* 
                       重点来咯,添加一个OnPreDrawListener这个监听,关于这个监听的含义大体
                       是在视图绘制之前进行回调,绘制的时候,视图的肯定是经过测量过宽高了,别问我为什么,我会打人的
                     */
                    observer.addOnPreDrawListener(layoutListener);
                }
            }
        }

我们看下SizeDeterminerLayoutListener这个类,实现了ViewTreeObserver.OnPreDrawListener接口,通过onPreDraw方法,执行回调队列进行统一的回调

private static class SizeDeterminerLayoutListener implements ViewTreeObserver.OnPreDrawListener {
            /* 这里为什么用弱引用,我想是因为我们的ViewTarget可能在测量ImageView前
               有可能进行多次的Glide.into(),还记得我们在GenericRequestBuilder.into()方法吗?
               里面有对之前的Request进行clear,recycle等操作,忘记的同学可以回头去看看代码
            */
            private final WeakReference<SizeDeterminer> sizeDeterminerRef;

            public SizeDeterminerLayoutListener(SizeDeterminer sizeDeterminer) {
                sizeDeterminerRef = new WeakReference<SizeDeterminer>(sizeDeterminer);
            }

            @Override
            public boolean onPreDraw() {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "OnGlobalLayoutListener called listener=" + this);
                }
                SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
                if (sizeDeterminer != null) {
                    // 统一回调,可以获取当前ImageView的宽高
                    sizeDeterminer.checkCurrentDimens();
                }
                return true;
            }
        }

SizeDeterminer.java

private void checkCurrentDimens() {
            if (cbs.isEmpty()) {
                return;
            }
            // 上面已经解释过了
            int currentWidth = getViewWidthOrParam();
            int currentHeight = getViewHeightOrParam();
            if (!isSizeValid(currentWidth) || !isSizeValid(currentHeight)) {
                return;
            }
            // 统一回调监听队列
            notifyCbs(currentWidth, currentHeight);
            // Keep a reference to the layout listener and remove it here
            // rather than having the observer remove itself because the observer
            // we add the listener to will be almost immediately merged into
            // another observer and will therefore never be alive. If we instead
            // keep a reference to the listener and remove it here, we get the
            // current view tree observer and should succeed.
            ViewTreeObserver observer = view.getViewTreeObserver();
            if (observer.isAlive()) {
                // 测量得到了结果,移除监听
                observer.removeOnPreDrawListener(layoutListener);
            }
            layoutListener = null;
        }

回调方法notifyCbs(currentWidth, currentHeight)最后回调GenericRequest.onSizeReady()方法对图片进行加载显示,本节暂时不对图片的加载源码进行分析
GenericRequest.java

    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;
        // 图片缩放参数,如果用户有设置thumbnail参数时`Glide.with(...).thumbnail(0.2f).into(...)`进行比例缩放
        // 默认值是1,不进行图片大小缩放
        width = Math.round(sizeMultiplier * width);
        height = Math.round(sizeMultiplier * height);
        // 图片加载
        ......
        ......
        ......
    }

总结

通过本节的源码分析,我们可以发现Glide将加载图片到ImageVIew封装成Request对象,在run Request的时候进行判断,如果ImageView有固定大小,那么就直接回调加载图片,如果图片还没有进行测量过,那么就设置监听ViewTreeObserver.OnPreDrawListener,等待ImageView绘制前回调监听获取视图大小,在加载确定大小的图片