(4.6.31)Android Bitmap 详解

一、从相册加载一张图片

我们先从简单的入手,看看从手机相册加载一张图片到ImageView的正确方式。

在这里插入图片描述
【图】

我们就以上图为列,这张图片在我手机里的信息如下:

在这里插入图片描述

可以看到,图片大小不足1M。那么把他加载到手机内存中时又会发生什么呢?
在这里插入图片描述
在这里插入图片描述
【图】

1.1 打开相册加载图片

/**
 * 打开手机相册
 */
private void selectFromGalley() {
	Intent intent = new Intent();
	intent.setType("image/*");
	intent.setAction(Intent.ACTION_GET_CONTENT);
	intent.addCategory(Intent.CATEGORY_OPENABLE);
	startActivityForResult(intent, REQUEST_CODE_PICK_FROM_GALLEY);
}

在Android 中打开相册是一件非常方便的事情,选择好图片之后就可以在onActivityResult中接收这张图片

if (resultCode == Activity.RESULT_OK) {
	Uri uri = data.getData();
	if (uri != null) {
		ProcessResult(uri);
	}
}

1.2 根据Uri得到Bitmap

  • onActivityResult 方法中返回的Intent返回的图片地址是一个Uri类型,包含具体协议

    • 为了方便使用BitmapFactory的decode方法,需要将这个个Uri类型的地址转换为普通的地址,stripFileProtocol具体实现可参考源码
  • showBitmapInfos 这个方法就是很简单,就是获取一下所要加载图片的信息

    • inJustDecodeBounds 这个参数,当此参数为true时,BitmapFactory 只会解析图片的原始宽/高信息,并不会去真正的加载图片

关于getByteCount和getAllocationByteCount的区别,这里暂时不讨论,只要知道他们都可以获取Bitmap占用内存大小

@TargetApi(Build.VERSION_CODES.KITKAT)
    private void ProcessResult(Uri destUrl) {
        String pathName = FileHelper.stripFileProtocol(destUrl.toString());
        showBitmapInfos(pathName);
        Bitmap bitmap = BitmapFactory.decodeFile(pathName);//【!!!】
        if (bitmap != null) {
            mImageView.setImageBitmap(bitmap);
            float count = bitmap.getByteCount() / M_RATE;
            float all = bitmap.getAllocationByteCount() / M_RATE;
            String result = "这张图片占用内存大小:\n" +
                    "bitmap.getByteCount()== " + count + "M\n" +
                    "bitmap.getAllocationByteCount()= " + all + "M";
            info.setText(result);
            Log.e(TAG, result);
            bitmap = null;
        } else {
            T.showLToast(mContext, "fail");
        }
    }

/**
 * 获取Bitmap的信息
 * @param pathName
 */
private void showBitmapInfos(String pathName) {
	BitmapFactory.Options options = new BitmapFactory.Options();
	options.inJustDecodeBounds = true;//【!!!】
	BitmapFactory.decodeFile(pathName, options);//【!!!】
	int width = options.outWidth;
	int height = options.outHeight;

	Log.e(TAG, "showBitmapInfos: \n" +
			"width=: " + width + "\n" +
			"height=: " + height);
	options.inJustDecodeBounds = false;
}

我们看一下输出日志及内存变化:

  1. 由于这张图片是放在手机内部SD卡上,所以showBitmapInfos 解析后获取的图片宽高信息和之前是一致的,宽x高为 2160x1920
  2. 看到所占用的内存 15M,是不是有点意外,一张658KB 的加载后居然要占这么大的内存

再看一下monitor检测的内存变化,在20s后选择图片后,占用内存有了一个明显的上升。

二、Bitmap 内存计算方式

Bitmap 在内存当中占用的大小其实取决于:

  1. 色彩格式
    • 前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
  2. 密度转换
    • 这个计算仅针对于drawable文件夹的图片来说**,而对于一个file或者stream那么inDensity和inTargetDensity是不考虑的!他们默认就是0
    • 原始文件存放的资源目录(是 hdpi 还是 xxhdpi 可不能傻傻分不清楚哈)
    • 目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
  3. 固定的精度转换公式
  4. 采样转换

2.1 density 和 densityDpi

本文涉及到屏幕密度的讨论,这里先要搞清楚 DisplayMetrics 的两个变量,简单来说,可以理解为

  1. density 的数值是 1dp=density px;
  2. densityDpi 是屏幕每英寸对应多少个点(不是像素点)

在 DisplayMetrics 当中,这两个的关系是线性的:

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

为了不引起混淆,本文所有提到的密度除非特别说明,都指的是 densityDpi,当然如果你愿意,也可以用 density 来说明问题。

2.2 getByteCount处理格式字节

通过getByteCount这个方法,我们就可以获取到一张 Bitmap 在运行时到底占用多大内存了

举个例子:一张 522x686PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就可以用这个方法获取到。

public final int getByteCount() {
    // int result permits bitmaps up to 46,340 x 46,340
    return getRowBytes() * getHeight();
}

我们来看下这个函数的实现,getHeight 就是图片的高度(单位:px),getRowBytes 是什么?

public final int getrowBytes() {
   if (mRecycled) {
          Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
   }
   return nativeRowBytes(mFinalizer.mNativeBitmap);
}

进入了jni代码,与 nativeRowBytes 对应的函数如下:

Bitmap.cpp

static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
     SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
     return static_cast<jint>(bitmap->rowBytes());
}

Bitmap 本质上就是一个 SkBitmap,而这个 SkBitmap 也是大有来头

//SkBitmap.h
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }


//SkBitmap.cpp
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}

SkImageInfo.h

static int SkColorTypeBytesPerPixel(SkColorType ct) {
   static const uint8_t gSize[] = {
    0,  // Unknown
    1,  // Alpha_8
    2,  // RGB_565
    2,  // ARGB_4444
    4,  // RGBA_8888
    4,  // BGRA_8888
    1,  // kIndex_8
  };
  SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
                size_mismatch_with_SkColorType_enum);

   SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
   return gSize[ct];
}

static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
    return width * SkColorTypeBytesPerPixel(ct);
}

我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes

那么结论出来了,一张 ARGB_8888 的 Bitmap 占用内存的计算公式:

*bitmapInRam = bitmapWidth*bitmapHeight 4 bytes

说到这儿你以为故事就结束了么?有本事你拿去试,算出来的和你获取到的总是会差个倍数,为啥呢?

还记得我们最开始给出的那个例子么?

一张 522x686PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,然而公式计算出来的可是1432368B。。。

2.3 Density处理转换映射字节

知道我为什么在举例的时候那么费劲的说放到xxx目录下,还要说用xxx手机么?你以为 Bitmap 加载只跟宽高有关么?Naive

还是先看代码,我们读取的是 drawable 目录下面的图片,用的是 decodeResource 方法,该方法本质上就两步:

  1. 读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;
  2. 调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射
    1. 原始资源的 density 其实取决于资源存放的目录(比如 xxhdpi 对应的是480)
    2. 屏幕 density 的赋值是一个固态值

BitmapFactory.java

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
    InputStream is, Rect pad, Options opts) {

	//【1】实际上,我们这里的opts是null的,所以在这里初始化。
	if (opts == null) {
		opts = new Options();
	}

	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; //【2】这里density的值如果对应资源目录为hdpi的话,就是240
		}
	}

	if (opts.inTargetDensity == 0 && res != null) {
		//【3】请注意,inTargetDensity就是当前的显示密度,比如三星s6时就是640
		opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
	}

	return decodeStream(is, pad, opts);
}

public Options() {
   inDither = false;
   inScaled = true;
   inPremultiplied = true;
}

我们需要知道的就是:inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density

  1. ldpi -----> 120
  2. mdpi -----> 160
  3. hdpi -----> 240
  4. xhdpi -----> 320
  5. xxhdpi -----> 480
  6. xxxhdpi -----> 640

紧接着,用到了 nativeDecodeStream 方法,不重要的代码直接略过,直接给出最关键的 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);//对应hdpi的时候,是240
			const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的为640
			const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
			if (density != 0 && targetDensity != 0 && density != screenDensity) {
				scale = (float) targetDensity / density;
			}
		}
	}

	const bool willScale = scale != 1.0f;
	......
	SkBitmap decodingBitmap;
	if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
	   return nullObjectReturn("decoder->decode returned false");
	}
	//【注意】这里这个deodingBitmap就是解码出来的bitmap,大小是图片原始的大小
	int scaledWidth = decodingBitmap.width();
	int scaledHeight = decodingBitmap.height();
	//【注意】 此处处理映射转换和精度转换
	if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
		scaledWidth = int(scaledWidth * scale + 0.5f);
		scaledHeight = int(scaledHeight * scale + 0.5f);
	}
	if (willScale) {
		const float sx = scaledWidth / float(decodingBitmap.width());
		const float sy = scaledHeight / float(decodingBitmap.height());

		// TODO: avoid copying when scaled size equals decodingBitmap size
		SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
		// FIXME: If the alphaType is kUnpremul and the image has alpha, the
		// colors may not be correct, since Skia does not yet support drawing
		// to/from unpremultiplied bitmaps.
		outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
				colorType, decodingBitmap.alphaType()));
		if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
			return nullObjectReturn("allocation failed for scaled bitmap");
		}

		// If outputBitmap's pixels are newly allocated by Java, there is no need
		// to erase to 0, since the pixels were initialized to 0.
		if (outputAllocator != &javaAllocator) {
			outputBitmap->eraseColor(0);
		}

		SkPaint paint;
		paint.setFilterLevel(SkPaint::kLow_FilterLevel);

		SkCanvas canvas(*outputBitmap);
		canvas.scale(sx, sy);
		canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
	}
	......
}
  1. density 是指decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(比如 hdpi 是240,xxhdpi 是480)
  2. targetDensity 是指实际上是我们加载图片的目标 density,这个值的来源我们已经在前面给出了,就是 DisplayMetrics 的 densityDpi,如果是三星s6那么这个数值就是640

sx 和sy 实际上是约等于 scale 的,因为 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。

输出图片的宽高= 原图片的宽高 / inSampleSize * (inTargetDensity / inDensity)

注意:

  • inSampleSize代表采样率,只能是2的幂,如不是2的幂下转到最大的2的幂,而且inSampleSize>=1)
  • 这个计算仅针对于drawable文件夹的图片来说,而对于一个file或者stream那么inDensity和inTargetDensity是不考虑的!他们默认就是0

一张522*686的PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,其中 density 对应 xxhdpi 为480,targetDensity 对应三星s6的密度为640:
522/480 * 640 * 686/480 *640 * 4 = 2546432B

2.4 精度调整

越来越有趣了是不是,你肯定会发现我们这么细致的计算还是跟获取到的数值 还是不一样

为什么呢?由于结果已经非常接近,我们很自然地想到精度问题。来,再把上面这段代码中的一句拿出来看看:

outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));
			
//我们看到最终输出的 outputBitmap 的大小是scaledWidth*scaledHeight,我们把这两个变量计算的片段拿出来给大家一看就明白了:
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}		

在我们的例子中,

  • scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
  • scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915

下面就是见证奇迹的时刻:

915 * 696 * 4 = 2547360

三、想办法减少 Bitmap 内存占用

我们知道在读入bitmap时,占用这么大的内存,显然是不好的,如何解决这种问题呢?

3.1 Jpg 和 Png

说到这里,肯定会有人会说,我们用 jpg 吧,jpg 格式的图片不应该比 png 小么?

确实是个好问题,因为同样一张图片,jpg 确实比 png 会多少小一些(甚至很多),原因很简单,jpg 是一种有损压缩的图片存储格式,而 png 则是 无损压缩的图片存储格式,显而易见,jpg 会比 png 小,代价也是显而易见的。

可是,这说的是文件存储范畴的事情,它们只存在于文件系统,而非内存或者显存

说得简单一点儿,我有一个极品飞车的免安装硬盘版的压缩包放在我的磁盘里面,这个游戏是不能玩的,我需要先解压,才能玩------jpg 也好,png 也好就是个压缩包的概念,而我们讨论的内存占用则是从使用角度来讨论的。

所以,jpg 格式的图片与 png 格式的图片在内存当中不应该有什么不同,休想通过这个方法来减少内存占用

肯定有人有意见,jpg 图片读到内存就是会小,还会给我拿出例子。当然,他说的不一定是错的。因为 jpg 的图片没有 alpha 通道!!所以读到内存的时候如果用 RGB565的格式存到内存,这下大小只有 ARGB8888的一半,能不小么

如果仅仅是为了 Bitmap 读到内存中的大小而考虑的话,jpg 也好 png 也好,没有什么实质的差别;二者的差别主要体现在:

  1. alpha 你是否真的需要?如果需要 alpha 通道,那么没有别的选择,用 png。
  2. 你的图色值丰富还是单调?就像刚才提到的,如果色值丰富,那么用jpg,如果作为按钮的背景,请用 png。
  3. 对安装包大小的要求是否非常严格?如果你的 app 资源很少,安装包大小问题不是很凸显,看情况选择 jpg 或者 png(不过,我想现在对资源文件没有苛求的应用会很少吧。。)
  4. 目标用户的 cpu 是否强劲?jpg 的图像压缩算法比 png 耗时。这方面还是要酌情选择,前几年做了一段时间 Cocos2dx,由于资源非常多,项目组要求统一使用 png,可能就是出于这方面的考虑。

3.2 压缩图片方案一(Compress)压缩文件

因为我们要处理的是Bitmap,首先从他自带的方法出发,果然找到了一个compress方法。

    private Bitmap getCompressedBitmap(Bitmap bitmap) {
        try {
            //创建一个用于存储压缩后Bitmap的文件
            File compressedFile = FileHelper.createFileByType(mContext, destType, "compressed");
            Uri uri = Uri.fromFile(compressedFile);
            OutputStream os = getContentResolver().openOutputStream(uri);
            Bitmap.CompressFormat format = destType == FileHelper.JPEG ?
                    Bitmap.CompressFormat.JPEG : Bitmap.CompressFormat.PNG;
            boolean success = bitmap.compress(format, compressRate, os);//【核心】
            if (success) {
                T.showLToast(mContext, "success");
            }

            final String pathName = FileHelper.stripFileProtocol(uri.toString());
            showBitmapInfos(pathName);
            bitmap = BitmapFactory.decodeFile(pathName);
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

bitmap.compress(format, compressRate, os) 会按照指定的格式和压缩比例将压缩后的bitmap写入到os 所对应的文件中。compressRate的取值在0-100之间,0表示压缩到最小尺寸。

在ProcessResult方法中,我们获取bitmap后,首先通过上述方法将bitmap压缩,然后在显示到ImageView中。我们看一下,压缩过后的情况。

在这里插入图片描述
【 压缩图片方案一(Compress)】

  • 第一个showBitmapInfos 显示的是选择的图片通过BitmapFactory解析后的信息,第二个showBitmapInfos
  • 显示的压缩后图片的宽高信息,最后很意外,我们的压缩方法似乎没起到作用,占用的内存没有任何变化,依旧是15M。

难道是compress方法没生效吗?其实不然,至少从UI上看compress的确生效了, 当compressRate=0时,懒羊羊的图片显示到ImageView上时已经非常不清晰了,失真非常严重。那么到底是为什么呢?

这里就得从概念上说起:

  • 一开始我们提到了这张懒羊羊的图片大小时658KB,这是它在手机存储空间所占的大小,而当我们在选择这张图片,并解析为Bitmap时,他所站的15MB是在内存中所占的大小
  • compress方法只能压缩前一种大小,也就是所使用Bitmap的compress方法只是压缩他在存储空间的大小,结果就是导致图片失真;而不能改变他在内存中所占用的大小

在上一环节我们已经说过了,决定一张图片所占内存大小的因素是图片的宽高和Bitmap的格式,我们先来从宽高处理

3.3 压缩图片方案二 (Crop)裁剪部分

    private void CropTheImage(Uri imageUrl) {
        Intent cropIntent = new Intent("com.android.camera.action.CROP");
        cropIntent.setDataAndType(imageUrl, "image/*");
        cropIntent.putExtra("cropWidth", "true");
        cropIntent.putExtra("outputX", cropTargetWidth);
        cropIntent.putExtra("outputY", cropTargetHeight);
        File copyFile = FileHelper.createFileByType(mContext, destType, String.valueOf(System.currentTimeMillis()));
        copyUrl = Uri.fromFile(copyFile);
        cropIntent.putExtra("output", copyUrl);
        startActivityForResult(cropIntent, REQUEST_CODE_CROP_PIC);
    }

这里调用了系统自带的图片裁剪控件,并创建了一个copyFile 的文件,裁剪过后的图片的地址指向就是这个文件所对应的地址。

当cropTargetWidth=1080,cropTargetHeight=920时,我们看一下日志:

在这里插入图片描述
【压缩图片方案二 (Crop)】

可以看到,Bitmap所占用的内存终于变小了,而且由于在裁剪时宽高各缩小了1/2,整个内存的占用也是缩小了1/4,变成了3.9M左右。同时图片在手机存储空间也变小了

当然,这里要注意的是,com.android.camera.action.CROP 中两个参数 “outputX” 和"outputY",决定了压缩后图片的大小,因此当这两个值的大小超过原始图片的大小时,内存占用反而会增加,这一点应该很好理解,所以需确保传递合适的值,否则会适得其反。

有同学指出,裁剪导致了图片缺失,起不到压缩的作用。其实这里我们给出了一种 长图的压缩方案:们可以把一张长图横向切割成多张,然后放在多个ImageView里显示,ImageView在ScrollView中显示时才加载bitmap

3.4 图片压缩方案三 (Sample )采样模糊

采用Sample,也就是是采样的方式压缩图片之前,我们首先需要了解一下inSampleSize 这个参数。

inSampleSize 是BitmapFactory.Options 的一个参数,当他为1时,采样后的图片大小为图片原始大小;当inSampleSize 为2时,那么采样后的图片其宽/高均为原图大小的1/2,而像素数为原图的1/4,其占有的内存大小也为原图的1/4。inSampleSize 的取值应该是2的指数。


	if (needSample) {
		//给一个200*200的图片
		bitmap = getRealCompressedBitmap(pathName, 200, 200);
	}
			
    private Bitmap getRealCompressedBitmap(String pathName, int reqWidth, int reqHeight) {
        Bitmap bitmap;
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(pathName, options);
        int width = options.outWidth / 2;
        int height = options.outHeight / 2;
        int inSampleSize = 1;

        while (width / inSampleSize >= reqWidth && height / inSampleSize >= reqHeight) {
            inSampleSize = inSampleSize * 2;
        }

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile(pathName, options);
        showBitmapInfos(pathName);
        return bitmap;
    }

我们希望将2160x1920像素的原图压缩到200x200 像素的大小,因此在getRealCompressedBitmap方法中,通过while循环inSampleSize的值最终为8,因此内存占用率将变为原来的1/64,这是一个很大的降幅。我们看一下日志,看看到底是否能够如我们所愿:

在这里插入图片描述
【图片压缩方案三 (Sample )】

可以看到,使用这种方法进行图片压缩后,增加的内存只有0.24M,几乎可以忽略不计了。

当然前提是我们要使用的图片的确不需要很大,比如这里,需要用这张图片作为用户头像的话,那么将原图缩略成200x200 px的大小是没有问题的。 否则会导致图片模糊不清晰,相当于强行放大的效果。

上面提到的三种压缩方案,通过对比可以发现

  1. 第一种方案适用于进行纯粹的文件压缩,而不适用进行图像处理压缩;
  2. 第二种方案压缩方案适用于进行图像编辑时的压缩,就像手机自带相册的编辑功能,可以随着裁剪区域的大小进行最终的压缩;
  3. 第三种方案相对来说,适应性较强,各种场景都会符合。

3.5 使用矩阵Matrix 小图形放大绘制

Bitmap 的像素点阵,还不就是个矩阵

大图小用用采样,小图大用用矩阵

还是用前面模糊图片的例子,我们不是采样了么?内存是小了,可是图的尺寸也小了啊,我要用 Canvas 绘制这张图可怎么办?当然是用矩阵了:

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);

这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小

如果我要把图片放到 ImageView 当中呢?一样可以,请看:

Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);

3.6 合理选择Bitmap的像素格式

其实前面我们已经多次提到这个问题。ARGB8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte。我们先看下有多少种格式可选:

格式 描述
ALPHA_8 只有一个alpha通道
ARGB_4444 这个从API 13开始不建议使用,因为质量太差
ARGB_8888 ARGB四个通道,每个通道8bit
RGB_565 每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit
  • ALPHA8 没必要用,因为我们随便用个颜色就可以搞定的。
  • ARGB4444 虽然占用内存只有 ARGB8888 的一半,不过已经被官方嫌弃,失宠了。。『又要占省内存,又要看着爽,臣妾做不到啊T T』。
  • ARGB8888 是最常用的,大家应该最熟悉了。
  • RGB565 看到这个,我就看到了资源优化配置无处不在,其实如果不需要 alpha 通道,特别是资源本身为 jpg 格式的情况下,用这个格式比较理想。

3.7 高能:索引位图(Indexed Bitmap)

索引位图,每个像素只占 1 Byte,不仅支持 RGB,还支持 alpha,而且看上去效果还不错!等等,请收起你的口水,Android 官方并不支持这个。是的,你没看错,官方并不支持。

 public enum Config {
    // these native values must match up with the enum in SkBitmap.h

    ALPHA_8     (2),
    RGB_565     (4),
    ARGB_4444   (5),
    ARGB_8888   (6);

    final int nativeInt;
}

不过,Skia 引擎是支持的,不信你再看:

enum Config {
   kNo_Config,   //!< bitmap has not been configured
     kA8_Config,   //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)

   //看这里看这里!!↓↓↓↓↓
    kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors  
    kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing)
    kRLE_Index8_Config,

    kConfigCount
};

其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值,所以,如果我们能够拿到这个索引是不是就可以了?对不起,拿不到。

不过呢,在 png 的解码库里面有这么一段代码:

bool SkPNGImageDecoder::getBitmapColorType(png_structp png_ptr, png_infop info_ptr,
                                       SkColorType* colorTypep,
                                       bool* hasAlphap,
                                       SkPMColor* SK_RESTRICT theTranspColorp) {
									   
	png_uint_32 origWidth, origHeight;
	int bitDepth, colorType;
	png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,
				 &colorType, int_p_NULL, int_p_NULL, int_p_NULL);

	#ifdef PNG_sBIT_SUPPORTED
	  // check for sBIT chunk data, in case we should disable dithering because
	  // our data is not truely 8bits per component
	  png_color_8p sig_bit;
	  if (this->getDitherImage() && png_get_sBIT(png_ptr, info_ptr, &sig_bit)) {
	#if 0
		SkDebugf("----- sBIT %d %d %d %d\n", sig_bit->red, sig_bit->green,
				 sig_bit->blue, sig_bit->alpha);
	#endif
		// 0 seems to indicate no information available
		if (pos_le(sig_bit->red, SK_R16_BITS) &&
			pos_le(sig_bit->green, SK_G16_BITS) &&
			pos_le(sig_bit->blue, SK_B16_BITS)) {
			this->setDitherImage(false);
		}
	}
	#endif


	if (colorType == PNG_COLOR_TYPE_PALETTE) {
		bool paletteHasAlpha = hasTransparencyInPalette(png_ptr, info_ptr);
		*colorTypep = this->getPrefColorType(kIndex_SrcDepth, paletteHasAlpha);
		// now see if we can upscale to their requested colortype
		//【这段代码,如果返回false,那么colorType就被置为索引了,那么我们看看如何返回false】
		if (!canUpscalePaletteToConfig(*colorTypep, paletteHasAlpha)) {
			*colorTypep = kIndex_8_SkColorType;
		}
	} else {
	...... 
	}
	
	return true;
}

//canUpscalePaletteToConfig函数如果返回false,那么colorType就被置为kIndex_8_SkColorType了。
static bool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
  switch (dstColorType) {
    case kN32_SkColorType:
    case kARGB_4444_SkColorType:
        return true;
    case kRGB_565_SkColorType:
        // only return true if the src is opaque (since 565 is opaque)
        return !srcHasAlpha;
    default:
        return false;
 }
}

如果传入的 dstColorTypekRGB_565_SkColorType,同时图片还有 alpha 通道,那么返回 false~~咳咳,那么问题来了,这个dstColorType 是哪儿来的??就是我们在 decode 的时候,传入的OptionsinPreferredConfig

下面我们做个实验: 在 assets 目录当中放了一个叫 index.png 的文件,大小192*192,这个文件是通过 PhotoShop 编辑之后生成的索引格式的图片。

try {
   Options options = new Options();
   options.inPreferredConfig = Config.RGB_565;
   Bitmap bitmap = BitmapFactory.decodeStream(getResources().getAssets().open("index.png"), null, options);
   Log.d(TAG, "bitmap.getConfig() = " + bitmap.getConfig());
   Log.d(TAG, "scaled bitmap.getByteCount() = " + bitmap.getByteCount());
   imageView.setImageBitmap(bitmap);
} catch (IOException e) {
    e.printStackTrace();
}

程序运行在 Nexus6上,由于从 assets 中读取不涉及前面讨论到的 scale 的问题,所以这张图片读到内存以后的大小理论值(ARGB8888):*192 * 192 4=147456

好,运行我们的代码,看输出的 Config 和 ByteCount:

D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864
  • 先说大小为什么只有 36864,我们知道如果前面的讨论是没有问题的话,那么这次解码出来的 Bitmap 应该是索引格式,那么占用的内存只有 ARGB 8888 的1/4是意料之中的;
  • 再说 Config 为什么为 null。官方说:Config没的类型就是黑户,返回null
public final Bitmap.Config getConfig ()

Added in API level 1

If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.

3.8 巨图加载

巨图加载,当然不能使用常规方法,必OOM。 原理比较简单,系统中有一个类BitmapRegionDecoder:

public static BitmapRegionDecoder newInstance(byte[] data, int offset,        int length, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(        FileDescriptor fd, boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(InputStream is,        boolean isShareable) throws IOException {
}
public static BitmapRegionDecoder newInstance(String pathName,        boolean isShareable) throws IOException {
}

可以按区域加载:

public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {
}

微博的大图浏览也是通过这个BitmapRegionDecoder实现的,具体可自行查阅。

其他

Bitmap为什么要调用recycle()方法来显示释放内存

itmap类有一个方法recycle(),从方法名可以看出意思是回收。这里就有疑问了,Android系统有自己的垃圾回收机制,可以不定期的回收掉不使用的内存空间,当然也包括Bitmap的空间。那为什么还需要这个方法呢?

Bitmap类的构造方法都是私有的,所以开发者不能直接new出一个Bitmap对象,只能通过BitmapFactory类的各种静态方法来实例化一个Bitmap。仔细查看BitmapFactory的源代码可以看到,生成Bitmap对象最终都是通过JNI调用方式实现的。
所以,加载Bitmap到内存里以后,是包含两部分内存区域的。简单的说,一部分是Java部分的,一部分是C部分的。

这个Bitmap对象是由Java部分分配的,不用的时候系统就会自动回收了,但是那个对应的C可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用recycle()方法来释放C部分的内存。从Bitmap类的源代码也可以看到,recycle()方法里也的确是调用了JNI方法了的。

附录

   try {

            Bitmap bitmap = ((BitmapDrawable)bitmImage.getDrawable()).getBitmap();
            int M_RATE = 1024 * 1024;
            float count = bitmap.getByteCount() / M_RATE;
            float all = 0;
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
                all = bitmap.getAllocationByteCount() / M_RATE;
            }
            DisplayMetrics dm = new DisplayMetrics();
            getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
            float width = bitmap.getWidth()/dm.density;
            float hight = bitmap.getHeight()/dm.density;

            double size = ( width * dm.density + 0.5) * ( hight * dm.density + 0.5) * 4 / M_RATE;

            String me =
                    "DisplayMetrics.density== " + dm.density + "\n" + "DisplayMetrics.densityDpi== " + dm.densityDpi + "\n" +
                    "这张图片占用内存大小:\n" +
                    "原始宽== " +  width+ "px\n" +
                    "原始高== " +  hight+ "px\n" +
                            "计算公式== " +  size+ "M\n" +
                    "bitmap.getByteCount()== " + count + "M\n" +
                    "bitmap.getAllocationByteCount()= " + all + "M";
            Log.e("yhf", me);
        }catch (Exception e){

        }

参考文献

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/84032922