第二章 JNI详解

面试题预览

  1. 阐述你对JNI的理解⭐⭐⭐⭐⭐
  2. 使用JNI有什么优缺点⭐⭐⭐⭐⭐
  3. 什么是JNI具体说说如何实现Java与C++的互调⭐⭐⭐⭐⭐
  4. 什么是NDK?为什么要使用NDK⭐⭐
  5. JNI开发的一般步骤是?⭐⭐⭐⭐
  6. JNI函数的注册方法都有什么?⭐⭐⭐⭐
  7. 谈谈你对JNI静态注册和动态注册的区别。⭐⭐

1 概述

面试题:阐述你对JNI的理解⭐⭐⭐⭐⭐

JNI 是Java Native Interface的缩写,表示"Java本地调用"。通过JNI技术可以实现:

  • Java调用C程序
  • C程序调用Java代码

我们的android源码中有很多代码都是Jni的实现的。例如MediaScanner的实现。就是通过jni的技术让我们在java层扫描到媒体相关的资源的

面试题:使用JNI有什么优缺点⭐⭐⭐⭐⭐

1.1 JNI的优缺点

1.1.1 JNI的优点

  • 首先,Java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。
  • 然后,Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。
  • 接着,使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。

通过对上面的几点的总结,可以归纳出一句话就是规避Java语言的弱点,然后利用C/C++的优点。

实际Android中的驱动都是C/C++开发的,通过JNI,Java可以调用C/C++实现的驱动,从而拓展Java虚拟机的能力。另外,在高效率的数学运算、游戏的实时渲染、音视频的编解码等方面,一般都是用C/C++开发的。

缺点部分:

1.1.2 JNI缺点

   任何事情都是由两面性的,所以JNI也不例外,所以在决定使用 JNI之前,我想各位一定要了解JNI那些缺点,如果能接受和容纳那就可以放心大胆的使用了。

  • 使用JNI细小的错误都能让这个JVM不稳定,并且这些错误很难再现和调试
  • 使用JNI的应用失去了JAVA本身提供的不同平台的可移植性。
  • JNI 框架不提供自动的垃圾回收机制,所以这部分代码要考虑内存的释放

面试题:具体说说如何实现Java与C++的互调⭐⭐⭐⭐⭐

1.2 Java调用C/C++的步骤:

  1. java声明native函数
  2. jni实现对应的c函数
  3. 编译生成so库
  4. java 加载so库,并调用native函数

1.3 C/C++调用Java的步骤:

  1. 从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。
  2. 获取类的默认构造方法ID。
  3. 查找实例方法的ID。
  4. 创建该类的实例。
  5. 调用对象的实例方法。

示例:

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod  
(JNIEnv *env, jclass cls)  
{  
    jclass clazz = NULL;  
    jobject jobj = NULL;  
    jmethodID mid_construct = NULL;  
    jmethodID mid_instance = NULL;  
    jstring str_arg = NULL;  
    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象  
    clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");  
    if (clazz == NULL) {  
        printf("找不到'com.study.jnilearn.ClassMethod'这个类");  
        return;  
    }  

    // 2、获取类的默认构造方法ID  
    mid_construct = (*env)->GetMethodID(env,clazz, "","()V");  
    if (mid_construct == NULL) {  
        printf("找不到默认的构造方法");  
        return;  
    }  

    // 3、查找实例方法的ID  
    mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");  
    if (mid_instance == NULL) {  

        return;  
    }  

    // 4、创建该类的实例  
    jobj = (*env)->NewObject(env,clazz,mid_construct);  
    if (jobj == NULL) {  
        printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");  
        return;  
    }  

    // 5、调用对象的实例方法  
    str_arg = (*env)->NewStringUTF(env,"我是实例方法");  
    (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);  

    // 删除局部引用  
    (*env)->DeleteLocalRef(env,clazz);  
    (*env)->DeleteLocalRef(env,jobj);  
    (*env)->DeleteLocalRef(env,str_arg);  
}  

2 JNI开发

2.1 JNI开发的一般步骤

面试题:JNI开发的一般步骤是?⭐⭐⭐⭐

  1. 编写声明了native方法的Java类
  2. 将Java源代码编译成class字节码文件
  3. 用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数)
  4. 用本地代码实现.h头文件中的函数
  5. 将本地代码编译成动态库(windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
  6. 拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序使用AndroidStudio创建JNI工程

上面的步骤是不是看起来有点复杂,但是在实际开发过程中不用担心。在Android Studio里集成了NDK工具集,能够快速的帮你完成上述步骤。

2.2 JNI与NDK

面试题:什么是NDK?⭐⭐

NDK全称是Native Development Kit,NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。NDK集成了交叉编译器(交叉编译器需要UNIX或LINUX系统环境),并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。

面试题:为什么使用NDK?⭐⭐

1.)代码的保护。由于apk的java层代码很容易被反编译,而C/C++库反汇难度较大。

2.)可以方便地使用现存的开源库。大部分现存的开源库都是用C/C++代码编写的。

3.)提高程序的执行效率。将要求高性能的应用逻辑使用C开发,从而提高应用程序的执行效率。

4.)便于移植。用C/C++写得库可以方便在其他的嵌入式平台上再次使用。

2.3 JNI的注册方法

2.3.1 静态方法

  • 创建Java类,声明 native 方法
  • javah 生成头文件 .h文件的作用
  • 创建 C/C++ 文件,实现对应的native方法

如何连接 Java 层方法和 native 层方法的:

Java方法被调用时,JVM会生成对应的 native 方法名,例如 com.example.StrHelper.getStr() ,JVM会在JNI库中查找 Java_com_example_StrHelper_getStr 函数,如果找到了,就会保存一个该 JNI 函数的指针,直接调用该指针。如果没找到就会报错。

上代码:

以下源码可以通过gitlab获取:JniTest

  1. 准备工作,Android Studio 中安装好NDK、 CMAK、 LLDB 工具
  2. 使用Android Studio创建一个工程,在Java代码中声明一个native方法
  1. 在java中声明native方法

public class MainActivity extends AppCompatActivity {

    /**
     *使用静态代码块加载'native-lib'库,该库即为C/C++代码编译后的共享库,
     * 加载后才能让java层调用C/C++的代码。
     * 库名称由CMakeLists.txt文件中的add_library指定。通常此处名称是native-lib的话,
     * 那么编译成功的共享库名称为libnative-lib.so。
     */
    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(sayHello());//本地方法调用和java函数调用毫无二致

    }

    /**
     * 本地'native-lib'库中实现了的本地方法,共享库会打包到本应用中
     * 区别于普通java函数,在函数申明中多了个native字段,以及没有函数体
     */
    public native String sayHello();
}

4) javac xxx.java 或者 build 生成class文件

  1. 将 .class 生成 .h文件,javah -cp  -d <生成路径>-jni com.hanson.jnitest.MainActivity 。如果有问题

  1. 点击"View->Tool Windows->Terminal",即在Studio中进行终端命令行工具.执行如下命令生成c语言头文件。
  2. 这里需要注意的是要进入 \app\src\main的目录下执行javah命令,为的是生成的 .h 文件同样是在\app\src\main路径下,可以在Studio的工程结构中直接看到。

操作命令:

javah -encoding UTF-8 -classpath D:\WorkSpace\AndroidStudio\JniTest2\app\src\main\java -D:\WorkSpace\AndroidStudio\JniTest2\app\src\main\jni -jni com.hanson.jnitest.MainActivity

然后就可以看到在-d指定的目录下生成了jni的头文件

具体操作图如下:

最后的生成结果:

 

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class com_hanson_jnitest_MainActivity */

#ifndef _Included_com_hanson_jnitest_MainActivity
#define _Included_com_hanson_jnitest_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_hanson_jnitest_MainActivity
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sayHello
  (JNIEnv *env, jobject);

#ifdef __cplusplus
}
#endif
#endif

6.最后就是实现native方法了,创建一个C++文件,最好保持名字和.h文件一致

#include 
#include "com_hanson_jnitest_MainActivity.h"
#include 
#include 
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__);
using namespace std;

extern "C"



JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sayHello(JNIEnv *env, jobject)
{
    std::string str = "Hello from C++";

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

NI中C/C++代码对应于java中的函数,命令有一定的规则。

我们看一下本例中,java函数和C/C++函数的对应关系:

java部分:

类名:com.hanson.jnitest.MainActivity

方法名:public native String sysHello();

c/c++部分:

JNIEXPORT jstring JNICALL Java_com_hanson_jnitest_MainActivity_sysHello(JNIEnv *env, jobject /* this */)

JNIEXPORT : 可以当做JNI方法的函数申明关键字。

jstring : 函数返回值,jstring 对应的是java的String对象。

JNICALL : 可以认为是JNI访问的关键字,固定格式啦。

Java_com_hanson_jnitest_MainActivity_sysHelloI: 在C函数中的方法名格式为Java_{package_and_classname}_{function_name}(JNI_arguments) ,只需要将包名中的点换成下横线就行。

JNIEnv *env: JNI的环境引用,一个非常有用的变量,可以通过它调用所有JNI函数。

jobject /* this */:函数调用者的对象,相当于java层中的this.

7)再在添加CMakeLists.txt,并在其中加入

add_library( 
             native-lib
             SHARED
             com_hanson_jnitest_MainActivity.cpp)//这个.cpp文件

8) 然后在build.gradle文件中指定CMakeLists.txt

如下

//    指定使用NDK的版本
    ndkVersion '20.0.5594570'
//    指定CMakeLists.txt
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

9) 点击编译就大功告成了,直接运行,可以调用native方法来获取string了

 

静态注册方法的弊端:

  • 编写不方便,JNI方法名字遵循规则,很长
  • 编写过程步骤太多,每个声明 native 方法的类都要生成一个 .h 头文件。
  • 初次调用需要在JNI 层根据函数名查找建立对应关系,耗时

2.3.2 动态方法

  • 创建Java类,声明native方法
  • 创建对应 C++ 类,在该类中实现JNI_OnLoad方法,定义JNINativeMethod列表,以及Java native方法具体实现(此时,方法的名称可以是任意的)

通过System.LoadLibrary()加载so库的时候,JVM会调用JNI_OnLoad方法,而我们可以通过在该方法中调用JNIEnv->RegisterNatives()方法将我们的native方法声明注册到JNI中,那是如何将native方法与Java方法联系起来的呢,就是通过JNINativeMethod结构体将两者联系起来的。

上代码:

typedef struct {
    const char* name;//这个是java层函数的名字
    const char* signature;//这个是Java层函数的签名,其他两个很好理解,这个函数签名是个啥???下面会讲
    void*       fnPtr;//这个是native层函数的名字
} JNINativeMethod;//这就是结构体的主要内容,然后我们怎么写呢

TestJni.java

package com.cn.mydynamic;
public class TestJni {
    static {
        System.loadLibrary("main");
    }
    public native String sayHello();//定义了一个native方法
}

上面的JNINativeMethod结构体中的第二项,Java层函数签名,就是按照一定的规则,将java层函数的参数返回值进行转化,为啥整出个这玩意儿?因为java支持函数重载,仅凭函数名称是找不对对应函数的,所以就用参数和返回值结合函数名称来找。

规则如下:

 当参数的类型是引用类型时,其格式是" L包名;",其中包名中的"." 换成"/"。

 很容易写错,但是可以通过javap -s -p xxx.class直接生成转换好的签名,上述的TestJni转换后为

>javap -s -p TestJni.class
Compiled from "TestJni.java"
public class com.cn.mydynamic.TestJni {
  public com.cn.mydynamic.TestJni();
    descriptor: ()V

  public native java.lang.String sayHello();
    descriptor: ()Ljava/lang/String;

  static {};
    descriptor: ()V
}

有了签名有了Java 函数,有了native函数,就可以放进结构体里了

//建立Java层函数与native层函数的对应关系
static const JNINativeMethod getMethod[] = {
        {"sayHello",
         "()Ljava/lang/String;",
         (void*)sya_hello
        }
};

JNIEXPORT jstring JNICALL sya_hello
        (JNIEnv *env, jobject job)
{
    char* app_key = "不要回答!!!不要回答!!!";

    return env->NewStringUTF(app_key);
}

接下来就要把对应关系注册上

#define NELEM(m) (sizeof(m) / sizeof((m)[0]))

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;
    if (vm ->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }

    assert(env != NULL);

    jclass clazz;
    clazz = env->FindClass("com/cn/mydynamic/TestJni");
    if (clazz == NULL) {
        return -1;
    }

    if (env->RegisterNatives(clazz, getMethods, NELEM(getMethods)) < 0) {
        return -1;
    }

    return JNI_VERSION_1_4;
};

运行结果

JNI 基础

上面做了点环境铺垫,接下来开始上正菜。

我们都知道,java的数据类型和C/C++的数据类型并不一致,典型的例子是:java中的String是一个引用数据类型,但在C语言中的String是以NULL结尾的字符串数组。所以协调数据类型,是JNI的重点内容。

JNI 定义了如下的JNI类型用于本地代码中,对应java的数据类型:

3.1 Java 基础数据类型:

下表是对应关系

JNI数据类型

java数据类型

jint

int

jbyte

byte

jshort

short

jlong

long

jfloat

float

jdouble

double

jchar

char

jboolean

boolean

3.2 java 引用数据类型:

JNI数据类型

java数据类型

jobject

java.lang.Object

jclass

java.lang.Class

jstring

java.lang.String

jthrowable

java.lang.Throwable

3.3 在java中,数组中的数据类型和JNI的数组类型对应定义:

JNI数据类型

java数据类型

jintArray

int []

jbyteArray

byte []

jshortArray

short []

jlongArray

long []

jfloatArray

float []

jdoubleArray

double []

jcharArray

char []

jbooleanArray

boolean []

jobjectArray

Object []

3.4 本地程序调用基本顺序

  1. 使用JNI数据类型接收参数(该参数通过java程序调用传递)
  2. 对于JNI引用数据类型,将参数转换或者复制为本地类型。比如:jstring 转为 C-string, jintArray转为C’s int[]等等。基本数据类型,例如jint, jdouble可以直接使用而不需要转换。
  3. 使用本地数据类型执行程序。
  4. 创建一个JNI类型的对象,用作返回(return),将程序运行的结果复制到返回对象中。
  5. 函数返回(return)。

在JNI程序开发过程中,比较困难而极具挑战的是JNI引用类型(例如 jstring, jobject, jintArray, jobjectArray) 和C本地数据类型(例如C-string, int[] )之间的转换。幸好,JNI环境提供了大量的函数来处理这种转换。

4 Java & Native 程序之间参数传递

4.1 基本数据类型传递

java中的8种基本数据类型可以直接被传递和使用,因为这些类型都在jni.h中申明了:

/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

示例

Java 层:

   public class MainActivity extends AppCompatActivity {
   // 在程序开始时使用静态代码块加载'native-lib'库
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
            Log.i("In java", String.valueOf(average(3, 4)));
    }
    //基本数据类型在c/java之间的传递
    public native double average(int arg1, int arg2);

C 层:

extern "C"
JNIEXPORT jdouble JNICALL
Java_com_dali_jnitest_MainActivity_average(JNIEnv *env, jobject instance, jint arg1,
                                           jint arg2) {
    jdouble result;//基本数据类型无需变化,在jni.h中已经设置了类型别名
    /**
    *想要使用该打印,请在C文件头增加下列代码:
    *include 
    *define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "In C/C++:", __VA_ARGS__);
    */
    LOGI("arg1: %d, ar2: %d", arg1, arg2);
    result = (arg1 + arg2) / 2;
    return result;
}

运行程序:

com.dali.jnitest I/In C/C++: arg1: 3, ar2: 4
com.dali.jnitest I/In java: 3.0

4.2 字符串传递

示例

java层:

public class MainActivity extends AppCompatActivity {
   // 在程序开始时使用静态代码块加载'native-lib'库
    static {
        System.loadLibrary("native-lib");
    }

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

        Log.i("In java", testString("hello"));
    }
    //字符串在c/java之间的传递
    public native String testString(String str);

c层:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_dali_jnitest_MainActivity_testString(JNIEnv *env, jobject instance, jstring str_) {
    //在C中的String是以NULL结尾的字符串数组,需要通过特定方法转换,但在C++中有对应的String,是否可以不     //转换?
    const char *str = env->GetStringUTFChars(str_, 0);
    LOGI("str: %s", str);
    char* returnValue = "hehe ,wo lai le";
    env->ReleaseStringUTFChars(str_, str);
    return env->NewStringUTF(returnValue);
}

JNI 定义了jstring类型来代表java的String。C层函数的最后一个参数(JNI类型的jstring)是Java层的String传递到C层的引用。该程序的返回值同样也是jstring类型。

传递字符串远比基本数据类型复杂,因为Java层的String是一个对象(引用数据类型),然而C层中的string是一个以NULL结尾的char数组。所以,使用时需要在Java层的String(以JNI 的jstring表示)和C层的string(char*)之间转换。

JNI环境(通过参数JNIENV*调用)提供了这种转换的函数:

使用const char* GetStringUTFChars(JNIEnv*, jstring, jboolean*)将JNIstring(jstring)类型转换为C层的string(char*)。

使用 jstring NewStringUTF(JNIEnv*, char*)将C层的string(char*)转为JNIstring(jstring)类型。

C层函数的实现步骤为:

从JNI的jstring接收数据,并通过GetStringUTFChars()转为C层的string (char*)类型。

然后执行程序,显示接收到的参数数据,并返回另外一个字符串。

将C层的string (char*)类型通过NewStringUTF()函数转换为JNI的jstring类型并返回。

运行程序:

com.dali.jnitest I/In C/C++: str: hello
com.dali.jnitest I/In java: hehe ,wo lai le

4.2.1 JNI本地String函数

JNI支持Unicode(16字节字符串)和UTF-8(1-3字节编码)不同格式字符串之间的转换。UTF-8编码的字符串和C语言中的字符串一样是以NULL结尾的char数组,用于C/C++程序中。

这些JNI字符串(jstring)为:

/** UTF-8 String (encoded to 1-3 byte, backward compatible with 7-bit ASCII)
* 获取以NULL结尾的字符数组,也就是C-string
*/
// 返回表示UTF-8编码字符串的数组指针
const char * GetStringUTFChars(jstring string, jboolean *isCopy);
// 通知VM 本地代码不再需要UTF引用。
void ReleaseStringUTFChars(jstring string, const char *utf);
// 根据字符串数组,构造一个UTF-8编码的java String新对象
jstring NewStringUTF(const char *bytes);
// 返回UTF-8编码字符串的长度
jsize GetStringUTFLength(jstring string);
// 将从偏移量start开始的length长度的Unicode字符转换为UTF-8编码,并将结果放在给定的缓冲区buf中。
void GetStringUTFRegion(jstring str, jsize start, jsize length, char *buf);


// Unicode Strings (16-bit character)
// 返回指向Unicode字符数组的指针
const jchar * GetStringChars(jstring string, jboolean *isCopy);
// 通知VM本机代码不再需要访问字符。
void ReleaseStringChars(jstring string, const jchar *chars);
// 从Unicode字符数组构造一个新的java.lang.String对象。
jstring NewString(const jchar *unicodeChars, jsize length);
// 返回Java字符串的长度(Unicode字符数)。
jsize GetStringLength(jstring string);
// 将从偏移量=start开始的length长度的Unicode字符数复制到给定的缓冲区buf。
void GetStringRegion(jstring str, jsize start, jsize length, jchar *buf);

4.2.2 UTF-8 strißngs & C-strings

GetStringUTFChars()函数可用于从给定的Java的jstring创建新的C字符串(char *)。 如果无法分配内存,则该函数返回NULL。 检查NULL是一个好习惯。

第三个参数isCopy(of jboolean *),它是一个“in-out”参数,如果返回的字符串是原始java.lang.String实例的副本,则将设置为JNI_TRUE。 如果返回的字符串是指向原始String实例的直接指针,则它将设置为JNI_FALSE- 在这种情况下,本机代码不应修改返回的字符串的内容。 如果可能,JNI运行时将尝试返回直接指针; 否则,它返回一份副本。 尽管如此,我们很少对修改底层字符串感兴趣,并且经常传递NULL指针。

不使用使用GetStringUTFChars()返回的字符串时,需要来释放内存和引用以便可以对其进行垃圾回收时,始终调用ReleaseStringUTFChars()。

NewStringUTF()函数使用给定的C字符串创建一个新的JNI字符串(jstring)。

JDK 1.2引入了GetStringUTFRegion(),它将jstring(或从长度开始的一部分)复制到“预分配”的C的char数组中。 可以使用它们代替GetStringUTFChars()。 由于预先分配了C的数组,因此不需要isCopy。

JDK 1.2还引入了Get / ReleaseStringCritical()函数。 与GetStringUTFChars()类似,如果可能,它返回一个直接指针; 否则,它返回一份副本。 本机方法不应阻止(对于IO或其他)一对GetStringCritical()和ReleaseStringCritical()调用。

有关详细说明,请始终参阅“Java Native Interface Specification”@ http://docs.oracle.com/javase/7/docs/technotes/guides/jni/index.html。

4.3 基本数据类型数组传递

4.4 访问对象的变量和函数回调

4.5 创建对象和对象数组

4.6 本地和全局引用

4.3~4.6小节,详细可以参考下面一篇不错的博客:

Android JNI 详解_SuperDali的博客-CSDN博客

本文不再赘述。

5 参考

JNI/NDK入门指南之正确姿势了解JNI和NDK

Android Studio开发JNI工程

JNI面试指南

Android JNI详解

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值