Android编译libjpeg-turbo so高效压缩图片

安卓 专栏收录该内容
111 篇文章 0 订阅

https://www.jianshu.com/p/8ebe0ddd21f7
https://www.jianshu.com/p/f305fb008ab6
一 . 图片的基本知识

图像是由像素组成的,而像素实际上是带着坐标的位置和颜色信息的。它是有若干行和若干列的点组成的,他们相交就会有无数个点。假如我们随便取出一个点A 那个这个点可以表示为

A[m,n] = [blue,green,red]

m和n 就是图像中的第m行和n列

blue 表示蓝色,是三原色(RGB)的第一个值

green 表示绿色,是三原色(RGB)的第二个值

red 表示红色,是三原色(RGB)的第三个值

  1. 分辨率

我们通常说图片分辨率其实就是指像素数,通俗的说是横向多少个像素 x 纵向多个像素 。那什么是像素: 每张图片是有色点组成的,每个色点就称之为像素。比如一张图片有10万个色点构成,那么这个图片像素就是 10W。

我们举个例子:一张图片分辨率为 500x400 ,那么图片是有横向 500个像素、纵向 400个像素,(合计20000像素点)构成。

  1. 图片格式

我们在实际项目开开发中遇到比较多的图片格式 一般有 .PNG、.JPG、.JPEG、 .WebP 、.GIF、.SVG 等等

PNG:它是一种无损数据压缩位图图形文件格式。这也就是说PNG 只支持无损压缩。对于PNG 格式是有8 位、24位、32位的三种形式的。区别就是对透明度的支持。

JPG:其实就是 JPEG的另一种叫法

JPEG:它是一种有损压缩的图片格式

WebP:Google 开发出的一种支持alpha 通道的有损压缩。同等质量情况下比 JPEG和PNG小 25%~45%.

GIF:它是动态图片的一种格式,和PNG 一样是一种无损压缩。

**SVG **: 是一种无损、矢量图(放大不失真)

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

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

我们不是说图片怎么压缩的吗?为什么要说图片格式。因为他们之间是存在联系的,比如其中 PNG是无损压缩格式的图片,JPEG是有损压缩格式的图片,所以对应的也有各自的压缩算法,比如在android中PNG压缩使用的就是 libpng 进行压缩的,而JPEG的压缩是用libjepg(7.0之前) 压缩的,7.0 之后改为libjpeg-turbo是基于libjepg修改的,而比libjepg更快。最大的变化就是相同质量的下7.0之后的机器比7.0之前的机器压缩的图片要小。

二 . Bitmap

对于开发android的 小伙伴,对Bitmap肯定是不陌生的,甚至有的小伙伴被它虐的“体无完肤”。在Android中任何图片资源的显示对象都是通过bitmap 来显示的(XML资源通Canvas绘制除外)。

Bitmap 它是图像处理中的一个非常重要对象。

1.何为bitmap?

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

2.bitmap 存储空间

随着android系统的不断升级,bitmap的存储空间也在发生变成,而bitmap 的存储空间主要有三个地方:

1)Native Memory

在android 2.3 以下的版本,bitmap像素数据是存储在 Native 空间中,如果需要释放是要主动调用 recycle()方法

2)Dalvik Heap

在Android 3.0 以上版本,bitmap的像素数据存储在虚拟机堆中,不需要我们再去主动调用recycle()方法,gc会帮我们去回收。

3)Ashmem

很多小伙伴可能不知道这个是什么,它是匿名共享存储空间。我们在实际开发中使用图片加载库中,有一个库就是利用这个一个空间来进行bitmap 对象的存储的,他就是大名鼎鼎的Fresco 图片加载库。

不过这里需要注意一点: 在Android 4.4 以前的版本中 Ashmem 是和App 进程空间是隔离的互相不影响,而在Android 4.4以后的版本中,Ashmemk空间是包含在App所占用的内存空间。

说了这么多我们还是不知道bitmap 在内存空间中到底是占用多大的,其实bitmap 在内存空间中所占用的内存计算是这样的:

pixelWidth x pixelHeight x bytesPerPixel(bitmap 的宽 x 高 x 每个像素所占的字节),所有如果相同的Bitmap对象, 每个像素所占用的字节大小,决定了这个bitmap 在内存中所占用的内存大小。

上面既然说了,所占用的字节大小决定bitmap内存大小,那么怎么样能让每个像素所占的字节变小。在我们Bitmap对象中有一个比较重要的枚举类 Config ,这个Config 是用来设置颜色配置信息的。对于Config配置有四个变量。

Config 配置:

  1. alpha_8 : 占用8位,1个字节,颜色信息只由透明组成。

  2. argb_4444: 占用16位,2个字节,颜色有透明度和R (red)G(green)B(blue)组成。

3.argb_8888: 占用34位,4个字节,颜色有透明度和R (red)G(green)B(blue)组成。

这里可能有人会问为什么 argb_4444和 argb_8888都是 有ARGB组成为什么所占的字节不同,因为每个部分所占用的字节是不同的,argb_4444每个部分占用4个字节,而argb_8888每个部分占用8位。

4.rgb_565: 占用16位,2个字节,颜色有R (red)G(green)B(blue)组成。

提示:我们通常在操作bitmap 的时候,是必须要和这个配置打交道的,搞明白对使用bitmap的时候,提供帮助。特别是防止OOM,有很大作用。如果我们平时对图片处理,如多对图片的透明度没特别要求,比较建议使用 rgb_565 这个配置,因为他和其它几个比较,性价比最高的。比argb_4444 显示图片清晰,argb_8888占用内存少一半,而alpha_8只有透明度,对图片没什么意思。既然我们知道这写参数的意义了,我们就可以通过设置该配置,来让我们bitmap 占用内存空间变小。

说了这么多,那么Bitmap在内存究竟占用多少内存?使用Android Api的 getByteCount 方法即可。

在这里插入图片描述
通过这个方法,我们就可以获取当前运行的 bitmap 占用的内存。

三. 压缩

既然我们知道了,bitmap在内存中占用空间 = bitmap 的宽 x 高 x 每个像素所占的字节数。那么Bitmap 压缩都是围绕这个来做文章的。这里的三个参数我们,减少任意一个的值,就可能会达到了我们压缩的效果了。

  1. 图片存在的形式

图片存在大致可以分为三种形式:

file形式 ,我们存在硬盘上的 都是以file 文件的形式存在的。

bitmap或 stream 形式,图片在内存中要么以bitmap形式要么已 stream形式存在的。

stream 形式,我们图片在网络传输的过程中,都是已stream 形式存在。

那么在android中 图片文件主要是有png,jpg,webP,gif 等几种类型格式进行存储的。其实我们图片压缩也是在几个类型中做处理。

我们为什么要说图片存在的形式,这会对我们以后开发过程中对图片处理需要有一定的帮助的。

  1. api

在介绍压缩之前我们先看下相关api吧,让我们在使用的时候更加方便和选择合适的pai来做相关操作。

我们再对图片进行相关操作的时候,主要涉及的类有 Bitmap,BitmapFactory,Matrix。等

Bitmap

//将位图压缩到指定的outputstream中

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

// 创建一个Bitmap 对象 ,该方法有个重载函数

Bitmap createBitmap (Bitmap src)

//根据新配置 拷贝一份新的bitmap ,第二次参数的意思 他的像素是否可以修改

//返回的位图具有与原图相同的密度和颜色空间

Bitmap copy (Bitmap.Config config, boolean isMutable)

//创建一个新的bitmap ,根据传入的宽和高进行缩放。

Bitmap createScaledBitmap (Bitmap src, int dstWidth,  int dstHeight, boolean filter)

// 表示图片以什么格式的算法进行压缩,压缩后为何格式

Bitmap.CompressFormat . JPEG / PNG/ WEBP

BitmapFactory

// 根据文件路径解码成位图

Bitmap decodeFile (String pathName,  BitmapFactory.Options opts)

//根据 留解码成位图

Bitmap decodeStream (InputStream is)

//根据 资源解码成为位图

Bitmap decodeResource (Resources res,  int id,  BitmapFactory.Options opts)

//根据 数组解码成位图

Bitmap decodeByteArray (byte[] data,  int offset, int length,  BitmapFactory.Options opts)

// opts.inJustDecodeBounds表示是否将图片加载到内存中 true 不加载但可以获取图片的宽高等相关信息 ,false 加载

这里另外需要注意的是, BitmapFactory 获取图片的宽/高和图片位置相关信息是和程序运行的设备有关的。

什么意思:就是将一张图片放到不同的 drawable 目录下,在不同屏幕密度的设置上运行,获取到的结果是不同的,这个是和android资源加载机制有关系的,感兴趣的小伙伴可以去研究研究。

BitmapFactory.Options.inSampleSize

图片缩放的倍数,主要这个只必须大于1,这个值很重要,后面说到的尺寸压缩,就是对这个值的计算,也就是对原图进行采样,最后放回一个较小的图片放到内存中的。例如 inSampleSize == 2 的时候返回一个图像,那它的宽和高 就是原图的 1/2 , 像素就是原来的 1/4。 这里inSampleSize 最终值必须是2的幂,任何其他值都将四舍五入到接近2的幂的值。

BitmapFactory.Options.inPreferredConfig

表示一个像素需要多大的空间存储,设置解码器色彩模式 ,默认模式为ARGB_8888 前面我们说了每个模式下占的字节数,通过改字段控制bitmap 最后在内存中占用多大内存。不过有点需要注意的是,就算我们设置了别的模式,也有可能还是默认模式的。为什么了,官方是这么解释的 : 解码器将尝试根据系统的屏幕深度选择最佳匹配配置,以及原始图像的特征,比如它是否具有每个像素的 alpha值。

Matrix

//对图片进行旋转

setRotate(float degrees, float px, float py)

//对图片进行缩放

setScale(float sx, float sy)

//对图片进行平移

setTranslate(float dx, float dy)

3.图片压缩

说了这么多总算说到正题了,对于图片压缩按照分类的话,大概可以分为两类吧,一种为质量压缩,一种为尺寸压缩。我们这里不管说的是那种压缩,其实用的都是 google 在android 中封装好了的压缩算法,我们只是使用封装好api进行讲解,和一些我们做压缩的时候的一些经验吧。

1)质量压缩

何为质量压缩,在文章开头摘要中就简单的介绍了,这里我们再详细的说下质量压缩,前面提到过,质量压缩其实是不改变原图片的尺寸的前提下改变图片的透明度和位深,原图尺寸不改变,像素自然也是没有改变的。随着他压缩图片文件大小变小,可是在bitmap内存中占用的内存是不会改变的和原图相比。它虽然没有改变图片的像素,可是它压缩了图片中每个像素的质量,这样就会出现,如果质量比较低,那么这个图片就会变得非常模糊,色彩失真很严重的。我在开发中对图片压缩的时候,一个压缩好的bitmap对象,在写入文件的时候,因为我设置图片CompressFormat 属性为 Bitmap.CompressFormat.PNG,结果写入到文件中的图片体积比原图还大。这里一定要注意的就是,png格式的图片是不适合质量压缩的,不管你压缩质量多低,内部压缩算法根本是不会对其进行压缩的,为什么? 我们前面在说图片格式的时候说过,png 图片是一种无损的图片压缩格式,这也是为什么在说图片压缩之前,对图片的一些基本知识做个简单介绍。在需求上,这种压缩非常不适合做缩略图,也不适合想通过压缩图片来减小对内存的使用。个人认为这个只适合,既想保证图片的尺寸或像素,而同时又希望减小文件大小的需求而已。说了这么多,那么在android中质量压缩通过什么api来实现的了,其实google 一下这种代码满屏都是的,为了减少看到这个文章的小伙伴去查阅代码的时间,就贴上一小段代码吧:

在这里插入图片描述
2)尺寸压缩

尺寸压缩 其实就是针对图片的尺寸进行修改,这个过程就是一个图片重新采样。在android 中图片重采样是提供了两种方法的,一种是临近采样,也是我们比较熟悉,通过改变 inSampSize 值,也叫这采样率压缩。第二种叫做双线性采样。前面我们在介绍Api是时候对这个字段进行详细介绍了。这个方法也是android 开发小伙伴人人皆知的办法了,还有很多比较出名的压缩库都说是通过该方法来做的。其实我个人认为,采样压缩其实是比较粗暴的。

  1. 临近采样,这个方式是比较粗暴的,它是直接选中其中的一个像素作为生成的像素,它采用的算法叫做临近点插值算法,它是图像缩放算法。可能还有小伙伴不明白,举个例子:
假设我们有张图片,他的像素是这样的

  绿 黄  绿 黄

  绿 黄  绿 黄

  绿 黄  绿 黄

  绿 黄  绿 黄       

这样的图片是一个绿色的像素隔着一个黄色的像素,官方给我的解释是 x (x为2的倍数)个像素最后对应一个像素,由于采样率设置为1/2,所以是两个像素生成一个像素,另一个自己就被抛弃了,如果只选择绿色,黄色被抛弃,造成图片只有一种颜色绿色了。

那到底怎么样获取采样率了?

将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片

从BitmapFactory.Options取出图片的原始宽高信息, 他们对应于outWidth和outHeight参数

根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize

将BitmapFactory.Options的inJustDecodeBounds参数设为false, 然后重新加载.

通过以上的四个步骤,我们最终获取到一个最接近我们想要的图片。在上面步骤中最重要的就是计算 inSampleSize的值,我们下官方推荐我们的计算做法是怎么写的,看代码:
在这里插入图片描述
这个就不做过多解释了,应该都能看得懂的,很简单,就是根据需要的宽和高,和图片的原始宽和高,一直循环算出一个合适的 inSampleSize 的值。

  1. 双线性采样

双线性采样使用的是双线性内插算法,这个算法不像临近采样那样粗暴,它是参考了源像素对应的位置周围 2*2个点的值根据相对位置取对应的权重,经过计算之后得到目标图像。

双线性采样在android使用方式 一般有两种,我们看实现代码:

在这里插入图片描述
其实第一种,在最终也是使用了第二种方式的 matrix 进行缩放的。我们发现 createScaledBitmap 函数的源码中

还是使用matrix进行缩放的
在这里插入图片描述
上面两种也是android中图片尺寸压缩中最常见的两种方法了。对临近采样它的方式是最快的,因为我们说过它是直接选择其中一个像素作为生成像素,生成的图片可能会相对比较失真,压缩的太厉害会产生明显的锯齿。而双线性采样相对来说失真就没有这样严重。这里可能有小伙伴就要问了,既然双线性采样比临近采样要好。为什么很多压缩框架都是采用临近采样来做的?问的好,我也查阅过相关资料和官方文档,可惜没有找到比较有权威的说法来证明这点。我说说我对这个观点的看法吧,如果我们要是使用双线性采样对图片尺寸压缩的话,不管是采用第一种还是第二种,我们都必须要有个bitmap对象,而再拿到这个bitmap对象的时候,我们是要写入到内存中的,而如果图片太大的话,在decode的时候,程序就已经OOM了,特别是处理大图的时候,这个方法肯定是不合适的。而临近采样是可以在图片不decode到内存的情况下,对图片进行压缩处理,最后获取到的bitmap 是很小的,基本不会导致OOM的。

3.LibJpeg压缩
通过Ndk调用LibJpeg库进行压缩,保留原有的像素,清晰度。这个库广泛的使用在开源的图片压缩上的。

4.压缩策略
其实对于压缩策略,我认为无法肯定的说一定是1,或者2。它其实是看需求的,根据需求来定一个合理的压缩策略。我个人认为网上的 luban 压缩库,他的策略在某种程度上还算比较具有通用性的,适合大部分需求吧。对应 luban的压缩 算法,我这里简单的介绍下,他是将图片的比例值(短边除以长边)分为了三个区间值:

[1, 0.5625) 即图片处于 [1:1 ~ 9:16) 比例范围内

[0.5625, 0.5) 即图片处于 [9:16 ~ 1:2) 比例范围内

[0.5, 0) 即图片处于 [1:2 ~ 1:∞) 比例范围内

然后再去判断图片的最长的边是否超过这个区间的边界值,然后再去计算这个临近采样的 inSampleSize的值。如果想要具体的了解这个策略,去github 上下载源码去研究研究,代码还是很简单的。在实际的开发的过程中我们可以根据我们需求,结合上面几种压缩机制,自己制定一个比较适合自己的压缩策略。不过一般情况都不需要我们自己制定图片的压缩策略,采样压缩 ,基本已经满足我们的需求的

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

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精度
在这里插入图片描述
我们看到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对象
相关更多内容查看
https://www.jianshu.com/p/072b6defd938


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

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

小结:

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

  1. 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 的比例对图片的宽度和高度进行缩放。

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

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

3.图片的几种压缩办法

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

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

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

  4. 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项目下载地址

首先确保我们安装了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文件写的,那里面有我们需要的参数还有注释,所以我们要能看懂那个才可以。一般情况出了问题我们在研究那个吧

引用https://blog.csdn.net/lincyang/article/details/51085737

  • 构建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
5.库函数的介绍

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

https://www.jianshu.com/p/072b6defd938

https://github.com/AndroidHensen/BitmapCompress
https://blog.csdn.net/qq_25412055/article/details/53878655
https://blog.csdn.net/talkxin/article/details/50696511
https://www.cnblogs.com/mc-ksuu/p/6443254.html
https://www.jianshu.com/p/8f21d88d4439

在Android项目中如何使用libjpeg-trubo

首先你要安装ndk,如果不知道ndk是什么,建议你先从一些简单的Android知识开始补这文章可能不太适合你;

第二你要安装git(如果不会请google)

git clone git://git.linaro.org/people/tomgall/libjpeg-turbo/libjpeg-turbo.git -b linaro-android

把最新的版本克隆下来

2、编译

克隆下来的文件夹名为libjpeg-turbo,所以我们在使用NDK编译前需要将文件夹命名为JNI:

mv libjpeg-turbo jni

使用NDK编译时,这里需要注意的是APP_ABI这个参数,若需要在不同的平台下运行,则需要设置平台参数,如例所示,将编译出两个cpu平台的so库,不同的平台用逗号分隔

ndk-build APP_ABI=armeabi-v7a,armeabi

这时就可以看到在jni同目录下会生成libs与objs两个文件夹,生成的.so类库就在libs文件夹内。

====以上内容来自http://blog.csdn.net/talkxin/article/details/50696511 ========

你还需要把头文件找齐,都在我们刚刚克隆下来的代码目录里

cderror.h
cdjpeg.h
config.h
jconfig.h
jerror.h
jinclude.h
jmorecfg.h
jpeglib.h
jversion.h

好了,接下来是怎么编写c代码

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "jpeg/jpeglib.h"
#import <omp.h>

#ifdef ANDROID

#include <jni.h>
#include <android/log.h>

#define LOGE(format, ...)  __android_log_print(ANDROID_LOG_ERROR, "    (>_<)", format, ##__VA_ARGS__)
#define LOGI(format, ...)  __android_log_print(ANDROID_LOG_INFO,  "(^_^)", format, ##__VA_ARGS__)
#else
#define LOGE(format, ...)  printf("(>_<) " format "\n", ##__VA_ARGS__)
#define LOGI(format, ...)  printf("(^_^) " format "\n", ##__VA_ARGS__)
#endif

int write_JPEG_file(const char *filename, unsigned char *yData, unsigned char *uData,
                unsigned char *vData, int quality, int image_width, int image_height);

void Java_${这里替换成类的全路径}_writeJpegFile(JNIEnv *env, jobject jobj,
                                                                     jstring fileName,
                                                                     jobject yBuffer,
                                                                     jint yLen,
                                                                     jobject cbBuffer,
                                                                     jint cbLen,
                                                                     jobject crBuffer,
                                                                     jint uvStride,
                                                                     jint quality,
                                                                     jint width, jint height) {
    char *filename[500] = {0};
    sprintf(filename, "%s", (*env)->GetStringUTFChars(env, fileName, NULL));
    jbyte *y = (*env)->GetDirectBufferAddress(env, yBuffer);
    jbyte *cb = (*env)->GetDirectBufferAddress(env, cbBuffer);
    jbyte *cr = (*env)->GetDirectBufferAddress(env, crBuffer);
    uint8_t *uData = malloc(cbLen);
    uint8_t *vData = malloc(cbLen);
    int j, k;

    int uLimit = 0;
    int vLimit = 0;
    if (uvStride == 2) { // yuv420 sp uv交错
        #pragma omp parallel for num_threads(4)
        for (j = 0; j < cbLen; j++) {
            if (j % 2 == 0) {
                uData[uLimit++] = cb[j];
            } else {
                vData[vLimit++] = cb[j];
            }
        }
        #pragma omp parallel for num_threads(4)
        for (k = 0; k < cbLen; k++) {
            if (k % 2 == 0) {
                uData[uLimit++] = cr[k];
            } else {
                vData[vLimit++] = cr[k];
            }
        }
        write_JPEG_file(filename, y, uData, vData, quality, width, height);
    } else {    // yuv420p
        write_JPEG_file(filename, y, cb, cr, quality, width, height);
    }

    free(uData);
    free(vData);
}

int write_JPEG_file(const char *filename, unsigned char *yData, unsigned char *uData,
                unsigned char *vData, int quality, int image_width, int image_height) {
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;

    FILE *outfile;
    JSAMPIMAGE buffer;
    unsigned char *pSrc, *pDst;
    int band, i, buf_width[3], buf_height[3];
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo); 
    if ((outfile = fopen(filename, "wb")) == NULL) {
    return -1;
    }
    jpeg_stdio_dest(&cinfo, outfile);
    cinfo.image_width = image_width;  // image width and height, in pixels
    cinfo.image_height = image_height;
    cinfo.input_components = 3;    // # of color components per pixel
    cinfo.in_color_space = JCS_RGB;  //colorspace of input image

    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, quality, TRUE);

    cinfo.raw_data_in = TRUE;
    cinfo.jpeg_color_space = JCS_YCbCr;
    cinfo.comp_info[0].h_samp_factor = 2;
    cinfo.comp_info[0].v_samp_factor = 2;

    jpeg_start_compress(&cinfo, TRUE);

    buffer = (JSAMPIMAGE) (*cinfo.mem->alloc_small)((j_common_ptr) &cinfo,
                                                JPOOL_IMAGE, 3 * sizeof(JSAMPARRAY));
    #pragma omp parallel for num_threads(4)
    for (band = 0; band < 3; band++) {
        buf_width[band] = cinfo.comp_info[band].width_in_blocks * DCTSIZE;
        buf_height[band] = cinfo.comp_info[band].v_samp_factor * DCTSIZE;
        buffer[band] = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo,
                                              JPOOL_IMAGE, buf_width[band],   buf_height[band]);
    }
    unsigned char *rawData[3];
    rawData[0] = yData;
    rawData[1] = uData;
    rawData[2] = vData;

    int src_width[3], src_height[3];
    #pragma omp parallel for num_threads(4)
    for (i = 0; i < 3; i++) {
        src_width[i] = (i == 0) ? image_width : image_width / 2;
        src_height[i] = (i == 0) ? image_height : image_height / 2;
    }
    int max_line = cinfo.max_v_samp_factor * DCTSIZE;
    int counter;
    #pragma omp parallel for num_threads(4)
    for (counter = 0; cinfo.next_scanline < cinfo.image_height; counter++) {
    //buffer image copy.
        #pragma omp parallel for num_threads(4)
        for (band = 0; band < 3; band++) {  //每个分量分别处理
            int mem_size = src_width[band];//buf_width[band];
            pDst = (unsigned char *) buffer[band][0];
            pSrc = (unsigned char *) rawData[band] + counter * buf_height[band] *
                                                 src_width[band];//buf_width[band];  //yuv.data[band]分别表示YUV起始地址
            #pragma omp parallel for num_threads(4)
            for (i = 0; i < buf_height[band]; i++) { //处理每行数据
                memcpy(pDst, pSrc, mem_size);
                pSrc += src_width[band];//buf_width[band];
                pDst += buf_width[band];
            }
        }
        jpeg_write_raw_data(&cinfo, buffer, max_line);
    }
    jpeg_finish_compress(&cinfo);
    fclose(outfile);
    jpeg_destroy_compress(&cinfo);
    return 0;
}

这里面用到了openMP对for循环进行并线处理,感兴趣的同学可以去google一下,这里只简单介绍一下怎么用

首先 建立一个项目 吧那个C++啥的勾上 然后当前Moudlegradle配置

apply plugin: 'com.android.application'

 android {
compileSdkVersion 28
defaultConfig {
    applicationId "com.example.a15735.test"
    minSdkVersion 15
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    externalNativeBuild {
        cmake {
            cppFlags ""
            abiFilters 'arm64-v8a', 'armeabi-v7a'
        }
    }
     }
    buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
externalNativeBuild {
    cmake {
          //这里选你复制在jni 文件下的自带的CMakeLists.txt
        path "src/main/jni/CMakeLists.txt"
    }
}
  }

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

在这里插入图片描述

复制所有下载号的源码
https://github.com/libjpeg-turbo/libjpeg-turbo 下载地址
然后就重新编译就行了 SO库在

在这里插入图片描述

  • 0
    点赞
  • 0
    评论
  • 3
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值