JNI 编程上手指南之 HelloWorld 实战

JNI 编程是高级/专家 Android 开发的必备技能之一,接下来我们就一步一步掌握 JNI 编程的方方面面。

1. 基本概念

JNI(Java Native Interface,JAVA 原生接口)。JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行互操作。通俗一点讲就是在 Java 代码里调用 C/C++ 等语言的代码或 C/C++ 代码调用 Java 代码。

JNI 技术在 Android 领域有大量的应用:

  • Java 程序可以通过 JNI 操作硬件
  • 音视频处理,数学运算,实时渲染等领域相关的库基本都使用 C/C++ 编写,我们可以使用 JNI 技术来调用这些库,而不用使用 Java 来重写
  • 相比 C/C++,Java 更容易被反编译,一些和安全相关的代码,我们可以使用 C/C++ 来编写,然后使用 JNI 技术调用

JNI 技术的应用当然不止以上列举的例子,更多的应用方式等待大家的进一步探索。

2. HelloWorld 实战

接下来我们通过一个简单的示例程序,快速地掌握 JNI 的基本使用。

首先编译一个 Java 文件: HelloJNI.java

public class HelloJNI { 

   static {
      System.loadLibrary("hello"); 
   }

   private native jstring sayHello();

   public static void main(String[] args) {
      new HelloJNI().sayHello(); 
   }
}

接着生成 C/C++ 头文件 HelloJNI.h

javac -h . HelloJNI.java

该命令会生成一个 HelloJNI.h,这个头文件描述了我们需要实现的函数。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
  • 生成的函数中有两个参数:
  • JNIEnv:JNIEnv 内部提供了很多函数,方便我们进行 JNI 编程。 C 代码中,JNIEnv 是指向 JNINativeInterface 结构的指针,为了访问任何一个 JNI 函数,该指针需要首先被解引用。因为 C 代码中的 JNI 函数不了解当前的 JNI 环境, JNIEnv 实例应该作为第一个参数传递给每一个 JNI 函数调用调用者,调用格式如下: (*env)->NewStringUTF(env,"Hello from JNI !"); 在 C++ 代码中,JNIEnv 实际上是 C++ 类实例,JNI 函数以成员函数的形式存在,因此 JNI 函数调用不要求 JNIEnv 实例作参数。在 C++ 中,完成同样功能的调用代码格式如下: env->NewstringUTF ( "Hello from JNI ! ");
  • jobject: 指向 “this” 的 Java 对象
  • 如果 java 中的 native 函数是 static 的,那第二个参数是 jclass,代表了 java 中的 Class 类。
  • extern “C” 告诉 C++ 编译器以 C 的方式来编译这个函数,以方便其他 C 程序链接和访问该函数。C 和 C++ 有着不同的命名协议,因为 C++ 支持函数重载,用了不同的命名协议来处理重载的函数。在 C 中函数是通过函数名来识别的,而在 C++ 中,由于存在函数的重载问题,函数的识别方式通过函数名,函数的返回类型,函数参数列表三者组合来完成的。因此两个相同的函数,经过C,C++编绎后会产生完全不同的名字。所以,如果把一个用 C 编绎器编绎的目标代码和一个用 C++ 编绎器编绎的目标代码进行链接,就会出现链接失败的错误。
  • JNIEXPORT、JNICALL 两个宏在 linux 平台的定义如下:
  //该声明的作用是保证在本动态库中声明的方法 , 能够在其他项目中可以被调用
  #define JNIEXPORT  __attribute__ ((visibility ("default")))
  //一个空定义
  #define JNICALL

接着我们来实现具体的 C 程序 HelloJNI.c

#include "HelloJNI.h"
#include <stdio.h>
#include <jni.h>

//方法名要和 Java 层包名对应上
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj)
{
    return (*env)->NewStringUTF(env,"Hello from JNI !");
}

编译和执行(需要配置好 JAVA_HOME 环境变量):

gcc -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
java -Djava.library.path=. HelloJNI

至此,一个简单的 demo 就完成了。

3. 动态注册

以上使用 JNI 的方式称为静态注册,还有一种方式叫动态注册,我们接下来看个动态注册的例子吧

java层:
com/example/ndk/NativeTest.java

package com.example.ndk;

public class NativeTest {
    static {
      System.loadLibrary("nativetest"); 
    }
    public native void init();

    public native void init(int age);

    public native boolean init(String name);

    public native void update();
}

C 层的实现主要有三步:

  • 实现 java 层本地方法
  • 构建一个 JNINativeMethod 类型的数组
  • 注册本地函数

NativeTest.c :

#include <jni.h>
#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

//1 实现 java 层本地方法
JNIEXPORT void JNICALL
c_init1(JNIEnv *env, jobject thiz) {
     printf("c_init1\n");
}

JNIEXPORT void JNICALL
c_init2(JNIEnv *env, jobject thiz, jint age) {
    printf("c_init2\n");
}

JNIEXPORT jboolean JNICALL
c_init3(JNIEnv *env, jobject thiz, jstring name) {
    printf("c_init3\n");
}

JNIEXPORT void JNICALL
c_update(JNIEnv *env, jobject thiz) {
    printf("c_update\n");
}

#ifdef __cplusplus
}
#endif


// typedef struct {
//     //Java层native方法名称
//    const char* name;
//     //方法签名
//    const char* signature;
//     //native层方法指针
//    void*       fnPtr;
// } JNINativeMethod;

//2 构建 JNINativeMethod 数组
//中间的方法签名看上去有点怪异,后面我们来讲它的命名规则
static JNINativeMethod methods[] = {
        {"init", "()V", (void *)c_init1},
        {"init", "(I)V", (void *)c_init2},
        {"init", "(Ljava/lang/String;)Z", (void *)c_init3},
        {"update", "()V", (void *)c_update},
};

/**
 * 3 完成动态注册的入口函数
 *  其内容基本固定
 */ 
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;

    // 获取JNI env变量
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
        // 失败返回-1
        return result;
    }

    // 获取native方法所在类
    const char* className = "com/example/ndk/NativeTest";
    jclass clazz = env->FindClass(className);
    if (clazz == NULL) {
        return result;
    }

    // 动态注册native方法
    if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
        return result;
    }

    // 返回成功
    result = JNI_VERSION_1_6;
    return result;
}

JNINativeMethod 第二个成员变量是方法签名,它的组成规则为:

(参数类型标识1参数类型标识2…参数类型标识n)返回值类型标识

其中的类型标识如下图所示:

类型标识Java数据类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L包名/类名;各种引用类型
Vvoid

编译和执行:

cd com/example/ndk
javac NativeTest.java
#回到项目根目录
cd -
g++ -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libnativetest.so NativeTest.c
java -Djava.library.path=. com.example.ndk.NativeTest

参考资料

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/20390,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?