didi/DoKit · error · IllegalStateException

Transformation <transformation.key()> mutated input Bitmap b

Error message

Transformation <transformation.key()> mutated input Bitmap but failed to recycle the original.

What it means

The complementary guard to error 86: when a Transformation returns a DIFFERENT bitmap (newResult != result), Picasso requires the original input to have been recycled. If result.isRecycled() is still false, it posts IllegalStateException('... mutated input Bitmap but failed to recycle the original.') and fails the request. This leaks otherwise — Picasso's memory model expects transformations to hand ownership of exactly one bitmap back.

Source

Thrown at Android/dokit/src/main/java/com/didichuxing/doraemonkit/picasso/BitmapHunter.java:473

        return null;
      }

      if (newResult == result && result.isRecycled()) {
        DokitPicasso.HANDLER.post(new Runnable() {
          @Override public void run() {
            throw new IllegalStateException("Transformation "
                + transformation.key()
                + " returned input Bitmap but recycled it.");
          }
        });
        return null;
      }

      // If the transformation returned a new bitmap ensure they recycled the original.
      if (newResult != result && !result.isRecycled()) {
        DokitPicasso.HANDLER.post(new Runnable() {
          @Override public void run() {
            throw new IllegalStateException("Transformation "
                + transformation.key()
                + " mutated input Bitmap but failed to recycle the original.");
          }
        });
        return null;
      }

      result = newResult;
    }
    return result;
  }

  static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
    int inWidth = result.getWidth();
    int inHeight = result.getHeight();
    boolean onlyScaleDown = data.onlyScaleDown;

    int drawX = 0;

View on GitHub (pinned to 626827cddb)

Solutions

  1. Add src.recycle() whenever you return a bitmap other than src
  2. Use the canonical pattern: Bitmap out = ...; if (out != src) src.recycle(); return out;
  3. If you must keep the input alive for caching, return src (unmodified) instead and copy inside your cache logic

Example fix

// before
@Override public Bitmap transform(Bitmap src) {
  return Bitmap.createScaledBitmap(src, 200, 200, true); // src never recycled
}

// after
@Override public Bitmap transform(Bitmap src) {
  Bitmap out = Bitmap.createScaledBitmap(src, 200, 200, true);
  if (out != src) src.recycle();
  return out;
}
Defensive patterns

Strategy: validation

Validate before calling

// Use one helper for the ownership handoff so the pattern is never forgotten:
static Bitmap handoff(Bitmap src, Bitmap out) {
  if (out != src && !src.isRecycled()) src.recycle();
  return out;
}
// in transform(): return handoff(src, Bitmap.createScaledBitmap(src, w, h, true));

Try / catch

// Main-thread throw from Picasso; prevention lives inside the Transformation:
@Override public Bitmap transform(Bitmap src) {
  Bitmap out = Bitmap.createBitmap(src, 0, 0, w, h);
  if (out != src) src.recycle(); // required
  return out;
}

Prevention

When it happens

Trigger: A transform that does Bitmap out = Bitmap.createBitmap(...); return out; without calling src.recycle(); returning a cached/derived bitmap while leaving the input alive.

Common situations: Transformations written for Glide or plain BitmapFactory code where the caller manages recycling; forgetting the recycle after refactoring a transform to return a new instance.

Related errors


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