Bitmap常用方法:

  • public boolean compress

将位图的压缩到指定的OutputStream,可以理解成将Bitmap保存到文件中! format:格式,PNG,JPG等; quality:压缩质量,0-100,0表示最低画质压缩,100最大质量(PNG无损,会忽略品质设定) stream:输出流 返回值代表是否成功压缩到指定流!

  • void recycle():

回收位图占用的内存空间,把位图标记为Dead

  • boolean isRecycled():

判断位图内存是否已释放

  • int getWidth():

获取位图的宽度

  • int getHeight():

获取位图的高度

  • boolean isMutable():

图片是否可修改

  • int getScaledWidth(Canvas canvas):

获取指定密度转换后的图像的宽度

  • int getScaledHeight(Canvas canvas):

获取指定密度转换后的图像的高度

  • Bitmap createBitmap(Bitmap src):

以src为原图生成不可变得新图像

  • Bitmap createScaledBitmap(Bitmap src, int dstWidth,int dstHeight, boolean filter):

以src为原图,创建新的图像,指定新图像的高宽以及是否变。

  • Bitmap createBitmap(int width, int height, Config config):

创建指定格式、大小的位图,一般创建空的Bitmap

  • Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)

以source为原图,创建新的图片,指定起始坐标以及新图像的高宽。

BitmapFactory

options

  • inJustDecodeBounds:

如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸。这个属性的目的是,如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时。这是一个非常有用的属性。

  • inSampleSize:

这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4。

  • inPreferredConfig:

这个值是设置色彩模式,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes。

  • inPremultiplied:

这个值和透明度通道有关,默认值是true,如果设置为true,则返回的bitmap的颜色通道上会预先附加上透明度通道。

  • inDither:

这个值和抖动解码有关,默认值为false,表示不采用抖动解码。

  • inDensity:

表示这个bitmap的像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

  • inTargetDensity:

表示要被画出来时的目标像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

  • inScreenDensity:

表示实际设备的像素密度(对应的是DisplayMetrics中的densityDpi,不是density)。

  • inScaled:

设置这个Bitmap是否可以被缩放,默认值是true,表示可以被缩放。

  • inPurgeable和inInputShareable:

这两个值一般是一起使用,设置为true时,前者表示空间不够是否可以被释放,后者表示是否可以共享引用。这两个值在Android5.0后被弃用。

  • inPreferQualityOverSpeed:

这个值表示是否在解码时图片有更高的品质,仅用于JPEG格式。如果设置为true,则图片会有更高的品质,但是会解码速度会很慢。

  • outWidth和outHeight:

表示这个Bitmap的宽和高,一般和inJustDecodeBounds一起使用来获得Bitmap的宽高,但是不加载到内存。

工厂方法:

  • decodeByteArray(byte[] data, int offset,int length):从指定字节数组的offset位置开始,将长度为length的字节数据解析成Bitmap对象。
  • decodeFIle(String pathName):从pathName指定的文件中解析、创建Bitmap对象。
  • decodeFileDescriptor(FileDescriptor fd):用于从FileDescriptor对应的文件中解析、创建Bitmap对象。
  • decodeResource(Resource res,int id):用于根据给定的资源ID从指定的资源文件中解析、创建Bitmap对象。
  • decodeStream(InputStream is):用于从指定输入流中介解析、创建Bitmap对象。

图片占用内存

如1920*1080图片占用多少内存?

每个像素的字节大小

每个像素的字节大小由bitmap的可配置的参数Config来决定。

Bitmap中,存在一个枚举类Config,定义了Android中支持Bitmap配置:

Config占用字节大小(byte)说明
ALPHA_81单透明通道
RGB_5652简单RGB色调
ARGB_44442已废弃
ARGB_8888424位真彩色
RGBA_F168Android 8.0新增(更丰富色彩表现HDR)
HARDWARESpecialAndroid 8.0新增(Bitmap直接存储在graphicmemory)

Bitmap加载方式

从获取方式分:

(1) 以文件流的方式
假设在sdcard下有 test.png图片
FileInputStream fis = new FileInputStream(“/sdcard/test.png”);
Bitmap bitmap=BitmapFactory.decodeStream(fis);

(2) 以R文件的方式
假设 res/drawable下有 test.jpg文件
Bitmap bitmap =BitmapFactory.decodeResource(getResources(), R.drawable.test);

BitmapDrawable bitmapDrawable = (BitmapDrawable) getResources().getDrawable(R.drawable. test );
Bitmap bitmap = bitmapDrawable.getBitmap();

(3) 以ResourceStream的方式,不用R文件
Bitmap bitmap=BitmapFactory.decodeStream(getClass().getResourceAsStream(“/res/drawable/test.png”));

(4) 以文件流+ R文件 的方式
InputStream in = getResources(). openRawResource(R.drawable. test );
Bitmap bitmap = BitmapFactory. decodeStream(in);

InputStream in = getResources(). openRawResource(R.drawable. test );
BitmapDrawable bitmapDrawable = new BitmapDrawable(in);
Bitmap bitmap = bitmapDrawable.getBitmap();

注意: openRawResource可以打开 drawable, sound, 和raw资源,但不能是string和color。

从资源存放路径分:

(1) 图片放在sdcard中
Bitmap imageBitmap = BitmapFactory.decodeFile(path);// (path 是图片的路径,跟目录是/sdcard)

(2)图片在项目的res文件夹下面
ApplicationInfo appInfo = getApplicationInfo();
//得到该图片的id(name 是该图片的名字,“drawable” 是该图片存放的目录,appInfo.packageName是应用程序的包)
int resID = getResources().getIdentifier(fileName, “drawable”, appInfo.packageName);
Bitmap imageBitmap2 = BitmapFactory. decodeResource(getResources(), resID);

(3) 图片放在src目录下
String path = “com/xiangmu/test.png”; //图片存放的路径
InputStream in = getClassLoader().getResourceAsStream(path); //得到图片流
Bitmap imageBitmap3 = BitmapFactory. decodeStream(in);

(4) 图片放在 Assets目录
InputStream in = getResources().getAssets().open(fileName);
Bitmap imageBitmap4 = BitmapFactory.decodeStream(in);

Bitmap Drawable byte[] InputStream 相互转换方法

  // 将byte[]转换成InputStream  
    public InputStream Byte2InputStream(byte[] b) {  
        ByteArrayInputStream bais = new ByteArrayInputStream(b);  
        return bais;  
    }  

    // 将InputStream转换成byte[]  
    public byte[] InputStream2Bytes(InputStream is) {  
        String str = "";  
        byte[] readByte = new byte[1024];  
        int readCount = -1;  
        try {  
            while ((readCount = is.read(readByte, 0, 1024)) != -1) {  
                str += new String(readByte).trim();  
            }  
            return str.getBytes();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  

    // 将Bitmap转换成InputStream  
    public InputStream Bitmap2InputStream(Bitmap bm) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
        InputStream is = new ByteArrayInputStream(baos.toByteArray());  
        return is;  
    }  

    // 将Bitmap转换成InputStream  
    public InputStream Bitmap2InputStream(Bitmap bm, int quality) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.PNG, quality, baos);  
        InputStream is = new ByteArrayInputStream(baos.toByteArray());  
        return is;  
    }  

    // 将InputStream转换成Bitmap  
    public Bitmap InputStream2Bitmap(InputStream is) {  
        return BitmapFactory.decodeStream(is);  
    }  

    // Drawable转换成InputStream  
    public InputStream Drawable2InputStream(Drawable d) {  
        Bitmap bitmap = this.drawable2Bitmap(d);  
        return this.Bitmap2InputStream(bitmap);  
    }  

    // InputStream转换成Drawable  
    public Drawable InputStream2Drawable(InputStream is) {  
        Bitmap bitmap = this.InputStream2Bitmap(is);  
        return this.bitmap2Drawable(bitmap);  
    }  

    // Drawable转换成byte[]  
    public byte[] Drawable2Bytes(Drawable d) {  
        Bitmap bitmap = this.drawable2Bitmap(d);  
        return this.Bitmap2Bytes(bitmap);  
    }  

    // byte[]转换成Drawable  
    public Drawable Bytes2Drawable(byte[] b) {  
        Bitmap bitmap = this.Bytes2Bitmap(b);  
        return this.bitmap2Drawable(bitmap);  
    }  

    // Bitmap转换成byte[]  
    public byte[] Bitmap2Bytes(Bitmap bm) {  
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  
        bm.compress(Bitmap.CompressFormat.PNG, 100, baos);  
        return baos.toByteArray();  
    }  

    // byte[]转换成Bitmap  
    public Bitmap Bytes2Bitmap(byte[] b) {  
        if (b.length != 0) {  
            return BitmapFactory.decodeByteArray(b, 0, b.length);  
        }  
        return null;  
    }  

    // Drawable转换成Bitmap  
    public Bitmap drawable2Bitmap(Drawable drawable) {  
        Bitmap bitmap = Bitmap  
                .createBitmap(  
                        drawable.getIntrinsicWidth(),  
                        drawable.getIntrinsicHeight(),  
                        drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888  
                                : Bitmap.Config.RGB_565);  
        Canvas canvas = new Canvas(bitmap);  
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),  
                drawable.getIntrinsicHeight());  
        drawable.draw(canvas);  
        return bitmap;  
    }  

    // Bitmap转换成Drawable  
    public Drawable bitmap2Drawable(Bitmap bitmap) {  
        BitmapDrawable bd = new BitmapDrawable(bitmap);  
        Drawable d = (Drawable) bd;  
        return d;  
    }  

Bitmap常见操作

缩放

   /**
     * 根据给定的宽和高进行拉伸
     *
     * @param origin    原图
     * @param newWidth  新图的宽
     * @param newHeight 新图的高
     * @return new Bitmap
     */
    private Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) {
        if (origin == null) {
            return null;
        }
        int height = origin.getHeight();
        int width = origin.getWidth();
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);// 使用后乘
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        if (!origin.isRecycled()) {
            origin.recycle();
        }
        return newBM;
    }

    /**
     * 按比例缩放图片
     *
     * @param origin 原图
     * @param ratio  比例
     * @return 新的bitmap
     */
    private Bitmap scaleBitmap(Bitmap origin, float ratio) {
        if (origin == null) {
            return null;
        }
        int width = origin.getWidth();
        int height = origin.getHeight();
        Matrix matrix = new Matrix();
        matrix.preScale(ratio, ratio);
        Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
        if (newBM.equals(origin)) {
            return newBM;
        }
        origin.recycle();
        return newBM;
    }

裁剪

/**
 * 裁剪
 *
 * @param bitmap 原图
 * @return 裁剪后的图像
 */
private Bitmap cropBitmap(Bitmap bitmap) {
    int w = bitmap.getWidth(); // 得到图片的宽,高
    int h = bitmap.getHeight();
    int cropWidth = w >= h ? h : w;// 裁切后所取的正方形区域边长
    cropWidth /= 2;
    int cropHeight = (int) (cropWidth / 1.2);
    return Bitmap.createBitmap(bitmap, w / 3, 0, cropWidth, cropHeight, null, false);
}

旋转

/**
 * 选择变换
 *
 * @param origin 原图
 * @param alpha  旋转角度,可正可负
 * @return 旋转后的图片
 */
private Bitmap rotateBitmap(Bitmap origin, float alpha) {
    if (origin == null) {
        return null;
    }
    int width = origin.getWidth();
    int height = origin.getHeight();
    Matrix matrix = new Matrix();
    matrix.setRotate(alpha);
    // 围绕原地进行旋转
    Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
    if (newBM.equals(origin)) {
        return newBM;
    }
    origin.recycle();
    return newBM;
}

偏移

/**
 * 偏移效果
 * @param origin 原图
 * @return 偏移后的bitmap
 */
private Bitmap skewBitmap(Bitmap origin) {
    if (origin == null) {
        return null;
    }
    int width = origin.getWidth();
    int height = origin.getHeight();
    Matrix matrix = new Matrix();
    matrix.postSkew(-0.6f, -0.3f);
    Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
    if (newBM.equals(origin)) {
        return newBM;
    }
    origin.recycle();
    return newBM;
}

获得圆角图片

private Bitmap bimapRound(Bitmap mBitmap,float index){
Bitmap bitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Config.ARGB_4444);

    Canvas canvas = new Canvas(bitmap);
    Paint paint = new Paint();
    paint.setAntiAlias(true);

    //设置矩形大小
    Rect rect = new Rect(0,0,mBitmap.getWidth(),mBitmap.getHeight());
    RectF rectf = new RectF(rect);

    // 相当于清屏
    canvas.drawARGB(0, 0, 0, 0);
    //画圆角
    canvas.drawRoundRect(rectf, index, index, paint);
    // 取两层绘制,显示上层
    paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));

    // 把原生的图片放到这个画布上,使之带有画布的效果
    canvas.drawBitmap(mBitmap, rect, rect, paint);
    return bitmap;

}

Bitmap内存模型

Bitmap 在内存中的组成部分,在任何系统版本中都会存在以下 3 个部分:

  • 1、Java Bitmap 对象: 位于 Java 堆,即我们熟悉的 android.graphics.Bitmap.java
  • 2、Native Bitmap 对象: 位于 Native 堆,以 Bitmap.cpp 为代表,除此之外还包括与 Skia 引擎相关的 SkBitmap、SkBitmapInfo 等一系列对象;
  • 3、图片像素数据: 图片解码后得到的像素数据。

其中,Java Bitmap 对象和 Native Bitmap 对象是分别存储在 Java 堆和 Native 堆的,毋庸置疑。唯一有操作性的是 3、图片像素数据,不同系统版本采用了不同的分配策略,分为 3 个历史时期:

  • 时期 1 - Android 3.0 以前: 像素数据存放在 Native 堆(这部分系统版本的市场占有率已经非常低,后文我们不再考虑);
  • 时期 2 - Android 8.0 以前: 从 Android 3.0 到 Android 7.1,像素数据存放在 Java 堆;
  • 时期 3 - Android 8.0 以后: 从 Android 8.0 开始,像素数据重新存放在 Native 堆。另外还新增了 Hardware Bitmap 硬件位图,可以减少图片内存分配并提高绘制效率。

Bitmap的内存回收

  1. 2.3.3以前Bitmap的像素内存是分配在natvie上,而且不确定什么时候会被回收。根据官方文档的说法我们需要手动调用Bitmap.recycle()去回收:
  2. 3.0之后没有强调Bitmap.recycle();而是强调Bitmap的复用

如何复用

  • 1.使用LruCache和DiskLruCache做内存和磁盘缓存;
  • 2.使用Bitmap复用,同时针对版本进行兼容(inMutable和inBitmap)
  • 3.使用inTempStorage

获取Bitmap的大小

/** 
  * 得到bitmap的大小 
  */  
 public static int getBitmapSize(Bitmap bitmap) {  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {    //API 19  
         return bitmap.getAllocationByteCount();  
     }  
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12  
         return bitmap.getByteCount();  
     }  
     // 在低版本中用一行的字节x高度  
     return bitmap.getRowBytes() * bitmap.getHeight();
 }  

getByteCount()与getAllocationByteCount()的区别 一般情况下两者是相等的;如果通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(getByteCount永远小于等于getAllocationByteCount)

Bitmap占用内存大小计算

在BitmapFactory.cpp中的 doDecode 方法

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;
    }
...
int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;

// Apply a fine scaling step if necessary.
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
    willScale = true;
    scaledWidth = codec->getInfo().width() / sampleSize;
    scaledHeight = codec->getInfo().height() / sampleSize;
}

...
if (willScale) {
    // Set the allocator for the outputBitmap.
    SkBitmap::Allocator* outputAllocator;
    if (javaBitmap != nullptr) {
        outputAllocator = &recyclingAllocator;
    } else {
        outputAllocator = &defaultAllocator;
    }

    SkColorType scaledColorType = 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(
            bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
    if (!outputBitmap.tryAllocPixels(outputAllocator)) {
        // This should only fail on OOM.  The recyclingAllocator should have
        // enough memory since we check this before decoding using the
        // scaleCheckingAllocator.
        return nullObjectReturn("allocation failed for scaled bitmap");
    }

    SkPaint paint;
    // kSrc_Mode instructs us to overwrite the uninitialized pixels in
    // outputBitmap.  Otherwise we would blend by default, which is not
    // what we want.
    paint.setBlendMode(SkBlendMode::kSrc);
    paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering

    SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
    canvas.scale(scaleX, scaleY);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}

从上述代码可以看出bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由

 scale = (float) targetDensity / density;

决定,即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的

  • inDensity: Bitmap位图自身的密度,分辨率
  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率
  • inScreenDensity: 设备屏幕分辨率

其中inDensit和图片存在的资源文件的目录有关,同一张图片放在不同目录下会有不同的值:

density0.7511.5233.54
densityDpi120160240320480560640
DpiFolderldpimdpihdpixhdpixxhdpixxxdpixxxxhdpi

总结:

  1. 图片放在drawable中,等同于放在drawable-mdpi中。原因为:drawable目录不与有屏幕密度特性,所以采用基准值,即mdpi
  2. 图片放在某个特性的drawable中,比如drawable-hdpi,如果设备的屏幕密度高于当前drawable目录所代表的密度,则图片会被放大,否则被缩小。

​ 放大或缩小比例 = 设备屏幕密度 / drawable目录所代表的屏幕密度

因此,bitmap占用公式,从之前

Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 x 纵向像素密度 x 每个像素的字节大小

细化为

Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 x 图片高 x(设备分辨率/资源目录分辨率)² x 每个像素的字节大小

如要进行bitmap的解码

try{
decode bitmap;
}catch(OutOfMemoryError e){//oom
质量压缩
再次解码
}

通过epic的hook框架可以对bitmap操作时进行优化处理

内存优化小结

  1. 设备分级,不能脱离设备,针对不同机型做优化
  2. Bitmap优化 统一图片库 线上线下监控 hook
  3. 内存泄漏工具使用
  4. glide
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐