##All About Bitmap ###Bitmap到底占用了多少内存 设计到屏幕密度,先要弄清楚density和densityDpi两个变量的含义
- density:1 dp = density px
- densityDpi:屏幕每英寸对应多少个点(不是像素点)
在DisplayMetrics中,两者的关系是线性的:
| Table | ||||||
|---|---|---|---|---|---|---|
| density | 1 | 1.5 | 2 | 3 | 3.5 | 4 |
| denDpi | 160 | 240 | 320 | 480 | 560 | 640 |
| ####占了多少内存? | ||||||
| 在客户端,每加一张图片都是需要慎重考虑的事。图片,吃内存,可能还会导致OOM。Android API提供了一个方法可以方便我们计算Bitmap的大小 |
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
通过这个方法,我们可以获取到一张Bitmap在运行时到底占用多少内存~
举个例子 一张 522x686 的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就可以用这个方法获取到。
###给我一张图,我算出Bitmap多少大 每次都在运行时,调用API得到Bitmap的大小,这个办法有点笨~~作为Android应该能手动计算出Bitmap的大小。下面看getByteCount的源码 在getByteCount中分别用到了getRowBytes和getHeight来计算Bitmap的大小;getHeight就是获取Bitmap的高度;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);
}
getRowBytes实际调用的native方法
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
return static_cast<jint>(bitmap->rowBytes());
}
从Native代码中,发现Bitmap本质上是SkBitmap。看SkBitmap代码
/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }
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 byte。 那么结论就是
一张 ARGB_8888 的 Bitmap 占用内存的计算公式
bitmapInRam = bitmapWidth*bitmapHeight *4 bytes
然后,还有坑。你会发现你算出来的和你获取到的值之间差了倍数~~
一张522*686的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B,就可以用这个方法获取到。然而公式计算出来的可是1432368B
####Density 为什么举例要强调xxx目录,xxx手机,Bitmap加载只和宽高有关吗?
我们读取的是drawable目录下面的图片,用的是decodeResource方法,该方法本质上完成:
- 读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;
- 调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射。
原始资源的 density 其实取决于资源存放的目录(比如 xxhdpi 对应的是480。而屏幕的density,看BitmapFactory源码
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
//实际上,我们这里的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; //这里density的值如果对应资源目录为hdpi的话,就是240
}
}
if (opts.inTargetDensity == 0 && res != null) {
//请注意,inTargetDensity就是当前的显示密度,比如三星s6时就是640
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
我们看到 opts 这个值被初始化,而它的构造居然如此简单:
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
所以我们就很容易的看到,Option.inScreenDensity 这个值没有被初始化,而实际上后面我们也会看到这个值根本不会用到;我们最应该关心的是什么呢?是 inDensity 和 inTargetDensity,这两个值与下面 cpp 文件里面的 density 和 targetDensity 相对应——重复一下,inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density。
紧接着,用到了 nativeDecodeStream 方法,不重要的代码直接略过,直接给出最关键的 doDecode 函数的代码:
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);
}
......
}
注意到其中有个 density 和 targetDensity,前者是 decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(比如 hdpi 是240,xxhdpi 是480),这部分代码我跟了一下,太长了,就不列出来了;targetDensity 实际上是我们加载图片的目标 density,这个值的来源我们已经在前面给出了,就是 DisplayMetrics 的 densityDpi,如果是三星s6那么这个数值就是640。sx 和sy 实际上是约等于 scale 的,因为 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。
####精度
越来越有趣了是不是,你肯定会发现我们这么细致的计算还是跟获取到的数值不一样~~
结果已经很接近了,是不是精度问题??!!
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 有木有很兴奋!有木有很激动!! 写到这里,突然想起《STL源码剖析》一书的扉页,侯捷先生只写了一句话: “源码之前,了无秘密”。
####小节 综上,Bitmap在内存中的大小取决于:
- 色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
- 原始文件存放的资源目录(是 hdpi 还是 xxhdpi 可不能傻傻分不清楚哈)
- 目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
####想办法减少Bitmap内存占用 ######Jpg和Png 同样一张图片,jpg格式会比png格式会小一些,原因很简单,jpg是一种有损压缩的图片存储格式,而png是无损压缩的图片存储格式,显而易见,jpg会比png小,代价也是显而易见的。 可是,这说的是文件存储范畴的事,它们只存在于文件系统,而非内存或者显存。打个比方,jpg是压缩包,png是解压之后的东西。
jpg和png格式 在内存上有什么不一样~~ jpg图片读到的内存会小?因为jpg图片没有alpha通道,所以读到内存的时候如果用RGB565格式存到内存,这下大小只有ARGB8888的一半~~ 抛开Android平台不谈,从出图的角度看,jpg格式的图片也不一定比png的小,这取决于图像信息的内容:
JPG 不适用于所含颜色很少、具有大块颜色相近的区域或亮度差异十分明显的较简单的图片。对于需要高保真的较复杂的图像,PNG 虽然能无损压缩,但图片文件较大。
如果仅仅是为了Bitmap读到内存中的大小而考虑的话,jpg和png,没有实质的差别;二者的差别主要体现在:
- alpha 你是否真的需要?如果需要alpha通道,那没有选择,只能用png
- 你的图片值丰富还是单调?就像刚才提到的,如果色值丰富,那么用jpg,如果作为btn的背景,用png
- 对安装包大小的要求是否严格?如果你的app资源很少,安装包大小问题不是很凸显,看情况选择jpg和png(不过现在对资源文件没有苛求的应用会很少~~~)
- 目标用户的cpu是否强劲?jpg的图像压缩算法比png耗时。这方面还要酌情考虑(在cocos2dx中,由于资源比较多,会要求统一使用png)
所以,jpg和png的选择,于减少内存基本没有关系~~
######使用inSampleSize 这个方法主要用在图片资源本身较大,或者适当地采样并不会影响视觉效果的条件下,这时候我们输出地目标可能相对较小,对图片分辨率、大小要求不是非常严格。 举例 现在有个需求,要求将一张图片进行模糊,然后作为ImageView的src呈现给用户,而我们的原始图片大小为1080*1920;如果我们直接拿来模糊的话,一方面模糊的过程费时费力,另一方面生成的图片又占用内存,实际上在模糊运算过程中可能会存在输入和输出并存的情况,此时内存将会有一个短暂的峰值。这时候就有可能OOM。 既然图片最后是要模糊的,也看不清,就还不如直接用一张采样后的图片,如果采样率为2,那么读出来的图片只有原始图片的1/4大小
BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
#####使用矩阵
用到Bitmap的地方,总会见到Matrix。其实,Bitmap的像素点阵,也就是个矩阵。
大图小用用采用,小图大用用矩阵
还是用前面模糊图片的例子,采样之后内存小了,图的尺寸也小了。我要用Canvas绘制这张图怎么办?当然用矩阵~
Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0f, 0f);
//如果使用直接替换矩阵的话,在Nexus6 5.1.1上必须关闭硬件加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0,0, paint);
需要注意的是,在使用搭载5.1.1原生系统的Nexus6进行测试是时发现,如果使用Canvas的setMatrix方法,bitmap将不会出现在屏幕上。因此尽量使用canvas的scale、ratate方法,或者使用concat方法。
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);
#####合理选择Bitmap的像素格式
前面已经提到这个问题,ARGB8888格式的图片,每像素占用4字节,而RGB则是2字节。
| 格式 | 描述 |
|---|---|
| ALPHA_8 | 只有一个alpha通道 |
| ARGB_4444 | 这个从API 13开始不建议使用,因为质量太差 |
| ARGB_8888 | ARGB四个通道,每个通道8bit |
| RGB_565 | 每个像素占2Byte,其中红色占5bit,绿色占6bit,蓝色占5bit |
| 其中,ALPHA8没必要用,随便用个色值就行;AGRB4444虽然占用内存只有ARGB8888的一半,不过官方已经弃用了; | |
| ARGB8888最常用,应该比较熟悉 | |
| RGB565,如果不需要alpha通道,特别是资源本身为jpg格式的情况下,用这个格式比较理想 |
#####索引位图(Indexed Bitmap) 索引位图,每个像素只占1Byte,不仅支持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;
}
}
如果传入的 dstColorType 是 kRGB_565_SkColorType,同时图片还有 alpha 通道,那么返回 false~~咳咳,那么问题来了,这个dstColorType 是哪儿来的??就是我们在 decode 的时候,传入的 Options 的 inPreferredConfig。
下面是实验时间
准备:在 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
运行我们的代码,看输出的Configuration和ByteCOunt:
D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864
先说大小为什么只有 36864,我们知道如果前面的讨论是没有问题的话,那么这次解码出来的 Bitmap 应该是索引格式,那么占用的内存只有 ARGB 8888 的1/4是意料之中的;再说 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.
看来这个法子还真行啊,占用内存一下小很多。不过由于官方并未做出支持,因此这个方法有诸多限制,比如不能在 xml 中直接配置,,生成的 Bitmap 不能用于构建 Canvas 等等
###Bitmap的OOM android的OOM是一个经常碰到的问题~~一种简便的解决方式就是分配更少的内存空间来存储,即在载入图片的时候以牺牲图片质量为代价,将图片进行缩放。但是,这种方法得不偿失,牺牲了图片质量。 在BitmapFactory中有一个内部类BitmapFactory.Options,其中值得我们注意的是inSampleSize和inJustDecodeBounds两个属性
-
inSampleSize是以2的指数的倒数被进行放缩
If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. (1 -> decodes full size; 2 -> decodes 1/4th size; 4 -> decode 1/16th size). Because you rarely need to show and have full size bitmap images on your phone. For manipulations smaller sizes are usually enough.
-
inJustDecodeBounds为Boolean型
设置inJustDecodeBounds为true后,decodeFile并不分配空间,但可计算出原始图片的长度和宽度,即options.outWidth和options.outHeight。
要对图片进行缩放,最大的问题就是怎么运行时改变inSampleSize的值,通过上面的inJustDecodeBounds可以知道图片原始的大小,那么这样就可以通过算法来得到一个恰当的inSampleSize。其动态算法可参考:
/**
* compute Sample Size
*
* @param options
* @param minSideLength
* @param maxNumOfPixels
* @return
*/
public static int computeSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
int initialSize = computeInitialSampleSize(options, minSideLength,
maxNumOfPixels);
int roundedSize;
if (initialSize <= 8) {
roundedSize = 1;
while (roundedSize < initialSize) {
roundedSize <<= 1;
}
} else {
roundedSize = (initialSize + 7) / 8 * 8;
}
return roundedSize;
}
/**
* compute Initial Sample Size
*
* @param options
* @param minSideLength
* @param maxNumOfPixels
* @return
*/
private static int computeInitialSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
// 上下限范围
int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math
.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == -1) ? 128 : (int) Math.min(
Math.floor(w / minSideLength), Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound;
}
if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
return 1;
} else if (minSideLength == -1) {
return lowerBound;
} else {
return upperBound;
}
}
有了上面的算法,我们可以轻易的get到Bitmap
/**
* get Bitmap
*
* @param imgFile
* @param minSideLength
* @param maxNumOfPixels
* @return
*/
public static Bitmap tryGetBitmap(String imgFile, int minSideLength,
int maxNumOfPixels) {
if (imgFile == null || imgFile.length() == 0)
return null;
try {
FileDescriptor fd = new FileInputStream(imgFile).getFD();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
// BitmapFactory.decodeFile(imgFile, options);
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = computeSampleSize(options, minSideLength,
maxNumOfPixels);
try {
// 这里一定要将其设置回false,因为之前我们将其设置成了true
// 设置inJustDecodeBounds为true后,decodeFile并不分配空间,即,BitmapFactory解码出来的Bitmap为Null,但可计算出原始图片的长度和宽度
options.inJustDecodeBounds = false;
Bitmap bmp = BitmapFactory.decodeFile(imgFile, options);
return bmp == null ? null : bmp;
} catch (OutOfMemoryError err) {
return null;
}
} catch (Exception e) {
return null;
}
}
###Bitmap.option的使用 首先列出BitmapFactory.options选项的所有字段
| Field | Description | |
|---|---|---|
| public Bitmap | inBitmap | |
| public int | inDensity | |
| public boolean | inDither | |
| public boolean | inInputShareable | |
| public boolean | inJustDecodeBounds | |
| public boolean | inMutable | |
| public boolean | inPreferQualityOverSpeed | |
| public Bitmap.Config | inPreferredCOnfig | |
| public boolean | inPurgeable | |
| public int | inSampleSize | |
| public boolean | inScaled | |
| public int | inScreenDensity | |
| public int | inTargetDensity | |
| public byte[] | inTempStorage | |
| public boolean | mCancel | |
| public int | outHeight | |
| public String | outMimeType | |
| public int | outWidth |
#####怎么获取图片大小 首先将图片转化为Bitmap,然后再利用Bitmap的getwidth()和geiHeight()方法就可以取得图片的宽和高。
但是,问题:在通过BitmapFacctory.decodeFile(String file)方法转化Bitmap时,遇到大一点的图片时,经常会碰到OOM的情况,怎么避免?
此时,就要用到BitmapFactory.options这个类: BitmapFactory.options这个类中有一个inJustDecodeBounds这个字段;如果把它设为true,那么BitmapFactory.decodeFile(String path,Options opt)并不会真的返回一个Bitmap给你,仅仅会把它的宽高取回来,这样就不会占用太多的内存,因此也不会频繁发生OOM。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
/* 这里返回的bmp是null */
这段代码之后,options.outWidth 和 options.outHeight就是我们想要的宽和高了。
有了宽高信息,问题:怎么在图片不变形的情况下获取到图片指定大小的缩略图? 比如,需要在图片不变形的情况下得到宽度200的缩略图。首先需要计算一下缩放之后,图片的高度是多少~~
int height = options.outHeight * 200 / options.outWidth;
options.outWidth = 200;
options.outHeight = height;
/* 这样才能真正的返回一个Bitmap给你 */
options.inJustDecodeBounds = false;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
image.setImageBitmap(bmp);
虽然这样得到了期望大小的ImageView,但是在执行BitmapFactory.decodeFile(path,options)时,并没有节约内存。想要节约内存,还需要使用inSampleSize这个成员变量。
inSampleSize = options.outWidth / 200;
另外,为了节约内存还可以使用以下几个字段:
options.inPreferredConfig = Bitmap.Config.ARGB_4444; // 默认是Bitmap.Config.ARGB_8888
/* 下面两个字段需要组合使用 */
options.inPurgeable = true;
options.inInputShareable = true;
###BitmapShader的使用 Android提供的Shader类主要是渲染图像以及一些几何图形 Shader有几个直接子类:
- BitmapShader:主要用来渲染图像
- LinearGradient:用来进行线性渲染
- RadialGradient:用来进行环形渲染
- SweepGradient:扫面渐变---围绕一个中心点扫描渐变就像电影里那种雷达扫描,用来梯度渲染
- ComposeShader:组合渲染,可以和其他几个子类组合起来使用
####BitmapShader 渲染器着色一个位图作为一个纹理。位图可以重复或设置模式
public BitmapShader(Bitmap bitmap,Shader.TileMode tileX,Shader.TileMode tileY)
调用这个方法来产生一个画有一个位图的渲染器(Shader)。
bitmap 在渲染器内使用的位图
tileX The tiling mode for x to draw the bitmap in. 在位图上X方向花砖模式
tileY The tiling mode for y to draw the bitmap in. 在位图上Y方向花砖模式
TileMode:(一共有三种)
CLAMP :如果渲染器超出原始边界范围,会复制范围内边缘染色。
REPEAT :横向和纵向的重复渲染器图片,平铺。
MIRROR :横向和纵向的重复渲染器图片,这个和REPEAT重复方式不一样,他是以镜像方式平铺。
具体实现
public class BitmapShaders extends View
{
private BitmapShader bitmapShader = null;
private Bitmap bitmap = null;
private Paint paint = null;
private ShapeDrawable shapeDrawable = null;
private int BitmapWidth = 0;
private int BitmapHeight = 0;
public BitmapShaders(Context context)
{
super(context);
//得到图像
bitmap = ((BitmapDrawable) getResources().getDrawable(R.drawable.h)).getBitmap();
BitmapWidth = bitmap.getWidth();
BitmapHeight = bitmap.getHeight();
//构造渲染器BitmapShader
bitmapShader = new BitmapShader(bitmap,Shader.TileMode.MIRROR,Shader.TileMode.REPEAT);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//将图片裁剪为椭圆形
//构建ShapeDrawable对象并定义形状为椭圆
shapeDrawable = new ShapeDrawable(new OvalShape());
//得到画笔并设置渲染器
shapeDrawable.getPaint().setShader(bitmapShader);
//设置显示区域
shapeDrawable.setBounds(20, 20,BitmapWidth-60,BitmapHeight-60);
//绘制shapeDrawable
shapeDrawable.draw(canvas);
}
}
###如何根据Bitmap.Config手写Bitmap ###使用LruCache,SD卡,手机缓存