一、引言
在之前的Opus编解码中,自测十几部手机都正常,但客户有反馈极个别手机运行异常,通过Log分析,是没有找JNI函数。其实对这种低概率的问题也是很无奈啊,但不得不解,花了两个下午的时间,从Opus的接口定义->CPU配置->Java的调用都查了遍,还是没找到问题,郁闷到所有的重写一次还是不行。报错信息:
10-22 17:28:36.420 8550-8550/com.cchip.cvoice E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.cchip.cvoice, PID: 8550
java.lang.UnsatisfiedLinkError: No implementation found for boolean com.score.rahasak.utils.OpusDecoder.nativeInitDecoder(int, int, int) (tried Java_com_score_rahasak_utils_OpusDecoder_nativeInitDecoder and Java_com_score_rahasak_utils_OpusDecoder_nativeInitDecoder__III)
at com.score.rahasak.utils.OpusDecoder.nativeInitDecoder(Native Method)
at com.score.rahasak.utils.OpusDecoder.init(OpusDecoder.java:16)
然而意外总是发生在一刹那,突然想起是不是so没调用到,然后开始查Opus库在Android 5.0手机上就已经有集成了,所以有可能调用到系统库Opus,而不是我们自定义库,所以修改下自定义Opus库的名称,重新导入运行,正常了,这种心情只有在碰到无数次墙壁后找到光明的才能体会到。同时也是一次深刻的教训,编译开源库一定要加特定的名称,不要与Demo一样,否则容易找错文件。因此,趁这个事情整理下JNI的使用及可能确碰到的问题。
二、JNI定义
JNI是Java Native Interface的缩写。从Java 1.1开始,JNI标准成为java平台的一部分,它允许Java和其他语言进行交互。JNI一开始为C/C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。使用Java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。
Android系统不允许一个纯粹使用C/C++的程序出现,我见过提供JNI的调用方式,让Java程序可以调用C/C++语言程序。Android中很多Java类都具有Native接口,这些接口由本地实现,然后注册到系统中。因此JNI对Android深度开发人员非常重要。
三、Android studio安装Cmake及NDK
打开Android studio工程项目->SDK Manager->SDKTools,确认勾选择Cmake及NDK,若没有,则勾选,然后执行Apply:
安装完成后,会在工程的根目录下的local.properties中添加
ndk.dir=你的NDK路径名
//我的是
ndk.dir=D\:\\Workspace\\android-sdk\\ndk-bundle
如果代码提示:
Plugin "Android NDK Support" was not loaded: required plugin "Android Support" is disabled.
则取消相应的插件,打开Android studio工程项目->File->Settings->Plugins,取消勾选Android APK Support。
四、NDK新加载方式
在Android Studio 2.2版本及以上已经支持新加载方式,创建新工程,打勾“Include C++ support”选项
然后就是下一步下一步,可以啥都不改,啥都不选。一直到“Customize C++ support”这个页面,涉及三个选项,一个是选择C++版本,下面两个勾选一个是异常,一个是debug功能。
在AS版本是3.1.4 创建完成之后的工程,自动生成的目录与文件:
最主要的是这个CMakeLists.txt。
五、NDK传统加载方式
NDK传统加载方式用的比较多,也比较常见,使用MK(makefile)文件来配置环境。
5.1 JNI的命名规则
这里说下JNI的命名规则,对于传统的JNI编程来说,JNI方法跟Java类方法的名称之间有一定的对应关系,要遵循一定的命名规则,如下所示:
- 前缀: Java_
- 包名,用下划线进行分隔(_):com_amiga_dogbt_utils
- 类名:OpusDecoder
- 方法名:nativeInitDecoder
- JNI函数指定第一个参数: JNIEnv *
- JNI函数指定第二个参数: jobject
- 实际Java参数: jint, jint ….
所以对于在Java类 com.amiga.dogbt.utils.OpusDecoder类的一个方法:
public native boolean nativeInitDecoder(int samplingRate, int numberOfChannels, int frameSize);
其对应的jni层的方法如下:
Java_com_amiga_dogbt_utils_OpusDecoder_nativeInitDecoder (JNIEnv *env, jobject obj, jint samplingRate, jint numberOfChannels, jint frameSize)
如果不这样命名,当把动态库加载进DVM的时候,通过JNIEnv *指针去查找Java Native方法对应的JNI方法的时候,就会找不到了。
注意,我们也可以利用函数注册的方法,将Java层的方法名跟JNI层的方法名的对应关系保存起来,注册到DVM中,就不需要这样的命名规范了。
5.2 字符的对应关系
具体的每一个字符的对应关系如下
字符 | Java类型 | C类型 |
---|---|---|
V | void | void |
Z | jboolean | boolean |
I | jint | int |
J | jlong | long |
D | jdouble | double |
F | jfloat | float |
B | jbyte | byte |
C | jchar | char |
S | jshort | short |
数组则以"["开始,用两个字符表示
字符 | Java类型 | C类型 |
---|---|---|
[I | jintArray | int[] |
[F | jfloatArray | float[] |
[B | jbyteArray | byte[] |
[C | jcharArray | char[] |
[S | jshortArray | short[] |
[D | jdoubleArray | double[] |
[J | jlongArray | long[] |
[Z | jbooleanArray | boolean[] |
5.3 JNI代码实现
其实大部份代码上,编写C和C++是没有啥区别,若是涉及到字符串的话,使用C++好处就是可以使用很多库但目前Android不支持STL,我们知道C表示字符串都是字符数组,但C++可以使用类似string这样的类型表示。
首先,接口定义*.h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_amiga_dogbt_utils_OpusDecoder */
#ifndef _Included_com_amiga_dogbt_utils_OpusDecoder
#define _Included_com_amiga_dogbt_utils_OpusDecoder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_amiga_dogbt_utils_OpusDecoder
* Method: nativeInitDecoder
* Signature: (III)Z
*/
JNIEXPORT jboolean JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeInitDecoder
(JNIEnv *, jobject, jint, jint, jint);
/*
* Class: com_amiga_dogbt_utils_OpusDecoder
* Method: nativeDecodeBytes
* Signature: ([B[S)I
*/
JNIEXPORT jint JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeDecodeBytes
(JNIEnv *, jobject, jbyteArray, jshortArray);
/*
* Class: com_amiga_dogbt_utils_OpusDecoder
* Method: nativeReleaseDecoder
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeReleaseDecoder
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
接口实现.c。参数中,我们也只需要关心在JAVA程序中存在的参数,至于JNIEnv和jclass我们一般没有必要去碰它。
JNIEXPORT jboolean JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeInitDecoder (JNIEnv *env, jobject obj, jint samplingRate, jint numberOfChannels, jint frameSize)
{
......
return ret;
}
JNIEXPORT jint JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeDecodeBytes (JNIEnv *env, jobject obj, jbyteArray in, jshortArray out)
{
jint inputArraySize = (*env)->GetArrayLength(env, in);
jint outputArraySize = (*env)->GetArrayLength(env, out);
jbyte* encodedData = (*env)->GetByteArrayElements(env, in, 0);
opus_int16 *data = (opus_int16*)calloc(outputArraySize,sizeof(opus_int16));
int decodedDataArraySize = opus_decode(decoder, encodedData, inputArraySize, data, 320, 0);
......
return decodedDataArraySize;
}
JNIEXPORT jboolean JNICALL Java_com_amiga_dogbt_utils_OpusDecoder_nativeReleaseDecoder (JNIEnv *env, jobject obj)
{
if(decoder !=NULL){
opus_decoder_destroy(decoder);
}
return 1;
}
5.4 Java层接口编写
创建一个java类.
package com.amiga.dogbt.utils;
public class OpusDecoder {
//这是一个接口,通过这个接口与jni交互
public native boolean nativeInitDecoder(int samplingRate, int numberOfChannels, int frameSize);
public native int nativeDecodeBytes(byte[] in, short[] out);
public native boolean nativeReleaseDecoder();
static {
//这个是打包好的so库,注意库的名称
System.loadLibrary("cchipopus");
}
public void init(int sampleRate, int channels, int frameSize) {
this.nativeInitDecoder(sampleRate, channels, frameSize);
}
public int decode(byte[] encodedBuffer, short[] buffer) {
int decoded = this.nativeDecodeBytes(encodedBuffer, buffer);
return decoded;
}
public void close() {
this.nativeReleaseDecoder();
}
}
5.5 build.gradle配置
在App目录下的build.gradle里,需要配置以下信息
android{
defaultConfig {
// ...
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
关于CPU的配置,在手机端一般只需配置'armeabi', 'armeabi-v7a'。
5.6 MK(makefile)配置
MK(makefile)配置涉及两个文件Android.mk和Application.mk。Android.mk的位置需与上一步定义的路径一致。
Android.mk配置了整个编译代码的环境
LOCAL_PATH := $(call my-dir) #加载当前路径
include $(CLEAR_VARS)
LOCAL_LDLIBS := -llog
include $(LOCAL_PATH)/celt_sources.mk #加载celt 所有.c的 mk
include $(LOCAL_PATH)/silk_sources.mk #加载silk 所有.c 的mk
include $(LOCAL_PATH)/opus_sources.mk #加载opus 所有.c 的mk
MY_MODULE_DIR := cchipopus #库的名称,非常关键
LOCAL_MODULE := $(MY_MODULE_DIR)
SILK_SOURCES += $(SILK_SOURCES_FIXED)
#编译的源代码.c
CELT_SOURCES += $(CELT_SOURCES_ARM)
SILK_SOURCES += $(SILK_SOURCES_ARM)
LOCAL_SRC_FILES := OpusDecoder.c OpusEncoder.c $(CELT_SOURCES) $(SILK_SOURCES) $(OPUS_SOURCES)
#LOCAL_LDLIBS := -lm –llog #加载系统的库 日志库
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(LOCAL_PATH)/silk \
$(LOCAL_PATH)/silk/fixed \
$(LOCAL_PATH)/celt
#附加编译选项
LOCAL_CFLAGS := -DNULL=0 -DSOCKLEN_T=socklen_t -DLOCALE_NOT_USED -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64
LOCAL_CFLAGS += -Drestrict='' -D__EMX__ -DOPUS_BUILD -DFIXED_POINT=1 -DDISABLE_FLOAT_API -DUSE_ALLOCA -DHAVE_LRINT -DHAVE_LRINTF -O3 -fno-math-errno
LOCAL_CPPFLAGS := -DBSD=1
LOCAL_CPPFLAGS += -ffast-math -O3 -funroll-loops
include $(BUILD_SHARED_LIBRARY) #编译动态库设置
注意,这里只是参考,里面内容要根据提示修改,比如LOCAL_SRC_FILES,要都改成自己写的c或cpp文件名。
Application.mk只是一些基本信息的配置
APP_ABI := armeabi armeabi-v7a #中间是空格,不是逗号
APP_PLATFORM := android-19 #设定ndk编译的版本
include $(BUILD_SHARED_LIBRARY)
六、报错总结
因为“UnsatisfiedLinkError”的问题,查了无数的资料,也了解他人在使用JNI过程中出现各种奇奇怪怪的问题,在些也做了整理:
6.1 文件名写错
正确的是jniLibs,但有人粗心,写成了iniLibs,导致找不着库,引起异常。
6.2 目标版本需一致
编译.so文件时的targetSdkVersion低于当前项目的targetSdkVersion,也会引起异常
6.3 CPU类型下的so缺失
在手机端一般只需配置'armeabi', 'armeabi-v7a',可以通过工程或解压APK查看对应的目录下是否有该文件。
6.4 JNI类型不一致
NDK开发中,JNI类型必须与本地接口一致,不可随意更改,否则就会现找不到接口的异常。