LoginSignup
31
21

More than 5 years have passed since last update.

PicassoにTargetの無名クラスを渡してはいけない

Last updated at Posted at 2016-08-09

Picassoがplaceholderのままで画像を表示してくれない問題に見舞われました。

TL;DR

  • Picassoで画像が表示されないことがあるよ
  • into(Target) を使ってると起こるよ
  • Picassoはtargetを弱参照で保持してるからGCでtargetがなくなりやすいよ
  • targetを強参照で保持するか、そもそもinto(ImageView, Callback)使おうな

問題

Picassoで画像をロードしたらついでに何かしたいパターンってあるじゃないですか。

ImageView mImageView;
// (略)
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            mImageView.setImageBitmap(bitmap);

           // 画像がロードされたらついでにやる処理
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            mImageView.setImageDrawable(errorDrawable);
        }

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

この書き方だと、動いたり動かなかったりします。具体的にはplaceholderだけ表示されて、その後なにも起こらなくなります。

Picassoのログを見ても画像のロードは成功したことになってるし、もうどうしたものやらという感じでした。

弱参照でした

↑の記事を見て知ったのですが、into(Target)に渡したtargetは弱参照扱いになるとのこと。タイミング次第でtargetがGCされて、ロードした画像を流し込めなくなってたようでした。(ログに出ないのつらい)

よく見たら、弱参照の件はちゃんとinto(Target)のjavadocに書いてあるんですよね。

Note: This method keeps a weak reference to the {@link Target} instance and will be garbage collected if you do not keep a strong reference to it. To receive callbacks when an image is loaded use {@link #into(android.widget.ImageView, Callback)}.
https://github.com/square/picasso/blob/5208d48afbdd4cb2b51dbfe11946a5a9e6c518e2/picasso/src/main/java/com/squareup/picasso/RequestCreator.java#L524-L526

既に強参照になっているオブジェクトを渡すか、into(ImageView, Callback)を使いましょう。

タイトルには無名クラスと限定して書いてしまいましたが、生存期間の短いローカル変数などに代入していれば、実装クラスから生み出したオブジェクトでも同様の問題が起こります。

MyTarget.java
class MyTarget implements Target { ... }
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(new MyTarget()); // GCで死ぬ
// or
Target target = new MyTarget();
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(target); // これもGCで死ぬ

正しい使い方

1. ついでに何かしたいだけの場合

ロード後についでに何かしたいだけなら、into(ImageView, Callback)を使うのが一番順当な方法です。

ImageView mImageView;
// (略)
Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into(mImageView, new Callback(){
        @Override
        public void onSuccess() {
            // 画像がロードされたらついでにやる処理
        }

        @Override
        public void onError() {
            // エラー時の処理
        }
    });

2. 独自の処理を挟みたい場合(力技)

どうしても無名クラスをintoに入れたい場合は、StackOverflowで紹介されていた、無理やり強参照にする解決方法が使えます。

ImageView mImageView;
// (略)
Target target = new Target() {
        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
            processBitmap(bitmap) // 非同期で独自の画像処理を行う
            .subscribe(newBitmap -> {
                mImageView.setImageBitmap(newBitmap);
            });
        }

        @Override
        public void onBitmapFailed(Drawable errorDrawable) {
            mImageView.setImageDrawable(errorDrawable);
        }

        @Override
        public void onPrepareLoad(Drawable placeHolderDrawable) {
            mImageView.setImageDrawable(placeHolderDrawable);
        }
    };
mImageView.setTag(target);

Picasso.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.image_loading)
    .error(R.drawable.image_loading_failed)
    .into((Target) mImageView.getTag()); // これなら強参照で渡せる

これだといかにも力技で見栄えが悪いので、設計が許せば次のやり方のほうがいいと思います。

3. カスタムビューやViewHolderに実装する

公式javadocオススメの使い方です。これが本来のinto(Target)の使い方のようですね。

Implementing on a View

public class ProfileView extends FrameLayout implements Target {
  @Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
    setBackgroundDrawable(new BitmapDrawable(bitmap));
  }

  @Override public void onBitmapFailed() {
    setBackgroundResource(R.drawable.profile_error);
  }

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

Implementing on a view holder object for use inside of an adapter

public class ViewHolder implements Target {
  public FrameLayout frame;
  public TextView name;

  @Override public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
    frame.setBackgroundDrawable(new BitmapDrawable(bitmap));
  }

  @Override public void onBitmapFailed() {
    frame.setBackgroundResource(R.drawable.profile_error);
  }

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

まとめ

というわけで、非常に親切なjavadocをガン無視したコードを書いた結果、画像が表示されないというバグになっていました。

PicassoをBlameしてみたところ、2013年の半ばにはinto(ImageView, Callback)や親切なjavadocが整備されていたようなので、それ以前に書いたまま放置しているコードがある人は、同様のバグを作りこんでいないか調べてみるといいかもしれません。

みんな、ちゃんとjavadocは読もうな。

宣伝

ウォーターセル株式会社では、ライブラリのソースコードを読んだりPullReq出したりしながらみんなの生産性を上げていけるモバイルエンジニアを募集しています。地球人口100億人時代の食糧問題を解決する礎になるお仕事に興味がある方はご連絡ください。

31
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
21