面试题十三:Java Native方法与Native函数是怎么绑定的?

Java Native方法与底层Native函数是怎么绑定的?

一、面试官视角:这道题想考察什么?

  • 是否有Native开发经验

    Java Native方法只是声明,没有实现,底层Native函数才是实现。

  • 是否面对知识善于发现背后的原因

二、题目剖析

1、静态绑定:通过命名规则映射

1
2
3
4
5
6
7
8
9
10
11
12
13
package io.github.apkok.nativec;
public class NativeCInf {
public static native void callNativeStatic();
}

-------------------JNI-------------------

extern "C" JNIEXPORT void JNICALL
// 命名规则:Java_包路径(.统一改为_)_类名_方法名
// 方法参数:
// 1、如果Java Native方法是静态的,对应的底层Native函数的参数就是jclass
// 2、如果Java Native方法是非静态的,对应的底层Native函数的参数就是jobject
Java_io_github_apkok_nativec_NativeCInf_callNativeStatic(JNIEnv *,jclass)

extern “C”的用处?

它是用来告诉编译器,编译这个底层Native函数的时候一定要按照C的规则保留名字(Java_io_github_apkok_nativec_NativeCInf_callNativeStatic),不能去混淆,因为它是直接将名字写到符号表里面的,不然你混淆了,我就找不着了,因此并不是你按照命名规则定义好了底层Native函数名称就好了,而是要看符号表具体存的是什么名称,才能够映射到Java的Native方法。

JNIEXPORT的用处?

上一篇文章,我们提到过优化so库的时候,尽可能的让它隐藏,因为不是所有的Native函数写出来都一定要在符号表里面出现的,但JNIEXPORT就是让Native方法强制扔到DEFAULT里面,这样的话,我们就可以在符号表里面看到。

JNICALL的用处?

1
2
#define JNIEXPORT _attribute_((visibility("default")))
#define JNICALL

我们看到#define JNICALL定义是空的,后面啥也没有,那定义它干嘛,其实并不是没用哟,因为有些平台,比如在mips上编译的时候就会有定义,还有在windows上编译的时候会定义为sddcall,告诉编译器,函数调用的惯例是什么,比如参数是怎么入栈的,函数返回了这个栈谁来清理等等,是用来约束这些的,所以JNI并不一定编译在Android系统上,windows上也是可以的,因此JNICALL主要是考虑兼容性的问题。

2、动态绑定:通过JNI函数注册

1
2
3
4
5
6
7
8
9
10
int registerMethods(JNIEnv *env,const char *className,const JNINativeMethod *methods,int methodsLength) {
// 1、获取Class
jclass clazz = env->FindClass(className);
if(clazz == NULL) return JNI_ERR;
// 2、注册方法
if((env->RegisterNatives(clazz,methods,methodsLength)) < 0) {
return JNI_ERR;
}
return JNI_OK;
}
  • 动态绑定可以在任何时刻触发
  • 动态绑定之前根据静态规则查找Native函数
  • 动态绑定可以在绑定后的任意时刻取消

3、动态和静态绑定对比

动态绑定 静态绑定
Native函数名 无要求 按照固有规则编写且采用C的名称修饰规则
Native函数可见性 无要求(会减小so库大小) 可见(会占用符号表的大小)
动态更换 可以 不可以
调用性能 无需查找 有额外的查找开销(查找符号表)
开发体验 几乎无副作用 重构代码时较为繁琐
Android Studio支持 不能自动关联跳转 自动关联JNI函数可跳转

三、题目结论

  • Native方法绑定主要有静态和动态两种
  • 静态绑定注意so库的符号表以及函数的名称修饰
  • 动态绑定注意与静态绑定的互补关系以及调用时机