前回は、画像の縮小や回転を行ってみた。今回は、画像を安全に編集したりコピーしてみる。画像の編集は Canvas クラスを、コピーは Bitmap.copy(Bitmap.Config, boolean)を使えばできるのだが、いろいろとはまったのでメモを残しておく。
Immutable bitmap passed to Canvas constructor
画像を編集する場合、ファイルを読み込んで Bitmap を生成、Canvas のコンストラクタに Bitmap を渡して Canvas を操作するだろう。コードにすると、以下のようになる。
File file = new File(<読み込みたい画像ファイル>); InputStream in = context.getContentResolver().openInputStream(Uri.fromFile(file)); Bitmap bitmap = BitmapFactory.decodeStream(in); in.close(); Canvas canvas = new Canvas(bitmap);
残念ながら、このままだと IllegalStateException になる。
java.lang.IllegalStateException: Immutable bitmap passed to Canvas constructor 09-10 12:05:19.099: E/AndroidRuntime(1640): at android.graphics.Canvas.(Canvas.java:127)
immutable(不変)な Bitmap を Canvas クラスのコンストラクターの引数に渡したということなのだが、最初はさっぱり意味が分からなかった。調べてみると、BitmapFactory で読み込んで生成した Bitmap は immutable とのこと。java.lang.String と同じということだろうか。コピーすれば mutable な、つまり変更可能で Canvas の引数として渡せる Bitmap になるようだが、メモリーを圧迫してしまう。Android 3.0以降では、オプションを渡せば mutable な Bitmap を戻してくれるようだ。
解決策を探す前に、BitmapFactory が immutable な Bitmap を戻す理由を考えてみた。以前も参考にした、Managing Bitmap Memoryには、2.x系は Bitmap のピクセルデータを native heap に、3.0以降は Dalvik heap に格納すると書いてある。Android 3.0以降で mutable な Bitmap を戻してくれるオプションがあることと、関係しているのだろうか。
2.x系で immutable な Bitmap を戻すのは、普通に考えるとスレッドセーフにするためだろうか。不変オブジェクトであれば複数のスレッドから、同時にアクセスすることができるので、同じデータを利用するときは参照を戻すことができる。ソースコードを追いかけてみたが、C++ のコードと Java のコードを読まないのといけない。コードを行き来するのがあまりに面倒で挫折した。
Bitmap をコピーする
Bitmap を画像として編集するには、Canvas に mutable な Bitmap を渡せばよいので、Bitmap を安全にコピーできれば問題は解決する。実はAndroid: convert Immutable Bitmap into Mutableという記事に解答がある。簡単にまとめると、以下のようになる。
- Bitmap.copy(Bitmap.Config, boolean)の2番目の引数に、true を渡して Bitmap をコピーする。メモリーは圧迫するが、どんな環境でも動く
- Bitmap.decodeFile(String, BitmapFactory.Options)の2番目の引数の inMutable を true にして読み込む。Android 3.0以降のみ可能
- Bitmap をファイルに書き出して読み込み直す。どんな環境でも動くが処理速度が遅い
・・・帯に短したすきに長しだ。最近、Android 2.x 系に対応しなければ、もっと楽に作れるんじゃないだろうかと考えている。なお、3番目の処理は、少しコードを書かなければならない。Android: convert Immutable Bitmap into Mutableのコードは同じファイルに書き出していたので、一時ファイルに書き出すように変更してみたのが以下のコードだ。
public static Bitmap copy(Context context, Bitmap bitmap) throws IOException { File file = File.createTempFile("_ub", ".tmp", context.getCacheDir()); RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); int width = bitmap.getWidth(); int height = bitmap.getHeight(); FileChannel channel = randomAccessFile.getChannel(); MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, width * height * 4); bitmap.copyPixelsToBuffer(map); bitmap.recycle(); Bitmap mutable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); map.position(0); mutable.copyPixelsFromBuffer(map); channel.close(); randomAccessFile.close(); file.delete(); return mutable; }