Bitmapのビットマップサンプリングとメモリ計算の詳細


原文は微信の公衆番号に先発します:jzman-blog、交流に関心を持つことを歓迎します!
Androidの開発でよく考えられる問題の一つは、OOM(Out Of Memory)であり、メモリオーバーフローであり、ピクチャを大量にロードするとOOMが発生する可能性があり、サンプリングによってピクチャを圧縮することでOOMを回避できる一方、1024 x 768ピクセルの画像が128 x 96のImageViewに略記表示されるなど、この方法は明らかに価値がなく、サンプリングによって適切な縮小バージョンをメモリにロードすることができる.メモリ消費量を削減するには、Bitmapの最適化には主に次の2つの側面があります.
  • 有効な処理の大きいビットマップ
  • キャッシュビットマップ
  • この文章は主に大きなビットマップをどのように効果的に処理するかに重点を置いている.
    また、Androidでビットマップサンプリングでメモリに縮小バージョンをロードするには考慮すべき要素は?
  • 完全画像のロードに必要なメモリ
  • を推定する.
  • この画像をロードするために必要な空間は、プログラムの他のメモリ要件
  • をもたらす.
  • ピクチャのロード先ImageViewまたはUIコンポーネントのサイズ
  • 現在のデバイスのスクリーンサイズまたは密度
  • ビットマップサンプリング
    画像には形状やサイズが異なり、大きな画像を読み取るのにメモリがかかります.ビットマップのサイズと種類を読み取り、複数のリソースからビットマップを作成するためにBitmapFactoryクラスは、画像データリソースに基づいて最適な復号方法を選択し、メモリの割り当てを要求してビットマップを構築しようとするため、OOM異常を招きやすい復号方法を多く提供します.それぞれのタイプの復号方法にはBitMapFactoryを通過する追加の特徴があります.Optionsクラスは復号オプションを指定します.復号時にinJustDecodeBoundsをtrueに設定すると、メモリを割り当てない前に画像のサイズとタイプを読み取ることができ、次のコードは簡単なビットマップサンプリングを実現します.
    /**
      *     
      * @param res
      * @param resId
      * @return
      */
    public Bitmap decodeSampleFromResource(Resources res, int resId){
        //BitmapFactory      
        BitmapFactory.Options options = new BitmapFactory.Options();
        //      
        options.inSampleSize = 200;
        Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options);
        return bitmap;
    }

    注意:他のdecode...方法はdecodeResourceと同様であり,ここではdecodeRedourceを例に挙げる.
    実際に使用する場合、ビットマップのサンプリングは、例えば、解像度2048 x 1536の画像をinSampleSize値4を用いて符号化して512 x 384の画像を生成するなど、特定のアスペクト要件に応じて適切なinSampleSizeを計算しなければならない.ここで、ビットマップがARGB_に構成されていると仮定する8888、メモリにロードされるのは、従来の12 Mではなく0.75 Mであり、画像に占めるメモリの計算については、以下に説明するが、必要な幅に基づいてサンプリングスケールを計算する方法は以下の通りである.
    /**
     * 1.        
     *
     * @param option
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) {
        //        
        int width = option.outWidth;
        int height = option.outHeight;
    
        int inSampleSize = 1;
        if (width > reqWidth || height > reqHeight) {
            if (width > height) {
                inSampleSize = Math.round((float) height / (float) reqHeight);
            } else {
                inSampleSize = Math.round((float) width / (float) reqWidth);
            }
        }
        return inSampleSize;
    }
    
    /**
     * 2.        
     * @param options
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    
        //        
        int height = options.outHeight;
        int width = options.outWidth;
    
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            //                
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            /**
             *              inSampleSize  ,              
             *               。
             */
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }
    

    サンプリングスケールを取得すると、必要な幅に応じて大きな画像を処理できます.以下は、必要な幅に基づいて計算されたinSampleSizeによって大きなビットマップをサンプリングします.
    /**
     *     
     * @param resources
     * @param resId
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) {
        //             
        BitmapFactory.Options options = new BitmapFactory.Options();
        //      true,       width、height、mimeType
        options.inJustDecodeBounds = true;
        //  
        BitmapFactory.decodeResource(resources, resId, options);
        //      
        int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
        //      false,      
        options.inJustDecodeBounds = false;
        //  
        Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options);
        return bitmap;
    }

    復号化にはBitmapFactoryが使用する.decodeResource()メソッドは、次のとおりです.
    /**
     *     id     
     */
    public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
        ...
        /**
         *      id         ,   TypeValue           density   
         *      drawable-xxhdpi,  density 480dpi
         */
        is = res.openRawResource(id, value);
        //         Bitmap  ,    opts       
        bm = decodeResourceStream(res, value, is, null, opts);
        ...
    }

    実際に復号化する方法は、decodeResourceStream()メソッドであることは明らかです.具体的には、次のとおりです.
    /**
     *           Bitmap,   Bitmap       
     */
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,
                         InputStream is, Rect pad, BitmapFactory.Options opts) {
    
        if (opts == null) {
            //       Option  
            opts = new BitmapFactory.Options();
        }
    
        /**
         *      inDensity  ,      inDensity   
         *             density  inDensity
         */
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
    
        /**
         *   ,     BitmapFactory.Option    inTargetDensity
         * inTargetDensity   densityDpi,      density
         *   DisplayMetrics  .densityDpi  
         */
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        //decodeStream()      native  
        return decodeStream(is, pad, opts);
    }

    InDensityとinTargetDensityの設定が完了すると、完全に復号されたBitmapオブジェクトを返すdecodeStream()メソッドが呼び出されます.具体的には、次のようになります.
    /**
     *       Bitmap,
     */
    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
        ...
        bm = nativeDecodeAsset(asset, outPadding, opts);
        //   native  :nativeDecodeStream(is, tempStorage, outPadding, opts);
        bm = decodeStreamInternal(is, outPadding, opts);
        Set the newly decoded bitmap's density based on the Options
        //  Options       Bitmap
        setDensityFromOptions(bm, opts);
        ...
        return bm;
    }

    明らかに、decodeStream()メソッドは主にローカルメソッドを呼び出してBitmapの復号を完了し、ソースコードを追跡してnativeDecodeAsset()メソッドとnativeDecodeStream()メソッドがdodecode()メソッドを呼び出していることを発見し、doDecodeメソッドのキーコードは以下の通りである.
    /**
     * BitmapFactory.cpp   
     */
    static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) {
        ...
        if (env -> GetBooleanField(options, gOptions_scaledFieldID)) {
            const int density = env -> GetIntField(options, gOptions_densityFieldID);
            const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID);
            const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
                //      
                scale = (float) targetDensity / density;
            }
        }
        ...
        //  Bitmap
        SkBitmap decodingBitmap;
        ...
    
        //       
        int scaledWidth = decodingBitmap.width();
        int scaledHeight = decodingBitmap.height();
    
        //  density targetDensity      
        if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
            scaledWidth = int(scaledWidth * scale + 0.5f);
            scaledHeight = int(scaledHeight * scale + 0.5f);
        }
        ...
        //x、y        ,   scale  
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());
        ...
        // canvas  scale,    Bitmap
        SkCanvas canvas (outputBitmap);
        canvas.scale(sx, sy);
        canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint);
    }
    

    上記のコードでは、スケーリングスケールの計算と、Bitmap幅の高さに対するdensityとtargetdensityの影響が、実際にはBitmapのメモリサイズに間接的に影響することがわかります.この問題は、drawable-xhdpiディレクトリに存在する画像がある場合、対応するBitmapのdensityが480 dpiであることに注意して、以下で例を挙げます.targetDensityはDisPlayMetricのdensityDpi、つまり携帯電話の画面代表のdensityです.では、Androidのローカルnativeメソッドの実装をどのように表示するか、BitmapFactory.cpp、nativeメソッドのメソッド名を直接検索すればいいので、試してみてください.
    Bitmapメモリ計算
    まず1枚の大きい図6000 x 4000に貢献して、ピクチャーは12 Mに近くて、【公衆番号の零点で小築索要することができます】このピクチャーを直接メモリにロードすると必ずOOMが発生します.もちろん、適切なビットマップサンプリングでピクチャーを縮小することでOOMを避けることができますが、Bitmapが占めるメモリはどのように計算されますか.一般的にはこのように計算されます.
    Bitmap Memory = widthPix * heightPix * 4

    bitmapを使用できます.getConfig()Bitmapのフォーマットを取得します.ここではARGB_です.8888、このBitmap形式の次のピクセルポイントは4バイトを占めているので、x 4にするには、Androidのリソースフォルダに画像を置くと、次のように計算されます.
    scale = targetDensity / density
    widthPix = originalWidth * scale
    heightPix = orignalHeight * scale
    Bitmap Memory = widthPix * scale * heightPix * scale * 4

    Bitmapが占めるメモリの計算方法について簡単にまとめましたが、検証時には、Bitmapが占めるメモリサイズを次の方法で取得できます.
    BitmapMemory = bitmap.getByteCount()

    選択したこの画像を直接ロードするとOOMになるので、以下の例ではサンプリング圧縮を行い、Bitmapが占めるメモリの計算を行います.
    ダイレクトサンプリング
    この方法は、サンプリングスケールinSampleSizeの値を直接指定し、サンプリングしてからサンプリング後のメモリを計算します.ここでinSampleSizeを200と指定します.
  • この画像をdrawable-xxhdpiディレクトリに配置します.drawable-xxhdpiが表すdensityは480(density)、私の携帯電話の画面が表すdensityは480(targetdensity)です.明らかにscaleは1で、もちろんまず画像をサンプリングしてから、画像をメモリにロードします.このときBitmapが占めるメモリは:
  • です.
    inSampleSize = 200
    scale = targetDensity / density} = 480 / 480 = 1
    widthPix = orignalScale * scale = 6000 / 200 * 1 = 30 
    heightPix = orignalHeight * scale = 4000 / 200 * 1 = 20
    Bitmap Memory =  widthPix * heightPix * 4 = 30 * 20 * 4 = 2400(Byte)
  • 画像をdrawable-xhdpiディレクトリに配置します.このときdrawable-xhdpiが表すdensityは320で、私の携帯電話の画面が表すdensityは480(targetdensity)で、画像をメモリにロードします.このときBitmapが表すメモリは
  • です.
    inSampleSize = 200
    scale = targetDensity / density = 480 / 320
    widthPix = orignalWidth * scale = 6000 / 200 * scale = 45
    heightPix = orignalHeight * scale = 4000 / 200 * 480 / 320 = 30
    Bitmap Memory =  widthPix * scale * heightPix * scale * 4 = 45 * 30 * 4 = 5400(Byte) 

    計算サンプル
    この方式は、inSampleSizeを任意に指定するのではなく、要求のアスペクトに基づいて適切なinSampleSizeを計算する方式であり、実際の開発では、要求のアスペクトは100 x 100であり、具体的なinSampleSize計算は上述の通りである.
  • 画像をdrawable-xxhdpiディレクトリに配置します.drawable-xxhdpiが表すdensityは480、私の携帯電話の画面が表すdensityは480(targetDensity)、画像をメモリにロードします.Bitmapが表すメモリは
  • です.
    inSampleSize = 4000 / 100 = 40
    scale = targetDensity / density = 480 / 480 = 1
    widthPix = orignalWidth * scale = 6000 / 40 * 1 = 150      
    heightPix = orignalHeight * scale = 4000 / 40 * 1 = 100
    BitmapMemory = widthPix * scale * heightPix * scale * 4 = 60000(Byte)
  • 画像をdrawable-xhdpiディレクトリに配置します.このときdrawable-xhdpiが表すdensityは320で、私の携帯電話の画面が表すdensityは480(targetdensity)で、画像をメモリにロードします.このときBitmapが表すメモリは
  • です.
    inSampleSize = 4000 / 100 = 40
    scale = targetDensity / density = 480 / 320
    widthPix = orignalWidth * scale = 6000 / 40 * scale = 225
    heightPix = orignalHeight * scale = 4000 / 40 * scale = 150
    BitmapMemory = widthPix * heightPix * 4 = 225 * 150 * 4 = 135000(Byte)

    ビットマップサンプリングおよびBitmapが異なる場合に占めるメモリの計算の大まかな手順は、上述したようになります.
    テスト効果
    テスト効果図は以下の通りです.
    drawable-xhdpi
    drawable-xxhdpi
    興味があれば、公衆番号:jzman-blogに注目して、一緒に交流して勉強することができます.