Android中的JNI使用简介

一、本文说明

本文不对Android工程的各种配置做说明,只是简单介绍下开发过程中如何进行C与Java互相调用以及出现异常情况的处理。

二、NDK简介

Android NDK 是一套允许您使用 C 和 C++ 等语言,以原生代码实现部分应用的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库。

三、JNI简介

JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为Java平台的一部分,它允许Java代码和其他语言写的代码进行交互。

四、Android Studio创建简单的JNI工程

在创建Android Project时,在创建的首页中勾选Include C++ support选项即可创建一个模板的hello world程序,可直接运行。相比于普通的Android工程,勾选该项后的工程有以下不同:
工程目录

gradle配置

可以看到,多出了一个native-lib.cpp(C++代码)文件和一个CMakeLists.txt(C++相关配置)文件,app模块下的build.gradle文件也添加了相关配置。

在该默认工程中,我们可以看到一些JNI相关代码:

Java中加载本地库,并且定义native方法:

    // 加载的library名称,注意:不是C++文件的名称
    static {
        System.loadLibrary("native-lib");
    }
    /**
     * java中定义方法的名称,会根据包名、类名寻、参数、返回值类型寻找对应的C++方法
     */
    public native String stringFromJNI();

C++中定义好对应的方法:

//对应于java中的stringFromJNI方法
extern "C" JNIEXPORT jstring JNICALL Java_com_wsy_jnitest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

这样即可实现C++向Java传递一个String

五、Java签名介绍

1. 什么是签名?
为了介绍后面的东西,先介绍一个概念,类型签名。

以下段落摘抄自维基百科:

In computer science, a type signature or type annotation defines the inputs and outputs for a function, subroutine or method. A type signature includes the number of arguments, the types of arguments and the order of the arguments contained by a function. A type signature is typically used during overload resolution for choosing the correct definition of a function to be called among many overloaded forms.

In the Java virtual machineinternal type signatures are used to identify methods and classes at the level of the virtual machine code.
Example: The method String String.substring(int, int) is represented in bytecode as Ljava/lang/String.substring(II)Ljava/lang/String;.
The signature of main() method looks like this:
public static void main(String[] args)
And in the disassembled bytecode, it takes the form ofLsome/package/Main/main:([Ljava/lang/String;)V.
The method signature for the main() method contains three modifiers:
public indicates that the main() method can be called by any object.
static indicates that the main() method is a class method.
void indicates that the main() method has no return value.

译:

在计算机科学中,类型签名或类型注释定义了函数,子程序或方法的输入和输出。类型签名包括参数的数量,参数的类型以及函数包含的参数的顺序。在重载解析期间通常使用类型签名来选择在许多重载函数中正确的那一项。

在Java虚拟机中,内部类型签名用于标识虚拟机代码级别的方法和类。
示例:
方法String String.substring(int,int)在字节码中表示为Ljava/lang/String.substring(II)Ljava/lang/String;
方法main()的签名如下所示:
public static void main(String[] args)
在反汇编的字节码中,它采用Lsome/package/Main/main:([Ljava/lang/String;)V的形式。
main()方法的方法签名包含三个修饰符:
public表示main()方法可以被任何对象调用。
static表示main()方法是一个类方法。
void表示main()方法没有返回值。

简单来说,签名就是能确保一个函数或一个变量的数据。jni在寻找一个java函数或者变量时,一般以如下方式寻找:

  • 寻找函数:需要知道java函数的函数名、返回值类型、参数类型
  • 寻找变量:需要知道java变量的变量名、数据类型

2.如何获取签名

Type SignatureJava Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
Lfully-qualified-class;fully-qualified-class
[typetype[]
( arg-types ) ret-typemethod type

For example, the Java method:
long f (int n, String s, int[] arr);
has the following type signature:
(ILjava/lang/String;[I)J

  • 使用javap命令获取
    1.获取class文件的签名
javap -s classFile

例如获取MainActivity.class中方法和变量的签名(Java文件在底部github分享中):

javap -s D:\android-project\study\JNIDemo\app\build\intermediates\classes\debug\com\wsy\jnidemo\MainActivity
Compiled from "MainActivity.java"
public class com.wsy.jnidemo.MainActivity extends android.support.v7.app.AppCompatActivity {
  public com.wsy.jnidemo.MainActivity();
    descriptor: ()V

  protected void onCreate(android.os.Bundle);
    descriptor: (Landroid/os/Bundle;)V

  public native java.lang.String testExceptionCrash() throws com.wsy.jnidemo.CustomException;
    descriptor: ()Ljava/lang/String;

  public native java.lang.String testExceptionNotCrash(int) throws com.wsy.jnidemo.CustomException;
    descriptor: (I)Ljava/lang/String;

  public native void nativeShowToast(android.app.Activity);
    descriptor: (Landroid/app/Activity;)V

  public native void testCallJava(com.wsy.jnidemo.MainActivity);
    descriptor: (Lcom/wsy/jnidemo/MainActivity;)V

  public native void methodNotExists();
    descriptor: ()V

  public void nativeThrowException(android.view.View);
    descriptor: (Landroid/view/View;)V

  public void cCallJava(java.lang.String);
    descriptor: (Ljava/lang/String;)V

  public void callJavaFromC(android.view.View);
    descriptor: (Landroid/view/View;)V

  public void nativeShowToast(android.view.View);
    descriptor: (Landroid/view/View;)V

  public void callMethodNotExists(android.view.View);
    descriptor: (Landroid/view/View;)V

  public void wrongSampleUsingJNIEnv(android.view.View);
    descriptor: (Landroid/view/View;)V

  static {};
    descriptor: ()V
}

2.获取jar中的class的方法和变量的签名

javap -classpath XXX.jar -s fullyQualifiedClass

例如获取android.jar中,android.widget.Toast类中方法和变量的签名:

javap -classpath android.jar -s android.widget.Toast
Compiled from "Toast.java"
public class android.widget.Toast {
  public static final int LENGTH_LONG;
    descriptor: I
  public static final int LENGTH_SHORT;
    descriptor: I
  public android.widget.Toast(android.content.Context);
    descriptor: (Landroid/content/Context;)V

  public void show();
    descriptor: ()V

  public void cancel();
    descriptor: ()V

  public void setView(android.view.View);
    descriptor: (Landroid/view/View;)V

  public android.view.View getView();
    descriptor: ()Landroid/view/View;

  public void setDuration(int);
    descriptor: (I)V

  public int getDuration();
    descriptor: ()I

  public void setMargin(float, float);
    descriptor: (FF)V

  public float getHorizontalMargin();
    descriptor: ()F

  public float getVerticalMargin();
    descriptor: ()F

  public void setGravity(int, int, int);
    descriptor: (III)V

  public int getGravity();
    descriptor: ()I

  public int getXOffset();
    descriptor: ()I

  public int getYOffset();
    descriptor: ()I

  public static android.widget.Toast makeText(android.content.Context, java.lang.CharSequence, int);
    descriptor: (Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

  public static android.widget.Toast makeText(android.content.Context, int, int) throws android.content.res.Resources$NotFoundException;
    descriptor: (Landroid/content/Context;II)Landroid/widget/Toast;

  public void setText(int);
    descriptor: (I)V

  public void setText(java.lang.CharSequence);
    descriptor: (Ljava/lang/CharSequence;)V
}

六、Java调用C++方法

JNI方法的注册一般分为两种:静态注册和动态注册

  • 静态注册就是通过固定的命名规则映射Java和native函数;
  • 动态注册一般是通过重写JNI_OnLoad函数,用jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)函数将Java中定义的native函数和C/C++中定义的函数进行映射。

静态注册:
在Java代码中定义好native方法,并且在C++代码中编写好对应的方法:

extern "C" JNIEXPORT RETURN_TYPE JNICALL Java_PackageConnectedByUnderline_ClassName_FunctionName(JNIEnv *env, jobject /* this */,  ...params)

动态注册:
编写JNI_OnLoad函数,在其内部实现动态注册,示例代码如下

jstring dynamicRegister(JNIEnv *jniEnv, jobject obj) {
    return jniEnv->NewStringUTF("dynamicRegister");
}

int JNI_OnLoad(JavaVM *javaVM, void *reserved) {
    JNIEnv *jniEnv;
    if (JNI_OK == javaVM->GetEnv((void **) (&jniEnv), JNI_VERSION_1_4)) {
        // 动态注册的Java函数所在的类
        jclass registerClass = jniEnv->FindClass("com/wsy/jnidemo/MainActivity");
        JNINativeMethod jniNativeMethods[] = {
                //3个参数分别为 Java函数的名称,Java函数的签名(不带函数名),本地函数指针
                {"dynamicRegister", "()Ljava/lang/String;", (void *) (dynamicRegister)}
        };
        if (jniEnv->RegisterNatives(registerClass, jniNativeMethods,
                                    sizeof(jniNativeMethods) / sizeof((jniNativeMethods)[0])) < 0) {
            return JNI_ERR;
        }
    }
    return JNI_VERSION_1_4;
}

其中,java的Object对象传递给C++时类型都用jobject表示,java中的基础类型都用j基础类型表示,java中的数组对象类型(java数组对象类型其实也是Object类型)都用jXXXXArray表示(包含Object数组基础类型数组),具体如下:

  • 基础类型:
/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */
  • Object(数组、异常等):
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;

例如:
com.wsy.jnidemo.MainActivity类中的定义的方法

public native void testCallJava(MainActivity activity)

对应的C++方法为

extern "C" JNIEXPORT void
JNICALL
Java_com_wsy_jnidemo_MainActivity_testCallJava(
        JNIEnv *env,
        jobject /* this */, jobject activity)

com.wsy.jnidemo.MainActivity类中的定义的方法

public native String testExceptionNotCrash(int i) throws CustomException;

对应的C++方法为

extern "C" JNIEXPORT jstring
JNICALL
Java_com_wsy_jnidemo_MainActivity_testExceptionNotCrash(
        JNIEnv *env,
        jobject /* this */, jint i)

七、C++中调用Java方法

####1.首先获取Java类

  • 根据包名+类名获取Java类
//参数name是带包名的类名,例如要获取android.widget.Toast类,那么写法就是
//jclass cls = env->FindClass("android/widget/Toast");
jclass FindClass(const char* name)
  • 获取jobject的Java类
//java中的Object对象传给native层后即为jobject,我们可根据jobject获取其Java类
jclass GetObjectClass(jobject obj)

####2.再获取Java类中指定的方法

  • 获取成员方法
//参数clazz是java类,name是java方法名称,sig是java方法签名
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
  • 获取静态方法
//参数clazz是java类,name是java方法名称,sig是java方法签名
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig)

####3.调用Java方法

  • 调用无返回值的成员方法
//参数obj是被调用方法的java对象,methodID是使用GetMethodID获取的回传值,后面填方法需要的参数
void CallVoidMethod(jobject obj, jmethodID methodID, ...)
  • 调用带返回值的成员方法
//调用返回值类型为Object的方法
jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);

//调用返回值类型为基本类型的方法
jint CallIntMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
jlong CallLongMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
jfloat CallFloatMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
...
  • 调用无返回值的静态方法
//参数clazz是被调用方法的类,methodID是使用GetStaticMethodID获取的回传值,后面填方法需要的参数
void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)
  • 调用带返回值的静态方法
//调用返回值类型为Object的方法
jobject CallStaticObjectMethod(jclass clazz, jmethodID methodId, ...);

//调用返回值类型为基本类型的方法
jint CallStaticIntMethod(JNIEnv* clazz, jclass, jmethodID, ...);
jlong CallStaticLongMethod(JNIEnv* clazz, jclass, jmethodID, ...);
jfloat CallStaticFloatMethod(JNIEnv* clazz, jclass, jmethodID, ...);
...
八、C++中获取Java类中的变量
  • 获取类中的成员变量

首先获取jfieldID

//参数clazz是被获取成员变量的对象的类,name是变量名,sig是变量的签名
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)

再根据jfieldID获取成员变量

//获取Object类型成员变量
jobject GetObjectField(jobject obj, jfieldID fieldID)
//获取基础类型成员变量
jint GetIntField(jobject obj, jfieldID fieldID)
jlong GetLongField(jobject obj, jfieldID fieldID)
jfloat GetFloatField(jobject obj, jfieldID fieldID)
....
  • 获取类中的静态变量

和获取成员变量相似,首先获取jfieldID

jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)

再根据fieldID获取静态变量

//获取Object类型静态变量
jobject GetStaticObjectField(jclass clazz, jfieldID fieldID)
//获取基础类型静态变量
jint GetStaticIntField(jclass clazz, jfieldID fieldID)
jlong GetStaticLongField(jclass clazz, jfieldID fieldID)
jfloat GetStaticFloatField(jclass clazz, jfieldID fieldID)
....
九、在native层向Java抛出Exception

首先需要找到Java中的Exception类,再使用ThrowNew方法抛出,要注意最终将jclass释放

jclass exceptionCls = env->FindClass("com/wsy/jnidemo/CustomException");
env->ThrowNew(exceptionCls, "i am an exception");
env->DeleteLocalRef(exceptionCls); 

需要注意的是,在exception发生时,在native层已不能调用大部分的方法,例如对象创建。
NDK官网说明如下:

You must not call most JNI functions while an exception is pending. Your code is expected to notice the exception (via the function’s return value,ExceptionCheck, or ExceptionOccurred) and return, or clear the exception and handle it.
The only JNI functions that you are allowed to call while an exception is pending are:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • ReleaseArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

例如在抛出exception后执行

env->NewStringUTF("hello world, after exception")

将导致crash

十、根据crash日志中的指令地址定位C++中的crash代码位置

NDK中有一个很方便的debug工具:addr2line,可直接根据指令地址定位代码crash位置。
addr2line的位置在:{NDK安装目录}\toolchains\{平台相关目录}\prebuilt\windows-x86_64\bin目录。
我们先在native层写个错误示范代码,再运行,可以看到日志中出现了crash信息:
crash信息
找到工程相关的动态库文件,可以看到出现crash的so文件为/lib/x86/libnative-lib.so以及指令地址为00000E61,动态库文件在工程目录的位置如下图:
so文件路径
此时使用addr2line工具进行crash位置查找,用法为:

addr2line -e so文件路径 指令地址

addr2line -e D:\android-project\study\JNIDemo\app\build\intermediates\cmake\debug\obj\x86\libnative-lib.so 00000e61

运行结果如下
运行addr2line结果

可以看到定位到的导致crash的代码是native-lib.cpp文件的33行:
crashCode


####demo可直接clone: https://github.com/wangshengyang1996/JNIDemo

####详细的JNI使用方法:
https://developer.android.google.cn/training/articles/perf-jni

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值