1 前言
上一篇已经简单的介绍了Cmake的基本使用以及编写规则了,这一篇博客我们简单介绍在NDK开发中常见的一些场景时如何编写cmake
2 NDK中cmake的使用场景
一般在NDK开发中,我们编译so库或者使用外部的so库,cmake的编写规则场景主要有以下几种。
1 cmake 编译单个c/cpp文件为so
这种单个的c/cpp文件一般为JNI层的文件,例如在我们新建一个支持C++的工程中native-lib.cpp:
#include <jni.h>
#include <string>
extern "C"
jstring
Java_com_qiyei_essayjoke_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
关于这里我们将native-lib.cpp编译成libnative-lib.so,我们只需要在cMakeLists.txt中如下配置:
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src/main/cpp/native-lib.cpp )
这里add_library就表示添加一个库,native-lib表示库名称,可以随便取,SHARED表示添加的动态库,后面的src/main/cpp/native-lib.cpp表示要编译成native-lib库的源文件
2 使用外部的so库 + c/cpp
这种使用的场景很多,在NDK开发中,我们往往会使用别人现成的so库,我们在编写自己的c/cpp代码时,需要调用别人so库的函数,这个时候的cmake就需要如下编写了。以使用libjpeg.so为例,先来看我们自己的c/cpp代码:
compress_image.h
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/7/23.
* Version: 1.0
* Description:
*/
#ifndef ESSAYJOKE_COMPRESS_IMAGE_H
#define ESSAYJOKE_COMPRESS_IMAGE_H
#include <jni.h>
#include <string>
/**
* Log打印
*/
#define LOG_TAG "jni"
#define LOG_W(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOG_I(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define true 1
#define false 0
/**
* 统一编译方式
*/
#ifdef __cplusplus
extern "C"{
#endif
#include "jpeg/jpeglib.h"
#include "jpeg/cdjpeg.h" /* Common decls for cjpeg/djpeg applications */
#include "jpeg/jversion.h" /* for version message */
#include "jpeg/jconfig.h"
JNIEXPORT jint JNICALL
Java_com_qiyei_sdk_util_ImageUtil_jpegCompressBitmap(JNIEnv *env, jclass type, jobject bitmap,jint quality, jstring path_);
#ifdef __cplusplus
};
#endif
#endif //ESSAYJOKE_COMPRESS_IMAGE_H
compress_image.cpp
/**
* Email: 1273482124@qq.com
* Created by qiyei2015 on 2017/7/23.
* Version: 1.0
* Description:
*/
#include "compress_image.h"
#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <stdio.h>
#include <setjmp.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
/**
* 定义BYTE 类型为u_int8_t
*/
typedef u_int8_t BYTE;
char *error;
/**
* error 结构体
*/
struct error_mgr {
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
};
/**
* 结构体类型定义
*/
typedef struct error_mgr *error_ptr;
/**
* 错误退出函数
*/
METHODDEF(void) error_exit(j_common_ptr info){
//获取j_common_ptr中的error
error_ptr err = (error_ptr)info->err;
//调用error中的output_message输出打印信息
(*info->err->output_message)(info);
error = (char *)err->pub.jpeg_message_table[err->pub.msg_code];
LOG_E("jpeg error_exit,jpeg_message_table[%d]:%s",err->pub.msg_code,err->pub.jpeg_message_table[err->pub.msg_code]);
//
longjmp(err->setjmp_buffer,1);
}
/**
* jpeg压缩图片
*/
jint compress_jpeg(BYTE * data,int width,int height,int quality,jboolean optimize,char * file_name){
struct jpeg_compress_struct jcs;
//当读完整个文件的时候就会回调my_error_exit这个退出方法。
struct error_mgr jpeg_err;
jcs.err = jpeg_std_error(&jpeg_err.pub);
jpeg_err.pub.error_exit = error_exit;
//setjmp是洗衣歌系统级函数,是一个回调
if (setjmp(jpeg_err.setjmp_buffer)){
return -1;
}
//初始化jcs结构体
jpeg_create_compress(&jcs);
//打开输出文件 wb 可写 rb 可读,最终会把压缩的图像保存到该文件中
FILE *f = fopen(file_name,"wb");
if ( f == NULL){
return -1;
}
//设置jcs的文件路径以及宽高
jpeg_stdio_dest(&jcs,f);
jcs.image_width = width;
jcs.image_height = height;
jcs.arith_code = false; //true 算数编码,flase 霍夫曼编码
///* 颜色的组成 rgb,三个 # of color components in input image */
int nComponent = 3;
jcs.input_components = nComponent; //颜色组成RGB
jcs.in_color_space = JCS_RGB;
jpeg_set_defaults(&jcs);
//是否采用霍夫曼编码
jcs.optimize_coding = optimize;
//设置质量
jpeg_set_quality(&jcs,quality,true);
//开始压缩
jpeg_start_compress(&jcs,TRUE);
JSAMPROW row_pointer[1];
int row_stride = jcs.image_width * nComponent;
//依次按照每一行循环扫描
while (jcs.next_scanline < jcs.image_height){
//得到一行的首地址。然后写入
row_pointer[0] = &data[jcs.next_scanline * row_stride];
jpeg_write_scanlines(&jcs,row_pointer,1);
}
//压缩结束
jpeg_finish_compress(&jcs);
//销毁回收内存
jpeg_destroy_compress(&jcs);
//关闭文件
fclose(f);
return 0;
}
JNIEXPORT jint JNICALL
Java_com_qiyei_sdk_util_ImageUtil_jpegCompressBitmap(JNIEnv *env, jclass type, jobject bitmap,
jint quality, jstring path_) {
//1. 解析RGB
//1.1 获取bitmap信息,w,h.format
AndroidBitmapInfo bitmapInfo;
//java你调用完方法往往返回的是对象,而C往往是参数
AndroidBitmap_getInfo(env,bitmap,&bitmapInfo);
//获取bitmap信息
int bitmap_width = bitmapInfo.width;
int bitmap_height = bitmapInfo.height;
int bitmap_format = bitmapInfo.format;
//对于不是ARGB8888格式的图片不支持
if (bitmap_format != ANDROID_BITMAP_FORMAT_RGBA_8888){
return -1;
}
LOG_W("bitmap,width = %d,height = %d",bitmap_width,bitmap_height);
//1.2 把bitmap解析到数组中,数组中保存的是rgb --> YCbCr
//1.2.2 锁定画布
BYTE * pixel_color;
//将bitmap数组导出到pixel_color中
AndroidBitmap_lockPixels(env,bitmap,(void **) &pixel_color);
//1.2.2 解析数据
BYTE *data;
BYTE r,g,b;
//申请内存,因为不需要保存A通道,所以只需要申请 bitmap_width * bitmap_height * 3
data = (BYTE *) malloc(bitmap_width * bitmap_height * 3);
//数组指针指向首地址,因为这块内存需要释放,所以先保存下
BYTE * head = data;
int i = 0;
int j = 0;
int color;
for (i = 0 ; i < bitmap_width ;i++){
for (j = 0;j < bitmap_height;j++){
color = *((int *)pixel_color);
//将rgb取出来 ARGB 每个字节占八位
r = (color & 0x00FF0000) >> 16;
g = (color & 0x0000FF00) >> 8;
b = (color & 0x000000FF) >> 0;
//保存到data中
*data = b;
*(data + 1) = g;
*(data + 2) = r;
data = data + 3;
// 一个像素点包括argb四个值,每+4下就是取下一个像素点
pixel_color = pixel_color + 4;
}
}
//1.2.3 解锁画布
AndroidBitmap_unlockPixels(env,bitmap);
//1.2.4 还差一个参数,jstring -> char*
char * file_name = (char *)env->GetStringUTFChars(path_,NULL);
int result = compress_jpeg(head,bitmap_width,bitmap_height,quality, true,file_name);
//释放内存
free(head);
env->ReleaseStringUTFChars(path_,file_name);
//释放bitmap,调用bitmap的recycle
//找到Bitmap类
jclass clazz = env->GetObjectClass(bitmap);
jmethodID jmethod_ID = env->GetMethodID(clazz,"recycle","()V");
//调用Bitmap的recycle()方法释放对象
env->CallVoidMethod(bitmap,jmethod_ID);
LOG_W("result = %d",result);
//4 返回结果
return result;
}
比如,在compress_image.cpp中我们使用大量的libjpeg.so库中的方法,例如jpeg_set_quality,jpeg_start_compress等。这个时候我们的cmake一般来说需要做如下修改:
#设置生成的so动态库最后输出的路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})
add_library( # Sets the name of the library.
compressimg
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
# Associated headers in the same location as their source
# file are automatically included.
src/main/cpp/compress_image.cpp )
# 添加libjpeg.so依赖库
add_library(jpeg SHARED IMPORTED)
#设置libjpeg.so依赖库路径
set_target_properties(
jpeg
PROPERTIES IMPORTED_LOCATION
${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpeg.so)
解释如下:
1 还是添加compressimg.so,源文件为src/main/cpp/compress_image.cpp
2 添加jpeg.so,因为是导入别人的,因此是IMPORTED
3 设置查找jpeg.so的路径 “${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpeg.so”,其中${PROJECT_SOURCE_DIR}表示工程源文件目录,与src文件这一目录同级,${ANDROID_ABI}表示armeabi, armeabi-v7a,arm64-v8a等目录
4 最后将jpeg.so库链接到compressimg.so中
3 多个c/cpp文件编译成so库
这种情况也比较多见,一般来说如果我们下载了某个so的源码,或者我们编写了多个c/cpp文件 ,需要将这些源文件编译成一个so库,这个情况实际和第一种差不多。
例如,我们需要将以下源文件编译成so库
这个时候我们的cmake如下:
#配置头文件路径
#include_directories(${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/)
#添加libbspatch 库
add_library( # Sets the name of the library.
bspatch
SHARED
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/huffman.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/randtable.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/crctable.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/blocksort.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/bzip2.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/compress.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/decompress.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/bzlib.c
${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/bspatch.c)
解释如下:
1 include_directories添加头文件,主要告诉cmake,去哪个目录下找对应的头文件
2 由于我这里不需要将${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/文件夹下所有的c/cpp文件编译成so库,因此我将文件单独添加,实际中不建议这么做,可使用变量将某个路径全部编译,详情请见下一节内容
3 cmake常用脚本
在编写cmake的过程中,我们往往会发现一些特别有用的脚本或者变量,现在记录下来,以备不时之需,也会做不定时的更新。
1 cmake中常用的变量
STATIC: 表示静态的.a的库。
SHARED: 表示.so的库,动态库
CMAKE_LIBRARY_OUTPUT_DIRECTORY: so库或者静态库的最后输出路径
${ANDROID_NDK}: Android Studio已经定义好的变量,可以直接使用它指定的是NDK源代码的根目录
${PROJECT_SOURCE_DIR}: 表示工程源文件目录,与src文件这一目录同级
${ANDROID_ABI}: 表示armeabi, armeabi-v7a,arm64-v8a等目录
${CMAKE_SOURCE_DIR}: 表示CMakeLists.txt的当前文件夹路径。
2 cmake中常用的脚本
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY path)
设置生成的so动态库最后输出的路径
include_directories(path)
配置头文件搜索路径
#添加源文件路径
aux_source_directory(${PROJECT_SOURCE_DIR}/src/main/cpp/bsdiff/ CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
添加源文件路径,这样${SRC_LIST}就表示src/main/cpp/bsdiff/目录了
set_target_properties(
xxx
PROPERTIES IMPORTED_LOCATION
path)
设置libxxx.so依赖库的路径