金戈铁马 Android NDK 实战篇:男人之间的那些事

对一个女人而言,和心爱的人在一起的幸福就是最重大的了。女人永远不需要悲悲壮壮的轰轰烈烈,只要温温柔柔地在一起开开心心。

正文

相对于 Android NDK 的枯燥乏味,大家更热衷于男人之间的那些事。那么男人之间又有哪些事呢?

1. Hello from c++

我们一般需要了解运行一个 NDK 项目需要做什么,有哪些配置以及配置的具体含义。 Android Studio(2.2以上版本)提供两种方式编译原生库:CMake( 默认方式) 和 ndk-build。对于初学者可以先了解 CMake 编译的常见指令,所谓工欲善其事必先利其器。

申明一个 native 的方法非常简单 public native String getNDKString(); 直接使用 AndroidStudio 预提示生成 cpp 文件,同时也可以通过 javah 命令生成 cpp 文件,如下:

public class NDKTest {
    public native String getNDKString();
}

javah 命令,注意 cd...\app\src\main\java ,执行 javah 包名(.号连接)+.类名

D:\AndroidSpace\NDKDemo\app\src\main\java>javah com.demo.ndkdemo.NDKTest

这里列出项目中涉及 NDK 的内容或配置几点需要注意的地方:

  • .externalNativeBuild 文件是 CMake 编译好的文件,显示支持的各种硬件平台的信息,如 ARMx86 等;

  • cpp 文件是放置 native 文件的地方,名字可以修改成其他的(只要里面函数名字对应Java native 方法);

  • CMakeLists.txt ,AS自动生成的 CMake 脚本配置文件;

# 指定cmake的最小版本号
cmake_minimum_required(VERSION 3.4.1)

add_library( # Sets the name of the library. ——> 生成函数库名称,so 库名称
             native-lib 

             # Sets the library as a shared library.  生成动态库还是静态库
             SHARED   # 动态库

             # Provides a relative path to your source file(s). 编译的文件路径
             src/main/cpp/native-lib.cpp )  # native-lib 的文件路径

find_library( # Sets the name of the path variable. 查找系统库名称
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate. # 指定要查询库的名字
              log )

target_link_libraries( # Specifies the target library.  目标库名称
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK. 
                       ${log-lib} ) # 需要链接库的名称 ${log-lib} 引用的变量 log-lib ,多个库的引用以逗号隔开

build.gradle 文件,注意两个 externalNativeBuild {} 的节点配置

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.demo.ndkdemo"
        minSdkVersion 15
        ...
        externalNativeBuild {
            cmake {
                // 如果使用 C++11 标准,则改为 "-std=c++11" 
                cppFlags ""
                // 生成.so库的目标平台,注意 x86 已经不支持
                abiFilters "armeabi-v7a", "armeabi" 
            }
        }
    }
    ...
    externalNativeBuild {
        cmake {
           // 配置 CMake 文件的路径
            path "CMakeLists.txt"
        }
    }
}

local.properties 新增了 ndk 路径的引用

ndk.dir=D\:\\AndroidSdk\\ndk-bundle
sdk.dir=D\:\\AndroidSdk

MainActivity 调用 so

public class MainActivity extends AppCompatActivity {

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

    public native String stringFromJNI();
    // 注意静态修饰符
    public native static String stringStaticFromJNI();
}

需要注意的是 JNI 的函数命名规则:JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jobject, 其它参数); 其中第一个参数 JNIEnvjni.h 文件最重要的部分,它的本质是指向函数表指针的指针(JavaVM 也是),函数表里面定义了很多 JNI 函数,同时它也是区分 CC++ 环境的,在 C 语言环境中,JNIEnvstrut JNINativeInterface* 的指针别名。

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)  
typedef _JNIEnv JNIEnv;   //C++中的 JNIEnv 类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;  //C语言的 JNIEnv 类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif

第二个参数,当 java native 方法为静态时,为 jclass,当为非静态方法时,为 jobject ,纸上得来终觉浅,绝知此事要躬行,接下来简单列举几个例子。

2. JNI日志打印

工欲善其事必先利其器,日志的重要性不言而喻,在 CMake 中引用了 log 库,那么可以引用头文件 #include log.h 进行封装

#ifndef NDKDEMO_LOG_H
#define NDKDEMO_LOG_H
#define LOG_TAG "JNI_LOG"

#include 

// 定义各种类型 Log 的函数别名
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG ,__VA_ARGS__)

#endif

引入头文件 #include "log.h" 就可以使用 LOG 函数进行日志打印。

3. JNI函数访问Java对象的变量

Java 对象的变量又分为静态变量与非静态变量,JNI 函数访问 Java 对象的变量步骤如下:

  • 1)通过 env->GetObjectClass(jobject); 获取 Java 对象的 class 实例,返回一个 jclass
  • 2)调用 env->GetFieldID(jclass clazz, const char* name, const char* sig) 得到该域(变量)的 id ,返回一个 jfieldID ;如果变量是静态的,则调用方法 GetStaticFieldID
  • 3)通过调用 Get{type}Field(jobject obj, jfieldID fieldID) 获取到该变量的值,其中 {type} 为变量的类型( 基本数据类型与引用类型 object );如果是静态变量,则调用 GetStatic{type}Field(jclass, fieldId) ,注意第一个参数是 jclass
  • 4)最后通过 3)获取的变量值进行变换;也可以调用 Set{type}Field(jobject obj, jfieldID fieldID, jint value) 设置该变量的值,如果变量是静态的,则调用 SetStaticIntField

3.1访问某个非静态变量,返回处理后的值

java 层 native 方法定义和调用:

private int age = 18;
public native int ageFromJNI();

Log.e(TAG, "调用前:age = " + age);
Log.e(TAG, "调用后:age = " + ageFromJNI());

c++ 层:

extern "C"
JNIEXPORT jint JNICALL
Java_com_demo_ndkdemo_MainActivity_ageFromJNI(JNIEnv *env, jobject instance) {
    // TODO
    // 获取到 MainActivity 对应的 class
    jclass jclazz = env->GetObjectClass(instance);
    // 获取到域 ID
    jfieldID ageFieldID = env->GetFieldID(jclazz, "age", "I");
    // 获取到 FieldID 对应的值
    jint age = env->GetIntField(instance, ageFieldID);
    // env->SetIntField()
    age++;
    return age;
}

输出结果:

调用前:age = 18
调用后:age = 19

3.2访问一个静态变量,并对其修改

java 层 native 方法定义和调用:

private static String name = "hello android";
public native void nameFromJni();

Log.e(TAG, "调用前:age = " + name);
nameFromJni();
Log.e(TAG, "调用后:age = " + name);

c++ 层:

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_ndkdemo_MainActivity_nameFromJni(JNIEnv *env, jobject instance) {
    // TODO
    jclass jclazz = env->GetObjectClass(instance);
    // 获取到静态域 ID
    jfieldID nameFieldID = env->GetStaticFieldID(jclazz, "name", "Ljava/lang/String;");
    // 获取到 name 的值
    jstring name = static_cast(env->GetStaticObjectField(jclazz, nameFieldID));
    // 字符串转换成字符
    const char *str = env->GetStringUTFChars(name, JNI_FALSE);

    char ch[30] = "google , ";
    strcat(ch, str);
    // 生成字符串
    jstring name_str = env->NewStringUTF(ch);
    // 重新设置 name 的值
    env->SetStaticObjectField(jclazz, nameFieldID, name_str);
}

输出结果:

调用前:age = hello android
调用后:age = google , hello android

4.JNI反射调用Java方法

抛砖引玉,通过 JNI 反射调用 Java 的静态方法与非静态方法,步骤如下:

  • 1)通过 env->FindClass(const char* name); 获取 Java 对象的 class 实例,返回一个 jclass,注,参数 name 格式为 包名(以"/"分割)+ 类名,不能是 L + 包名(以"/"分割)+ 类名 + ; 的形式
  • 2)调用 env->GetMethodID(jclass clazz, const char* name, const char* sig) 得到构造函数方法 id ,返回一个 jmethodID,如果是静态方法则不需要此步骤
  • 3)通过调用 env->NewObject(jclass clazz, jmethodID methodID, ...) 新建类的实例对象,返回 jobject 类型,如果是静态方法则不需要此步骤
  • 4)通过调用 env->GetMethodID(jclass clazz, const char* name, const char* sig) 得到被调用的方法 id,返回一个 jmethodID 类型,如果是静态方法则调用 GetStaticMethodID
  • 5)最后通过 env->Call{type}Method(jobject, jmethod, param...) 调用 Java 的方法;如果是 static 方法,则使用 CallStatic{type}Method(jclass, jmethod, param...) ,使用的是 jclass

4.1反射调用Java非静态方法

java 层 Student 类:

public class Student {

    public String name;
    public int score;

    public Student() {
    }

    public String printScore(String name, int score) {
        return "name = " + name + ", score= " + score;
    }

    public static String printName(String name) {
        return "name = " + name;
    }

}

java 层 native 方法定义和调用:

public native String studentScoreFromJni();

Log.e(TAG, "JNI反射调用Java非静态方法:" + studentScoreFromJni());

c++ 层:

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

    // TODO
    // 获取到 student 实例
    jclass studentClass = env->FindClass("com/demo/ndkdemo/Student");
    if (studentClass == NULL) {
        return env->NewStringUTF("cannot find student class");
    }
    // 获取到构造方法 ID
    jmethodID constructorMid = env->GetMethodID(studentClass, "", "()V");
    if (constructorMid == NULL) {
        return env->NewStringUTF("not find student constructor method");
    }
    // 获取到 student 对象
    jobject studentObject = env->NewObject(studentClass, constructorMid);

    jmethodID scoreMid = env->GetMethodID(studentClass, "printScore", "(Ljava/lang/String;I)Ljava/lang/String;");
    if (scoreMid == NULL) {
        return env->NewStringUTF("not find student printScore method");
    }

    return static_cast(env->CallObjectMethod(studentObject, scoreMid, env->NewStringUTF("mei"), 18));
}

输出结果:

JNI反射调用Java非静态方法:name = mei, score= 18

4.2反射调用Java静态方法

java 层 native 方法定义和调用:

public native String studentNameFromJni();

Log.e(TAG, "JNI反射调用Java静态方法:" + studentNameFromJni());

c++ 层:

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

    // TODO
    // 获取到 student 实例
    jclass studentClass = env->FindClass("com/demo/ndkdemo/Student");
    if (studentClass == NULL) {
        return env->NewStringUTF("cannot find student class");
    }

    // 获取到 name 的方法 ID
    jmethodID nameMid = env->GetStaticMethodID(studentClass, "printName", "(Ljava/lang/String;)Ljava/lang/String;");
    if (nameMid == NULL) {
        return env->NewStringUTF("not find student printName method");
    }

    // 反射调用静态方法
    return static_cast(env->CallStaticObjectMethod(studentClass, nameMid, env->NewStringUTF("body")));
}

输出结果:

JNI反射调用Java静态方法:name = body

5.JNI异常处理

JNI 没有像 Java 一样有 try…catch…final 这样的异常处理机制,面且在本地代码中调用某个 JNI 接口时如果发生了异常,后续的本地代码不会立即停止执行,而会继续往下执行后面的代码。JNI 异常处理与 Java 类似,大致分为异常检测,异常处理,抛出异常

5.1异常检测

检测是否发生异常有两种方式:

  • 1)通过特定的返回值来表示发生了一个错误,如 NULL ,有一个空指针异常需要处理
  • 2)通过调用 JNI 提供的 ExceptionCheck 来检测是否有异常发生

判空处理异常:

// 获取到 student 实例
jclass studentClass = env->FindClass("com/demo/ndkdemo/Student");
if (studentClass == NULL) {
    LOGE("cannot find student class");
    return env->NewStringUTF("cannot find student class");
}

同样也可以使用 ExceptionCheck 来检测异常:

jclass studentClass = env->FindClass("com/demo/ndkdemo/Student");
if(env->ExceptionCheck()){
    // 如果发生异常,则返回字符提示
    return env->NewStringUTF("cannot find student class exception");
}

5.2异常处理

由于 JNI 发生了异常,依旧会执行后续代码,所以我们需要对异常进行相应处理,否则会发生不可预知的错误,处理的步骤如下:

  • 1)一旦发生了异常,立即返回,让调用者处理异常
  • 2)通过 ExceptionClear 清除异常,释放资源,让调用者处理异常

如下示例:

const char *result = env->GetStringUTFChars(str, NULL);
if (env->ExceptionCheck()) {
    env->ExceptionDescribe();
    // 清除异常
    env->ExceptionClear();
    // 释放资源
    env->ReleaseStringUTFChars(str,result);
    
    // 直接返回
    return ;
}

5.3抛出异常

当我们不需要处理异常时,需要抛出异常。抛出异常也分为两种方式:

  • 1)抛出现有异常 env->Throw(env->ExceptionOccurred());
  • 2)抛出新异常 env->ThrowNew(jclass clazz, const char* message)

java 层的方法:

private void nullPointerMethod() {
    Student student = null;
    student.printScore(name, age);
}

java 层 native 方法定义和调用:

public native void callJavaExceptionMethodFromJni();

try {
    callJavaExceptionMethodFromJni();
} catch (Exception e) {
    e.printStackTrace();
    Log.e(TAG, "JNI发生了异常:" + e.getMessage());
}

c++ 层:

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_ndkdemo_MainActivity_callJavaExceptionMethodFromJni(JNIEnv *env, jobject instance) {

    // TODO
    jclass jclazz = env->GetObjectClass(instance);

    // 获取到方法 id
    jmethodID mid = env->GetMethodID(jclazz, "nullPointerMethod", "()V");

    // 判空处理
    if (NULL == mid) {
        LOGE("not find nullPointerMethod");
        return;
    }

    env->CallVoidMethod(instance, mid);

    // 异常检测
    if (env->ExceptionCheck()) {
        jthrowable throwable = env->ExceptionOccurred();

        /**
         * have a throw is occurred
         */
        env->ExceptionDescribe();
        // 清除异常
        env->ExceptionClear();

        // 可以直接抛出 env->Throw(throwable);

        // 抛出新异常
        jclass exceptionCls = env->FindClass("java/lang/NullPointerException");

        if (NULL == exceptionCls) {
            return;
        }
        env->ThrowNew(exceptionCls, "throw a new exception:NullPointerException");
        // 释放资源
        env->DeleteLocalRef(exceptionCls);
    }
}

输出结果:

JNI发生了异常:throw a new exception:NullPointerException

6.JNI多线程

多线程就不得不考虑并发带来的数据同步问题,Java 提供了 synchronize 同步锁,JNI 同时也提供了监视器机制,用来对临界区进行保护性访问。为了更好理解临界区这个概念,我们可以先来看一下 Java 中最简单同步锁的例子:

synchronized (this) {
    // 临界区
}

临界区的特点就是在同一时刻有且仅有一个线程运行在临界区内。其他线程会被阻塞在临界区外,直到临界区内没有任何线程运行。

JNI 中,通过调用 env->MonitorEnter(jobject); 函数进入一个临界区,当执行完需要同步的代码后,必须调用 env->MonitorExit(jobject); 函数退去临界区,否则程序将会发生死锁。注,MonitorEnterMonitorExit 需成对出现。

// 进入同步代码块
if (env->MonitorEnter(instance) != JNI_OK) {
    // 处理错误
}
/**
 * 同步代码块业务处理
 */
/**
 * 出现异常,结束同步代码块
 */
if (env->ExceptionOccurred()) {
    if (env->MonitorExit(instance) != JNI_OK) {
        return;
    }
}
if (env->MonitorExit(instance) != JNI_OK) {
    // 处理错误
}

实践才是检验真理的唯一标准,来看一个简单的案例,在 Java 中开启多个线程,并打印当前的索引值。

java 层 native 方法定义和调用:

public native void synchronizeThreadFromJni(int index);

for (int i = 0; i < 5; i++) {
    final int index = i;
    new Thread() {
        @Override
        public void run() {
            super.run();
            synchronizeThreadFromJni(index);
        }
    }.start();
}

c++ 层:

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_ndkdemo_MainActivity_synchronizeThreadFromJni(JNIEnv *env, jobject instance,
                                                            jint index) {
    // TODO
    // 进入同步代码块
    if (env->MonitorEnter(instance) != JNI_OK) {
        // 处理错误
    }

    /**
     * 同步代码块业务处理
     */
    LOGE("current thread index = %d", index);

    /**
     * 出现异常,结束同步代码块
     */
    if (env->ExceptionOccurred()) {
        if (env->MonitorExit(instance) != JNI_OK) {
            return;
        }
    }

    if (env->MonitorExit(instance) != JNI_OK) {
        // 处理错误
    }
}

输出结果:

current thread index = 0
current thread index = 1
current thread index = 2
current thread index = 3
current thread index = 4

有线程的地方都应有相应的等待唤醒机制,比较简单直接的方式,直接调用 Javaobject 类中的 waitnotifynotifyAll 方法,具体的实现如下:

void initThread(JNIEnv *env, jobject lock) {
    jclass cls = env->GetObjectClass(lock);
    THREAD_WAIT = env->GetMethodID(cls, "wait", "(J)V");
    THREAD_NOTIFY = env->GetMethodID(cls, "notify", "(V)V");
    THREAD_NOTIFY_ALL = env->GetMethodID(cls, "notifyAll", "(V)V");
}

void wait(JNIEnv *env, jobject lock, jlong timeout) {
    // 调用 object 的 wait 方法
    env->CallVoidMethod(lock, THREAD_WAIT, timeout);
}

void notify(JNIEnv *env, jobject lock) {
    // 调用 object 的 notify 方法
    env->CallVoidMethod(lock, THREAD_NOTIFY);
}

void notifyAll(JNIEnv *env, jobject lock) {
    // 调用 object 的 notifyAll 方法
    env->CallVoidMethod(lock, THREAD_NOTIFY_ALL);
}

看到这里,肯定有小伙伴心中会有疑问,JNI 中开启线程又是怎么样的姿势?一起来看下以下例子,JNI 中开启多线程,打印线程索引值:

java 层 native 方法定义和调用:

public native void multiThreadFromJni();

private void getIndexCurrentThread(int i) {
    Log.e(TAG, "当前线程的索引值为: " + i);
}

c++层:

#include 

// 线程数
const int NUM_THREADS = 5;

// 全局变量
JavaVM *g_jvm = NULL;
jobject g_obj = NULL;

void *thread_fun(void *arg) {

    JNIEnv *env;
    jclass jclazz;

    // TODO
    // attach 主线程
    if (g_jvm->AttachCurrentThread(&env, NULL) != JNI_OK) {
        LOGE("attach current thread fail");
        return NULL;
    }

    if (NULL == env)return NULL;

    // 获取类实例
    jclazz = env->GetObjectClass(g_obj);

    if (NULL == jclazz) {
        LOGE("%s", "not find MainActivity class");
        g_jvm->DetachCurrentThread();
        return NULL;
    }

    // 再获取类中的方法 id
    jmethodID mid = env->GetMethodID(jclazz, "getIndexCurrentThread", "(I)V");

    if (mid == NULL) {
        LOGE("not find getIndexCurrentThread method");
        g_jvm->DetachCurrentThread();
        return NULL;
    }

    // 最后调用方法
    env->CallVoidMethod(g_obj, mid, arg);

    if (g_jvm->DetachCurrentThread() != JNI_OK) {
        LOGE("detach current thread fail");
    }

    pthread_exit(0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_demo_ndkdemo_MainActivity_multiThreadFromJni(JNIEnv *env, jobject instance) {

    int i;
    pthread_t pt[NUM_THREADS];

    // 保存全局的 JVM 对象,以便在子线程中使用
    env->GetJavaVM(&g_jvm);
    g_obj = env->NewGlobalRef(instance);

    // 创建线程
    for (i = 0; i < NUM_THREADS; i++) {
        pthread_create(&pt[i], NULL, &thread_fun, (void *) i);
    }
}

输出结果:

当前线程的索引值为: 0
当前线程的索引值为: 2
当前线程的索引值为: 4
当前线程的索引值为: 3
当前线程的索引值为: 1

注,g_jvm->AttachCurrentThread(JNIEnv** p_env, void* thr_args)g_jvm->DetachCurrentThread(); ,两函数须成对出现。

7.NIO

NIO 用于大量数据传输,NIO 是直接地址访问,绕过了 JVM 操作极大地提高了程序的运行效率。

7.1新建直接字节缓冲区

原生代码可以创建 Java 应用程序直接使用直接字节缓冲区,该过程是以提供一个原生 C 字节数组为基础,如:

unsigned char *buffer = (unsigned char *) (malloc(1024));

jobject directBuffer;
// address:缓冲区指针
// capacity:缓冲区容量
env->NewDirectByteBuffer(buffer, 1024);

7.2获取直接字节缓冲区

地址:

unsigned char *buffer;
// 直接缓冲区的地址指针,发生异常时返回NULL
buffer = (unsigned char *) (env->GetDirectBufferAddress(directBuffer));

容量:

jlong capacity;
// 缓冲区容量,发生异常时返回-1
capacity = (env->GetDirectBufferCapacity(directBuffer));

8.动态注册JNI

我们不难发现,静态注册生成的 JNI 函数名太长,文件、类名、变量或方法重构时,需要重新修改头文件或 C/C++ 内容代码(而且还是各个函数都要修改,没有一个统一的地方),动态注册 JNI 的方法就可以解决这个问题。

动态注册 JNI 的原理:通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系。步骤如下:

  • 先编写 Java 的 native 方法;
  • 编写 JNI 函数的实现(函数名可以随便命名);
  • 利用结构体 JNINativeMethod 保存Java native方法和 JNI 函数的对应关系;
  • 利用 registerNatives(JNIEnv* env) 注册类的所有本地方法;
  • 在 JNI_OnLoad 方法中调用注册方法;
  • 在 Java中 通过 System.loadLibrary 加载完 JNI 动态库之后,会自动调用 JNI_OnLoad 函数,完成动态注册;

代码实例:
java 层 native 方法定义和调用:

// 动态注册 jni 类
public class DynamicRegisterJni {
    public native String stringFromJni();
}

String strFromJni = new DynamicRegisterJni().stringFromJni();
Log.e(TAG, "动态注册返回的字符串: " +strFromJni);

c++动态注册 JNI 代码:

// 动态注册类名
static const char *dynamicClassName = "com/demo/ndkdemo/DynamicRegisterJni";

// 定义对用的 java native 方法的 c++ 函数,函数名可以随便命名
static jstring helloWorld(JNIEnv *env, jobject instance) {
    const char *hello = "hello world";
    return env->NewStringUTF(hello);
}

/**
 * 定义对应的函数映射表 数组可以定义多对映射关系
 * 参数1:java 方法名
 * 参数2:方法描述符,也就是签名
 * 参数3:c++ 定义对应的 java native 方法的函数名(这里定义的函数名为 helloWorld)
 *
 */
static JNINativeMethod jni_methods_table[]{
        {"stringFromJni", "()Ljava/lang/String;", (void *) helloWorld},
};

// 根据函数映射表映射函数
static int
registerNativeMethods(JNIEnv *env, const char *className, const JNINativeMethod *gMethods,
                      int numMethods) {
    jclass clazz;
    LOGI("registering %s natives\n", className);
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        LOGE("native register not find %s\n", className);
        return JNI_ERR;
    }

    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        LOGE("register natives failed %s \n", className);
        return JNI_ERR;
    }

    // 删除本地引用
    env->DeleteLocalRef(clazz);
    return JNI_OK;
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGI("call JNI_OnLoad");

    JNIEnv *env = NULL;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_EVERSION;
    }

    // 调用注册映射函数
    registerNativeMethods(env, dynamicClassName, jni_methods_table,
                          sizeof(jni_methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

输出结果:

动态注册返回的字符串: hello world

9.JNI字符串加解密

字符串加解密是常见的需求,那么怎么用 JNI 来加解密字符串呢,相关代码如下:

java 层 native 方法定义和调用:

public native String encryptStringFromJni(String str);

String str = "hi beauty";
String encrypt = encryptStringFromJni(str);
String decrypt = decryptStringFromJni(encrypt);
Log.e(TAG, "加密后的字符串:" + encrypt);
Log.e(TAG, "解密后的字符串:" + decrypt);

c++层:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_demo_ndkdemo_MainActivity_encryptStringFromJni(JNIEnv *env, jobject instance,
                                                        jstring str_) {
    const char *str = env->GetStringUTFChars(str_, NULL);

    // TODO
    if (str == NULL || strlen(str) == 0) {
        // encrypt string not empty
        return env->NewStringUTF("");
    }

    // 注,字符数组的长度 +1
    char newStr[strlen(str) + 1];
    int i;

    for (i = 0; i < strlen(str); ++i) {
        newStr[i] = str[i] ^ '0x01';
    }

    // 字符串是以 \0 结尾
    newStr[strlen(str)] = 0;

    env->ReleaseStringUTFChars(str_, str);

    return env->NewStringUTF(newStr);
}

输出结果:

加密后的字符串:YX�STPDEH
解密后的字符串:hi beauty

10.小结

本文抛砖引玉,讲解了 JNI 相关的知识,希望对 JNI 薄弱的童鞋有所帮助,学习来不得半点马虎, 撸一撸,才能发现问题,踩踩坑,才能积累经验。

《魔道祖师》就是讲述的两个男人之间的事,我喜欢那个吹笛的,但打不过那个弹琴的;我喜欢那个吃糖的,但我打不过那个眼瞎的。

项目地址

小编维护的超炫的动画库欢迎大家 star

你可能感兴趣的:(金戈铁马 Android NDK 实战篇:男人之间的那些事)