JNI和NDK编程-JNI入门

1,807 阅读9分钟
原文链接: blog.csdn.net

版权声明:本文为博主原创文章,欢迎大家转载!

转载请标明出处: blog.csdn.net/guiying712/…,本文出自:【张华洋的博客】


1、原生开发工具包 (NDK) 是一组可让您在 Android 应用中利用 C 和 C++ 代码的工具。 NDK可以让您将 C 和 C++ 源代码构建为可用于Android应用的共享库,或者利用现有的预构建库。

2、JNI是Java Native Interface。它定义了一种Java代码与本地代码交互的方式,是一种允许运行与 JVM 的Java程序去调用(反向依然)本地代码(通常 JNI 面向的本地代码是用C、C++以及汇编语言编写的)的编程框架。

JNI和NDK编程-使用AndroidStudio进行NDK开发 中已经详细介绍了如果创建一个支持C/C++的新项目,以及如何让已有的项目支持 C/C++。这篇文章将介绍如何进行JNI开发。

在介绍 JNI 开发之前,我们先考虑个问题,在Android开发中我们真的需要 JNI 吗? 本地代码通常与硬件或者操作系统有关联,我们平时都是用Java语言来开发Android应用的,好像并不需要使用本地语言,但是实际上在Android系统中就采用了大量JNI手段去调用本地层的实现库,如果我们想要深入学习Android系统原理就必须掌握JNI,当然在Android开发中,还有以下几种情况需要用到 JNI :

  • 应用程序需要一些平台相关的 特性的支持,而Java是无法满足的;
  • 兼容已有的用其他语言编写的代码库(例如在Android上集成 FFmpeg),使用JNI技术可以让Java层的代码访问这些现成的库,实现一定程度的代码复用;
  • 应用程序的某些关键操作对运行速度要求较高,这部分代码可以本地语言如C语言来编写,再通过JNI向Java层提供访问接口;
  • 在Android中,本地代码通常是已 so库 的形式存在,由于 so库 反编译比较困难,因此可以使用本地语言编写某些重要业务代码,提高代码的安全性;

假设你已经阅读过 JNI和NDK编程-使用AndroidStudio进行NDK开发 ,并且按照教程创建好了项目,那么我就可以进入JNI的开发流程了。

Java函数的本地实现

1、首先看下Java代码:

package com.guiying712.ndkdemo;

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

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

很明显,MainActivity 跟我们以往编写的 Activity 有一些区别:

1、在MainActivity 中有个静态代码块,并且在 static块中出现了 System.loadLibrary(“native-lib”) 这行代码。上面的注释已经很清楚了,这段代码的作用就是在应用程序启动的时候将 ‘native-lib’ 这个动态链接库加载进来, ‘native-lib’ 是动态链接库的标识,so库的完整名称是:libnative-lib.so 。

2、修饰 stringFromJNI() 方法 中有 native 关键字 , 在 Java 代码中声明本地方法必须有 native 标识符, native修饰的方法,在 Java 代码中只作为声明存在,需要我们用本地语言去实现这个 native方法,而我们实现 native方法的代码就在 ‘native-lib’ 这个动态链接库中,所以在调用stringFromJNI()之前,就必须先将 ‘native-lib’ 库 加载进来,将 System.loadLibrary(“native-lib”) 置于 static 块中,可以在 Java VM 初始化一个类时,首先执行这部分代码,这可保证调用本地方法前,已经装载了本地库。

2、接下来看下native 代码:


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

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

是不是感觉很奇怪,Java_com_guiying712_ndkdemo_MainActivity_stringFromJNI 这玩意怎么这么长,跟我们Java方法的命名方式区别太大了。其实Java_com_guiying712_ndkdemo_MainActivity_stringFromJNI就是Java代码中stringFromJNI() 的本地代码实现。在JNI 中,函数名的格式需要遵循如下的规则:

Java _ 包名 _ 类名 _ 方法名

我们可以比照下上面的native代码中的函数名称是不是这样命名的,我的Java代码包名是:com.guiying712.ndkdemo。当然你可能会想,Java代码中那么多本地方法声明,要是一个一个都这样命名岂不是很痛苦,其实AndroidStudio是有快捷方式创建native代码中的本地函数的,我们可以在 MainActivity 中再另创建一个本地方法:getStringFromJNI(String name);

public native String getStringFromJNI(String name);

当你在Java代码中声明一个native方法后,由于还没有实现,所以这个方法是红色,这时候你用鼠标点击 这个方法名称 ,然后在键盘上按:ALT键+Enter键(回车键),就会弹出创建这个native方法的提示,点击创建即可。当一个Java的native方法有了本地函数实现后,AndroidStudio就会贴心的在编辑器旁边显示一个箭头图标,就像下图 红圈标出来的那个图标,点击这个图标可以直接跳转到native函数中,同理native函数也可以通过点击这个箭头跳转到Java方法声明中:

本地函数创建

我们继续介绍上面的本地函数,其中 jstring 是代表的是 stringFromJNI()方法中返回的String类型参数,jstring是JNI中的一种数据类型,这个我们完了再介绍,这里只需要知道Java的String对应于JNI的jstring即可。JNIEXPORT、 JNICALL、JNIEnv和jobject都是JNI标准中所定义的类型或者宏, 它们的含义如下:

JNIEXPORT和JNICALL: 它们是JNI中所定义的宏, 可以在jni.h这个头文件中查找到;

JNIEnv*: 表示一个指向JNI环境的指针, 可以通过它来访问JNI提供的接口方法;

jobject: 表示Java对象中的this;

另外在JNI函数前必须有extern “C”,它指定extern “C”内部的函数采用C语言的命名风格来编译,否则当JNI采用C++来实现时, 由于C和C++编译过程中对函数的命名风格不同, 这将导致 JNI 在链接时无法根据函数名查找到具体的函数, 那么无法JNI调用。

从上面的 Java_com_guiying712_ndkdemo_MainActivity_stringFromJNI 函数中,我们可以看到诸如 JNIEnv 、jobject这样的参数类型,而Java代码中的stringFromJNI()本身却没有携带者两个参数。我们通过名称可能会想到它们应该类似于C++中的 this 指针,即代表了类的一个实例化对象。 事实上也确实如此,JNIEnv 的定义如下:

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

/*
 * Table of interface function pointers.
 */
struct JNINativeInterface {
  //各种JNI的数据类型和函数
  jint        (*GetVersion)(JNIEnv *);
  jclass      (*FindClass)(JNIEnv*, const char*);
  ...
  void        (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
  ...
}

JNINativeInterface 是一个 JNI 的本地接口,包含了很多实用的函数,而 jobject 则代表了这个 本地类方法对应的 Java 类 实例

不过 Java_com_guiying712_ndkdemo_MainActivity_stringFromJNI() 函数返回的是jstring,而不是 String类型,这是因为 JNI 已经对所有的 Java标准数据类型做了相应的 typedef ,具体可以看下表:

———————————————– JNI 基础类型对照表 —————————————–

Java类型 JNI类型 说明
boolean jboolean unsinged 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort singed 16 bits
int jint signed 32 bits
long jlong signed 64bits
float jfloat 32 bits
double jdouble 64 bits
void void void

———————————————– JNI 引用数据类型对照表 ———————————————–

Java类型 JNI类型 说明
Object jobject Object类型
java.lang.Class jclass Class类型
java.lang.String jstring 字符串
java.lang.Throwable jthrowable Throwable
Object[] jobjectArray 对象数组
boolean[] jbooleanArray boolean数组
byte[] jbyteArray byte数组
char[] jcharArray char数组
short[] jshortArray short数组
int[] jintArray int数组
long[] jlongArray long数组
float[] jfloatArray float数组
double[] jdoubleArray double数组

end

基础类型的变量可以在 Java和 本地代码件进行拷贝,而Java对象需要通过引用类型进行传递。JVM需要跟踪所有它传递给本地代码的对象实例,这样才能保证它们不被垃圾回收器回收,而当本地代码不再使用这些对象时,也要及时通知JVM。

类型转换

还记得我们前面创建的 getStringFromJNI(String name)方法吗?


    /**
     * 这个方法的目的是输入程序员的名称,返回 Hello : 大佬程序员
     *
     * @param name 程序员的名称
     * @return Hello : 大佬
     */
    public native String getStringFromJNI(String name);

native实现函数:


extern "C"
JNIEXPORT jstring JNICALL
Java_com_guiying712_ndkdemo_MainActivity_getStringFromJNI(JNIEnv *env, jobject instance,
                                                          jstring name_) {

    const char *name = env->GetStringUTFChars(name_, 0);

    std::string hello = "Hello : ";

    std::string coderName = std::string(name);

    std::string helloCoder = hello + coderName;

    //释放GetStringUTFChars(name_, 0)的内存
    env->ReleaseStringUTFChars(name_, name);

    return env->NewStringUTF(helloCoder.c_str());
}

相比基本类型,对象类型的传递要复杂很多。 Java 层对象作为 对象引用(指针) 传递到 JNI 层。对象引用(指针) 是一种 C 的指针类型,它指向 JavaVM 内部数据结构。使用这种指针的目的是:不希望 JNI 用户了解 JavaVM 内部数据结构。对 对象引用(指针) 所指结构的操作,都要通过 JNI 方法进行。 比如,”java.lang.String”对象,JNI 层对应的类型为 jstring,对该 对象引用(指针) 的操作要通过 JNIEnv->GetStringUTFChars 进行。

JNI 支持 Unicode/UTF-8 字符编码互转。Unicode 以 16-bits 值编码;UTF-8 是一种以字节为单位变长格式的字符编码,并与 7-bits ASCII 码兼容,UTF-8 字串与 C 字串一样,以 NULL(‘\0’) 做结束符, 当 UTF-8 包含非 ASCII 码字符时,以’\0’做结束符的规则不变,7-bit ASCII 字符的取值范围在 1-127 之间,这些字符的值域与 UTF-8 中相同,当最高位被设置时,表示多字节编码。

std::string hello = “Hello : “是C++代码中的字符串,我们需要将 C/C++字符串转换成 JNI 中的 jstring 返回给Java代码。 env->NewStringUTF( helloCoder.c_str() ) ,使用 JNIEnv->NewStringUTF 构造一个 java.lang.String,如果此时没有足够的内存,NewStringUTF 将抛 OutOfMemoryError 异常,同时返回 NULL。