上周由于业务需要,需要实现一个支持ReactNative的MP3录音库,这里我抽离了其中转码的部分来系统的演示如何使用NDK调用C/C++代码。
通过本文你可以学到以下知识:
在日常开发中,我们可能会遇到以下场景:
LAME是目前最好的MP3编码器,速度快,效果好,特别是中高码率和VBR编码方面。
原生开发工具包,即帮助我们开发原生代码的一系列工具,包括但不限于编译工具、一些公共库、开发IDE等。它提供了完整的一套将C/C++ 代码编译成静态/动态库的工具,而 Android.mk
和 Application.mk
你可以认为是描述编译参数和一些配置的文件。比如指定使用C++11还是C++14编译,会引用哪些共享库,并描述关系等,还会指定编译的abi
。只有有了这些 NDK 中的编译工具才能准确的编译 C/C++代码。
CMake
是一个跨平台的编译工具,它并不会直接编译出对象,而是根据自定义的语言规则(CMakeLists.txt
)生成 对应 makefile 或 project 文件,然后再调用底层的编译。Android Studio 2.2以后开始支持CMake
,所以现在我们有2种方式来编译C/C++ 代码。一个是 ndk-build + Android.mk + Application.mk
组合,另一个是 CMake + CMakeLists.txt
组合,它们都不会影响我们的Android代码和C/C++ 代码,只是构建方式和结构不同。
CMake
相对ndk-build
的优点在于:无需手动生成Java的头文件、相对于mk文件配置更简单、可以自动生成对应abi
的*.so
动态链接库、支持设置断点调试(我认为这是最方便的地方)、可以引用其他已经生成的so库。
通过这张图我们可以更形象的理解CMake构建NDK的结构和方式。
Tips:如果你对CMake刚接触,可以先用Android Studio创建一个项目,然后勾选上Include C++ support
选项,去看下demo的结构,帮助理解,我就是这样做的,效果还不错。
src/main/
目录下新建一个cpp
文件夹,我们可以将Lame源码中libmp3lame
拷贝到cpp
文件夹下,当然这里我们也可以重命名,例如我命名为lamemp3
(以下介绍我将沿用此名)。include
文件夹下的lame.h
复制到lamemp3
文件夹中。lamemp3
中不必要的文件和目录,只保留.c
和.h
文件,因为其他文件大多都是批处理文件,对于Android不是必需的。util.h
的源码。在570行找到ieee754_float32_t
数据类型,将其修改为float
类型,因为ieee754_float32_t
是Linux或者是Unix下支持的数据类型,在Android下并不支持。set_get.h
中24行将include
改为include "lame.h"
。id3tag.c
和machine.h
两个文件里,將HAVE_STRCHR
和HAVE_MEMCPY
的ifdef结构体注释掉,不然编译会报错。#ifdef STDC_HEADERS
# include
# include
#else
/*
# ifndef HAVE_STRCHR
# define strchr index
# define strrchr rindex
# endif
*/
char *strchr(), *strrchr();
/*
# ifndef HAVE_MEMCPY
# define memcpy(d, s, n) bcopy ((s), (d), (n))
# define memmove(d, s, n) bcopy ((s), (d), (n))
# endif
*/
#endif
在src
中新建一个名为CMakeLists.txt
的文件(注意,这里的CMakeLists.txt
不一定非要放到这里,只要它的位置和build.gradle
文件的配置相对应就行)。
我们看下CMakeLists.txt
的内容,这里我把注释已经写得很详细了,大家看下就明白了:
# 指定CMake最低版本
cmake_minimum_required(VERSION 3.4.1)
# 定义常量
set(SRC_DIR src/main/cpp/lamemp3)
# 指定关联的头文件目录
include_directories(src/main/cpp/lamemp3)
# 查找在某个路径下的所有源文件
aux_source_directory(src/main/cpp/lamemp3 SRC_LIST)
# 设置 *.so 文件输出路径,要放在在add_library之前,不然不会起作用
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
# 声明库名称、类型、源码文件
add_library(lame-mp3-utils SHARED src/main/cpp/lame-mp3-utils.cpp ${SRC_LIST})
# 定位某个NDK库,这里定位的是log库
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# 将NDK库链接到native库中,这样native库才能调用NDK库中的函数
target_link_libraries( # Specifies the target library.
lame-mp3-utils
# Links the target library to the log library
# included in the NDK.
${log-lib} )
android {
......
defaultConfig {
......
externalNativeBuild {
cmake {
cppFlags ""//设置cpp配置参数,c文件请使用CFlags
abiFilters 'armeabi-v7a','arm64-v8a','mips','armeabi','mips64','x86','x86_64' //要支持的abi
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"//配置文件路径
}
}
}
你可能在build时会遇到以下错误:
Execution failed for task ':library:transformNativeLibsWithMergeJniLibsForDebug'.
> More than one file was found with OS independent path 'lib/x86_64/liblame-mp3-utils.so'
解决方案是在build.gradle中添加如下配置:
packagingOptions {
pickFirst 'lib/armeabi-v7a/liblame-mp3-utils.so'
pickFirst 'lib/arm64-v8a/liblame-mp3-utils.so'
pickFirst 'lib/armeabi/liblame-mp3-utils.so'
pickFirst 'lib/x86/liblame-mp3-utils.so'
pickFirst 'lib/mips/liblame-mp3-utils.so'
pickFirst 'lib/mips64/liblame-mp3-utils.so'
pickFirst 'lib/x86_64/liblame-mp3-utils.so'
}
这里的pickFirst
官方文档给出的解释是:
Paths that match a first-pick pattern will be selected into the APK.
If more than one path matches the first-pick, only the first found will be selected.
通俗点讲,如果有两个重复的匹配,它会选择你设置的第一匹配项。
这里我在代码中注释已经写得非常详细了,关于一些参数我会在下面做更详细的解释。
public class Mp3Converter {
static {
System.loadLibrary("lame-mp3-utils");
}
/**
* init lame
* @param inSampleRate
* input sample rate in Hz
* @param channel
* number of channels
* @param mode
* 0 = CBR, 1 = VBR, 2 = ABR. default = 0
* @param outSampleRate
* output sample rate in Hz
* @param outBitRate
* rate compression ratio in KHz
* @param quality
* quality=0..9. 0=best (very slow). 9=worst.
* recommended:
* 2 near-best quality, not too slow
* 5 good quality, fast
* 7 ok quality, really fast
*/
public native static void init(int inSampleRate, int channel, int mode,
int outSampleRate, int outBitRate, int quality);
/**
* file convert to mp3
* it may cost a lot of time and better put it in a thread
* @param input
* file path to be converted
* @param mp3
* mp3 output file path
*/
public native static void convertMp3(String input, String mp3);
/**
* get converted bytes in inputBuffer
* @return
* converted bytes in inputBuffer
* to ignore the deviation of the file size,when return to -1 represents convert complete
*/
public native static long getConvertBytes();
/**
* get library lame version
* @return
*/
public native static String getLameVersion();
}
先看一个上面Java文件中native init(args...)
方法在C++里是如何关联和调用的:
extern "C" JNIEXPORT void JNICALL
Java_jaygoo_library_converter_Mp3Converter_init(JNIEnv *env, jclass type, jint inSampleRate,
jint channel, jint mode, jint outSampleRate,
jint outBitRate, jint quality) {
lameInit(inSampleRate, channel, mode, outSampleRate, outBitRate, quality);
}
extern "C"
因为我们写的是cpp是C++文件,所以当我们调用一些C文件的方法时需要加上extern "C"
,不然会提示找不到方法。Java_jaygoo_library_converter_Mp3Converter_init
这里方法名是和Java文件中的native方法对应的,只有这样才能让Java native方法找到对应的cpp方法。格式是:Java_包名_类名_方法名
,这里包名的.
用_
代替,所以我们native的方法名命名尽量采用不要包含_
,但如果真的包含了,那么在cpp文件中用1
代替Java native 中的_
。JNIEXPORT void JNICALL
JNI的关键字,表示此函数是要被JNI调用的。JNIEnv *env
JNIEnv是指向JNINativeInterface结构的指针,当我们需要调用JNI方法时,都需要通过这个指针才能进行调用。其实我们还可以通过Android Studio来自动生成这些方法和参数,在Android Studio中点击native方法名,快捷键alt+enter
即可自动生成了。
看到这里,大家基本对如何编写cpp代码有一定的了解,接下来我来介绍下lame-mp3-utils.cpp
的实现,由于篇幅有限,这里就只介绍一些关键的代码。
这里主要是对Lame进行一些初始化,主要的参数包括:
这里的代码没什么可看的,主要是调用一些Lame自带的方法设置一些配置参数,最后调用lame_init_params(lame)
完成初始化,这里我对上面参数中出现的名词做下解释:
采样率
每秒从连续信号中提取并组成离散信号的采样个数,单位Hz。数值越高,音质越好,常见的如8000Hz、11025Hz、22050Hz、32000Hz、44100Hz等。码率
又称比特率是指每秒传送的比特(bit)数,单位kbps,越高音质越好(相同编码格式下)。CBR
常数比特率编码,码率固定,速度较快,但压缩的文件相比其他模式较大,音质也不会有很大提高,适用于流式播放方案,Lame默认的方案是这种。VBR
动态比特率编码,码率不固定。适用于下载后在本地播放或者在读取速度有限的设备播放,体积和为CBR
的一半左右,但是输出码率不可控。ABR
平均比特率编码,是Lame针对CBR不佳的文件体积比和VBR生成文件大小不定的特点独创的编码模式。是一种折中方案,码率基本可控,但是好像用的不多。
首先我们要将jstring
转换为c++中的char*
后才可以使用,我们可以通过JNI提供的GetStringUTFChars
方法完成转换:
const char* cInput = env->GetStringUTFChars(jInputPath, 0);
const char* cMp3 = env->GetStringUTFChars(jMp3Path, 0);
然后我们通过fopen
来打开需要操作的文件,用rb
来读取输入文件,用wb
来写转换后的文件。
FILE* fInput = fopen(cInputPath,"rb");
FILE* fMp3 = fopen(cMp3Path,"wb");
接下来我们申请两个buffer来缓存文件数据,我们边读边转换,然后再将转换后的数据写入文件。由于Lame的要求,这里的buffer数据必须要不小于7200,下面是具体的转换代码:
//convert to mp3
do{
//这里将输入文件内容读取到inputBuffer中,当全部读取会返回0
read = static_cast<int>(fread(inputBuffer, sizeof(short int) * 2, 8192, fInput));
//这里用于计算读取的原文件的byte数,可以用于计算转换的进度
total += read * sizeof(short int)*2;
nowConvertBytes = total;
if(read != 0){
//这里用lame将inputBuffer转换为MP3格式的数据放入mp3Buffer中
write = lame_encode_buffer_interleaved(lame, inputBuffer, read, mp3Buffer, BUFFER_SIZE);
//将转换好的mp3Buffer的数据写入文件
fwrite(mp3Buffer, sizeof(unsigned char), static_cast(write), fMp3);
}
//最后全部读取完成后及时flush
if(read == 0){
lame_encode_flush(lame,mp3Buffer, BUFFER_SIZE);
}
}while(read != 0);
最后记得转换结束后释放资源:
resetLame();
fclose(fInput);
fclose(fMp3);
env->ReleaseStringUTFChars(jInputPath, cInput);
env->ReleaseStringUTFChars(jMp3Path, cMp3);
为了支持不同的设备,我们需要根据不同的ABI生成不同的so库来调用,我们可以通过Android Studio的Make
来调用CMakeList.txt
脚本生成支持各种ABI版本的so库。文件输出路径可以通过配置CMakeList.txt
来修改:
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
其中PROJECT_SOURCE_DIR
是指脚本所在目录,ANDROID_ABI
是指在build.gradle
中配置的abiFilters
。
ABI(Application binary interface)应用程序二进制接口。不同的CPU 与指令集的每种组合都有定义的ABI (应用程序二进制接口),一段程序只有遵循这个接口规范才能在该CPU上运行,所以同样的程序代码为了兼容多个不同的CPU,需要为不同的ABI构建不同的库文件。当然对于CPU来说,不同的架构并不意味着一定互不兼容。
https://github.com/Jay-Goo/Mp3Converter
https://blog.csdn.net/allen315410/article/details/42456661
https://www.jianshu.com/p/6332418b12b1