分析Picasso加载图片偶现回调不触发问题

632 阅读2分钟

问题背景

 项目中有用到Picasso库加载图片,发现存在Target或Callback的回调函数会偶现不执行的情况,问题也能在官方库的ISSUE中找到。以下是源码跟进分析过程:

以下代码,通常用在有预加载图片诉求的业务中。

第一种用法:使用Target

Picasso.get().load(URL).into(new Target() {
    @Override
    public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
        Log.i(TAG, "onBitmapLoaded: " + from);
    }

    @Override
    public void onBitmapFailed(Exception e, Drawable errorDrawable) {
        Log.e(TAG, "onBitmapFailed");
    }

    @Override
    public void onPrepareLoad(Drawable placeHolderDrawable) {
    }
});

第二种用法:使用Callback

ImageView imageView = new ImageView(this);
Picasso.get().load(URL).into(imageView, new Callback() {
    @Override
    public void onSuccess() {
        Log.i(TAG, "onSuccess: ");
    }

    @Override
    public void onError(Exception e) {
        Log.e(TAG, "onError" );
    }
});

问题原因

 对于这样偶现的问题,第一反应应该可能是引用持有问题,一番排查后果然看到了关键字: WeakReference

// 1.先看into方法:RequestCreator.java
public void into(@NonNull Target target) {
    // ...
    Action action =
        new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
            requestKey, tag, errorResId);
    picasso.enqueueAndSubmit(action);
}

// 2.跟到Action:TargetAction.java
final class TargetAction extends Action<Target> {

  TargetAction(Picasso picasso, Target target, Request data, int memoryPolicy, int networkPolicy,
      Drawable errorDrawable, String key, Object tag, int errorResId) {
    // target被传到了super构造器
    super(picasso, target, data, memoryPolicy, networkPolicy, errorResId, errorDrawable, key, tag,
        false);
  }

  @Override 
  void complete(Bitmap result, Picasso.LoadedFrom from) {
    if (result == null) {
      throw new AssertionError(
          String.format("Attempted to complete action with no result!\n%s", this));
    }
    Target target = getTarget();
    if (target != null) { // 这一句判断很关键
      target.onBitmapLoaded(result, from);
      if (result.isRecycled()) {
        throw new IllegalStateException("Target callback must not recycle bitmap!");
      }
    }
  }

  @Override 
  void error(Exception e) {
    Target target = getTarget();
    if (target != null) { // 这一句判断很关键
      if (errorResId != 0) {
        target.onBitmapFailed(e, picasso.context.getResources().getDrawable(errorResId));
      } else {
        target.onBitmapFailed(e, errorDrawable);
      }
    }
  }
}

// 3.往上跟到Action的构造方法:Action.java
Action(Picasso picasso, T target, Request request, int memoryPolicy, int networkPolicy,
    int errorResId, Drawable errorDrawable, String key, Object tag, boolean noFade) {
// ...
this.target =
    target == null ? null : new RequestWeakReference<>(this, target, picasso.referenceQueue);
// ...
}

由此可见,如果 this.target 这个虚引用,要是在 gc 的时候被回收了,回调自然也不会有了。

场景复现

 以下代码模拟系统内存紧张时候的gc过程,这样偶现问题便成了必现,或极大概率出现。

public void preloadImage() {
    Picasso.get().load(URL).into(new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            Log.i(TAG, "onBitmapLoaded: " + from);
        }

        @Override
        public void onBitmapFailed(Exception e, Drawable errorDrawable) {
            Log.e(TAG, "onBitmapFailed");
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
        }
    });

    for (int i = 0; i < 1000; i++) {
        double[] b = new double[1000000];
    }
    System.gc();
}

解决方式

 可以考虑通过加持引用,不让若引用销毁。方式有很多种,这里可以考虑用匿名内部类来延长变量的生命周期。

第一种:使用Target

public void preloadImage() {
    Log.d(TAG, "preloadImage: preloadImage.");
    class Ref {
        private Target t;
    }
    final Ref ref = new Ref();
    ref.t = new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            Log.i(TAG, "preloadImage.onBitmapLoaded: " + from + ref.hashCode());
        }

        @Override
        public void onBitmapFailed(Exception e, Drawable errorDrawable) {
            Log.e(TAG, "preloadImage.onBitmapFailed" + ref.hashCode());
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            Log.e(TAG, "target.finalize!");
        }
    };
    Picasso.get().load(URL).into(ref.t);
}

第二种:使用Callback

public void preloadImage() {
    Log.d(TAG, "preloadImage: start.");
    final ImageView imageView = new ImageView(this);
    Picasso.get().load(URL).into(imageView, new Callback() {
        @Override
        public void onSuccess() {
            Log.i(TAG, "preloadImage.onSuccess: " + imageView.hashCode());
        }

        @Override
        public void onError(Exception e) {
            Log.e(TAG, "preloadImage.onError" + imageView.hashCode());
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            Log.e(TAG, "callback.finalize!");
        }
    });
}

总结

  1. 使用第三方库的时候,传入匿名对象作为回调时需注意,防止库内因没有强引用持有导致对象被回收。
  2. 作为库的开发者,需斟酌仅使用 SoftReference | WeakReference 保存外部传入的回调是否符合预期。