Android JNI介绍(二)- 第一个JNI工程的详细分析

2,966 阅读5分钟

本系列文章列表:

Android JNI介绍(一)- 第一个Android JNI工程

Android JNI介绍(二)- 第一个JNI工程的详细分析

Android JNI介绍(三)- Java和Native的互相调用

Android JNI介绍(四)- 异常的处理

Android JNI介绍(五)- 函数的注册

Android JNI介绍(六)- 依赖其他库

Android JNI介绍(七)- 引用的管理

Android JNI介绍(八)- CMakeLists的使用


在上一篇文章中,我们已经介绍了一个JNI工程的大致结构,接下来本文将对这个工程中的一些细节进行介绍。

一、运行流程介绍

对于一般的JNI工程,编译、安装、运行的流程大致如下:

编译:

  1. 配置工程
  2. 编译动态库
  3. 将动态库打包进apk

安装:

  1. 系统将根据设备支持的ABI,首选主ABI的动态库进行安装,如果以主ABI找动态库找不到,就会继续以次ABI进行安装,如果还是找不到,就不拿动态库了,这一点在Android开发者官网中也有说明

运行:

  1. 在运行时,需要先加载动态库,我们一般会在一个类的静态代码块中使用System.loadLibrary进行加载动态库,解析其中的符号
  2. 若找不到库,则会提示java.lang.UnsatisfiedLinkError,并在日志中打印相关信息
  3. 若加载成功,会进行函数解析,首先会使用dlsym函数检查是否重写了JNI_OnLoad函数,如果重写了该函数,则执行该函数
  4. 在运行时,对于已经在JNI_OnLoad函数中进行动态注册的函数,则可以直接找到对应的函数运行;对于未进行动态注册的函数,会按照Java_包名_类名_函数名的规则去寻找native函数,当然了,函数的参数、回传值也是要进行验证的

以上大致就是使用的一个流程,接下来回到工程,对这个默认的工程的一些细节进行解释。

二、工程细节说明

  • Java部分
    Java部分主要做了两件事情:

    1. 加载动态库
      因为动态库只需要加载一次,所以一般我们会在类的静态代码块中进行加载,这样还有个好处就是早出错,早发现
      static {
          System.loadLibrary("native-lib");
      }
      
    2. native函数声明
      以下声明表示这个函数是native函数,什么参数也不传,回传一个String
      public native String stringFromJNI();
      
  • native部分
    native函数的实现:
    函数标识:extern "C" JNIEXPORT
    回传值类型:jstring
    参数类型:自动添加了JNIEnv*jobject

    extern "C" JNIEXPORT jstring JNICALL Java_com_wsy_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */){
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
  • CMakeLists部分

    # 声明最低的cmake版本
    cmake_minimum_required(VERSION 3.4.1)
    # 添加一个名称叫native-lib的动态库,该库的源文件为src/main/native-lib.cpp
    add_library( native-lib # 库的名称
                 SHARED # SHARED:动态库、STATIC:静态库
                 src/main/native-lib.cpp # 源文件,可以是多个
                 )
                 
    # 寻找系统中的log库,保存在log-lib变量中
    find_library( log-lib 
                  log )
                  
    # native-lib这个库会去依赖log-lib这个库
    target_link_libraries( native-lib
                           ${log-lib} )
    

三、native函数声明详解

我们仔细看一下这个native函数,虽然它的实现很简单,但是这些乱七八糟的符号是什么鬼?

1. extern "C"

  • extern "C"说明

    在进行静态注册时,是要加上extern "C"的,它的作用主要是为了让编译器以C的方式去编译它,而不是C++,我们知道,C++是一门面向对象的语言,是支持函数重载的,如下:

    而对于C语言,它是不支持函数重载的,若进行重载,在Android Studio下编译器就会报错,提示Duplicate declaration of function xxx

  • 那如果去掉extern "C",效果会怎样?

    首先,编译器会很友好地提示你:

    并建议你

  • 如果不接受建议,效果会怎样?

    那就是crash

        Process: com.wsy.jnidemo, PID: 27921 
        java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.wsy.jnidemo.MainActivity.stringFromJNI()
    (tried Java_com_wsy_jnidemo_MainActivity_stringFromJNI and
    Java_com_wsy_jnidemo_MainActivity_stringFromJNI__)
        at com.wsy.jnidemo.MainActivity.stringFromJNI(Native Method)
        at com.wsy.jnidemo.MainActivity.onCreate(MainActivity.java:22)
        at android.app.Activity.performCreate(Activity.java:7815)
        at android.app.Activity.performCreate(Activity.java:7804)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1318)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3349)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3513)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2109)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7682)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:516)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)
    

    这个提示是在运行后点击按钮出现的,也就是说,动态库加载成功了,那只能说明函数名被改了。

  • 函数名被修改成了什么?

    • 工具路径

      我们可以使用NDK中提供的nm进行分析,假如我们的动态库是armeabi-v7a的,那么我们选择的nm工具最好是toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin下的arm-linux-androideabi-nm.exe,而不要选择其他目录下的,虽然可能会存在兼容,但是有些是不兼容的。

    • 工具使用方式

      nm -D 动态库文件

    • 分析加extern 'C'和不加extern 'C'的两个动态库

      执行gradlew externalNativeBuildRelease获取动态库,进行解析

      • extern 'C'的动态库
      • 不加extern 'C'的动态库

对于静态注册的函数而言,函数名发生了变更,按照原有的规则找不到对应的函数,因此就会报java.lang.UnsatisfiedLinkError

2. JNIEXPORT

定义如下

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

JNIEXPORT描述了其可见性为default,因此在不进行native代码混淆的情况下,其实我们去掉也没有问题,但是如果我们把visibility修改为hidden效果又如何?

我们来试试,将函数声明修改为:

 extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL Java_com_wsy_jnidemo_MainActivity_stringFromJNI

运行:

可以看到,运行直接崩溃,因为函数名被隐藏了,所以找不到native函数。

3. JNICALL

这个宏的定义如下

#define JNICALL

没错是空的,这个可用于标记,编译时标记函数用了这个宏,至于作用,至今未发现,有了解的朋友麻烦告知下。

四、native函数参数及返回值说明

函数声明:

extern "C" JNIEXPORT jstring JNICALL
Java_com_wsy_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"JNIEXPORTJNICALL都已在上述进行说明,接下来看下这个函数的函数名和返回值。

1. 函数名

JNI提供了一套规则来实现静态注册时的函数名查找,方式就是:

Java_包名_类名_函数名

顺便一提,在我们未进行动态注册时,函数的注册是在首次调用这个函数时进行的,因此在动态库加载完成时,一个native函数的首次调用耗时会高于后续的调用耗时。我们可以验证一下:

  • 调用方式
    for (int i = 0; i < 100; i++) {
        long start = System.nanoTime();
        stringFromJNI();
        long end = System.nanoTime();
        Log.i(TAG, "onCreate: " + (end - start));
    }
  • 日志

2. 参数

JNI函数会自动添加两个参数:JNIEnv *jobject

第一个参数是JNIEnv *env,我们平时的JNI操作也基本都依赖这个变量来实现;
第二个参数在Java函数是静态函数时是jclass,是成员函数时是jobject,其中jclassjobject的子类。

3. 返回值

对于Java函数而言,它需要的返回值是一个java.lang.String对象,与之对应的是native的jstring对象。

以上就是对一个JNI工程的介绍。