安卓面试题_安卓开发面经(22/30)之JNI全解析

牛客高级系列专栏:

安卓(安卓系统开发也要掌握)

嵌入式

本人是2020年毕业于广东工业大学研究生:许乔丹,有国内大厂CVTE和世界500强企业安卓开发经验,该专栏整理本人对常见安卓高频开发面试题的理解;

网上安卓资料千千万,笔者将继续维护专栏,一杯奶茶价格不止提供答案解析,承诺提供专栏内容免费技术答疑,直接咨询即可。助您提高安卓面试准备效率,为您面试保驾护航!

正文开始⬇

JNI开发是安卓开发必备技能,实用性100分,不过校招或者基础的社招面试时,面试官一般会问有没有用过JNI,用过就加分,讲述一下曾经使用的场景,没用过就下一个问题。所以对于基础面试级别的同学,知道简单的概念和使用方法就行了。本文只分析JNI的基础知识点,更多的语法细节的介绍不属于本文范围。面试官可能会问:

  1. 什么是JNI?它主要用来干什么。 ⭐⭐⭐⭐⭐
  2. Java 声明的Native方法如何和Native层的Native函数进行绑定的?(也就是介绍两种注册方法)⭐⭐⭐⭐⭐
  3. JNI如何实现数据传递?⭐⭐⭐⭐
  4. 如何全局捕获Native发生的异常?⭐⭐⭐
  5. JNIEnv与JavaVM的关系⭐⭐⭐⭐
  6. C和C++的JNIEnv的区别 ⭐⭐⭐
  7. JNI项目配置和数据映射 ⭐⭐

看完以下的解析,一定可以让面试官眼前一亮。

目录

  • 1、什么是JNI、NDK
    • 1.1 JNI
    • 1.2 JNI 与 NDK 的联系和区别
  • 2、JNI的两种注册方式
    • 2.1 静态注册
    • 2.2 native层函数命名规则
    • 2.3 静态注册的优缺点
    • 2.4 动态注册
  • 3、JNI语法
    • 3.1 JNI项目配置
      • 3.1.1 build.gradle
      • 3.1.2 CMakeLists.txt文件
    • 3.2 数据映射
      • 3.2.1 基本数据类型影射
      • 3.2.2 引用数据类型映射
      • 3.2.3 方法和变量ID
    • 3.3 JNI 描述符
      • 3.3.1 域描述符
        • 3.3.1.1 基础类型描述符
        • 3.3.1.2 引用类型描述符
    • 3.3.2 类描述符
    • 3.3.3 方法描述符
    • 3.4 JNIEnV分析
      • 3.4.1 C和C++的JNIEnv的区别
      • 3.4.2 JNIEnv的特点
  • 4、JNI异常处理

JNI

1、什么是JNI、NDK

1.1 JNI

JNI(Java Native Interface)就是Java本地化接口。在Windows,Linux,MacOS等操作系统的底层驱动都是使用C/C++开发的,因此这些系统提供的API函数都是C/C++编写的。而在安卓开发中,我们使用java编程写的代码都是在Java虚拟机中,编译成虚拟机可以运行的Java字节码.class文件,再通过JIT技术即时编译成本地机器码,所以效率是比不上C/C++的。因此,很容易联想到,我们希望能有这么一个中间件,支持我们在Java代码中与本地系统的C/C++代码做个交互,这个中间件就是JNI。

Java一次编译到处执行: JVM在不同的操作系统都有实现,Java可以一次编译到处运行,字节码文件一旦编译好了,可以放在任何平台的虚拟机上运行;

Java语言执行流程

  • 编译字节码:Java编译器编译 .java源文件,获得.class 字节码文件;
  • 装载类库:使用类装载器装载平台上的Java类库,并进行字节码验证;
  • Java虚拟机:将字节码加入到JVM中,Java解释器和即时编译器同时处理字节码文件,将处理后的结果放入运行时系统;
  • 调用JVM所在平台类库:JVM处理字节码后,转换成相应平台的操作,调用本平台底层类库进行相关处理;

alt

1.2 JNI 与 NDK 的联系和区别

NDK(Native Development Kit),翻译过来是“本地开发工具”,是Google开发的一套开发和编译工具集,可快速生成C、C++的动态库,并自动把so和应用打包成apk,主要用于Android的JNI开发;

因此,JNI是一套编程接口,可以实现Java代码和本地C/C++代码进行交互。而NDK可以理解为Android实现JNI的一种工具,通过该工具打包C/C++动态库并自动打包进APK/AAR中。

2、JNI的两种注册方式

JNI有静态注册和动态注册两种方式,本人在实际开发中,多用动态注册。

2.1 静态注册

静态注册的原理是:根据函数名建立Java方法和JNI函数的一一对应关系。步骤如下:

  1. 先声明 Java 的 native 方法;
  2. 使用 javah 工具生成对应的头文件,在Terminal控制台执行以下任一命令生成由包名加类名命名的 jni 层头文件:
  • javah packagename.classname
  • javah -o my_jni.h packagename.classname,其中 my_jni.h 为自定义的文件名;
  1. 实现对应的native方法,并在Java中通过System.loadLibrary()方法加载 so 库即可;

因为有Android Studio这个强大的工具,我们可以很轻松建立一个JNI项目工程。

alt

新创建的项目就有了默认的JNI函数了,下面做简单介绍:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'myapplication' library on application startup.
    static {
        System.loadLibrary("myapplication"); //1
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI()); //2 
    }

    /**
     * A native method that is implemented by the 'myapplication' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI(); //3
}

在[注释3]先声明 native 方法,并在[注释1]加载so库,最后在[注释2]调用native函数stringFromJNI(),其对应的实现在:native-lib.cpp文件里:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL //4
Java_com_example_myapplication_MainActivity_stringFromJNI( //5
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello World";
    return env->NewStringUTF(hello.c_str()); //6
}

[注释4]是两个关键词:JNIEXPORT 和 JNICALL,这两个都是宏定义,在jni.h文件中定义:

#define JNIIMPORT
#define JNIEXPORT  __attribute__ ((visibility ("default")))
#define JNICALL

主要作用是注明该函数Java_com_example_myapplication_MainActivity_stringFromJNI()是JNI函数,那么当虚拟机加载so库时,就会将该函数链接到对应的Java层native方法,World"的字符串。

2.2 native层函数命名规则

上一小节的[注释3]stringFromJNI()和[注释5]Java_com_example_myapplication_MainActivity_stringFromJNI(JNIEnv* env,jobject)有匹配关系,其Natvie层函数命名遵循以下规则:

JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数); 同时还有以下几个需要注意的小点:

  • 如果是c++文件,如上述例子Natvie方法放在native-lib.cpp文件里,此时需要在Native函数前面加上 extern “C”
  • 如果该函数是重载的方法,则需要加上“参数签名”,参数签名见3.3.1小节,上述例子非重载方法,因此命名上不需要加“参数签名”;
  • 包名或类名或方法名中含下划线 _ 要用 _1 连接;
  • 重载的Native方法命名中的“方法名”后面要用双下划线 __ 连接;
  • 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 '['改为 '_3' 连接 ;
  • 如果Java层方法是static方法,则Native层方法的第二个形参是jclass,否则是jobject,如上述例子,Java层为非static方法,所以Native层方法的第二个参数是jobject。

2.3 静态注册的优缺点

静态注册优点就是实现简单,编写好Java方法后用javah工具就可以将Java代码中声明的native方法转换为native层的代码函数,并直接实现native层代码逻辑就行。然而缺点也比较明显:

  • 每次增加新的函数或者修改函数名等,都需要手动在运行javah命令,比较麻烦。同时,生成的Native层函数名字太长了,可读性不高;
  • 首次调用Native函数时,需要根据函数名在Java层和Native层直接建立函数链接,比较耗时;

因此,无论是实用性还是效率,都推荐使用动态注册。

2.4 动态注册

动态注册的原理是通过使用 JNINativeMethod 结构来保存Java层声明的native方法和Native层函数的关联关系,直接告Java层声明的native方法其在Native层中对应函数的指针。该结构体的定义和动态注册需要用到的关键函数也在jni.h文件中定义:

//JNINativeMethod结构体
typedef struct {
    const char* name;       //Java层声明的中的native方法的名字
    const char* signature;  //Java中native方法的函数签名
    void*       fnPtr;      //对应Native层函数的指针
} JNINativeMethod;

/**
 * @param clazz java类名,通过 FindClass 获取
 * @param methods JNINativeMethod 结构体指针
 * @param nMethods 方法个数
 */
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

//JNI_OnLoad 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);

动态注册的步骤:

  1. 先声明 Java层 的 native 方法;
  2. 同步实现Native层函数的实现,函数名可以任意取!
  3. 利用结构体 JNINativeMethod 保存Java层native方法和 Native层的JNI函数的对应关系;
  4. 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
  5. 在 JNI_OnLoad() 方法中调用步骤4的注册方法;
  6. 在Java中通过System.loadLibrary加载完JNI动态库之后,会调用JNI_OnLoad()方法,完成动态注册;

代码实例如下:

// Java层的MainActivity.java文件:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'myapplication' library on application startup.
    static {
        System.loadLibrary("myapplication"); //7
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());  // 8
    }

    /**
     * A native method that is implemented by the 'myapplication' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI(); //9
}

// 自定义的interface.cpp文件:

jint JNI_OnLoad(JavaVM* vm, void* reserved) //10
{
    ...
    if (JNI_FALSE == registerMethods(m_pEnv, CLASSNAME, gMethods, NELEM(gMethods))) // 11
    {
        LOGE(LOG_TAG, "load method fail");
        return

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Android高频面试题全解析 文章被收录于专栏

#提供免费售后答疑!!花一杯奶茶的钱获得安卓面试答疑服务,稳赚不赔# Android发展已经很多年,安卓资料网上千千万,本专栏免费提供专栏内容技术答疑!!私聊当天必回。在阅读过程或者其他安卓学习过程有疑问,都非常欢迎私聊交流。

全部评论
静态注册就是简单,动态注册还是比较实用
1 回复
分享
发布于 2023-02-21 22:21 广东
实际开发中动态注册用的比较多吧
点赞 回复
分享
发布于 2023-02-21 21:33 广东
滴滴
校招火热招聘中
官网直投

相关推荐

5 13 评论
分享
牛客网
牛客企业服务