简介

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

OOM,一不留神就用OOM来冲冲喜,所以网上就有了很多解决oom问题的建议,但是由于网友的水平不一也导致建议参差不齐。(内存)
图片压缩再加载失真严重,或者压缩率不够达不到项目要求的效果。(外存)
那我今天就要解决的就是通过今天查阅的资料和自己的判断,还有实践归档一下图片在Android上的问题。并且给出自己解决图片压缩问题的解决方案和实际操作。

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

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

libjpeg在压缩图像时,有一个参数叫optimize_coding,关于这个参数,libjpeg.doc有如下解释:如果设置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 methodif (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_ENCODESkAutoTime atm("JPEG Encode");
#endifSkAutoLockPixels 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 setjmpSkAutoMalloc    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_YUVcinfo.in_color_space = JCS_YCbCr;
#elsecinfo.in_color_space = JCS_RGB;
#endifcinfo.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_SUPPORTEDcinfo.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

Definition

source

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;

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

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

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

/** 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():返回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,  // Unknown1,  // Alpha_82,  // RGB_5652,  // ARGB_44444,  // RGBA_88884,  // BGRA_88881,  // 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时就是640if (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、图片的几种压缩办法

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

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

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

  • 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进行矩阵变换,改变图片的大小。

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

4、图片压缩的最终解决方案

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

  • 压缩内存,防止产生OOM
    压缩存储空间,目的节约空间,但是解压到内存中大小不变。还是原来没有压缩图片时候的大小。
    那么我们应该怎么压缩才合理呢,其实这个需要根据需求来定,可能有人就会说我说的是废话,但是事实如此。我提供一些建议:

  • 使用libjpeg开源项目,不使用Android集成的libjpeg,因为我们可以根据需要修改参数,更符合我们项目的效果。

  • 合理通过尺寸变换和矩阵变换在内存上优化。

  • 对不同屏幕分辨率的机型压缩进行压缩的程度不一样。

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

5、编译libjpeg生成so库

libjpeg项目下载地址

首先确保我们安装了ndk环境,不管是Linux还是windows还是macOs都可以编译,只要我们有NDK,我们必须知道我们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文件写的,那里面有我们需要的参数还有注释,所以我们要能看懂那个才可以。一般情况出了问题我们在研究那个吧
引荐大牛方法

构建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目录下

复制我们的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

得到 libjpegpi.so and libpijni.so

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

 static {System.loadLibrary("jpegpi");System.loadLibrary("pijni");}

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

6、库函数的介绍

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) {// 最大图片大小 150KBint 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即清空baosbaos.reset();// 每次都减少10options -= 10;// 这里压缩options%,把压缩后的数据存放到baos中result.compress(Bitmap.CompressFormat.JPEG, options, baos);}// JNI保存图片到SD卡 这个关键NativeUtil.saveBitmap(result, options, filePath, true);// 释放Bitmapif (!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) {//根据地址获取bitmapBitmap 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即清空baosbaos.reset();// 每次都减少10quality -= 10;// 这里压缩quality,把压缩后的数据存放到baos中result.compress(Bitmap.CompressFormat.JPEG, quality, baos);}// JNI保存图片到SD卡 这个关键NativeUtil.saveBitmap(result, quality, targetFilePath, true);// 释放Bitmapif (!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;}// 最小比率为1if (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

7、压缩结果

下面第一张图5M,第二张图是140k,但是我截图看上去效果差不多。看下效果:


原创作者:我叫王菜鸟,原文链接:https://www.jianshu.com/p/072b6defd938

欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。

使用libjpeg进行图片压缩相关推荐

  1. 使用libjpeg进行图片压缩(哈夫曼算法,无损压缩)

    Huffman算法也是一种无损压缩算法,但与LZW压缩算法不同,Huffman需要得到每种字符出现概率的先验知识.通过计算字符序列中每种字符出现的频率,为每种字符进行唯一的编码设计,使得频率高的字符占 ...

  2. Android使用libjpeg实现图片压缩

    一.Android中使用的图片压缩库 Android和IOS 中图片处理使用了一个叫做skia的开源图形处理引擎.他位于android源码的/external/skia 目录.我们平时在java层使用 ...

  3. 【Android 内存优化】Android 原生 API 图片压缩原理 ( Bitmap_compress 方法解析 | Skia 二维图形库 | libjpeg 函数库 | libpng 函数库 )

    文章目录 一. 图片质量压缩方法 二. Skia 二维图形库 三. libjpeg.libpng 函数库引入 在博客 [Android 内存优化]图片文件压缩 ( Android 原生 API 提供的 ...

  4. Android LibJpeg图片压缩

    Android的图片压缩 Android的图片压缩的几种方式:质量压缩,尺寸压缩,采样率压缩,通过NDK调用libjpeg库进行压缩! 质量压缩 通过设置bitmap options属性,降低图片的质 ...

  5. Android性能优化之图片压缩优化

    1 分类 Android图片压缩结合多种压缩方式,常用的有尺寸压缩.质量压缩.采样率压缩以及通过JNI调用libjpeg库来进行压缩. 参考此方法:Android-BitherCompress 备注: ...

  6. 【Android 内存优化】Android 原生 API 图片压缩原理 ( 哈夫曼编码开关 | 哈夫曼编码原理 | libjpeg-turbo 函数库 )

    文章目录 一. 哈夫曼编码开关 二. 哈夫曼编码原理 三. libjpeg-turbo 函数库 四. libjpeg-turbo 函数库下载 [Android 内存优化]图片文件压缩 ( Androi ...

  7. 服务器搭建:3.1、openresty图片压缩之GraphicsMagick

    2019独角兽企业重金招聘Python工程师标准>>> 服务器环境 CentOS6 版本 openresty   1.7.10.2   下载地址:https://openresty. ...

  8. java jpeg压缩解码_图片压缩(iOS)

    场景很简单,上传图片前压缩图片,节省流量和发图时间.最近看了看 iOS 的静态图片压缩,这里记个笔记.本人之前没学过 iOS 和 Swift,本文是一篇入门文章,描述不到位之处请大家多多批评斧正. ̄ω ...

  9. Android图片压缩,不失真,上线项目

    当然了,图片压缩是利用了libjpeg库的基础上,牛逼的同学可以自行生成so.jar.在此给出一个链接: http://www.cnblogs.com/hrlnw/p/4403334.html 在生成 ...

最新文章

  1. 微软大神“玩”出新花样,求平均值代码还能这样写?
  2. 程序员修炼之路:你该知道的 7 个必经阶段
  3. JSONP跨域请求数据报错 “Unexpected token :”的解决办法
  4. OSI强调:SSPL并不是开源许可证
  5. apk源码查看工具_如何查看Linux命令工具的源码?
  6. weakreference_Java中WeakReference,SoftReference,PhantomReference和Strong Reference之间的区别...
  7. 内蒙古大学计算机组成原理实验,内蒙古大学计算机组成原理期末练习0
  8. 【Python】从0开始写爬虫——豆瓣电影
  9. oracle安装后怎么用plsql连接,oracle11g安装和使用PLSQL连接
  10. Hibernate的Cascade——级联操作
  11. 机器视觉光源之LED光源
  12. Netty+SpringBoot+FastDFS+Html5实现聊天App详解(四)
  13. HFSS阵列天线设计与仿真2
  14. DSB matlab仿真
  15. 静态IP和动态IP有什么区别?
  16. Python变量赋值出现SyntaxError: invalid syntax
  17. 【高等数学笔记】证明:闭包一定是闭集
  18. 厦门大学计算机考研经验分享,【图片】一战厦大计算机上岸,经验帖。慢更【考研吧】_百度贴吧...
  19. 中西方对时间的差异_中西方时间观念差异_英文
  20. 解线性方程组的直接法

热门文章

  1. 深圳计算机中级职称入户,深圳积分入户中级职称积分,这些证书助你轻松入户!...
  2. FT232RL 芯片资料整理
  3. 微信小程序WIFI检测
  4. 计算机桌面怎么自定义,电脑怎么换壁纸自定义
  5. Unicode 和多字节字符集 (MBCS) 支持
  6. android 2k 屏幕 字体模糊,显示字体小到有些模糊?高分屏别忘了这些设置
  7. 李宏毅_机器学习_作业1(详解)_COVID-19 Cases Prediction (Regression)
  8. git rebase 的几种用法
  9. git的学习笔记(一):git本地操作
  10. 城市公共交通规划掌握内容