第一:相关资料的下载
1,本章内容我们要使用到NDK,我们编译一个第三方库来来完成图片压缩。因为有了makelist,编译起来十分简单。
2,什么是NDK?
NDK就是我们允许C和C++语言在Android中开发,我们一般不会用jni去访问C/C++,而是把它编译成动态链接库或者是静态链接库。简单说NDK就是一个工具集,
3,LibJpeg库
下载地址:https://libjpeg-turbo.org/
点击该网站
编译LibJpeg,(需要在Xshell中编译)
https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md
我们点击该网址查看
(1)CMake v2.8.12 or later cmake必须是2.8.12以上的版本。cmake下载地址
(2)安装NASM或YASM(用来编译生成.a库)
下载路径 https://www.nasm.us/pub/nasm/releasebuilds/
下载压缩包 wget https://www.nasm.us/pub/nasm/releasebuilds/2.14/nasm-2.14.tar.gz
解压 tar xvf nasm-2.14.tar.gz
进入解压后的目录可以看到一个文件configure 用于生成Makefile文件,编译出该文件 ./configure
二,Libjepg压缩步骤
上面的怎么搞,明白了没?反正我没搞定,只能记录下步骤,用包的时候就拿来主义了。不过没事我们接下来看看怎么使用
生成的文件在这里下载,新建项目的时候记得勾选include c++ support.
我们把生成的四个.h文件,和静态库libturboipeg.a放入工程里面。
所有运行的C代码都是运行在静态库libturboipeg.a中。
改下.gradle文件,注释的地方就是需要修改的地方。
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.administrator.lsn_5_demo"
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
//编译的过滤器
abiFilters "x86_64"
//指定android的编译器17以下
arguments '-DANDROID_TOOLCHAIN=gcc'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
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'
}
注意这里
修改CMakeLists.txt,做注释的地方是新添加的
cmake_minimum_required(VERSION 3.4.1)
add_library(
#表示cpp文件夹下的native-lib.cpp文件
native-lib
SHARED
src/main/cpp/native-lib.cpp)
#libjpeg是导入的静态库
add_library(libjpeg STATIC IMPORTED)
#填写libturbojpeg.a的路径 {CMAKE_SOURCE_DIR}表示当前路径
set_target_properties(libjpeg PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libturbojpeg.a)
#引入头文件 类似于Java的import,这里就是我们拷贝进来的四个头文件。src是CMakeLists的同级目录
include_directories(src/main/cpp/include)
target_link_libraries(
native-lib
#增加该库
libjpeg
#jnigraphics是安卓NDK目录中直接有的
jnigraphics
log)
注意以上配置用的是NDK17版本,NDK19有好多问题。如果是ndk19我们需要这么配置
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.example.administrator.lsn_5_demo"
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
abiFilters "x86"
//指定android的编译器,NDK19配置需要修改的地方
arguments '-DANDROID_TOOLCHAIN=clang'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
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'
}
NDK19配置文件下载猛戳这里
另外本篇文章涉及到哈夫曼树。这里我简要介绍下。注意,哈夫曼树到Android7.0才可以使用,6.0之前不能使用。但我们今天是自己打包写的代码,所以每个版本都可以使用。
我们看下如何生成哈夫曼树以及如何使用哈夫曼树编码
native-lib的代码
#include <jni.h>
#include <string>
//import .......
#include <malloc.h>
#include <android/bitmap.h>
#include <jpeglib.h>
/**
本方法七个步骤:固定步骤
3.1、创建jpeg压缩对象
3.2、指定存储文件
3.3、设置压缩参数
3.4、开始压缩
3.5、循环写入每一行数据
3.6、压缩完成
3.7、释放jpeg对象
|*/
void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {
// 3.1、创建jpeg压缩对象
//拿到压缩结构体,等下信息往结构体里面存
jpeg_compress_struct jcs;
//错误回调,
jpeg_error_mgr error;
//发生错误的时候存到该结构体
jcs.err = jpeg_std_error(&error);
//创建压缩对象,压缩信息存到jcs里面
jpeg_create_compress(&jcs);
// 3.2、指定存储文件,wb就是 write binary的缩写
FILE *f = fopen(path, "wb");
//我们以二进制的形式写到文件定位的地方
jpeg_stdio_dest(&jcs, f);
// 3.3、设置压缩参数
jcs.image_width = w;
jcs.image_height = h;
//这里是按照BGR的格式存进去的
jcs.input_components = 3;
jcs.in_color_space = JCS_RGB;
//设置默认值
jpeg_set_defaults(&jcs);
//开启哈夫曼功能
jcs.optimize_coding = true;
//设置质量
jpeg_set_quality(&jcs, q, 1);
// 3.4、开始压缩 ,1代表true
jpeg_start_compress(&jcs, 1);
// 3.5、循环写入每一行数据
//一行图片有3*图片宽度个字节
int row_stride = w * 3;//一行的字节数
//大小为1的数组
JSAMPROW row[1];
//jcs.next_scanline:图片有多少行,从0开始
while (jcs.next_scanline < jcs.image_height) {
//取一行数据
//*pixels定位到每一行的起始位置
uint8_t *pixels = data + jcs.next_scanline * row_stride;
row[0]=pixels;
//写到结构体里面
jpeg_write_scanlines(&jcs,row,1);
}
// 3.6、压缩完成
jpeg_finish_compress(&jcs);
// 3.7、释放jpeg对象
fclose(f);
jpeg_destroy_compress(&jcs);
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_administrator_lsn_15_1demo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_administrator_lsn_15_1demo_MainActivity_testImage(JNIEnv *env, jobject instance) {
}
/**
path:文件最后存放的地方
q:压缩质量,可选值是0-100,一般是30-50,写的太低可能导致图片失真
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_example_administrator_lsn_15_1demo_MainActivity_nativeCompress(JNIEnv *env,
jobject instance,
jobject bitmap, jint q,
jstring path_) {
//这句话不能删除,删了后用path_时候会报错 ,
const char *path = env->GetStringUTFChars(path_, 0);
//从bitmap获取argb数据,ARGB是四个字节32位
AndroidBitmapInfo info;//info=new 对象();
//获取里面的信息 &info就是该指针指向bitmap这块内存(Java中已经开辟好了这块内存),把bitmap的信息放到AndroidBitmapInfo info中。
AndroidBitmap_getInfo(env, bitmap, &info);
//得到图片中的像素信息
uint8_t *pixels;//uint8_t是c中的char,java中的byte *pixels可以当byte[]用
//锁住像素,记得要解锁
//**相当于二维数组
//&pixels指向图片的起始位置,是一个8位数据
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
//jpeg argb中去掉他的a ===>去掉透明度,只剩下rgb信息
int w = info.width;//获取图片的宽高
int h = info.height;
//color是四个字节(一个字节占8位内存),能存透明度和rgb
//等下我们在原图中用color取一个像素点,像素点本来是四个字节,我们去掉一个只剩下3个(RGB),这样就实现了图片的压缩
int color;
//开一块内存用来存入rgb信息
//颜色是一个8位的信息,所以这里使用uint8_t
//malloc相当于new就是开辟一块多大的内存,
//开辟一个w * h * 3字节的内存,新开辟的内存空间是原来内存的3/4。原来内存大小是4乘以像素点的个数,现在开辟内存的大小是3乘以像素点的个数
//乘以三的意思是后面操作我只存入rgb信息,透明度信息不要了
uint8_t *data = (uint8_t *) malloc(w * h * 3);//data中可以存放图片的所有内容,
//data 和temp都是定位在图片在内存中开始的地方,我们所有的操作都在temp中操作,而data不动,还是在开始的地方
uint8_t *temp = data;
//定义三个字节的数据,每个字节8位,透明度我不要了。
uint8_t r, g, b;
//循环取图片的每一个像素,一个一个一行一行取出来。
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
//pixes现在有0-3四个字节,此时pixes也指向bitmap上
//color是4个字节 一次color取到的就是一个像素点
color = *(int *) pixels;
//取出rgb,通过位移来取
r = (color >> 16) & 0xFF;// #00rrggbb color从初始位置移动16位就定位到#00rr,然后跟0xFF作与操作,得到的就是rr
g = (color >> 8) & 0xFF;
b = color & 0xFF;
//存放,以前的主流格式jpeg存放数据是以bgr格式存储的。我们要根据具体的情况确定RGB的排序。把结果存到data里。
*data = b;
//下一位存g
*(data + 1) = g;
*(data + 2) = r;
//data每次跳三个字节,pixels每次跳四个字节。最后data中存的东西就是原来图片中每隔四个少存一个。
data += 3; //内存中跳过三格
pixels += 4; //指针跳过4个字节
}
}
//把得到的新的图片的信息存入一个新文件 中
write_JPEG_file(temp,w,h,q,path);
//内存空间用完要释放
AndroidBitmap_unlockPixels(env, bitmap);
//释放data内存
free(data);
env->ReleaseStringUTFChars(path_, path);
}
void write_JPEG_file(uint8_t *temp, int w, int h, jint q, const char *path) {
//这里可以继续压缩,第一次压缩我们是去掉alpha通道,现在我们使用halfMan压缩
}
MainActivity中调用
package com.example.administrator.lsn_5_demo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
Bitmap inputBitmap=null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File input = new File(Environment.getExternalStorageDirectory(), "girl.jpg");
inputBitmap = BitmapFactory.decodeFile(input.getAbsolutePath());
// 这是Android原生API的压缩
// /**
// * 质量压缩
// */
// compress(bitmap, Bitmap.CompressFormat.JPEG,50,Environment.getExternalStorageDirectory()+"/test_q.jpeg");
// /**
// * 尺寸压缩
// * 改变图片尺寸,这个压缩用的比较少,正常情况下我们压缩一张图片是不会修改尺寸的。
// */
// //filter 图片滤波处理 色彩更丰富
// Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 300, 300, true);
// compress(scaledBitmap, Bitmap.CompressFormat.JPEG,100,Environment.getExternalStorageDirectory()+"/test_scaled.jpeg");
// //png格式
// compress(bitmap, Bitmap.CompressFormat.PNG,100,Environment.getExternalStorageDirectory()+"/test.png");
// //webp格式
// compress(bitmap, Bitmap.CompressFormat.WEBP,100,Environment.getExternalStorageDirectory()+"/test.webp");
}
/**
* 压缩图片到制定文件
*
* @param bitmap 待压缩图片
* @param format 压缩的格式
* @param q 质量
* @param path 文件地址
*/
// private void compress(Bitmap bitmap, Bitmap.CompressFormat format, int q, String path) {
// FileOutputStream fos = null;
// try {
// fos = new FileOutputStream(path);
// bitmap.compress(format, q, fos);
// } catch (FileNotFoundException e) {
// e.printStackTrace();
// } finally {
// if (null != fos) {
// try {
// fos.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
//
// }
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
* btitmap:需要压缩的对象
*/
public native void nativeCompress(Bitmap bitmap, int q, String path);
public void click(View view) {
nativeCompress(inputBitmap, 50, Environment.getExternalStorageDirectory() + "/girl2.jpg");
Toast.makeText(this, "执行完成", Toast.LENGTH_SHORT).show();
}
}
我们对比下发现原来的图片61KB,压缩后仅剩下8KB
附源码猛戳这里
三图片存放路径的问题
我们第二部是在压缩图片,那么我们怎么优化图片占用的内存呢?
我们一张图片存放在磁盘中只有16,384字节,但是在手机中运行内存却占用6153472字节。图片在磁盘中的大小与图片在运行中的大小并没有直接的关系。运行时图片内存的计算是图片的像素点(长X宽)乘以像素的格式(Bitmap.Config)
我们看下代码
package com.example.administrator.lsn6_demo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 建议图片以后用这种格式,比较节省内存看,腾讯处理图片用的就是RGB_565
// Bitmap.Config.RGB_565;
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.wyz_p);
i(bitmap);
}
void i(Bitmap bitmap){
//打印输出占用内存大小
//打印输出的结果为1118X376 内存大小:6153472字节
Log.i("jett","图片"+bitmap.getWidth()+"X"+bitmap.getHeight()+" 内存大小:"+bitmap.getByteCount());
}
}
注意这张图片的是webp图片,应用比较广泛的一种图片。
为什么会造成这种情况呢?这与我们的大家用到的目录有关系。我们刚才是把图片放到res—>drawable-v24目录下.。
我们现在放到res—>mipmap目录下,我们修改下代码
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p);
现在图片的长和宽变为:373X459,内存变成684828(373X459X4=684828,每个像素点占用4个字节所以乘以4)字节,缩小了近十倍
所以我们的图片放的路径很重要,一定要注意图片的分辨率问题。如果是xxh的分辨率图片,你却放到xh里面,图片会进行缩放。这样内存占用就变大。
四
这里列举这样的一个场景,我们有一张图片大小为373X459,但是我只需要80X80的一个小框框展示,这个时候我们是不需要加载这么多内训的。很多像素点我们是不需要的,可以压缩掉。
我们自己定义一个类来完成这个功能。
package com.example.administrator.lsn6_demo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class ImageResize {
/**
*
* @param context
* @param id
* @param maxW 图片允许最大的宽,比如我们允许的最大宽高是80X80
* @param maxH
* @param hasAlpha 是否需要alpha通道
* @return
*/
//1,decodeResourcer 查看Android源码可以发现,从drawable中解码出图片,控制参数主要是两个,一个是opts.inDensity表示
/像素密度,该密度根据drawable目录进行计算
//2,一个是 opts.inTargetDensity 目标像素密度,即画到屏幕上的
//像素密度。我们在操作的时候可以自己控制
//3, 从资源中拿到该图片,我们修改它的解码方式
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
//获取bitmap时候需要使用
Resources resources = context.getResources();
//我们自己定义option,
BitmapFactory.Options options = new BitmapFactory.Options();
//需要拿得到系统处理的信息 比如解码出宽高,...
// 加了这句话只能得到图片的解码信息,而不是得到图片.
options.inJustDecodeBounds = true;
//我们把原来的解码参数改了再去生成bitmap,这里的Option使我们自己修改的
BitmapFactory.decodeResource(resources, id, options);
//取到宽高
int w = options.outWidth;
int h = options.outHeight;
//1,设置缩放系数(int类型,不能设置为小数),缩放系数是2的倍数。
//比如1000X1000的,我们可以解码成125X125,然后让系统自己
//缩小成80X80
//2,修改inSampleSize,android会根据这个数据进行缩放
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
//图片不需要Rlpha(透明度)通道
if(!hasAlpha){
options.inPreferredConfig=Bitmap.Config.RGB_565;
}
//关掉
options.inJustDecodeBounds=false;
//1,得到新的图片,我们用的Options的参数全是气门改了以后的。
return BitmapFactory.decodeResource(resources,id,options);
}
//返回结果是原来解码的图片的大小 是我们需要的大小的
// 最接近2的几次方倍.比如1000缩放到80最接近的就是8
/**
*
* @param w decode得到的值
* @param h
* @param maxW 最大允许的宽,我要现实图片,但我的控件只有80X80,
* 这里就传一个80X80
* @param maxH
* @return
*/
private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
//默认大小不缩放
int inSampleSize = 1;
if (w > maxW && h > maxH) {
//做一次缩放
inSampleSize = 2;
while (w / inSampleSize > maxW && h / inSampleSize > maxH){
inSampleSize*=2;
}
}
//多除以了一次2,这里乘以2
inSampleSize/=2;
return inSampleSize;
}
}
在MainActivity中调用
package com.example.administrator.lsn6_demo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.wyz_p);
i(bitmap);
//decodeResourcer 从drawable解码出图片来,解码的时候会读取两个参数,
//一个是opts.inDensity表示像素密度,该密度根据drawable目录进行计算
//一个是 opts.inTargetDensity 目标像素密度,即画到屏幕上的像素密度。
//我们在操作的时候可以自己控制。怎么空着请看ImageResize.resizeBitmap();
Bitmap bitmap2=ImageResize.resizeBitmap(getApplicationContext(),R.mipmap.wyz_p,80,80,false);
i(bitmap2);
}
void i(Bitmap bitmap){
Log.i("jett","图片"+bitmap.getWidth()+"X"+bitmap.getHeight()+" 内存大小:"+bitmap.getByteCount());
}
}
``
第一次打印输出:373X459 684828
第二次打印输出:47X58 5452