Android图片压缩终结篇

先发一张昨天去看我雷哥演唱会的皂片然后再说正文哈哈。


qq_pic_merged_1506931428106.jpg

简介

由于工作原因,boss下达的任务就大概说了对图片进行压缩寻找比较合理的方式,还举了一个项目中的坑,就是系统原生的Bitmap.compress设置质量参数为100生成图片会变大的坑。所以我打算用一点时间研究研究Bitmap在内存和外存中的情况。
首先需要对图片进行压缩,大家都知道图片是Android里面一个大坑,具体的问题有

  • oom,一不留神就用OOM来冲冲喜,所以网上就有了很多解决oom问题的建议,但是由于网友的水平不一也导致建议参差不齐。(内存)
  • 图片压缩再加载失真严重,或者压缩率不够达不到项目要求的效果。(外存)

那我今天就要解决的就是通过今天查阅的资料和自己的判断,还有实践归档一下图片在Android上的问题。并且给出自己解决图片压缩问题的解决方案和实际操作。

1. 为什么Android上的图片就不如IOS上的?

我在全球男性交友中心找到了一个外国基友写的一段情书:
gihubLink

There are so many comparations between Android phone and iPhone. We cannot make the conclusion about which one is better, but we all knows that the image quality of Android phone is much worse than iPhone. No matter you are using Facebook, Twitter or even Instagram, after taking the photo, adding a filter, then sharing to the social network, the images produced by Android phone are always coarse. Why?

Our team had been working on this issue in the last year. After very deep research, we found that this was a "TINY" mistake made by Google. Although tiny, but the influence was very huge (all Android Apps related to image), and lasted till today.

The problem is : libjpeg.

We all know that libjpeg is widely used open source JPEG library. Android also uses libjpeg to compress images. After digging into the source code of Android, we can find that instead of using libjpeg directly, Android is based on an open source image engine called Skia. The Skia is a wonderful engine maintained by Google himself, all image functions are implemented in it, and it is widely used by Google and other companies' products (e.g.: Chrome, Firefox, Android......). The Skia has a good encapsulation of libjpeg, you can easily develop image utilites base on this engine.

When using libjpeg to compress images, optimize_coding is a very important parameter. In libjpeg.doc, we can find following introductions about this parameter:

boolean optimize_coding
    TRUE causes the compressor to compute optimal Huffman coding tables
    for the image.  This requires an extra pass over the data and
    therefore costs a good deal of space and time.  The default is
    FALSE, which tells the compressor to use the supplied or default
    Huffman tables.  In most cases optimal tables save only a few percent
    of file size compared to the default tables.  Note that when this is
    TRUE, you need not supply Huffman tables at all, and any you do
    supply will be overwritten.

As the libjpeg.doc, we now know that because setting the optimize_coding to TRUE may cost a good deal of space and time, the default in libjpeg is FALSE.

Everything seems fine about the doc, and libjpeg is very stable. But many people ignored that this document was writen for more than 10 years. At that time, space and computing abilities are very limited. With today's modern computers or even mobile phones, this is not an issue. On the contrary, we should pay more attention to the image quality (retina screens) and image size (cloud services).

Google's engineers of skia project did not set this parameter, so the optimize_coding in Skia was remained to FALSE as the default value, and Skia concealed this setting, you could not change the setting outside of Skia. This became to a big problem, we had to endure worse image and bigger file size.

Our team had tested optimize_coding for many different images. If you want the same quality of image compressing, the file size are 5-10 times bigger when setting the optimze_coding to FALSE than to TRUE. The difference is quite significant.

We also compared the jpeg compressing between iOS and Android (they both concealed the optimize_coding parameter). With the same original images, if you want same quality level, you need 5-10 times file size on Android.

The result is clear, Apple does know the importance of optimize_coding and Huffman tables and Google does not. (Apple uses their own Huffman table algorithm, not like libjpeg or libjpeg-turbo. It seems that Apple has done more tuning works on image compressing.)

Finally, we decided not to use JPEG compress functions provided by Android, and we compiled our own native library based on libjpeg-turbo (libjpeg-turbo also has performance improvements). Now we can save 5-10 times of image space and enjoy the same or even better image quality. This work is totally worth to do.

Thanks for reading, :)

说的大概意思是:
libjpeg是广泛使用的开源JPEG图像库,安卓也依赖libjpeg来压缩图片。但是安卓并不是直接封装的libjpeg,而是基于了另一个叫Skia的开源项目来作为的图像处理引擎。Skia是谷歌自己维 护着的一个大而全的引擎,各种图像处理功能均在其中予以实现,并且广泛的应用于谷歌自己和其它公司的产品中(如:Chrome、Firefox、 Android等)。Skia对libjpeg进行了良好的封装,基于这个引擎可以很方便为操作系统、浏览器等开发图像处理功能。
libjpeg在压缩图像时,有一个参数叫optimize_coding,关于这个参数,libjpeg.doc有如下解释:

就是上面那个解释optimize_coding这段

这段话大概的意思就是如果设置optimize_coding为TRUE,将会使得压缩图像过程中基于图像数据计算哈弗曼表(关于图片压缩中的哈弗曼表,请自行查阅相关资料),由于这个计算会显著消耗空间和时间,默认值被设置为FALSE。

谷歌的Skia项目工程师们最终没有设置这个参数,optimize_coding在Skia中默认的等于了FALSE,这就意味着更差的图片质量和更大的图片文件,而压缩图片过程中所耗费的时间和空间其实反而是可以忽略不计的。那么,这个参数的影响究竟会有多大呢?
经我们实测,使用相同的原始图片,分别设置optimize_coding=TRUE和FALSE进行压缩,想达到接近的图片质量(用Photoshop 放大到像素级逐块对比),FALSE时的图片大小大约是TRUE时的5-10倍。换句话说,如果我们想在FALSE和TRUE时压缩成相同大小的JPEG 图片,FALSE的品质将大大逊色于TRUE的(虽然品质很难量化,但我们不妨说成是差5-10倍)。

什么意思呢?意思就是现在设备发达啦,是时候将optimize_coding设置成true了,但是问题来了,Android系统代码对于APP来说修改不了,我们有没有什么办法将这个参数进行设置呢?答案肯定是有的,那就是自己使用自己的so库,不用系统的不就完了。

分析源码

既然外国基友都说了是Android系统集成了这个库,但是参数没设置好,咱也不明白为啥Android就是不改...但是我们也得验证一下外国基友说的对不对是吧。

那我们就从Bitmap.compress这个方法说起

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

这个方法进行质量压缩,而且可能失去alpha精度

public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        // do explicit check before calling the native method
        if (stream == null) {
            throw new NullPointerException();
        }
        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }
        return nativeCompress(mNativeBitmap, format.nativeInt, quality,
                              stream, new byte[WORKING_COMPRESS_STORAGE]);
    }

我们看到quality只能是0-100的值

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
                            int format, int quality,
                            jobject jstream, jbyteArray jstorage) {
    SkImageEncoder::Type fm;  //创建类型变量
    //将java层类型变量转换成Skia的类型变量
    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return false;
    }
    //判断当前bitmap指针是否为空
    bool success = false;
    if (NULL != bitmap) {
        SkAutoLockPixels alp(*bitmap);

        if (NULL == bitmap->getPixels()) {
            return false;
        }

    //创建SkWStream变量用于将压缩后的图片数据输出
        SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
        if (NULL == strm) {
            return false;
        }
    //根据编码类型,创建SkImageEncoder变量,并调用encodeStream对bitmap
    //指针指向的图片数据进行编码,完成后释放资源。
        SkImageEncoder* encoder = SkImageEncoder::Create(fm);
        if (NULL != encoder) {
            success = encoder->encodeStream(strm, *bitmap, quality);
            delete encoder;
        }
        delete strm;
    }
    return success;
}

利用流和byte数组生成SkJavaOutputStream对象

SkWStream* CreateJavaOutputStreamAdaptor(JNIEnv* env, jobject stream, jbyteArray storage) {
    static bool gInited;
    if (!gInited) {
        gInited = true;
    }
    return new SkJavaOutputStream(env, stream, storage);
}
bool SkImageEncoder::encodeStream(SkWStream* stream, const SkBitmap& bm,
                                  int quality) {
    quality = SkMin32(100, SkMax32(0, quality));
    return this->onEncode(stream, bm, quality);
}

在SkImageEncoder中定义如下:

/**
 * Encode bitmap 'bm' in the desired format, writing results to
 * stream 'stream', at quality level 'quality' (which can be in
 * range 0-100).
 *
 * This must be overridden by each SkImageEncoder implementation.
 */
virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) = 0;

但是总体来说,Android是使用skia库的,我们同样在源码目录下也能找到对应位置:

external\skia

同样我们观察一个现象:

就是在SkImageEncoder中定义的onEncode函数,是个virtual的,那我们应该把她所有的实现类都找出来。

class SkKTXImageEncoder : public SkImageEncoder {}
class SkImageEncoder_CG : public SkImageEncoder {}
class SkPNGImageEncoder : public SkImageEncoder {}
class SkWEBPImageEncoder : public SkImageEncoder {}
class SkImageEncoder_WIC : public SkImageEncoder {}
class SkARGBImageEncoder : public SkImageEncoder {}

这么多类实现了这个接口而且他们都有个共同的路径:

\external\skia\src\images

那我们就看看SkPNGImageEncoder中的onEncode方法是什么样子

class SkJPEGImageEncoder : public SkImageEncoder {
protected:
    virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) {
#ifdef TIME_ENCODE
        SkAutoTime atm("JPEG Encode");
#endif

        SkAutoLockPixels alp(bm);
        if (NULL == bm.getPixels()) {
            return false;
        }

        jpeg_compress_struct    cinfo;//申请并初始化jpeg压缩对象,同时要指定错误处理器
        skjpeg_error_mgr        sk_err;// 声明错误处理器,并赋值给jcs.err域
        skjpeg_destination_mgr  sk_wstream(stream);

        // allocate these before set call setjmp
        SkAutoMalloc    oneRow;
        SkAutoLockColors ctLocker;

        cinfo.err = jpeg_std_error(&sk_err);
        sk_err.error_exit = skjpeg_error_exit;
        if (setjmp(sk_err.fJmpBuf)) {
            return false;
        }

        // Keep after setjmp or mark volatile.
        const WriteScanline writer = ChooseWriter(bm);
        if (NULL == writer) {
            return false;
        }

        jpeg_create_compress(&cinfo);
        cinfo.dest = &sk_wstream;
        cinfo.image_width = bm.width();
        cinfo.image_height = bm.height();
        cinfo.input_components = 3;
#ifdef WE_CONVERT_TO_YUV
        cinfo.in_color_space = JCS_YCbCr;
#else
        cinfo.in_color_space = JCS_RGB;
#endif
        cinfo.input_gamma = 1;
    /**
    jpeg_set_defaults函数一定要等设置好图像宽、高、色彩通道数计色彩空间四个参数后才能调用,
    因为这个函数要用到这四个值,调用jpeg_set_defaults函数后,jpeglib库采用默认的设置对图像进行压缩,
    如果需要改变设置,如压缩质量,调用这个函数后,可以调用其它设置函数,如jpeg_set_quality函数。
    其实图像压缩时有好多参数可以设置,但大部分我们都用不着设置,只需调用jpeg_set_defaults函数值为默认值即可。
    */
        jpeg_set_defaults(&cinfo);
        jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);//给cinfo中设置quality
#ifdef DCT_IFAST_SUPPORTED
        cinfo.dct_method = JDCT_IFAST;
#endif


    /*
    上面的工作准备完成后,就可以压缩了,压缩过程非常简单,首先调用jpeg_start_compress,然后可以对每一行进行压缩,
    也可以对若干行进行压缩,甚至可以对整个的图像进行一次压缩,压缩完成后,记得要调用jpeg_finish_compress函数
    */

        jpeg_start_compress(&cinfo, TRUE);//设置开始压缩的必要天剑

        const int       width = bm.width();
        uint8_t*        oneRowP = (uint8_t*)oneRow.reset(width * 3);

        const SkPMColor* colors = ctLocker.lockColors(bm);
        const void*      srcRow = bm.getPixels();
        //下面是对每一行进行压缩
        while (cinfo.next_scanline < cinfo.image_height) {
            JSAMPROW row_pointer[1];    //一行位图

            writer(oneRowP, srcRow, width, colors);
            row_pointer[0] = oneRowP;
            (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);//向压缩容器中写数据
            srcRow = (const void*)((const char*)srcRow + bm.rowBytes());
        }
        //最后就是释放压缩工作过程中所申请的资源了,主要就是jpeg压缩对象
        jpeg_finish_compress(&cinfo);
        jpeg_destroy_compress(&cinfo);

        return true;
    }
};

里面牵扯到JCS_RGB,JCS_YCbCr

00206 typedef enum {
00207         JCS_UNKNOWN,            /* error/unspecified */
00208         JCS_GRAYSCALE,          /* monochrome */
00209         JCS_RGB,                /* red/green/blue */
00210         JCS_YCbCr,              /* Y/Cb/Cr (also known as YUV) */
00211         JCS_CMYK,               /* C/M/Y/K */
00212         JCS_YCCK                /* Y/Cb/Cr/K */
00213 } J_COLOR_SPACE;
//Definition at line 206 of file jpeglib.h.

而且我们看出来里面使用:

00217 typedef enum {
00218         JDCT_ISLOW,             /* slow but accurate integer algorithm */
00219         JDCT_IFAST,             /* faster, less accurate integer method */
00220         JDCT_FLOAT              /* floating-point: accurate, fast on fast HW */
00221 } J_DCT_METHOD;

一种快但是不精准的方法进行变换。按照网上有关基友的说法:
link

1.Skia默认先将图片转为YUV444格式,再进行编码(WE_CONVERT_TO_YUV宏默认打开状态,否则就是先转为RGB888格式,再传入Jpeg编码时转YUV)
2.默认使用JDCT_IFAST方法做傅立叶变换,很明显会造成一定的图片质量损失(即使quality设成100也存在,是计算精度的问题)

jpeg_start_compress:

link

看文档还是这只一些安全检查所需要的参数为压缩做准备

/*
 * Compression initialization.
 * Before calling this, all parameters and a data destination must be set up.
 *
 * We require a write_all_tables parameter as a failsafe check when writing
 * multiple datastreams from the same compression object.  Since prior runs
 * will have left all the tables marked sent_table=TRUE, a subsequent run
 * would emit an abbreviated stream (no tables) by default.  This may be what
 * is wanted, but for safety's sake it should not be the default behavior:
 * programmers should have to make a deliberate choice to emit abbreviated
 * images.  Therefore the documentation and examples should encourage people
 * to pass write_all_tables=TRUE; then it will take active thought to do the
 * wrong thing.
 */
 
jpeg_start_compress (j_compress_ptr cinfo, boolean write_all_tables)
{
  if (cinfo->global_state != CSTATE_START)
    ERREXIT1(cinfo, JERR_BAD_STATE, cinfo->global_state);

  if (write_all_tables)
    jpeg_suppress_tables(cinfo, FALSE); /* mark all tables to be written */

  /* (Re)initialize error mgr and destination modules */
  (*cinfo->err->reset_error_mgr) ((j_common_ptr) cinfo);
  (*cinfo->dest->init_destination) (cinfo);
  /* Perform master selection of active modules */
  jinit_compress_master(cinfo);
  /* Set up for the first pass */
  (*cinfo->master->prepare_for_pass) (cinfo);
  /* Ready for application to drive first pass through jpeg_write_scanlines
   * or jpeg_write_raw_data.
   */
  cinfo->next_scanline = 0;
  cinfo->global_state = (cinfo->raw_data_in ? CSTATE_RAW_OK : CSTATE_SCANNING);
}

至此压缩就完成了,我们也就看出Android系统是通过libjpeg进行压缩的。

但是Android集成的libjpeg和我们使用的也有一些不一样,所以我建议使用自己编译开元so进行操作,这样可以根据我们需求来定制参数达到更好的符合我们项目的目的。

小结:

我们已经知道Android系统中是使用skia库进行压缩的,skia库中又是使用其他开元库进行压缩对于jpg的压缩就是使用libjpeg这个库。

2. Android中有图片所占内存因素分析

有个大仙分析的很好借用成果

我们经常因为图片太大导致oom,但是很多小伙伴,只是借鉴网上的建议和方法,并不知道原因,那么我们接下来就大致分析一下图片在Android中加载由那些因素决定呢?

getByteCount()
表示存储bitmap像素所占内存

public final int getByteCount() {
        return getRowBytes() * getHeight();
}

getAllocationByteCount()

Returns the size of the allocated memory used to store this bitmap's pixels.

返回bitmap所占像素已经分配的大小

This can be larger than the result of getByteCount() if a bitmap is reused to decode other bitmaps of smaller size, or by manual reconfiguration. See reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap. If a bitmap is not modified in this way, this value will be the same as that returned by getByteCount().

This value will not change over the lifetime of a Bitmap.

如果一个bitmap被复用更小尺寸的bitmap编码,或者手工重新配置。那么实际尺寸可能偏小。具体看reconfigure(int, int, Config), setWidth(int), setHeight(int), setConfig(Bitmap.Config), and BitmapFactory.Options.inBitmap.如果不牵扯复用否是新产生的,纳闷就和getByteContent()相同。

这个值在bitmap生命周期内不会改变

所以从代码看mBuffer.length就是缓冲区真是长度

public final int getAllocationByteCount() {
    if (mBuffer == null) {
        //mBuffer 代表存储 Bitmap 像素数据的字节数组。
        return getByteCount();
    }
    return mBuffer.length;
}

然后我们看看占用内存如何计算的

Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存

那么一个像素占用的内存多大呢?这个就和配置的规格有关系

SkBitmap.cpp

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
  };

常用的就是RGBA_8888也就是一个像素占用四个字节大小

  • ARGB_8888:每个像素占四个字节,A、R、G、B 分量各占8位,是 Android 的默认设置;
  • RGB_565:每个像素占两个字节,R分量占5位,G分量占6位,B分量占5位;
  • ARGB_4444:每个像素占两个字节,A、R、G、B分量各占4位,成像效果比较差;
  • Alpha_8: 只保存透明度,共8位,1字节;

于此同时呢,在BitmapFactory 的内部类 Options 有两个成员变量 inDensity 和 inTargetDensity其中

  • inDensity 就 Bitmap 的像素密度,也就是 Bitmap 的成员变量 mDensity默认是设备屏幕的像素密度,可以通过 Bitmap#setDensity(int) 设置
  • inTargetDensity 是图片的目标像素密度,在加载图片时就是 drawable 目录的像素密度

当资源加载的时候会进行这两个值的初始化

调用的是 BitmapFactory#decodeResource 方法,内部调用的是 decodeResourceStream 方法

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
          InputStream is, Rect pad, Options opts) {
      //实际上,我们这里的opts是null的,所以在这里初始化。
      /**
      public Options() {
        inDither = false;
        inScaled = true;
        inPremultiplied = true;
      }
      */
      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
          }
      }
      //请注意,inTargetDensity就是当前的显示密度,比如三星s6时就是640
      if (opts.inTargetDensity == 0 && res != null) {
          opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
      }

      return decodeStream(is, pad, opts);
  }

会根据设备屏幕像素密度到对应 drawable 目录去寻找图片,这个时候 inTargetDensity/inDensity = 1,图片不会做缩放,宽度和高度就是图片原始的像素规格,如果没有找到,会到其他 drawable 目录去找,这个时候 drawable 的屏幕像素密度就是 inTargetDensity,会根据 inTargetDensity/inDensity 的比例对图片的宽度和高度进行缩放。

所以归结上面影响图片内存的原因有:

  • 色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
  • 原始文件存放的资源目录
  • 目标屏幕的密度
  • 图片本身的大小

3.图片的几种压缩办法

  1. 质量压缩

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

注意这种方式,是通过改变alpha通道,改变色彩度等方式达到压缩图片的目的,压缩使得存储大小变小,但是并不改变加载到内存的大小,也就是说,如果你从1M压缩到了1K,解压缩出来在内存中大小还是1M。而且有个很坑的问题,就是如果设置quality=100,这个图片存储大小会增大,而且会小幅度失真。具体原因,我在上面分析源码的时候还没仔细研究,初步判断可能是利用傅里叶变换导致。

  1. 尺寸压缩
    尺寸压缩在使用的时候BitmapFactory.Options 类型的参数当置 BitmapFactory.Options.inJustDecodeBounds=true只读取图片首行宽高等信息,并不会将图片加载到内存中。设置 BitmapFactory.Options 的 inSampleSize 属性可以真实的压缩 Bitmap 占用的内存,加载更小内存的 Bitmap。
    设置 inSampleSize 之后,Bitmap 的宽、高都会缩小 inSampleSize 倍。
    inSampleSize 比1小的话会被当做1,任何 inSampleSize 的值会被取接近2的幂值

  2. 色彩模式压缩
    也就是我们在色彩模式上进行变换,通过设置通过 BitmapFactory.Options.inPreferredConfig改变不同的色彩模式,使得每个像素大小改变,从而图片大小改变

  3. Matrix 矩阵变换
    使用:

int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
Matrix matrix = new Matrix();
float rate = computeScaleRate(bitmapWidth, bitmapHeight);
matrix.postScale(rate, rate);
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);

其实这个操作并不是节省内存,他只是结合我们对尺寸压缩进行补充,我们进行尺寸压缩之后难免不会满足我们对尺寸的要求,所以我们就借助Matrix进行矩阵变换,改变图片的大小。

  1. Bitmap#createScaledBitmap

这个也是和Matrix一个道理,都是进行缩放。不改变内存。

3.图片压缩的最终解决方案

我们通过上面的总结我们归纳出,图片的压缩目的有两种:

  • 压缩内存,防止产生OOM
  • 压缩存储空间,目的节约空间,但是解压到内存中大小不变。还是原来没有压缩图片时候的大小。

那么我们应该怎么压缩才合理呢,其实这个需要根据需求来定,可能有人就会说我说的是废话,但是事实如此。我提供一些建议:

  • 使用libjpeg开源项目,不使用Android集成的libjpeg,因为我们可以根据需要修改参数,更符合我们项目的效果。
  • 合理通过尺寸变换和矩阵变换在内存上优化。
  • 对不同屏幕分辨率的机型压缩进行压缩的程度不一样。

那么我们就开始我们比较难的一个环节就是集成开源库。

4.编译libjpeg生成so库

libjpeg项目下载地址

  1. 首先确保我们安装了ndk环境,不管是Linux还是windows还是macOs都可以编译,只要我们有ndk

  2. 我们必须知道我们NDK能够使用,并且可以调用到我们ndk里面的工具,这就要求我们要配置环境变量,当然Linux和windows不一样,macOS由于我这种穷逼肯定买不起所以我也布吉岛怎么弄。但是思想就是要能用到ndk工具

    • windows是在我们环境变量中进行配置

    • Linux呢

      echo "export ANDROID_HOME='Your android ndk path'" >> ~/.bash_profile
      source ~/.bash_profile
      

      当然Linux还可以写.sh来个脚本岂不更好

      NDK=/opt/ndk/android-ndk-r12b/
      PLATFORM=$NDK/platforms/android-15/arch-arm/
      PREBUILT=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86/
      CC=$PREBUILT/bin/arm-linux-androideabi-gcc
      ./configure --prefix=/home/linc/jpeg-9b/jni/dist --host=arm CC="$CC --sysroot=$PLATFORM"
      

      最执行写的.sh

      这个脚本是根据config文件写的,那里面有我们需要的参数还有注释,所以我们要能看懂那个才可以。一般情况出了问题我们在研究那个吧
      引荐大牛方法

  3. 构建libjpeg-turbo.so

    cd ../libjpeg-turbo-android/libjpeg-turbo/jni
    ndk-build APP_ABI=armeabi-v7a,armeabi
    

    这个时候就可以得到libjpegpi.so在../libjpeg-turbo-android/libjpeg-turbo/libs/armeabi和armeabi-v7a目录下

  4. 复制我们的libjpegpi.so到 ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni

     cd ../bither-android-lib/libjpeg-turbo-android/use-libjpeg-turbo-android/jni
     ndk-build
    
  5. 得到 libjpegpi.so and libpijni.so

  6. jni使用的时候一定java的类名要和jni里面方法前面的单词要对上

 static {

    System.loadLibrary("jpegpi");
   
    System.loadLibrary("pijni");

 }
 
所以如果不改项目的话类名必须为com.pi.common.util.NativeUtil

5.库函数的介绍

net.bither.util.NativeUtil:

package net.bither.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.media.ExifInterface;
import android.util.Log;

public class NativeUtil {
    private static String Tag = NativeUtil.class.getSimpleName();

    private static int DEFAULT_QUALITY = 95;

    /**
     * @Description: JNI基本压缩
     * @param bit
     *            bitmap对象
     * @param fileName
     *            指定保存目录名
     * @param optimize
     *            是否采用哈弗曼表数据计算 品质相差5-10倍
     * @author XiaoSai
     * @date 2016年3月23日 下午6:32:49
     * @version V1.0.0
     */
    public static void compressBitmap(Bitmap bit, String fileName, boolean optimize) {
        saveBitmap(bit, DEFAULT_QUALITY, fileName, optimize);
    }

    /**
     * @Description: 通过JNI图片压缩把Bitmap保存到指定目录
     * @param image
     *            bitmap对象
     * @param filePath
     *            要保存的指定目录
     * @author XiaoSai
     * @date 2016年3月23日 下午6:28:15
     * @version V1.0.0
     */
    public static void compressBitmap(Bitmap image, String filePath) {
        // 最大图片大小 150KB
        int maxSize = 150;
        // 获取尺寸压缩倍数
        int ratio = NativeUtil.getRatioSize(image.getWidth(),image.getHeight());
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(image.getWidth() / ratio,image.getHeight() / ratio,Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, image.getWidth() / ratio, image.getHeight() / ratio);
        canvas.drawBitmap(image,null,rect,null);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int options = 100;
        result.compress(Bitmap.CompressFormat.JPEG, options, baos);
        // 循环判断如果压缩后图片是否大于100kb,大于继续压缩
        while (baos.toByteArray().length / 1024 > maxSize) {
            // 重置baos即清空baos
            baos.reset();
            // 每次都减少10
            options -= 10;
            // 这里压缩options%,把压缩后的数据存放到baos中
            result.compress(Bitmap.CompressFormat.JPEG, options, baos);
        }
        // JNI保存图片到SD卡 这个关键
        NativeUtil.saveBitmap(result, options, filePath, true);
        // 释放Bitmap
        if (!result.isRecycled()) {
            result.recycle();
        }
    }

    /**
     * @Description: 通过JNI图片压缩把Bitmap保存到指定目录
     * @param curFilePath
     *            当前图片文件地址
     * @param targetFilePath
     *            要保存的图片文件地址
     * @author XiaoSai
     * @date 2016年9月28日 下午17:43:15
     * @version V1.0.0
     */
    public static void compressBitmap(String curFilePath, String targetFilePath,int maxSize) {
        //根据地址获取bitmap
        Bitmap result = getBitmapFromFile(curFilePath);
        if(result==null){
            Log.i(Tag,"result is null");
            return;
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int quality = 100;
        result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        // 循环判断如果压缩后图片是否大于100kb,大于继续压缩
        while (baos.toByteArray().length / 1024 > maxSize) {
            // 重置baos即清空baos
            baos.reset();
            // 每次都减少10
            quality -= 10;
            // 这里压缩quality,把压缩后的数据存放到baos中
            result.compress(Bitmap.CompressFormat.JPEG, quality, baos);
        }
        // JNI保存图片到SD卡 这个关键
        NativeUtil.saveBitmap(result, quality, targetFilePath, true);
        // 释放Bitmap
        if (!result.isRecycled()) {
            result.recycle();
        }

    }

    /**
     * 计算缩放比
     * @param bitWidth 当前图片宽度
     * @param bitHeight 当前图片高度
     * @return int 缩放比
     * @author XiaoSai
     * @date 2016年3月21日 下午3:03:38
     * @version V1.0.0
     */
    public static int getRatioSize(int bitWidth, int bitHeight) {
        // 图片最大分辨率
        int imageHeight = 1280;
        int imageWidth = 960;
        // 缩放比
        int ratio = 1;
        // 缩放比,由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        if (bitWidth > bitHeight && bitWidth > imageWidth) {
            // 如果图片宽度比高度大,以宽度为基准
            ratio = bitWidth / imageWidth;
        } else if (bitWidth < bitHeight && bitHeight > imageHeight) {
            // 如果图片高度比宽度大,以高度为基准
            ratio = bitHeight / imageHeight;
        }
        // 最小比率为1
        if (ratio <= 0)
            ratio = 1;
        return ratio;
    }

    /**
     * 通过文件路径读获取Bitmap防止OOM以及解决图片旋转问题
     * @param filePath
     * @return
     */
    public static Bitmap getBitmapFromFile(String filePath){
        BitmapFactory.Options newOpts = new BitmapFactory.Options();
        newOpts.inJustDecodeBounds = true;//只读边,不读内容  
        BitmapFactory.decodeFile(filePath, newOpts);
        int w = newOpts.outWidth;
        int h = newOpts.outHeight;
        // 获取尺寸压缩倍数
        newOpts.inSampleSize = NativeUtil.getRatioSize(w,h);
        newOpts.inJustDecodeBounds = false;//读取所有内容
        newOpts.inDither = false;
        newOpts.inPurgeable=true;//不采用抖动解码
        newOpts.inInputShareable=true;//表示空间不够可以被释放,在5.0后被释放
//      newOpts.inTempStorage = new byte[32 * 1024];
        Bitmap bitmap = null;
        FileInputStream fs = null;
        try {
            fs = new FileInputStream(new File(filePath));
        } catch (FileNotFoundException e) {
            Log.i(Tag,"bitmap   :"+e.getStackTrace());
            e.printStackTrace();
        }
        try {
            if(fs!=null){
                bitmap = BitmapFactory.decodeFileDescriptor(fs.getFD(),null,newOpts);

                //旋转图片
                int photoDegree = readPictureDegree(filePath);
                if(photoDegree != 0){
                    Matrix matrix = new Matrix();
                    matrix.postRotate(photoDegree);
                    // 创建新的图片
                    bitmap = Bitmap.createBitmap(bitmap, 0, 0,
                            bitmap.getWidth(), bitmap.getHeight(), matrix, true);
                }
            }else{
                Log.i(Tag,"fs   :null");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally{
            if(fs!=null) {
                try {
                    fs.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }

    /**
     *
     * 读取图片属性:旋转的角度
     * @param path 图片绝对路径
     * @return degree旋转的角度
     */

    public static int readPictureDegree(String path) {
        int degree = 0;
        try {
            ExifInterface exifInterface = new ExifInterface(path);
            int orientation = exifInterface.getAttributeInt(
                    ExifInterface.TAG_ORIENTATION,
                    ExifInterface.ORIENTATION_NORMAL);
            switch (orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    degree = 90;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    degree = 180;
                    break;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    degree = 270;
                    break;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return degree;
    }

    /**
     * 调用native方法
     * @Description:函数描述
     * @param bit
     * @param quality
     * @param fileName
     * @param optimize
     * @author XiaoSai
     * @date 2016年3月23日 下午6:36:46
     * @version V1.0.0
     */
    private static void saveBitmap(Bitmap bit, int quality, String fileName, boolean optimize) {
        compressBitmap(bit, bit.getWidth(), bit.getHeight(), quality, fileName.getBytes(), optimize);
    }

    /**
     * 调用底层 bitherlibjni.c中的方法
     * @Description:函数描述
     * @param bit
     * @param w
     * @param h
     * @param quality
     * @param fileNameBytes
     * @param optimize
     * @return
     * @author XiaoSai
     * @date 2016年3月23日 下午6:35:53
     * @version V1.0.0
     */
    private static native String compressBitmap(Bitmap bit, int w, int h, int quality, byte[] fileNameBytes,
                                                boolean optimize);
    /**
     * 加载lib下两个so文件
     */
    static {
        System.loadLibrary("jpegbither");
        System.loadLibrary("bitherjni");
    }

}

所以我们最后的核心就是使用saveBitmap就会将图片压缩并且保存在sd卡上。而且我们获取图片的时候也对内存做了判断,防止产生oom

6.压缩结果

一张5M,一张是140k但是我截图看上去效果差不多,哈哈。也就是说,外国基友说的其实很有道理哈哈。

Screenshot_2017-10-02-15-46-26-071_com.miui.galle.png
Screenshot_2017-10-02-15-46-15-675_com.miui.galle.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容