IjkPlayer系列之JNI基础及源码目录介绍

本篇文章是阅读 IjkPlayer 播放器源码的第一篇，记得在之前的工作中也编译过 IjkPlayer，为了后续方便继续阅读其源码，下文中简单汇总下 JNI 开发的一些基础知识，本文主要内容如下：

IjkPlayer 编译

IjkPlayer 源码目录

NDK 介绍

JNI 基础知识

总结

IjkPlayer 编译

IjkPlayer 的编译之前单独写过一篇文章，内容还算详细，具体参考如下:

如何正确编译 ijkplayer

IjkPlayer 源码目录

IjkPlayer 源码目录介绍：

Copy
├── android // android相关目录
│ ├── compile-ijk.sh
│ ├── contrib // ffmpeg编译目录
│ │ ├── compile-ffmpeg.sh // ffmpeg编译脚本
│ │ ├── compile-libsoxr.sh // libsoxr编译脚本
│ │ ├── compile-openssl.sh // openssl编译脚本
│ │ ├── ffmpeg-arm64
│ │ ├── ffmpeg-armv5
│ │ ├── ffmpeg-armv7a
│ │ ├── ffmpeg-x86
│ │ ├── ffmpeg-x86_64
│ ├── ijk-addr2line.sh
│ ├── ijk-ndk-stack.sh
│ ├── ijkplayer // android ijkPlayer源码目录
│ │ ├── ijkplayer-arm64
│ │ ├── ijkplayer-armv5
│ │ ├── ijkplayer-armv7a
│ │ ├── ijkplayer-example // ijkPlayer使用案例
│ │ ├── ijkplayer-exo
│ │ ├── ijkplayer-java
│ │ ├── ijkplayer-x86
│ │ ├── ijkplayer-x86_64
├── compile-android-j4a.sh
├── config // ffmpeg编译脚本配置目录
│ ├── module-default.sh // ffmpeg默认配置脚本文件
│ ├── module-lite-hevc.sh // ffmpeg最小化配置添加hevc功能脚本文件
│ ├── module-lite.sh // ffmpeg最小化配置脚本文件
│ └── module.sh // ffmpeg当前编译配置脚本文件
├── doc
│ └── preflight_checklist.md
├── extra // ijkPlayer使用的开源库的下载目录
│ ├── ffmpeg // ffmpeg
│ ├── libyuv // yuv图像处理库
│ └── soundtouch // 音频处理库，主要是变速、变调等
├── ijkmedia // ijkPlayer native层的核心代码
│ ├── ijkj4a // native层和 java层回调的接口层,开源项目jni4android生成
│ ├── ijkplayer // ijkPlayer native层代码
│ ├── ijksdl // ijkPlayer音视频渲染SDL库
│ ├── ijksoundtouch // ijk封装后的soundtouch库
│ └── ijkyuv // yuv图像处理库
├── ijkprof // ijkplayer的性能调试库
├── init-android-exo.sh // 初始化exoPlayer脚本
├── init-android-j4a.sh // 初始化j4a脚本
├── init-android-libsoxr.sh // 初始化soxr脚本
├── init-android-libyuv.sh // 初始化yuv脚本
├── init-android-openssl.sh // 初始化openssl脚本
├── init-android-prof.sh // 初始化android-ndk-profile脚本
├── init-android.sh // 初始化android平台脚本，主要拉取ffmpeg、第三方库等
├── init-android-soundtouch.sh
├── init-config.sh // ffmpeg脚本文件配置脚本
├── init-ios-openssl.sh
├── init-ios.sh
├── ios // IOS相关目录

NDK 介绍

对于大部分的应用开发者可能都不会接触到 NDK，但如果涉及到硬件操作的话就不得不使用 NDK 了，使用 NDK 还有一个原因就是 C/C++ 的效率比较高，因此我们可以把一些耗时操作放在 NDK 中进行实现。

NDK 是 Native Development Kit 的简称，它是一个工具集，继承了 Android 的交叉编译环境，并提供了一套比较方便的 MakeFile，可以帮助开发者快速开发 C/C++ 的动态库，并自动的将 so 和 java 程序打包成 apk 在 Android 中运行。

JNI 基础知识

JNI 是 Java Native Interface 的缩写，中文为 Java 本地调用，从 Java 1.1 开始，JNI 标准成为 Java 平台的一部分，它允许 Java 代码和其他语言写的代码进行交互。

JavaVM 和 JNIEnv

JavaVM 表示 Java 虚拟机，定义在 jni.h 中，每个进程可以有多个 JavaJVM，但 Android 中只允许有一个，这个对应的 javaJVM 对象可以在进程中的各线程间共享，在使用的时候全局保存一个 JavaVM 变量即可共用，其常用的获取方式如下：

第一种：

Copy
static JavaVM* g_jvm;
// 第一种
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL;
// 为JavaVM指针赋值
g_jvm = vm;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
// ...
return JNI_VERSION_1_4;
}

第二种:

Copy
static JavaVM* g_jvm;
JNIEXPORT jint JNICALL Java_manu_com_iptvsamples_ndk_NDKSampleActivity_sum
(JNIEnv * env, jobject obj, jint addend1, jint addend2){
// 为JavaVM指针赋值
env->GetJavaVM(&g_jvm);
return addend1 + addend2;
}

此外还可以通过 JNI 函数JNI_CreateJavaVM 来创建 JavaVM。

JNIEnv 提供了大部分 JNI 函数，Native 函数都接收 JNIEnv 作为第一个参数，JNIEnv 用于线程本地存储，不能在线程之间共享 JNIEnv，如果一段代码没有其他方法获取它的 JNIEnv，可以使用共享 JavaVM 通过 GetEnv 来获取线程的 JNIEnv。

JNI 注册方式

注册 JNI 函数主要有两种方式，即静态注册方式和动态注册方式，典型的比如音视频开源项目 ijkPlayer 就是动态注册的方式，后续文中再继续分析。

静态注册

静态注册方式主要就是通过定义的包含 native 方法的 .java 文件，通过 javah 相关命令生成对应的 .h 头文件

在 Activity 中定义本地方法如下：

Copy
public native int sum(int addend1, int addend2);

为了方便，将目录切换到项目的 java 目录下，使用如下命令生成供 C/C++ 使用的头文件：

Copy
javah -jni com.manu.ndksamples.MainActivity

如果执行报找不到类文件的异常，可以尝试添加 -classpath 参数生成对应头文件，上述本地方法生成的头文件代码如下：

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

#ifndef _Included_com_manu_ndksamples_MainActivity
#define _Included_com_manu_ndksamples_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_manu_ndksamples_MainActivity
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_com_manu_ndksamples_MainActivity_sum
(JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

对应的文件名是包名 + 类名：com_manu_ndksamples_MainActivity.h，导入头文件，使用 C/C++ 实现 Java 中定义的 native 方法，参考如下：

Copy
#include "com_manu_ndksamples_MainActivity.h"

/*
* Class: com_manu_ndksamples_MainActivity
* Method: sum
* Signature: (II)I
*/
extern "C" JNIEXPORT jint JNICALL Java_com_manu_ndksamples_MainActivity_sum
(JNIEnv * env, jobject obj, jint addend1, jint addend2){
return addend1 + addend2;
}

动态注册

动态注册方式是通过 JNI 中的 JNINativeMethod 的结构体来保存 native 函数与 JNI 函数之间的一一对应关系，该结构体定义如下：

Copy
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;

上面静态注册的 sum 方法也可以用动态注册的方式，如下：

Copy
#include <jni.h>
#include <cassert>
#include <iostream>

using namespace std;

#define JNI_CLASS "com/manu/ndksamples/MainActivity"

static JavaVM *g_jvm;

static jint sample_sum(JNIEnv *env, jobject thiz, jint add1, jint add2) {
cout << "sample_sum" << endl;
return add1 + add2;
}

static JNINativeMethod g_methods[] = {
{"sum", "(II)I", (void *) sample_sum}
};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = nullptr;
g_jvm = vm;
if ((*vm).GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != nullptr);
jclass clazz = (*env).FindClass(JNI_CLASS);
// 注册函数对应关系
(*env).RegisterNatives(clazz, g_methods, sizeof(g_methods) / sizeof((g_methods)[0]));
return JNI_VERSION_1_4;
}

JNIEXPORT void JNI_OnUnload(JavaVM *jvm, void *reserved) {
// JNI_OnUnload
}

上述代码中函数 sample_sum 与 Java 中的 native 方法 sum 相对应，这种对应关系是通过 RegisterNatives 函数来进行注册的，其基本流程是当调用 System.loadLibrary 加载库的时候会去查找 JNI_OnLoad 这个函数，然后再该函数回调中进行注册，同样的在JNI_OnUnload中进行销毁操作。

总结

本文主要介绍了 IjkPlayer 的源码目录、IjkPlayer 的编译，以及一些必备的 JNI 相关的基础知识，下篇将正式开始 IjkPlayer 源码的阅读。