Bitmap基础

本文深入探讨Android中Bitmap类的使用,包括构造、配置、压缩方法,以及如何通过BitmapFactory进行图片的下采样和质量压缩,有效管理图片在内存和磁盘上的占用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

Bitmap代表一张图片,其存储的是像素点,安卓中不同类型的图片如jpeg,png都可以用Bitmap表示。安卓中对图片的裁剪、缩放等一系列的操作都需要把图片文件以Bitmap的形式加载到内存中进行操作。本节就来简单认识下Bitmap,为以后的各种位图处理、图片框架分析打个基础~

一、常见的三个类

  • Bitmap
  • BitmapFactory
  • BitmapFactory.options
1、Bitmap的构造

Bitmap的创建通常是使用BitmapFactory类来创建的,因为通过看Bitmap构造源码可知使用构造函数来创建Bitmap对象的操作都是再jni层来完成的。我们只需使用BitmapFactory即可创建出Bitmap对象。

简单认识下源码~ 可见都不支持我们直接使用构造~

//android.graphics 包下
package android.graphics;
//final类,实现了序列化接口
public final class Bitmap implements Parcelable {

    /**
     * Private constructor that must receive an already allocated native bitmap
     * int (pointer).
     * JNI now calls the version below this one. 
     * This is preserved due to UnsupportedAppUsage.
     */
    @UnsupportedAppUsage(maxTargetSdk = 28)
    Bitmap(long nativeBitmap, 
            int width, 
            int height,
            int density,
            boolean requestPremultiplied, 
            byte[] ninePatchChunk,
            NinePatch.InsetStruct ninePatchInsets) {
            
            this(nativeBitmap, 
                 width, 
                 height, 
                 density, 
                 requestPremultiplied, 
                 ninePatchChunk,
                 ninePatchInsets, 
                 true);
    }
    // called from JNI and Bitmap_Delegate.
    Bitmap(long nativeBitmap, 
           int width, 
           int height, 
           int density,
           boolean requestPremultiplied, 
           byte[] ninePatchChunk,
           NinePatch.InsetStruct ninePatchInsets, 
           boolean fromMalloc) {
           ...
           }
}
2、Bitmap的常见普通方法

(1)public void recycle()

  • 释放bitmap关联的native对象,并且清除像素数据。
  • 注意这个方法不会立即同步释放像素数据,在像素数据没有其他引用时垃圾回收才会收集。
  • 方法调用后Bitmap被标记为dead状态,此时若是再调用getPixels() 或者 setPixels() 会抛异常。
  • 只有确保bitmap没有使用时才调用这个方法,这个方法通常不使用,因为当不再引用此位图时,正常的GC进程将释放此内存。

(2)宽高:

  • public void setWidth(int width)设置宽
  • public void setHeight(int height)设置高
  • public final int getWidth()获取宽
  • public final int getHeight()获取高

(3)配置信息:主要是像素所占字节如ARGB_8888每个像素占四个字节。

  • public void setConfig(Config config)设置配置信息
  • public final Config getConfig() 获取配置信息
   public enum Config {
   /**
     设置这种配置后每个像素占1字节内存,这种配置只支持透明度通道编码。
     
     每个像素存储单个alpha通道值,不存储颜色值。

     通常使用这种配置可以高效存储masks
    */
    
       ALPHA_8     (1),
       
   /**
     设置这种配置后每个像素占2字节内存,这种配置只支持RGB三种通道编码。
     R:存储5bit,有32种可能
     G:存储6bit,有64种可能
     B:存储5bit。

      这种配置可以产生轻微的视觉效果,如资源不设置抖动时,结果可能显示为绿色。
      
      当不要求高颜色保真度的不透明位图时可以设置这个。
    */
    
       RGB_565     (3),
       
   /**
     弃用,推荐使用ARGB_8888   
    */
      @Deprecated
       ARGB_4444   (4),
   /**
    每个像素存储占四个字节。有RGBA四个通道,每个通道占8位。每个通道有256种可能。
    */
       ARGB_8888   (5),
    /**
    每个像素存储占8个字节。每个通道存储半精度浮点值。
    
    这种配置很适合wide-gamut和 HDR
    */
       RGBA_F16    (6),
    /**
     特殊的配置,bitmap 仅仅使用在内存中,而且bitmap永远是不可变得。
    */
       HARDWARE    (7);

...
}

(4)public final boolean isRecycled()

  • bitmap是否已经被回收

(5)public Bitmap copy(Config config, boolean isMutable)

  • 基于原图尺寸创建一个bitmap,可手动配置Config信息,设置像素信息是否可修改。
  • copy失败时返回空bitmap
  • copy成功返回的位图与原始位图具有相同的密度和颜色空间
  • copy时设置Bitmap.Config.ALPHA_8,颜色空间就不可用了。
  • copy时设置Bitmap.Config.RGBA_F16,或者原图的配置信息为Bitmap.Config.RGBA_F16时EXTENDED or non-EXTENDED变体可能会根据需要进行调整。

isMutable的意义在于哪里呢,想要操作像素这个必须设置为true否则如使用Canvas操作bitmap时就会抛异常,如下常见栗子:

    public Canvas(@NonNull Bitmap bitmap) {
        if (!bitmap.isMutable()) {
            throw new IllegalStateException(
            //不可变的bitmap传递给了Canvas
            "Immutable bitmap passed to Canvas constructor"
            );
        }
        throwIfCannotDraw(bitmap);
        mNativeCanvasWrapper = nInitRaster(bitmap.getNativeInstance());
        mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
                this, mNativeCanvasWrapper);
        mBitmap = bitmap;
        mDensity = bitmap.mDensity;
    }

(6) 质量压缩 :压缩bitmap到本地。

public boolean compress(CompressFormat format, int quality, OutputStream stream)

  • 方法运行在工作线程
  • CompressFormat format:要压缩的图片格式,可为JPEG、PNG、WEBP等。
  • int quality:压缩质量,取值范围【0,100】0表示质量最差,100表示压缩到最高视觉质量。

CompressFormat的格式:
1、JPEG:有损压缩,质量与quality有关。
2、PNG:无损压缩,设置这种格式时quality可以忽略。
3、WEBP,已经弃用参考WEBP_LOSSY、WEBP_LOSSLESS。在安卓Q上quality=100时会文件是无损webp格式,quality<100时文件是有损webp格式。
4、WEBP_LOSSY:有损压缩。质量与quality有关。
5、WEBP_LOSSLESS:压缩到WEBP无损格式,这时质量还是可以设置的,但是质量的数值有其他意义了,0代表快速压缩,但是产生的文件较大,100代表压缩时间较长,但是产生文件较小。

(7) public final boolean isMutable()

  • 像素是否可修改

(8)public final int getRowBytes()

  • 获取bitmap 行之间的像素字节数。
  • 注意像素的引用被存储在native层,平时调用getPixels() 或者 setPixels()时,像素被统一处理为32位值,然后根据颜色类别进行了压缩。
  • 安卓4.4及不建议使用这个方法计算内存,参考getAllocationByteCount()
    public final int getRowBytes() {
        if (mRecycled) {
            Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
        }
        return nativeRowBytes(mNativePtr);
    }

(9)public final int getByteCount()

  • 返回bitmap像素的最小字节数。安卓4.4及不建议使用这个方法计算内存,参考getAllocationByteCount()。
    public final int getByteCount() {
        if (mRecycled) {
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

(10) public final int getAllocationByteCount()

  • 返回bitmap字节所占内存,这个值通常与getByteCount返回相等。

如果位图被重新用于解码其他较小尺寸的位图,或通过手动重新配置。这个值大于getByteCount。参考:
1、reconfigure(int, int, Bitmap.Config),
2、setWidth(int), setHeight(int),
3、setConfig(Bitmap.Config),
4、BitmapFactory.Options.inBitmapreconfigure

(11)public void setHasAlpha(boolean hasAlpha)

  • false:设置bitmap所有的像素不透明
  • true:bitmap的某些像素可能包含透明值。
  • 对于一些配置,这个方法可以忽略,如设置图片RGB_565,RGB_565都不支持像素的透明值。

(12)获取/设置像素值

  • public int getPixel(int x, int y):获取x,y坐标的argb颜色int值。
  • public Color getColor(int x, int y)获取x,y坐标的Color对象。
  • public void setPixel(int x, int y, @ColorInt int color)
  • public void setPixels(@ColorInt int[] pixels,
    int offset,
    int stride,
    int x,
    int y,
    int width,
    int height
    )
3、Bitmap的重要方法createBitmap

从已有Bitmap中创建新的bitmap最常用的方法就是这个方法了,这个方法提供很多重载方法~

但看下方法源码可大致归纳为如下几类:

/*
方法1

根据源bitmap创建一个子bitmap,通过可选矩阵可进行变换。新的bitmap可能是原图的实例或者是原图的copy。

新的位图使用与原始位图相同的密度(density)和颜色空间初始化。

如果源位图是不可变的(immutable),并且请求的子集与源位图本身相同(x,y,weight、height同),则返回源位图,并且不创建新位图。

除了以下情况外返回的位图始终是“可变”的:
(1)方法返回的就是源位图,并且源位图本身就不可变。
(2)源位图是hardware bitmap,也即Config的值是Bitmap.Config.HARDWARE。

参数:
source:源位图
     x:源位图x坐标
     y:源位图y坐标
height:要从源位图copy的行数
 width:每一行的像素数目
     m:要应用到像素上的可选矩阵。
filter:设置为true代表源位图要被过滤。当矩阵包含的操作不仅仅是平移时才适用。
*/
public static Bitmap createBitmap(
                   @NonNull Bitmap source, 
                             int x, 
                             int y, 
                             int width, 
                             int height,
                    @Nullable Matrix m, 
                             boolean filter)
 /*
  方法2
 
  根据给定的宽高创建一个“可变”的Bitmap。

  初始化的密度由DisplayMetrics决定。
  
  新的Bitmap使用sRGB颜色空间。

  参数:
  DisplayMetrics:屏幕相关信息,内部记录了屏幕的大小、密度等信息。
  对象通过context.getResources().getDisplayMetrics()来获取。
  width: 要创建bitmap宽
  height:要创建bitmap高
  config:Config信息
 */
            
 public static Bitmap createBitmap(
                   @Nullable DisplayMetrics display, 
                             int width,
                             int height, 
                             @NonNull Config config)  
/*
方法3:
根据给定的信息返回“可变”的Bitmap

参数:
    display:
      width:
     height:
     config:
   hasAlpha:当Bitmap的配置为ARGB_8888 或者 RGBA_16F时,这个flag可以用于将bitmap
             标记为不透明。这样可以清除黑色而非透明的bitmap。
 ColorSpace:bitmap的颜色空间,如果bitmap设置了Bitmap.Config.RGBA_F16和sRGB,
             或者Linear sRGB,符合的颜色空间会被应用。        
*/
public static Bitmap createBitmap(
                   @Nullable DisplayMetrics display, 
                             int width, 
                             int height,
                   @NonNull Config config, 
                             boolean hasAlpha, 
                   @NonNull ColorSpace colorSpace)
  /*
    方法4:
 
    根据指定的信息,返回一个“不可变”的bitmap。Bitmap的每一个像素值设置为colors数组的值。
    初始化的密度(density)为getDensity的值。新创建的位图位于sRGB颜色空间中。

    参数:
    DisplayMetrics:
            colors:sRGB颜色数组,用于初始化Bitmap的像素。
            offset:颜色数组内要跳过的颜色。
            stride:每行之间数组中的颜色数
             width:bitmap宽
            height:bitmap高
            config:若bitmap设置Config不支持像素的alpha(如RGB_565),此时颜色数组中
            的alpha将会被忽略,alpha值当做FF处理。
  */                 
 public static Bitmap createBitmap(
                   @NonNull DisplayMetrics display,
                   @NonNull @ColorInt int[] colors, 
                            int offset, 
                            int stride,
                            int width, 
                            int height, 
                            @NonNull Config config) 
 /*
   方法5:
   
   通过给定的Picture 资源来创建一个“不可变”的bitmap。当给定的宽、高与Picture的宽高不相同
   时,Picture会缩放到指定的宽高。
  */
 public static @NonNull Bitmap createBitmap(
                   @NonNull Picture source, 
                            int width, 
                            int height,
                   @NonNull Config config) 
4、BitmapFactory

这个是一个比较常用的类,我们一般通过这个api来获取bitmap对象。这个类提供了一些列的
decodeXXX方法来从文件、流、字节数组中来解码获取bitmap对象。常见的如下:

    /*
      1、给定一个文件,从文件中解析出bitmap对象。pathName为空或者解析失败时方法返回null
    */
    public static Bitmap decodeFile(String pathName)
    /*
      2、从指定的 图片资源id中解析出Bitmap对象,图片资源解析失败时返回null
     */
    public static Bitmap decodeResource(Resources res, int id)
    /*
     3、从给定的字节数组中解析出一张Bitmap。解析失败返回null
        offset:偏移量,一般为0表示解析完整Bitmap。
        length:要解析的字节长度,一般为数据总长度。
     */
    public static Bitmap decodeByteArray(byte[] data, int offset, int length) 
    /*
     4、从InputStream中解析出Bitmap对象。一般用于网络请求图片资源直接可获取到
        InputStream对象
     */
    public static Bitmap decodeStream(InputStream is)
    /*
      5、从文件描述符中解析bitmap,解析失败返回null。解析完成后描述符的位置不会改变,
         因此可以多次使用FileDescriptor对象来获取Bitmap对象。
     */
    public static Bitmap decodeFileDescriptor(FileDescriptor fd)

看过BitmapFactory源码我们会发现上述这些方法好多都带Options参数的重载,其实Options也是一个重要的类,在解析bitmap时结合这个类可以达到很好的效果。

5、BitmapFactory.Options

这个类定义了很多常见的属性,在解码Bitmap中发挥着重要作用,常见如下:

  • inSampleSize

采样率,解码器对原始图像进行二次采样,以返回较小的图像来节省内存。其数值一般为正整数n,代表采样后图片宽高都为原来1/n,占用像素为原来1/n²。

如n为1时,表示采样后的图片和原始图片比例一样,n为2时,表示采样后的图片宽高为原来的1/2,占用像素为原来的1/4.

注意inSampleSize值设置为小于1的数值时都会被当做1处理。我们在进行图片缩放时经常会使用到这个字段。

  • inJustDecodeBounds

如果设置为true,解码器不会真正解码位图,所以不用分配内存,但会返回图片的高度宽度信息。

  • inDensity

bitmap的像素密度。一般不用,decodeResource源码中有使用。

  • inTargetDensity

bitmap最终的像素密度,一般不用,decodeResource源码中有使用。

二、图片的下采样压缩

在大多数情况下,官方建议使用 Glide 库获取、解码和显示应用中的位图。在处理这些任务以及与位图和 Android 上的其他图片相关的其他任务时,Glide 会将大部分的复杂工作抽象出来。内部对“如何高效加载大位图”、“缓存位图”、“管理位图内存”做了很好的封装处理,很方便我们直接使用。

但是我们也可选择直接使用 Android 框架中内置的较低级 API,来亲自实现下这些工作,这里就实现下最简单的“如何高效加载大位图”

首先我们可以拍一张大的照片,放到模拟器的指定目录下:

cacheDir.absolutePath+"/1.jpeg"

然后综合上述所了解的api尝试进行宽高缩小为原图的一半:

    private fun scaleAndLoadImage() {
        val options = BitmapFactory.Options()
        // 1、不真正解码位图 ,不真正解码图片,但是会图片宽高信息获得.
        options.inJustDecodeBounds = true
        BitmapFactory.decodeFile(cacheDir.absolutePath+"/1.jpeg", options)
        // 2、获得图片宽高
        val weight = options.outWidth
        val height = options.outHeight
        // 3、 计算缩放比(手动指定比较快捷,我们这里根据手机尺寸计算,具体实现自己而定)
        val scale: Int = calculateScale(weight, height)
        // 4、指定缩放比例,宽高都缩放为原图1/2
        options.inSampleSize = 2
        // 5、真正解码  按照缩放比.
        options.inJustDecodeBounds = false
        val bitmap = BitmapFactory.decodeFile(cacheDir.absolutePath+"/1.jpeg", options)
        image.setImageBitmap(bitmap)
    }

这样就成功的缩放了一张图片~

我们还知道,宽高会缩放到原来的1/2,则像素数缩小为原来1/4,不妨一并验证下:

  // 直接获取原图bitmap,打印log:
  Log.i(TAG, "before scale ByteCount:${bitmap.byteCount/1024/1024}M")
  // 宽高缩放为原来1/2打印log:
  Log.i(TAG, "after scale ByteCount:${bitmap.byteCount/1024/1024}M")

  // log:
  I/MainActivity: before scale ByteCount:61M
  I/MainActivity: after scale ByteCount:15M

可见像素数变2为了原来1/4,所占内存也是约为原来1/4.

缩放比的值一般根据相应的条件进行计算,如下以手机屏幕宽高为栗子计算:

    /**
     * 根据屏幕尺寸宽高与图片宽高比例进行计算缩放比。
     * @param weight 图片宽
     * @param height 图片高
     * @return 缩放比例
     * @function 计算缩放比例
     */
    private fun calculateScale(weight: Int, height: Int): Int {
        // 获得手机宽高
        val wm = getSystemService(WINDOW_SERVICE) as WindowManager
        val display: Display = wm.defaultDisplay
        val point = Point()
        display.getSize(point)
        val phoneWidth: Int = point.x
        val phoneHeight: Int = point.y
        //默认缩放比为1
        var scale = 1
        val scaleX = weight / phoneWidth
        val scaleY = height / phoneHeight
        // 原图宽高大于手机宽高则进行缩放,缩放值取计算结果较大的。
        if (scaleX >= scaleY && scaleX > scale) {
            scale = scaleX
        }
        if (scaleY > scaleX && scaleY > scale) {
            scale = scaleY
        }
        return scale
    }

三、图片的质量压缩

Bitmap的下采样是为了减小位图所占运行内存,下采样是以减少像素个数来减少所占运行内存,当需要减少图片所占磁盘大小时就需要进行质量压缩了。

前面也了解到了Bitmap#compress方法,这里就实践下质量压缩,还是上述下采样所使用的图片,来我们通过模拟器看下所占磁盘内存:
在这里插入图片描述

接下来进行质量压缩:

        // 原图
        val bitmap = BitmapFactory.decodeFile(cacheDir.absolutePath + "/1.jpeg")
        val compressFile = File(cacheDir.absolutePath + "/2.jpeg")
        if (!compressFile.exists()) compressFile.createNewFile()
        FileOutputStream(compressFile).use { fos ->
            //压缩文件,通过输出流缓存到本地
            bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos)
        }

在这里插入图片描述

可见质量缩设置为50时,文件所占磁盘内存大大缩减~

小结

本节有几个知识点需要留意

1、Bitmap#Config类中常见的几个常量代表含义
2、Bitmap#CompressFormat类中几个常量的含义
3、质量压缩与下采样压缩的区别
  • 质量压缩是减小文件所占磁盘的内存,下采样压缩是减小Bitmap所占运行内存大小。
  • 质量压缩是通过算法扣掉一些像素附近的相近像素,从而达到减小文件大小的目的。注意质量压缩影响的是文件,对加载到内存中的bitmap是无法影响的。因为Bitmap在内存中的大小是按照像素计算的(宽高总像素以及所占字节)。对于质量压缩并不会改变图片的真实像素。

例如质量压缩:

AB
CD
压缩后
AA
AA
虽然AAAA和ABCD都是占四个像素的位置。但是保存文件时可以通过算法描述A为同样时只保存一个A的信息。这样就减少了文件的磁盘质量。

参考

官方文档
Android Bitmap(位图)详解

The end

有了这些基础,后续就可以继续深入了解Bitmap的各种特效处理、图片处理框架的研究了。继续加油 💪🏻

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值