NDK 系列(5):JNI 从入门到实践,万字爆肝详解!(下)

简介: NDK 系列(5):JNI 从入门到实践,万字爆肝详解!(下)

4. JNI 访问 Java 字段与方法


这一节我们来讨论如何从 Native 层访问 Java 的字段与方法。在开始访问前,JNI 首先要找到想访问的字段和方法,这就依靠字段描述符和方法描述符。


4.1 字段描述符与方法描述符


在 Java 源码中定义的字段和方法,在编译后都会按照既定的规则记录在 Class 文件中的字段表和方法表结构中。例如,一个 public String str; 字段会被拆分为字段访问标记(public)、字段简单名称(str)和字段描述符(Ljava/lang/String)。 因此,从 JNI 访问 Java 层的字段或方法时,首先就是要获取在 Class 文件中记录的简单名称和描述符。


Class 文件的一级结构:

image.png


字段表结构: 包含字段的访问标记、简单名称、字段描述符等信息。例如字段 String str 的简单名称为 str,字段描述符为 Ljava/lang/String;


image.png

方法表结构: 包含方法的访问标记、简单名称、方法描述符等信息。例如方法 void fun(); 的简单名称为 fun,方法描述符为 ()V

image.png

4.2 描述符规则


  • 字段描述符: 字段描述符其实就是描述字段的类型,JVM 对每种基础数据类型定义了固定的描述符,而引用类型则是以 L 开头的形式:


Java 类型 描述符
boolean Z
byte B
char C
short S
int I
long J
floag F
double D
void V
引用类型 以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String;
  • 方法描述符: 方法描述符其实就是描述方法的返回值类型和参数表类型,参数类型用一对圆括号括起来,按照参数声明顺序列举参数类型,返回值出现在括号后面。例如方法 void fun(); 的简单名称为 fun,方法描述符为 ()V


4.3 JNI 访问 Java 字段


本地代码访问 Java 字段的流程分为 2 步:


  • 1、通过 jclass 获取字段 ID,例如:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");
  • 2、通过字段 ID 访问字段,例如:Jstr = env->GetObjectField(thiz, Fid);


Java 字段分为静态字段和实例字段,相关方法如下:


  • GetFieldId:获取实例方法的字段 ID
  • GetStaticFieldId:获取静态方法的字段 ID
  • GetField:获取类型为 Type 的实例字段(例如 GetIntField)
  • SetField:设置类型为 Type 的实例字段(例如 SetIntField)
  • GetStaticField:获取类型为 Type 的静态字段(例如 GetStaticIntField)
  • SetStaticField:设置类型为 Type 的静态字段(例如 SetStaticIntField)

示例程序


extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, jobject thiz) {
    // 获取 jclass
    jclass clz = env->GetObjectClass(thiz);
    // 示例:修改 Java 静态变量值
    // 静态字段 ID
    jfieldID sFieldId = env->GetStaticFieldID(clz, "sName", "Ljava/lang/String;");
    // 访问静态字段
    if (sFieldId) {
        // Java 方法的返回值 String 映射为 jstring
        jstring jStr = static_cast<jstring>(env->GetStaticObjectField(clz, sFieldId));
        // 将 jstring 转换为 C 风格字符串
        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
        // 释放资源
        env->ReleaseStringUTFChars(jStr, sStr);
        // 构造 jstring
        jstring newStr = env->NewStringUTF("静态字段 - Peng");
        if (newStr) {
            // jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
            env->SetStaticObjectField(clz, sFieldId, newStr);
        }
    }
    // 示例:修改 Java 成员变量值
    // 实例字段 ID
    jfieldID mFieldId = env->GetFieldID(clz, "mName", "Ljava/lang/String;");
    // 访问实例字段
    if (mFieldId) {
        jstring jStr = static_cast<jstring>(env->GetObjectField(thiz, mFieldId));
        // 转换为 C 字符串
        const char *sStr = env->GetStringUTFChars(jStr, JNI_FALSE);
        // 释放资源
        env->ReleaseStringUTFChars(jStr, sStr);
        // 构造 jstring
        jstring newStr = env->NewStringUTF("实例字段 - Peng");
        if (newStr) {
            // jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
            env->SetObjectField(thiz, mFieldId, newStr);
        }
    }
}
复制代码


4.4 JNI 调用 Java 方法


本地代码访问 Java 方法与访问 Java 字段类似,访问流程分为 2 步:


  • 1、通过 jclass 获取「方法 ID」,例如:Mid = env->GetMethodID(jclass, "helloJava", "()V");
  • 2、通过方法 ID 调用方法,例如:env->CallVoidMethod(thiz, Mid);


Java 方法分为静态方法和实例方法,相关方法如下:


  • GetMethodId:获取实例方法 ID
  • GetStaticMethodId:获取静态方法 ID
  • CallMethod:调用返回类型为 Type 的实例方法(例如 GetVoidMethod)
  • CallStaticMethod:调用返回类型为 Type 的静态方法(例如 CallStaticVoidMethod)
  • CallNonvirtualMethod:调用返回类型为 Type 的父类方法(例如 CallNonvirtualVoidMethod)

示例程序


extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, jobject thiz) {
    // 获取 jclass
    jclass clz = env->GetObjectClass(thiz);
    // 示例:调用 Java 静态方法
    // 静态方法 ID
    jmethodID sMethodId = env->GetStaticMethodID(clz, "sHelloJava", "()V");
    if (sMethodId) {
        env->CallStaticVoidMethod(clz, sMethodId);
    }
    // 示例:调用 Java 实例方法
    // 实例方法 ID
    jmethodID mMethodId = env->GetMethodID(clz, "helloJava", "()V");
    if (mMethodId) {
        env->CallVoidMethod(thiz, mMethodId);
    }
}
复制代码


4.5 缓存 ID


访问 Java 层字段或方法时,需要先利用字段名 / 方法名和描述符进行检索,获得 jfieldID / jmethodID。这个检索过程比较耗时,优化方法是将字段 ID 和方法 ID 缓存起来,减少重复检索。


提示: 从不同线程中获取同一个字段或方法 的 ID 是相同的,缓存 ID 不会有多线程问题。


缓存字段 ID 和 方法 ID 的方法主要有 2 种:


  • 1、使用时缓存: 使用时缓存是指在首次访问字段或方法时,将字段 ID 或方法 ID 存储在静态变量中。这样将来再次调用本地方法时,就不需要重复检索 ID 了。例如:
  • 2、类初始化时缓存: 静态初始化时缓存是指在 Java 类初始化的时候,提前缓存字段 ID 和方法 ID。可以选择在 JNI_OnLoad 方法中缓存,也可以在加载 so 库后调用一个 native 方法进行缓存。


两种缓存 ID 方式的主要区别在于缓存发生的时机和时效性:


  • 1、时机不同: 使用时缓存是延迟按需缓存,只有在首次访问 Java 时才会获取 ID 并缓存,而类初始化时缓存是提前缓存;
  • 2、时效性不同: 使用时缓存的 ID 在类卸载后失效,在类卸载后不能使用,而类加载时缓存在每次加载 so 动态库时会重新更新缓存,因此缓存的 ID 是保持有效的。


5. JNI 中的对象引用管理


5.1 Java 和 C/C++ 中对象内存回收区别(重点理解)


在讨论 JNI 中的对象引用管理,我们先回顾一下 Java 和 C/C++ 在对象内存回收上的区别:


  • Java: 对象在堆 / 方法区上分配,由垃圾回收器扫描对象可达性进行回收。如果使用局部变量指向对象,在不再使用对象时可以手动显式置空,也可以等到方法返回时自动隐式置空。如果使用全局变量(static)指向对象,在不再使用对象时必须手动显式置空。
  • C/C++: 栈上分配的对象会在方法返回时自动回收,而堆上分配的对象不会随着方法返回而回收,也没有垃圾回收器管理,因此必须手动回收(free/delete)。

而 JNI 层作为 Java 层和 C/C++ 层之间的桥接层,那么它就会兼具两者的特点:对于

  • 局部 Java 对象引用: 在 JNI 层可以通过 NewObject 等函数创建 Java 对象,并且返回对象的引用,这个引用就是 Local 型的局部引用。对于局部引用,可以通过 DeleteLocalRef 函数手动显式释放(这类似于在 Java 中显式置空局部变量),也可以等到函数返回时自动释放(这类似于在 Java 中方法返回时隐式置空局部变量);
  • 全局 Java 对象引用: 由于局部引用在函数返回后一定会释放,可以通过 NewGlobalRef 函数将局部引用升级为 Global 型全局变量,这样就可以在方法使用对象(这类似于在 Java 中使用 static 变量指向对象)。在不再使用对象时必须调用 DeleteGlobalRef 函数释放全局引用(这类似于在 Java 中显式置空 static 变量)。


提示: 我们这里所说的 ”置空“ 只是将指向变量的值赋值为 null,而不是回收对象,Java 对象回收是交给垃圾回收器处理的。


5.2 JNI 中的三种引用


  • 1、局部引用: 大部分 JNI 函数会创建局部引用,局部引用只有在创建引用的本地方法返回前有效,也只在创建局部引用的线程中有效。在方法返回后,局部引用会自动释放,也可以通过 DeleteLocalRef 函数手动释放;
  • 2、全局引用: 局部引用要跨方法和跨线程必须升级为全局引用,全局引用通过 NewGlobalRef 函数创建,不再使用对象时必须通过 DeleteGlobalRef 函数释放。
  • 3、弱全局引用: 弱引用与全局引用类似,区别在于弱全局引用不会持有强引用,因此不会阻止垃圾回收器回收引用指向的对象。弱全局引用通过 NewGlobalWeakRef 函数创建,不再使用对象时必须通过 DeleteGlobalWeakRef 函数释放。

示例程序


// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
env->DeleteLocalRef(localRefClz);
// 全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
env->DeleteGlobalRef(globalRefClz);
// 弱全局引用
jclass weakRefClz = env->NewWeakGlobalRef(localRefClz);
env->DeleteGlobalWeakRef(weakRefClz);
复制代码


5.3 JNI 引用的实现原理


在 JavaVM 和 JNIEnv 中,会分别建立多个表管理引用:


  • JavaVM 内有 globals 和 weak_globals 两个表管理全局引用和弱全局引用。由于 JavaVM 是进程共享的,因此全局引用可以跨方法和跨线程共享;
  • JavaEnv 内有 locals 表管理局部引用,由于 JavaEnv 是线程独占的,因此局部引用不能跨线程。另外虚拟机在进入和退出本地方法通过 Cookie 信息记录哪些局部引用是在哪些本地方法中创建的,因此局部引用是不能跨方法的。


5.4 比较引用是否指向相同对象


可以使用 JNI 函数 IsSameObject 判断两个引用是否指向相同对象(适用于三种引用类型),返回值为 JNI_TRUE 时表示相同,返回值为 JNI_FALSE 表示不同。例如:

示例程序


jclass localRef = ...
jclass globalRef = ...
bool isSampe = env->IsSamObject(localRef, globalRef)
复制代码


另外,当引用与 NULL 比较时含义略有不同:


  • 局部引用和全局引用与 NULL 比较: 用于判断引用是否指向 NULL 对象;
  • 弱全局引用与 NULL 比较: 用于判断引用指向的对象是否被回收。


6. JNI 中的异常处理


6.1 JNI 的异常处理机制(重点理解)


JNI 中的异常机制与 Java 和 C/C++ 的处理机制都不同:


  • Java 和 C/C++: 程序使用关键字 throw 抛出异常,虚拟机会中断当前执行流程,转而去寻找匹配的 catch{} 块,或者继续向外层抛出寻找匹配 catch {} 块。
  • JNI: 程序使用 JNI 函数 ThrowNew 抛出异常,程序不会中断当前执行流程,而是返回 Java 层后,虚拟机才会抛出这个异常。


因此,在 JNI 层出现异常时,有 2 种处理选择:


  • 方法 1: 直接 return 当前方法,让 Java 层去处理这个异常(这类似于在 Java 中向方法外层抛出异常);
  • 方法 2: 通过 JNI 函数 ExceptionClear 清除这个异常,再执行异常处理程序(这类似于在 Java 中 try-catch 处理异常)。需要注意的是,当异常发生时,必须先处理-清除异常,再执行其他 JNI 函数调用。 因为当运行环境存在未处理的异常时,只能调用 2 种 JNI 函数:异常护理函数和清理资源函数。


JNI 提供了以下与异常处理相关的 JNI 函数:


  • ThrowNew: 向 Java 层抛出异常;
  • ExceptionDescribe: 打印异常描述信息;
  • ExceptionOccurred: 检查当前环境是否发生异常,如果存在异常则返回该异常对象;
  • ExceptionCheck: 检查当前环境是否发生异常,如果存在异常则返回 JNI_TRUE,否则返回 JNI_FALSE;
  • ExceptionClear: 清除当前环境的异常。

jni.h


struct JNINativeInterface {
    // 抛出异常
    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);
    // 检查异常
    jthrowable  (*ExceptionOccurred)(JNIEnv*);
    // 检查异常
    jboolean    (*ExceptionCheck)(JNIEnv*);
    // 清除异常
    void        (*ExceptionClear)(JNIEnv*);
};
复制代码

示例程序

// 示例 1:向 Java 层抛出异常
jclass exceptionClz = env->FindClass("java/lang/IllegalArgumentException");
env->ThrowNew(exceptionClz, "来自 Native 的异常");
// 示例 2:检查当前环境是否发生异常(类似于 Java try{})
jthrowable exc = env->ExceptionOccurred(env);
if(exc) {
    // 处理异常(类似于 Java 的 catch{})
}
// 示例 3:清除异常
env->ExceptionClear();
复制代码


6.2 检查是否发生异常的方式


异常处理的步骤我懂了,由于虚拟机在遇到 ThrowNew 时不会中断当前执行流程,那我怎么知道当前已经发生异常呢?有 2 种方法:


  • 方法 1: 通过函数返回值错误码,大部分 JNI 函数和库函数都会有特定的返回值来标示错误,例如 -1、NULL 等。在程序流程中可以多检查函数返回值来判断异常。
  • 方法 2: 通过 JNI 函数 ExceptionOccurredExceptionCheck 检查当前是否有异常发生。


7. JNI 与多线程


这一节我们来讨论 JNI 层中的多线程操作。


7.1 不能跨线程的引用


在 JNI 中,有 2 类引用是无法跨线程调用的,必须时刻谨记:

  • JNIEnv: JNIEnv 只在所在的线程有效,在不同线程中调用 JNI 函数时,必须使用该线程专门的 JNIEnv 指针,不能跨线程传递和使用。通过 AttachCurrentThread 函数将当前线程依附到 JavaVM 上,获得属于当前线程的 JNIEnv 指针。如果当前线程已经依附到 JavaVM,也可以直接使用 GetEnv 函数。

示例程序


JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
// 使用 JNIEnv*
vm->DetachCurrentThread();
复制代码
  • 局部引用: 局部引用只在创建的线程和方法中有效,不能跨线程使用。可以将局部引用升级为全局引用后跨线程使用。

示例程序


// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 释放全局引用(非必须)
env->DeleteLocalRef(localRefClz);
// 局部引用升级为全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 释放全局引用(必须)
env->DeleteGlobalRef(globalRefClz);
复制代码


7.2 监视器同步


在 JNI 中也会存在多个线程同时访问一个内存资源的情况,此时需要保证并发安全。在 Java 中我们会通过 synchronized 关键字来实现互斥块(背后是使用监视器字节码),在 JNI 层也提供了类似效果的 JNI 函数:


  • MonitorEnter: 进入同步块,如果另一个线程已经进入该 jobject 的监视器,则当前线程会阻塞;
  • MonitorExit: 退出同步块,如果当前线程未进入该 jobject 的监视器,则会抛出 IllegalMonitorStateException 异常。

jni.h


struct JNINativeInterface {
    jint        (*MonitorEnter)(JNIEnv*, jobject);
    jint        (*MonitorExit)(JNIEnv*, jobject);
}
复制代码

示例程序


// 进入监视器
if (env->MonitorEnter(obj) != JNI_OK) {
    // 建立监视器的资源分配不成功等
}
// 此处为同步块
if (env->ExceptionOccurred()) {
    // 必须保证有对应的 MonitorExit,否则可能出现死锁
    if (env->MonitorExit(obj) != JNI_OK) {
        ...
    };
    return;
}
// 退出监视器
if (env->MonitorExit(obj) != JNI_OK) {
    ...
};
复制代码


7.3 等待与唤醒


JNI 没有提供 Object 的 wati/notify 相关功能的函数,需要通过 JNI 调用 Java 方法的方式来实现:

示例程序


static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {
    env->CallVoidMethod(object, MID_Object_wait, timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notifyAll);
}
复制代码


7.4 创建线程的方法


在 JNI 开发中,有两种创建线程的方式:


  • 方法 1 - 通过 Java API 创建: 使用我们熟悉的 Thread#start() 可以创建线程,优点是可以方便地设置线程名称和调试;
  • 方法 2 - 通过 C/C++ API 创建: 使用 pthread_create()std::thread 也可以创建线程

示例程序


// 
void *thr_fn(void *arg) {
    printids("new thread: ");
    return NULL;
}
int main(void) {
    pthread_t ntid;
    // 第 4 个参数将传递到 thr_fn 的参数 arg 中
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("can't create thread: %s\n", strerror(err));
    }
    return 0;
}
复制代码



8. 通用 JNI 开发模板


光说不练假把式,以下给出一个简单的 JNI 开发模板,将包括上文提到的一些比较重要的知识点。程序逻辑很简单:Java 层传递一个媒体文件路径到 Native 层后,由 Native 层播放媒体并回调到 Java 层。为了程序简化,所有真实的媒体播放代码都移除了,只保留模板代码。


  • Java 层:start() 方法开始,调用 startNative() 方法进入 Native 层;
  • Native 层: 创建 MediaPlayer 对象,其中在子线程播放媒体文件,并通过预先持有的 JavaVM 指针获取子线程的 JNIEnv 对象回调到 Java 层 onStarted() 方法。

MediaPlayer.kt


// Java 层模板
class MediaPlayer {
    companion object {
        init {
            // 注意点:加载 so 库
            System.loadLibrary("hellondk")
        }
    }
    // Native 层指针
    private var nativeObj: Long? = null
    fun start(path : String) {
        // 注意点:记录 Native 层指针,后续操作才能拿到 Native 的对象
        nativeObj = startNative(path)
    }
    fun release() {
        // 注意点:使用 start() 中记录的指针调用 native 方法
        nativeObj?.let {
            releaseNative(it)
        }
        nativeObj = null
    }
    private external fun startNative(path : String): Long
    private external fun releaseNative(nativeObj: Long)
    fun onStarted() {
        // Native 层回调(来自 JNICallbackHelper#onStarted)
        ...
    }
}
复制代码

native-lib.cpp

// 注意点:记录 JavaVM 指针,用于在子线程获得 JNIEnv
JavaVM *vm = nullptr;
jint JNI_OnLoad(JavaVM *vm, void *args) {
    ::vm = vm;
    return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT jlong JNICALL
Java_com_pengxr_hellondk_MediaPlayer_startNative(JNIEnv *env, jobject thiz, jstring path) {
    // 注意点:String 转 C 风格字符串
    const char *path_ = env->GetStringUTFChars(path, nullptr);
    // 构造一个 Native 对象
    auto *helper = new JNICallbackHelper(vm, env, thiz);
    auto *player = new MediaPlayer(path_, helper);
    player->start();
    // 返回 Native 对象的指针
    return reinterpret_cast<jlong>(player);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_pengxr_hellondk_MediaPlayer_releaseNative(JNIEnv *env, jobject thiz, jlong native_obj) {
    auto * player = reinterpret_cast<MediaPlayer *>(native_obj);
    player->release();
}
复制代码

JNICallbackHelper.h

#ifndef HELLONDK_JNICALLBACKHELPER_H
#define HELLONDK_JNICALLBACKHELPER_H
#include <jni.h>
#include "util.h"
class JNICallbackHelper {
private:
    // 全局共享的 JavaVM*
    // 注意点:指针要初始化 0 值
    JavaVM *vm = 0;
    // 主线程的 JNIEnv*
    JNIEnv *env = 0;
    // Java 层的对象 MediaPlayer.kt
    jobject job;
    // Java 层的方法 MediaPlayer#onStarted()
    jmethodID jmd_prepared;
public:
    JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job);
    ~JNICallbackHelper();
    void onStarted();
};
#endif //HELLONDK_JNICALLBACKHELPER_H
复制代码

JNICallbackHelper.cpp

#include "JNICallbackHelper.h"
JNICallbackHelper::JNICallbackHelper(JavaVM *vm, JNIEnv *env, jobject job) {
    // 全局共享的 JavaVM*
    this->vm = vm;
    // 主线程的 JNIEnv*
    this->env = env;
    // C 回调 Java
    jclass mediaPlayerKTClass = env->GetObjectClass(job);
    jmd_prepared = env->GetMethodID(mediaPlayerKTClass, "onPrepared", "()V");
    // 注意点:jobject 无法跨越线程,需要转换为全局引用
    // Error:this->job = job;
    this->job = env->NewGlobalRef(job);
}
JNICallbackHelper::~JNICallbackHelper() {
    vm = nullptr;
    // 注意点:释放全局引用
    env->DeleteGlobalRef(job);
    job = nullptr;
    env = nullptr;
}
void JNICallbackHelper::onStarted() {
    // 注意点:子线程不能直接使用持有的主线程 env,需要通过 AttachCurrentThread 获取子线程的 env
    JNIEnv * env_child;
    vm->AttachCurrentThread(&env_child, nullptr);
    // 回调 Java 方法
    env_child->CallVoidMethod(job, jmd_prepared);
    vm->DetachCurrentThread();
}
复制代码

MediaPlayer.h

#ifndef HELLONDK_MEDIAPLAYER_H
#define HELLONDK_MEDIAPLAYER_H
#include <cstring>
#include <pthread.h>
#include "JNICallbackHelper.h"
class MediaPlayer {
private:
    char *path = 0;
    JNICallbackHelper *helper = 0;
    pthread_t pid_start;
public:
    MediaPlayer(const char *path, JNICallbackHelper *helper);
    ~MediaPlayer();
    void doOpenFile();
    void start();
    void release();
};
#endif //HELLONDK_MEDIAPLAYER_H
复制代码

MediaPlayer.cpp

#include "MediaPlayer.h"
MediaPlayer::MediaPlayer(const char *path, JNICallbackHelper *helper) {
    // 注意点:参数 path 指向的空间被回收会造成悬空指针,应复制一份
    // this->path = path;
    this->path = new char[strlen(path) + 1];
    strcpy(this->path, path);
    this->helper = helper;
}
MediaPlayer::~MediaPlayer() {
    if (path) {
        delete path;
    }
    if (helper) {
        delete helper;
    }
}
// 在子线程执行
void MediaPlayer::doOpenFile() {
    // 省略真实播放逻辑...
    // 媒体文件打开成功
    helper->onStarted();
}
// 在子线程执行
void *task_open(void *args) {
    // args 是 主线程 MediaPlayer 的实例的 this变量
    auto *player = static_cast<MediaPlayer *>(args);
    player->doOpenFile();
    return nullptr;
}
void MediaPlayer::start() {
    // 切换到子线程执行
    pthread_create(&pid_start, 0, task_open, this);
}
void MediaPlayer::release() {
    ...
}
复制代码



9. 总结


到这里,JNI 的知识就讲完了,你可以按照学习路线图来看。下一篇,我们开始讲 Android NDK 开发。关注我,带你建立核心竞争力,我们下次见。


目录
相关文章
|
4月前
|
Java 编译器 开发工具
Android StudioJNI开发之NDK环境的搭建以及添加JNI支持(图文解释 简单易懂)
Android StudioJNI开发之NDK环境的搭建以及添加JNI支持(图文解释 简单易懂)
34 0
|
6月前
|
Linux API C++
[笔记]c/cpp跨平台开发 知识点
[笔记]c/cpp跨平台开发 知识点
|
10月前
|
自然语言处理 Java 编译器
史上最详细的JNI入门教程HelloNative
史上最详细的JNI入门教程HelloNative
137 1
|
缓存 Java 编译器
JNI基础简介
JNI系列入门连载,开启JNI学习之旅吧。
81 0
|
编解码 Java Android开发
so库你应该知道的基础知识
Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
213 0
|
算法 Java Linux
NDK 系列(5):JNI 从入门到实践,万字爆肝详解!(上)
NDK 系列(5):JNI 从入门到实践,万字爆肝详解!
319 0
NDK 系列(5):JNI 从入门到实践,万字爆肝详解!(上)
|
存储 缓存 Java
NDK | C 语言复习笔记
NDK | C 语言复习笔记
62 0
NDK | C 语言复习笔记
|
Java 编译器 C语言
NDK | C++ 复习笔记
NDK | C++ 复习笔记
79 0
DHL
|
前端开发 算法 Java
图解多平台 AndroidStudio 技巧(三)
文章中没有奇淫技巧,都是一些在实际开发中、阅读源码的时候常用的快捷键,可能这些快捷键之前用过,但是在不同场景下有不同的用法,强烈建议收藏。
DHL
167 0
图解多平台 AndroidStudio 技巧(三)
|
Java Shell Android开发
NDK入门项目实战
  目标:利用NDK 生成 SO 库,使用 SO 库进行 JNI 调用,在 Android sdcard 创建文件并写入数据。   工具:NDK1.5 R1, android SDK1.5 R1, SDCARD, Eclipse , ADT 0.9, Eclipse Galileo for C/C++, Cygwin 1.5。
1007 0

相关实验场景

更多