大容量のビットマップを効率的に読み込む

注: 画像の読み込みに関するおすすめの方法に基づくライブラリはいくつかあります。アプリでこれらのライブラリを使用すると、最適な方法で画像を読み込むことができます。おすすめのライブラリである Glide を使用すると、画像の読み込みと表示をできる限り迅速かつスムーズに行うことができます。画像読み込み用のライブラリとして、Square の Picasso、Instacart のCoil、Facebook の Fresco なども人気があります。これらのライブラリを使用すると、Android でビットマップなどの画像に関連付けられている複雑なタスクのほとんどを簡素化できます。

画像の形状やサイズはそれぞれ異なります。多くの場合、画像のサイズは一般的なアプリのユーザー インターフェース(UI)に必要なサイズより大きくなります。たとえば、システムのギャラリー アプリでは、Android デバイスのカメラを使って撮影した写真が表示されますが、通常、それらの写真の解像度はデバイスの画面密度よりはるかに高くなります。

メモリに限りがある場合、理想的には、解像度の低い画像のみをメモリに読み込みたいところです。解像度の低い画像は、それを表示する UI コンポーネントのサイズと一致する必要があります。解像度が高い画像は、表示上のメリットがないだけでなく、貴重なメモリも消費します。また、瞬時に追加のサイズ調整を行うため、パフォーマンスのオーバーヘッドがさらに発生します。

このレッスンでは、サブサンプリングされた縮小画像をメモリに読み込むことで、アプリごとのメモリの制限を超えずに大容量のビットマップをデコードする方法について説明します。

ビットマップのディメンションとタイプを読み取る

BitmapFactory クラスには、さまざまなソースから Bitmap を作成するための複数のデコード メソッド(decodeByteArray()decodeFile()decodeResource() など)が用意されています。画像のデータソースに基づいて、最も適切なデコード メソッドを選択してください。これらのメソッドは作成済みのビットマップに対してメモリを割り当てようとするため、OutOfMemory 例外が頻繁に発生します。各タイプのデコード メソッドには、BitmapFactory.Options クラスを介してデコード オプションを指定できる追加のシグネチャがあります。デコード中に inJustDecodeBounds プロパティを true に設定すると、メモリの割り当てを回避できます。その結果、ビットマップ オブジェクト用に null が返されますが、outWidthoutHeightoutMimeType が設定されます。この手法により、ビットマップの作成(およびメモリの割り当て)が行われる前に画像データのディメンションとタイプを読み取ることができます。

Kotlin

    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }
    BitmapFactory.decodeResource(resources, R.id.myimage, options)
    val imageHeight: Int = options.outHeight
    val imageWidth: Int = options.outWidth
    val imageType: String = options.outMimeType
    

Java

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
    int imageHeight = options.outHeight;
    int imageWidth = options.outWidth;
    String imageType = options.outMimeType;
    

java.lang.OutOfMemory 例外が発生しないようにするには、使用可能なメモリ内に容易に収まる予測可能なサイズの画像データの提供元を絶対的に信頼できる場合を除き、デコードする前にビットマップのディメンションを確認します。

縮小画像をメモリに読み込む

画像のディメンションがわかったので、そのディメンションを使用して、画像全体をメモリに読み込むか、サブサンプリングされた画像を読み込むかを決定します。考慮すべき要素は次のとおりです。

  • 画像全体をメモリに読み込む場合の推定メモリ使用量。
  • 他にアプリのメモリ要件がある場合に、画像の読み込みに使用するメモリの量。
  • 画像の読み込み先の ImageView または UI コンポーネントのディメンション。
  • 現在のデバイスの画面のサイズと密度。

たとえば、1024x768 ピクセルの画像を最終的に ImageView 内の 128x96 ピクセルのサムネイルに表示する場合、その画像をメモリに読み込む価値はありません。

画像をサブサンプリングするようデコーダに指示するには、縮小画像をメモリに読み込んで、BitmapFactory.Options オブジェクトで inSampleSizetrue に設定します。たとえば、inSampleSize を 4 に設定し、解像度が 2048x1536 の画像をデコードすると、約 512x384 のビットマップが生成されます。このビットマップをメモリに読み込む場合、画像全体で 12 MB ではなく 0.75 MB を使用します(ビットマップの設定を ARGB_8888 と仮定)。以下に、サンプルサイズの値(ターゲットの幅と高さに基づく 2 のべき乗の値)を計算するメソッドを示します。

Kotlin

    fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
        // Raw height and width of image
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var inSampleSize = 1

        if (height > reqHeight || width > reqWidth) {

            val halfHeight: Int = height / 2
            val halfWidth: Int = width / 2

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }

        return inSampleSize
    }
    

Java

    public static int calculateInSampleSize(
                BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;

        if (height > reqHeight || width > reqWidth) {

            final int halfHeight = height / 2;
            final int halfWidth = width / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }
    

注: 2 のべき乗の値を算出する理由は、inSampleSize のドキュメントに記載されているように、最も近い 2 のべき乗の値に切り捨てることによって決定された値がデコーダで使用されるためです。

このメソッドを使用するには、まず true に設定された inJustDecodeBounds を使用してデコードし、オプションを渡します。次に、新しい inSampleSize の値と false に設定された inJustDecodeBounds を使用してもう一度デコードします。

Kotlin

    fun decodeSampledBitmapFromResource(
            res: Resources,
            resId: Int,
            reqWidth: Int,
            reqHeight: Int
    ): Bitmap {
        // First decode with inJustDecodeBounds=true to check dimensions
        return BitmapFactory.Options().run {
            inJustDecodeBounds = true
            BitmapFactory.decodeResource(res, resId, this)

            // Calculate inSampleSize
            inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

            // Decode bitmap with inSampleSize set
            inJustDecodeBounds = false

            BitmapFactory.decodeResource(res, resId, this)
        }
    }
    

Java

    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
            int reqWidth, int reqHeight) {

        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
    

このメソッドを使用すると、次のサンプルコードに示すように、任意の大きなサイズのビットマップを、100x100 ピクセルのサムネイルを表示する ImageView に簡単に読み込むことができます。

Kotlin

    imageView.setImageBitmap(
            decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
    )
    

Java

    imageView.setImageBitmap(
        decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
    

必要に応じて適切な BitmapFactory.decode* メソッドを代用することで、同様のプロセスに従って他のソースからビットマップをデコードすることができます。