王学岗性能优化5、6——图片压缩初体验(一)(二)

第一:相关资料的下载
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字节。我们看下代码

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 解码控制参数主要是两个,一个是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

猜你喜欢

转载自blog.csdn.net/qczg_wxg/article/details/90028926