Android应用与JNI技术



一、引言

在之前的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类型必须与本地接口一致,不可随意更改,否则就会现找不到接口的异常。


参考