banner
jzman

jzman

Coding、思考、自觉。
github

IjkPlayer系列之JNI基礎及源碼目錄介紹

本篇文章是閱讀 IjkPlayer 播放器源碼的第一篇,記得在之前的工作中也編譯過 IjkPlayer,為了後續方便繼續閱讀其源碼,下文中簡單彙總下 JNI 開發的一些基礎知識,本文主要內容如下:

  1. IjkPlayer 編譯
  2. IjkPlayer 源碼目錄
  3. NDK 介紹
  4. JNI 基礎知識
  5. 總結

IjkPlayer 編譯#

IjkPlayer 的編譯之前單獨寫過一篇文章,內容還算詳細,具體參考如下:

IjkPlayer 源碼目錄#

IjkPlayer 源碼目錄介紹:

├── 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 變量即可共用,其常用的獲取方式如下:

  • 第一種:
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;
}
  • 第二種:
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 中定義本地方法如下:

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

為了方便,將目錄切換到項目的 java 目錄下,使用如下命令生成供 C/C++ 使用的頭文件:

javah -jni com.manu.ndksamples.MainActivity

如果執行報找不到類文件的異常,可以嘗試添加 -classpath 參數生成對應頭文件,上述本地方法生成的頭文件代碼如下:

/* 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 方法,參考如下:

#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 函數之間的一一對應關係,該結構體定義如下:

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

上面靜態註冊的 sum 方法也可以用動態註冊的方式,如下:

#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 源碼的閱讀。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。