Android图片系列-2.Android App图片压缩、裁剪分析整理

移动端常用的图片格式有PNG和JPEG,目前ios手机和大部分安卓手机拍照生成的图片默认格式都是JPEG。我们开发APP的时候通常使用的是PNG,这可能是考虑到图片质量效果。PNG图片是无损压缩格式,JPEG是有损压缩,PNG格式相比JPEG占用空间要大很多。2010年GOOGLE开发了一种新的图片压缩格式WEBP,可以将图片空间压缩的更小,平均是JPEG的2/3,Android从4.0开始默认支持WEBP格式。关于JPEG、PNG网上有很多介绍,本文主要介绍自己在实际开发中遇到问题处理方法的一些总结经验。

一 图片格式

在电脑中,JPEG(发音为jay-peg, IPA:[ˈdʒeɪpɛg])是一种针对照片视频而广泛使用的一种有损压缩标准方法。这个名称代表Joint Photographic Experts Group(联合图像专家小组)。此团队创立于1986年,1992年发布了JPEG的标准而在1994年获得了ISO 10918-1的认定。JPEG与视频音频压缩标准的MPEG(Moving Picture Experts Group)很容易混淆,但两者是不同的组织及标准。
JPEG格式介绍

便携式网络图形(Portable Network Graphics,PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。PNG的开发目标是改善并取代GIF作为适合网络传输的格式而不需专利许可,所以被广泛应用于互联网及其他方面上。
PNG格式介绍

二 图片压缩

1.1 压图片

压图片是指图片占用存储空间和图片质量的压。记得以前做手机rom的时候,为了缩减app包的体积,我们会使用工具比如tinypng对UI工程师给的PNG图片进行压缩,平均下来可以压缩百分之三四十,这与图片本身的色彩有关系。

IOS压图片

UIImageJPEGRepresentation压缩图片,压缩率用1图片处理后占用的存储空间会变大,使用0.8的时候大小与图片原来存储大小接近。看到一些分析,图片压缩率根据场景需要自己来配置,建议参考范围是0.6~1。如果压缩率太小,图片质量会非常糟糕。开始使用阿里云OSS存储的时候发现这个问题,以为是阿里SDK造成的,还给阿里提了一个工单,前后沟通了一个多月,阿里的售后坚持说他们的SDK没有这个问题,最终不了了之。

压缩次数 图片大小
0 1M
1 4M
2 4.8M
3 5M

表1.1.1 UIImageJPEGRepresentation使用压缩率1处理图片

ANDROID压图片

Android 使用Bitmap来处理图片,先来看下bitmap的压缩方法compress,代码如下:

//Bitmap.java
/** 
    * Write a compressed version of the bitmap to the specified outputstream. 
    * If this returns true, the bitmap can be reconstructed by passing a 
    * corresponding inputstream to BitmapFactory.decodeStream(). Note: not 
    * all Formats support all bitmap configs directly, so it is possible that 
    * the returned bitmap from BitmapFactory could be in a different bitdepth, 
    * and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque 
    * pixels). 
    * 
    * @param format   The format of the compressed image 
    * @param quality  Hint to the compressor, 0-100. 0 meaning compress for 
    *                 small size, 100 meaning compress for max quality. Some 
    *                 formats, like PNG which is lossless, will ignore the 
    *                 quality setting 
    * @param stream   The outputstream to write the compressed data. 
    * @return true if successfully compressed to the specified stream. 
*/
compress(CompressFormat format, int quality, OutputStream stream)
  1. CompressFormat支持三种格式PNG、JPG、WEBP
  2. quality 取值范围0~100,100代表最小压缩,图片保存质量最高。
  3. 经过实测,将PNG或者JPG图片压缩成WEBP非常耗时,对于实时处理需求的场景不适合通过此方法来达到减小图片占用空间的目的。

另外,Bitmap还提供了一个内部类Config

 /**
     * Possible bitmap configurations. A bitmap configuration describes
     * how pixels are stored. This affects the quality (color depth) as
     * well as the ability to display transparent/translucent colors.
     */
    public enum Config {
        // these native values must match up with the enum in SkBitmap.h

        /**
         * Each pixel is stored as a single translucency (alpha) channel.
         * This is very useful to efficiently store masks for instance.
         * No color information is stored.
         * With this configuration, each pixel requires 1 byte of memory.
         */
        ALPHA_8     (2),

        /**
         * Each pixel is stored on 2 bytes and only the RGB channels are
         * encoded: red is stored with 5 bits of precision (32 possible
         * values), green is stored with 6 bits of precision (64 possible
         * values) and blue is stored with 5 bits of precision.
         * 
         * This configuration can produce slight visual artifacts depending
         * on the configuration of the source. For instance, without
         * dithering, the result might show a greenish tint. To get better
         * results dithering should be applied.
         * 
         * This configuration may be useful when using opaque bitmaps
         * that do not require high color fidelity.
         */
        RGB_565     (4),

        /**
         * Each pixel is stored on 2 bytes. The three RGB color channels
         * and the alpha channel (translucency) are stored with a 4 bits
         * precision (16 possible values.)
         * 
         * This configuration is mostly useful if the application needs
         * to store translucency information but also needs to save
         * memory.
         * 
         * It is recommended to use {@link #ARGB_8888} instead of this
         * configuration.
         *
         * Note: as of {@link android.os.Build.VERSION_CODES#KITKAT},
         * any bitmap created with this configuration will be created
         * using {@link #ARGB_8888} instead.
         * 
         * @deprecated Because of the poor quality of this configuration,
         *             it is advised to use {@link #ARGB_8888} instead.
         */
        @Deprecated
        ARGB_4444   (5),

        /**
         * Each pixel is stored on 4 bytes. Each channel (RGB and alpha
         * for translucency) is stored with 8 bits of precision (256
         * possible values.)
         * 
         * This configuration is very flexible and offers the best
         * quality. It should be used whenever possible.
         */
        ARGB_8888   (6);

        final int nativeInt;

        @SuppressWarnings({"deprecation"})
        private static Config sConfigs[] = {
            null, null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
        };

        Config(int ni) {
            this.nativeInt = ni;
        }

        static Config nativeToConfig(int ni) {
            return sConfigs[ni];
        }
    }

从代码看到bitmap包括ALPHA_8(8位)、ALPHA_565(16位)、ALPHA_4444(16位)、ALPHA_8888(32位)四种配置色彩。bitmap是一种使用像素点来表达图片的存储方式,一张图片分辨率是3776 * 2520,图片色彩模式是ARGB 32位,那么图片加载的时候消耗的内存空间:3776 * 2520 * 4byte = 38062080byte = 38M。bitmap是非压缩存储格式,因此占用内存空间会非常大。所以通过不同的色彩配置也可以达到图片压缩的效果,比如选择ALPHA_565(16位),图片占用内存空间就会比32位减小一半,保存成jpg格式的时候也会小。

1.2 图片缩

图片缩是指图片分辨率的裁剪。图片的裁剪使用场景很多:
1. listview ,recycleview加载图片列表,此时应该使用缩略图。当点击查看item的时候才呈现大图。
2. 假设移动设备分辨率只有1024X600,设备端应用分配的堆内存是64M。存储的图片分辨率是3776 * 2520,图片色彩模式是ARGB 32位,那么图片加载的时候消耗的内存空间38M。这个数值已经快接近该应用能够分配的最大内存空间了,如果图片分辨率再高一些,就会直接报内存溢出的错误提示。因此在加载之前,我们可能需要针对设备配置对图片进行裁剪。在下文会谈到的开源图片加载库Android Universal Imageloader里面就做了类似处理,代码里面有限制加载的图片分辨率如果长或者宽大于等于2048,就会把这个图片分辨率减小。

/**
 * UIL 图片裁剪
 */
public final class ImageSizeUtils {
    //设置最大值为2048,超过这个值就会将分辨率缩小
    private static final int DEFAULT_MAX_BITMAP_DIMENSION = 2048;

    private static ImageSize maxBitmapSize;

    static {
        int[] maxTextureSize = new int[1];
        GLES10.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, maxTextureSize, 0);
        int maxBitmapDimension = Math.max(maxTextureSize[0], DEFAULT_MAX_BITMAP_DIMENSION);
        maxBitmapSize = new ImageSize(maxBitmapDimension, maxBitmapDimension);
    }

...

//ImageLoader.java
//可以使用UIL加载指定大小的图片,具体的参数由ImageSize来控制。
    public void loadImage(String uri, ImageSize targetImageSize, ImageLoadingListener listener) {
        loadImage(uri, targetImageSize, null, listener, null);
    }

3 . 比如我们做的电子相框,显示图片的时候由于图片的分辨率长宽比与相框设备分辨率长宽比不一样,会导致图片无法全屏展示。如果我们想要实现全屏显示图片,就需要对图片进行裁剪,裁剪思路就是将图片裁成一个长宽比与设备长宽比一致。如果是针对竖屏显示的设备,方法是相通的。

//将图片裁剪成与设备长宽比一致。裁剪方法是针对横屏显示设备只裁剪宽大于等于高的图片,因为如果对高大于宽的图片也裁剪,会使得图片内容变化较大。因此,我们这里的裁剪算是一种优化,比如一张4:3的图要显示在16:9的设备上,下面的处理就可以将图片优化成全屏显示。
public Bitmap imageCrop(Bitmap bitmap) {
        boolean deviceLand = DEVICE_WIDTH >DEVICE_HEIGHT;
        float w = bitmap.getWidth(); // 得到图片的宽,高
        float h = bitmap.getHeight();
        boolean bitmapLand = w>=h;
        float retX = 0;
        float retY = 0;
        //先处理横屏设备,横屏图片
        if(deviceLand && bitmapLand){
            float deviceRatio = Constants.DEVICE_WIDTH / Constants.DEVICE_HEIGHT;
            float bitmapRatio = w / h ;
            if (bitmapRatio > deviceRatio){
                float cropWidth = h * deviceRatio;
                retX = (w - (int)cropWidth)/2;
                Bitmap cropBitmap = Bitmap.createBitmap(bitmap, (int)retX, (int)retY, (int)cropWidth, (int)h, null, false);
                return cropBitmap;
            }else {
                float cropHeight = w / deviceRatio;
                retY = (h - (int)cropHeight)/2;
                Bitmap cropBitmap = Bitmap.createBitmap(bitmap, (int)retX, (int)retY, (int)w, (int)cropHeight, null, false);
                return cropBitmap;
            }
        }else if(!deviceLand && !bitmapLand){

        }
        return bitmap;
    }

安卓开源有一个很不错的图片裁剪控件,是知名公司yalantis的作品。ucrop图片裁剪库

三 图片格式转换

既然WEBP格式最小,那么在对图片清晰度没有非常高的要求下,为了节省带宽流量,移动端可以在上传的时候将PNG和JPEG格式的图片转换为WEBP,但是实际使用过程发现在移动端转换成WEBP格式图片的耗时非常久,体验不好,因此如果这种方式并不能通用。服务器也可以在下发给移动端图片的时候将PNG和JPEG格式的图片转换为WEBP,目前阿里云已经开通了这个服务,不过是收费的,但是费用相比因为流量减小节省的成本还是非常划算的。

阿里云图片处理服务 提供了图片格式转换、图片裁剪、图片旋转、图片效果等在线api服务。

四 图片的方向

现在苹果手机、一些安卓手机自带的相机应用,拍照的时候不管手机方向怎么旋转,拍出来的图片用相册预览的时候发现都是正的。使用数码相机或者手机倒着拍一些照片,然后将图片导入windows7系统,可以看到倒着的图片,导入windows10系统图片预览的时候全是正的。这说明图片内部存储了方向这个信息,图片浏览器在展示图片的时候可以选择是否依据图片方向的值对图片进行校正。

可交换图像文件格式常被简称为Exif(Exchangeable image file format),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。
Exif最初由日本电子工业发展协会在1996年制定,版本为1.0。1998年,升级到2.1,增加了对音频文件的支持。2002年3月,发表了2.2版。
Exif可以附加于JPEG、TIFF、RIFF等文件之中,为其增加有关数码相机拍摄信息的内容和索引图或图像处理软件的版本信息。
Windows 7操作系统具备对Exif的原生支持,通过鼠标右键点击图片打开菜单,点击属性并切换到详细信息标签下即可直接查看Exif信息。
Exif信息是可以被任意编辑的,因此只有参考的功能。
Exif信息以0xFFE1作为开头标记,后两个字节表示Exif信息的长度。所以Exif信息最大为64 kB,而内部采用TIFF格式
WIKI

//Android读取图片方向信息
public int readPictureDegree(String imagePath) {
        int imageDegree = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(imagePath);
            int orientation = exifInterface.getAttributeInt(
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
            case ExifInterface.ORIENTATION_ROTATE_90:
                imageDegree = 90;
                break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                imageDegree = 180;
                break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                imageDegree = 270;
                break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return imageDegree;
    }

    //根据图片方向信息旋转图片
    public Bitmap rotaingImageView(int angle, Bitmap mBitmap) {
        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        Bitmap b = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(),
                mBitmap.getHeight(), matrix, true);
        return b;
    }

五 安卓开源的图片加载框架

安卓图片处理框架,目前我只用过UIL。最近准备把另外三个框架学习一下,毕竟UIL已经停止维护了,要跟着新技术的脚步,是时候切换一下新的生产力工具了。毕竟从说明来看,fresco在内存OOM问题上表现的更为出色。拭目以待。

Memory
A decompressed image - an Android Bitmap - takes up a lot of memory. This leads to more frequent runs of the Java garbage collector. This slows apps down. The problem is especially bad without the improvements to the garbage collector made in Android 5.0.
On Android 4.x and lower, Fresco puts images in a special region of Android memory. It also makes sure that images are automatically released from memory when they’re no longer shown on screen. This lets your application run faster - and suffer fewer crashes.
Apps using Fresco can run even on low-end devices without having to constantly struggle to keep their image memory footprint under control.

图片系列链接:Android图片系列-1.如何选择图片加载框架

  1. UIL Universal ImageLoader
  2. Picasso
  3. Glide
  4. Fresco

参考:

  1. 阿里云图片处理服务
  2. EXIF WIKI文档
  3. JPEG格式介绍
  4. PNG格式介绍
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章