Glide解析四:为什么用Target而不直接用ImageView

阅读本篇文章,建议先阅读:
Glide解析一:Glide整体流程
在解析Glide的整体流程时,我们发现Glide用的是Target而不是ImageView,为什么不直接在图片加载Request中持有ImageView,然后在图片加载完成时在onResourceReady方法中将图片设置ImageView呢?
其实Glide涉及到ImageView的工作量还是顶多的:

  • 获取ImageView的大小
  • 根据ImageView的生命周期调度重新加载、取消加载图片
  • ImageView的动画效果
  • ImageView加载过程中显示的默认图片、加载失败时显示的图片、加载成功时显示的是Bitmap还是Drawable以及动效效果
    如果将这些功能都放到图片加载Request中实现,那么就违背了设计模式里的单一职责原则,而且会比较冗余而设Rquest变得复杂、混乱等不良后果。所以Glide秉承单一职责,Request只处理图片加载的逻辑;至于ImageView相关将其包装为Target,使用Target实现ImageView的逻辑。
    我们先看下Target的主要相关的类图关系:
    image

    BaseTarget
    它是一个抽象类,值定义了一个Request成员变量,用于保存与之关联的图片加载请求Request,方便在ImageView绑定到window时去加载图片或者从window卸载时取消图片加载:
public abstract class BaseTarget<Z> implements Target<Z> {

  private Request request;

  @Override
  public void setRequest(@Nullable Request request) {
    this.request = request;
  }

  @Override
  @Nullable
  public Request getRequest() {
    return request;
  }

  @Override
  public void onLoadCleared(@Nullable Drawable placeholder) {
    // Do nothing.
  }

  @Override
  public void onLoadStarted(@Nullable Drawable placeholder) {
    // Do nothing.
  }

  @Override
  public void onLoadFailed(@Nullable Drawable errorDrawable) {
    // Do nothing.
  }

  @Override
  public void onStart() {
    // Do nothing.
  }

  @Override
  public void onStop() {
    // Do nothing.
  }

  @Override
  public void onDestroy() {
    // Do nothing.
  }
}

ViewTarget
ViewTarget主要做两件事:
a、获取ImageView的大小
在解析Glide整体流程时,有提出Glide是怎么获取ImageView的大小的?其实作为App开发工程师来说,获取一个View的大小无非是getWidth()、layoutParam.width,而当View还没绘制时是拿不到大小的,那么此时通过Activity的onWindowFocusChanged或者ViewTreeObserver来监听View的绘制完成时期在调用getWidth就可以拿到大小了。
Glide获取的时期是不太可能通过onWindowFocusChanged的了,剩下就只剩下ViewTreeObserver了,对的Glide就是通过ViewTreeObserver来获取的。我们看下其实现:

public void getSize(@NonNull SizeReadyCallback cb) {
    //调用成员遍历sizeDeterminer的getSize()方法
    sizeDeterminer.getSize(cb);
  }

//SizeDeterminer.java ViewTarget的一个内部类
void getSize(@NonNull SizeReadyCallback cb) {
      //获取当前view的宽度
      int currentWidth = getTargetWidth();
      //获取当前view的高度
      int currentHeight = getTargetHeight();
      if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
        //如果View的大小大于0
        //回调告知view的大小
        cb.onSizeReady(currentWidth, currentHeight);
        return;
      }

      if (!cbs.contains(cb)) {
        //添加大小观察者
        cbs.add(cb);
      }
      if (layoutListener == null) {
        //获取ViwTreeObserver
        ViewTreeObserver observer = view.getViewTreeObserver();
        layoutListener = new SizeDeterminerLayoutListener(this);
        //监听View的preDraw行为
        observer.addOnPreDrawListener(layoutListener);
      }
    }

//获取宽度
private int getTargetWidth() {
      int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight();
      LayoutParams layoutParams = view.getLayoutParams();
      int layoutParamSize = layoutParams != null ? layoutParams.width : PENDING_SIZE;
      return getTargetDimen(view.getWidth(), layoutParamSize, horizontalPadding);
    }
    //判断宽高是否大于0
    private boolean isViewStateAndSizeValid(int width, int height) {
      return isDimensionValid(width) && isDimensionValid(height);
    }
    //判断指定的大小是否大于0或者==Integer.MAX_VALUE
    private boolean isDimensionValid(int size) {
      return size > 0 || size == SIZE_ORIGINAL;
    }

这段代码的核心思想就是先判断当前View的大小是否大于0,如果大于0就直接回调onSizeReady告知View大小已知;否则通过ViewTreeObserver监听View的onPreDraw行为来获取View的大小并告知监听者view的大小已经测量好:

public boolean onPreDraw() {
        
        SizeDeterminer sizeDeterminer = sizeDeterminerRef.get();
        if (sizeDeterminer != null) {
          sizeDeterminer.checkCurrentDimens();
        }
        return true;
      }

void checkCurrentDimens() {
      if (cbs.isEmpty()) {
        return;
      }
      //获取宽度
      int currentWidth = getTargetWidth();
      //获取高度
      int currentHeight = getTargetHeight();
      if (!isViewStateAndSizeValid(currentWidth, currentHeight)) {
        //如果宽高小于0,表示view尚未测量好
        return;
      }
      //回调通知监听则view的大小已经测量好
      notifyCbs(currentWidth, currentHeight);
      //移除监听者
      clearCallbacksAndListener();
    }

b、监听与Window的绑定关系

public final ViewTarget<T, Z> clearOnDetach() {
    if (attachStateListener != null) {
      return this;
    }
    //创建绑定状态监听者
    attachStateListener = new OnAttachStateChangeListener() {
      @Override
      public void onViewAttachedToWindow(View v) {
        //绑定到window
        resumeMyRequest();
      }

      @Override
      public void onViewDetachedFromWindow(View v) {
        //从window解绑
        pauseMyRequest();
      }
    };
    maybeAddAttachStateListener();
    return this;
  }

//设置view绑定window状态的监听者
private void maybeAddAttachStateListener() {
    if (attachStateListener == null || isAttachStateListenerAdded) {
      return;
    }
    //添加绑定状态监听者
    view.addOnAttachStateChangeListener(attachStateListener);
    isAttachStateListenerAdded = true;
  }

@Synthetic void resumeMyRequest() {
    //绑定window时
    //获取图片加载对象request
    Request request = getRequest();
    if (request != null && request.isCleared()) {
      //开始请求加载
      request.begin();
    }
  }

  @SuppressWarnings("WeakerAccess")
  @Synthetic void pauseMyRequest() {
    //从window解绑时
    //获取图片加载对象request
    Request request = getRequest();
    if (request != null) {
      isClearedByUs = true;
      //取消图片加载
      request.clear();
      isClearedByUs = false;
    }
  }

通过监听view与window的绑定关系,进而调度图片加载发起加载请求或者取消加载请求。
ImageViewTarget
ImageViewTarget的主要工作有:
a、设置加载中的显示图片

public void onLoadStarted(@Nullable Drawable placeholder) {
    super.onLoadStarted(placeholder);
    setResourceInternal(null);
    setDrawable(placeholder);
  }

public void setDrawable(Drawable drawable) {
    view.setImageDrawable(drawable);
  }

设置加载中显示的图片很简单,先将原来的图片资源设置为空,在设置placeHolder为加载中显示的图片
b、设置加载失败的显示图片

public void onLoadFailed(@Nullable Drawable errorDrawable) {
    super.onLoadFailed(errorDrawable);
    setResourceInternal(null);
    setDrawable(errorDrawable);
  }

与加载中一样,加载失败时,先将原来的图片资源设置为空,在设置errorDrawable为加载失败显示的图片
c、图片加载成功的模板

public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
    if (transition == null || !transition.transition(resource, this)) {
      //没有动画
      //调用setResourceInternal设置加载成功的图片
      setResourceInternal(resource);
    } else {
      //有动画,调用maybeUpdateAnimatable实现动效
      maybeUpdateAnimatable(resource);
    }
  }

private void setResourceInternal(@Nullable Z resource) {
    //调用setResource设置图片资源
    setResource(resource);
   //执行动效
    maybeUpdateAnimatable(resource);
  }

protected abstract void setResource(@Nullable Z resource);

在图片加载成功回调时,如果没有动效调用setResource设置加载成功的图片资源,而setResource是抽象方法,其实现是在DrawableImageViewTarget和BitmapImageVIewTarget来实现的;如果有动效则使用maybeUpdateAnimatable实现动效的逻辑。
d、动画的实现
上面的代码分析指导动效的实现是在函数maybeUpdateAnimatable中,我们看下其代码实现:

private void maybeUpdateAnimatable(@Nullable Z resource) {
    if (resource instanceof Animatable) {
      animatable = (Animatable) resource;
      animatable.start();
    } else {
      animatable = null;
    }
  }

maybeUpdateAnimatable很简单,就是判断图片资源是否是Animatable的实现类,是的话就转换为Animatable,并调用start开始动效。
BitmapImageViewTarget
BitmapImageViewTarget就是以bitmap的形式设置图片的资源,在分析ImageViewTarget的时候就明确指出设置图片资源是在子类的setResource来实现,我们看下BitmapImageViewTarget的setResource方法:

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

很简单就是调用setImageBitmap设置图片资源
DrawableImageViewTarget
DrawableImageViewTarget就是以bitmap的形式设置图片的资源,在分析ImageViewTarget的时候就明确指出设置图片资源是在子类的setResource来实现,我们看下DrawableImageViewTarget的setResource方法:

protected void setResource(@Nullable Drawable resource) {
    view.setImageDrawable(resource);
  }

很简单就是调用setImageDrawable设置图片资源

ok,总结下Target的核心:

  • 通过ViewTreeObserver实现View的大小测量,测量到大小之后回到监听者的onSizeReady告知view的大小已经测量ok
  • 通过监听View与Window的绑定关系发起加载图片的请求或者取消加载图片
  • 设置加载中的显示图片
  • 设置加载失败时显示的图片
  • 设置加载成功时的图片、动效

综合起来Target的工作还是蛮多的,如果融入Request中那么就会导致Request更为复杂、乱,所以Glide单独Target模块来实现View的相关逻辑,体现了单一职责原则、高内聚低耦合的特性

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容