didi/DoKit · error · IllegalStateException

Target callback must not recycle bitmap!

Error message

Target callback must not recycle bitmap!

What it means

Thrown by TargetAction.complete(Bitmap, LoadedFrom) after it invokes target.onBitmapLoaded(result, from) and then finds result.isRecycled() is true. Picasso retains ownership of delivered bitmaps — they may still live in the memory cache and be drawn later — so a Target that recycles the bitmap inside onBitmapLoaded corrupts shared state. The library detects this immediately and throws IllegalStateException.

Source

Thrown at Android/dokit/src/main/java/com/didichuxing/doraemonkit/picasso/TargetAction.java:38

final class TargetAction extends Action<Target> {

  TargetAction(DokitPicasso picasso, Target target, Request data, int memoryPolicy, int networkPolicy,
               Drawable errorDrawable, String key, Object tag, int errorResId) {
    super(picasso, target, data, memoryPolicy, networkPolicy, errorResId, errorDrawable, key, tag,
        false);
  }

  @Override void complete(Bitmap result, DokitPicasso.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() {
    Target target = getTarget();
    if (target != null) {
      if (errorResId != 0) {
        target.onBitmapFailed(picasso.context.getResources().getDrawable(errorResId));
      } else {
        target.onBitmapFailed(errorDrawable);
      }
    }
  }
}

View on GitHub (pinned to 626827cddb)

Solutions

  1. Remove every bitmap.recycle() call inside onBitmapLoaded; Picasso manages the bitmap lifecycle, including cache eviction.
  2. If you mutate the bitmap, do it via Picasso's transform(RequestHandler.Transform) API — Picasso recycles the source of a transformation itself.
  3. If you truly need exclusive ownership, load with .memoryPolicy(MemoryPolicy.NO_CACHE) and understand the consequences instead of recycling a cached bitmap.

Example fix

// before
@Override public void onBitmapLoaded(Bitmap b, Picasso.LoadedFrom from) {
    imageView.setImageBitmap(b);
    b.recycle(); // crashes: bitmap may be cached/shared
}

// after
@Override public void onBitmapLoaded(Bitmap b, Picasso.LoadedFrom from) {
    imageView.setImageBitmap(b); // Picasso owns lifecycle; do not recycle
}
Defensive patterns

Strategy: validation

Validate before calling

// Contract: never recycle inside onBitmapLoaded
@Override public void onBitmapLoaded(Bitmap b, Picasso.LoadedFrom from) {
    imageView.setImageBitmap(b);
    // no b.recycle() anywhere in this callback
}

Try / catch

Catching this IllegalStateException is wrong — recycling already corrupted shared cache state. Prevent it by auditing every recycle() call in Target implementations.

Prevention

When it happens

Trigger: Implementing Target.onBitmapLoaded to call bitmap.recycle() (directly or via a helper like a custom drawable's recycle, or transforming the bitmap and recycling the original); the check runs right after the callback returns.

Common situations: Legacy code from the pre-Picasso era where manual recycling was routine; calling bitmap.recycle() after setting it on a custom view 'to free memory'; recycling in onBitmapLoaded before pushing the bitmap through another pipeline.

Related errors


AI-assisted analysis of didi/DoKit@626827cddb (2026-08-14). Data as JSON: /api/errors/69c50b2e3904eacb. Report an issue: GitHub.