Android Bitmap 是谁?从哪里来?到哪里去?

2,834 阅读8分钟

1.何为bitmap?

2.开发中bitmap遇到的那些问题

3.bitmap几种压缩方式

4.android中加载大图片的正确方式

5.Android Skia 图像引擎

1.何为bitmap?

我们可以称之为位图,是一种存储像素的数据结构,通过这个对象我们可以获取到一系列和图片相关的属性, 并且可以对图像进行处理,比如切割,放大等等,相关操作。

1.1怎么计算一张图片占用的内存大小?

bitmap 在内存空间中所占用的内存计算是这样的:

bitmap 的宽 x 高 x 每个像素所占的字节

其中每个像素占用的字节可以通过Bitmap.Config动态配置

Config每个像素占用的字节说明
ALPHA_81 bytes每个像素仅仅储存透明度通道
RGB_5652 bytes每个像素的RGB通道会保存,透明度不会保存,红色通道5位,有2^5 =32种表现形式,绿色通道6位,有2^6 =64种表现形式;蓝色通道5位,有2^5=32种表现形式
ARGB_44442 bytes每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道4位,有2^4=16种表现形式
ARGB_88884 bytes每个像素的ARGB通道都会保存,透明度/红色/绿色/蓝色通道8位,有2^8=256种表现形式

有什么区别呢?最简单的,当一个颜色表现形式越多,那么画面整体的色彩就会更丰富,图片质量就会越高,当然,图片占用的储存空间也越大。

1.2图片存在形式

1.文件形式(即以二进制形式存在于硬盘上)

2.流的形式(即以二进制形式存在于内存中)

3.Bitmap形式

这三种形式的区别: 文件形式和流的形式对图片体积大小并没有影响,也就是说,如果你手机SD卡上的如果是100K,那么通过流的形式读到内存中,也一定是占100K的内存,注意是流的形式,不是Bitmap的形式,当图片以Bitmap的形式存在时,其占用的内存会瞬间变大, 我做分享的时候一个9.9M的图片保存在手机相册中显示是238KB,80M的图片在手机相册中显示是1.24M。

2.开发中bitmap遇到的问题

在Android的开发中,我们经常回去处理一些图片相关的问题,比如当加载图片到内存中产生的OOM(OutOfMemory)异常、图片太大压缩造成失真,图片不显示,图片压缩之后出现黑色,分享图片渐变色出现色块等。

3.bitmap几种压缩方式

bitmap 的宽 x 高 x 每个像素所占的字节 从这个公式可以看出想要压缩图片大小有两种方式:

1.减小图片的长宽
2.减小图片每个像素占用的字节数

3.1质量压缩

bitmap.compress(CompressFormat format, int quality, OutputStream stream);

图片格式说明
PNG它是一种无损数据压缩位图图形文件格式。这也就是说PNG 只支持无损压缩。对于PNG 格式是有8 位、24位、32位的三种形式的。区别就是对透明度的支持。
JPG其实就是 JPEG的另一种叫法
JPEG它是一种有损压缩的图片格式
WEBPGoogle 开发出的一种支持alpha 通道的有损压缩和无损压缩。同等质量情况下比 JPEG和PNG小 25%~45%.WebP格式图像的编码时间“比JPEG格式图像长8倍”(占用cpu,节省内存
GIF它是动态图片的一种格式,和PNG 一样是一种无损压缩。
SVG是一种无损、矢量图(放大不失真)

WEBP的体积对比

图片的格式有很多种,除了我们熟知的 JPG、PNG、GIF,还有Webp,BMP,TIFF,CDR 等等几十种,用于不同的场景或平台。

图片常见格式

这些格式可以分为两大类:有损压缩和无损压缩。

1.有损压缩:是对图像数据进行处理,去掉那些图像上会被人眼忽略的细节,然后使用附近的颜色通过渐变或其他形式进行填充。这样既能大大降低图像信息的数据量,又不会影响图像的还原效果。最典型的有损压缩格式是 jpg。

2.无损压缩:先判断图像上哪些区域的颜色是相同的,哪些是不同的,然后把这些相同的数据信息进行压缩记录,(例如一片蓝色的天空之需要记录起点和终点的位置就可以了),而把不同的数据另外保存(例如天空上的白云和渐变等数据)。常见的无损压缩格式是 png,gif。

Android 原生支持的格式只有 JPEG、PNG、GIF、WEBP(android 4.0 加入)、BMP。而在android层代码中我们只能调用的编码方式只有PNG、JPEG、和WEBP 三种。不过目前来说android 还不支持对GIF 这种的动态编码。

注意 :我们日常所有的 .png、.jpg、.jpeg 等等指的是图像数据经过压缩编码后在媒体上的封存形式,是不能和PNG 、JPG、JEPG 混为一谈的。

quality=100

quality=50

通过此种方式,图片的大小是没有变的,因为质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。

3.2尺寸压缩

3.2.1邻近采样

BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; //inSampleSize 为压缩比 此处为1/2

bm = BitmapFactory.decodeFile("/DCIM/Camera/test.jpg", options);

采样前:

采样前

采样后:

采样后

接着我们来看看 inSampleSzie 的官方描述:

If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

从官方的inSampleSzie描述看我们可以看到 x(x 为 2 的倍数)个像素最后对应一个像素,由于采样率设置为 1/2,所以是两个像素生成一个像素。邻近采样的方式比较粗暴,直接选择其中的一个像素作为生成像素,另一个像素直接抛弃,这样就造成了图片变成了纯绿色,也就是红色像素被抛弃。

3.2.2双线性采样

1.Matrix matrix = new Matrix(); matrix.setScale(0.5f, 0.5f); bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);

2.Bitmap.createScaledBitmap(bitmapOld, 150, 150, true);

采样前:

采样前

采样后:

采样后

可以看到处理之后的图片不是像邻近采样一样纯粹的一种颜色,而是两种颜色的混合。双线性采样使用的是双线性內插值算法,这个算法不像邻近点插值算法一样,直接粗暴的选择一个像素,而是参考了源像素相应位置周围 2x2 个点的值,根据相对位置取对应的权重,经过计算之后得到目标图像。

3.2.3邻近采样和双线性采样对比

邻近采样的方式是最快的,因为它直接选择其中一个像素作为生成像素,但是生成的图片可能会相对比较失真,产生比较明显的锯齿,最具有代表性的就是处理文字比较多的图片在展示效果上的差别,对比:

原图

邻近采样

双线性采样对比

3.3像素压缩

BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; //将格式设置成RGB_565 bm = BitmapFactory.decodeFile( "/DCIM/Camera/test.jpg", options);

 注意:由于ARGB_4444的画质惨不忍睹,一般假如对图片没有透明度要求的话,可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。

4.Android中如何加载大图片和长图片

如果我们要加载的图片远远大于ImageView的大小,直接用ImageView去展示的话,就会带来不好的视觉效果,也会占用太多的内存和性能开销。甚至这张图片足够大到导致程序oom崩溃

1.压缩

2.局部展示

有时候我们通过压缩可以取得很好的效果,但有时候效果就不那么美好了,例如长图像清明上河图,像这类的长图,如果我们直接压缩展示的话,这张图完全看不清,很影响体验。这时我们就可以采用局部展示,然后滑动查看的方式去展示图片。

public class LargeImageView extends View implements GestureDetector.OnGestureListener {

int bitmapLeft;
Paint paint;
private BitmapRegionDecoder mDecoder;

/**
 * 绘制的区域
 */
public volatile Rect mRect = new Rect();

//    private int mScaledTouchSlop;

// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
/**
 * 图片的宽度和高度
 */
public int mImageWidth, mImageHeight;
private GestureDetector mGestureDetector;
private BitmapFactory.Options options;


public LargeImageView(Context context) {
    this(context, null);
}

public LargeImageView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}

public LargeImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

private void init(Context context) {
    options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_4444;


 //        mScaledTouchSlop = ViewConfiguration.get(getContext())
//                .getScaledTouchSlop();
    //初始化手势控制器
    mGestureDetector = new GestureDetector(context, this);
    paint = new Paint();
    paint.setColor(Color.TRANSPARENT);
    paint.setAntiAlias(true);
}

/**
 * setInputStream里面去获得图片的真实的宽度和高度,以及初始化我们的mDecoder。
 *
 * @param is
 */
public void setInputStream(InputStream is) {
    try {
        mDecoder = BitmapRegionDecoder.newInstance(is, false);
        BitmapFactory.Options bfOptions = new BitmapFactory.Options();
        //设置为true后。不加载到内存就能获取Bitmap尺寸大小。
        bfOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(is, null, bfOptions);

        mImageWidth = mDecoder.getWidth();
        mImageHeight = mDecoder.getHeight();

        requestLayout();
        invalidate();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                is.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


@Override
public boolean onTouchEvent(MotionEvent ev) {
    //把触摸事件交给手势控制器处理
    return mGestureDetector.onTouchEvent(ev);
}


@Override
public boolean onDown(MotionEvent e) {
    mLastX = (int) e.getRawX();
    mLastY = (int) e.getRawY();
    return true;
}

@Override
public void onShowPress(MotionEvent e) {

}

@Override
public boolean onSingleTapUp(MotionEvent e) {

    return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    int x = (int) e2.getRawX();
    int y = (int) e2.getRawY();
    move(x, y);

    return true;
}

/**
 * 移动的时候更新图片显示的区域
 *
 * @param x
 * @param y
 */
private void move(int x, int y) {

    boolean isInvalidate = false;

    int deltaX = x - mLastX;
    int deltaY = y - mLastY;
    //如果图片宽度大于屏幕宽度
    if (mImageWidth > getWidth()) {
        //移动rect区域
        mRect.offset(-deltaX, 0);
        //检查是否到达图片最右端
        if (mRect.right > mImageWidth) {
            mRect.right = mImageWidth;
            mRect.left = mImageWidth - getWidth();
        }

        //检查左端
        if (mRect.left < 0) {
            mRect.left = 0;
            mRect.right = getWidth();
        }
        isInvalidate = true;

    }
    //如果图片高度大于屏幕高度
    if (mImageHeight > getHeight()) {
        mRect.offset(0, -deltaY);

        //是否到达最底部
        if (mRect.bottom > mImageHeight) {
            mRect.bottom = mImageHeight;
            mRect.top = mImageHeight - getHeight();
        }

        if (mRect.top < 0) {
            mRect.top = 0;
            mRect.bottom = getHeight();
        }
        isInvalidate = true;

    }

    if (isInvalidate) {
        invalidate();
    }

    mLastX = x;
    mLastY = y;
}

@Override
public void onLongPress(MotionEvent e) {
    mLastX = (int) e.getRawX();
    mLastY = (int) e.getRawY();
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    int x = (int) e2.getRawX();
    int y = (int) e2.getRawY();
    move(x, y);
    return true;
}


@SuppressLint("CheckResult")
@Override
protected void onDraw(final Canvas canvas) {
    //显示图片
    if (null != mDecoder) {
        Bitmap bm = mDecoder.decodeRegion(mRect, options);
        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        canvas.drawBitmap(bm, bitmapLeft, 0, null);
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int width = getMeasuredWidth();
    int height = getMeasuredHeight();

    bitmapLeft = width / 2 - mImageWidth / 2;//图片距离左边大小
    mRect.left = 0;
    mRect.top = 0;
    mRect.right = mImageWidth;
    mRect.bottom = mRect.top + height;
}
}

根据上述源码:

在setInputStream方法里面初始BitmapRegionDecoder,获取图片的实际宽高; onMeasure方法里面给Rect赋初始化值,控制开始显示的图片区域; onTouchEvent监听用户手势,修改Rect参数来修改图片展示区域,并且进行边界检测,最后invalidate; 在onDraw里面根据Rect获取Bitmap并且绘制。

5、Android Skia 图像引擎

Skia 是一个 Google 自己维护的 c++ 实现的图像引擎,实现了各种图像处理功能,并且广泛地应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等),基于它可以很方便为操作系统、浏览器等开发图像处理功能。

Skia 在 Android 中提供了基本的画图和简单的编解码功能,可以挂接其他的第三方编码解码库或者硬件编解码库,例如 libpng 和 libjpeg,libgif 等等。因此,这个函数调用bitmap.compress(Bitmap.CompressFormat.JPEG...),实际会调用 libjpeg.so 动态库进行编码压缩。

最终 Android 编码保存图片的逻辑是 Java 层函数→Native 函数→Skia函数→对应第三库函数(例如 libjpeg)。所以 skia 就像一个胶水层,用来链接各种第三方编解码库,不过 Android 也会对这些库做一些修改,比如修改内存管理的方式等等。

Android 在之前从某种程度来说使用的算是 libjpeg 的功能阉割版,压缩图片默认使用的是 standard huffman,而不是 optimized huffman,也就是说使用的是默认的哈夫曼表,并没有根据实际图片去计算相对应的哈夫曼表,Google 在初期考虑到手机的性能瓶颈,计算图片权重这个阶段非常占用 CPU 资源的同时也非常耗时,因为此时需要计算图片所有像素 argb 的权重,这也是 Android 的图片压缩率对比 iOS 来说差了一些的原因之一。

图片格式

阿里图片库

压缩图片工具类

图好快

在线抠图

为什么Android的图片质量会比iPhone的差?

Android中的图片压缩技术详解