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 ネイティブ層のコアコード
│   ├── ijkj4a                  // ネイティブ層と Java 層のコールバックインターフェース層、オープンソースプロジェクト jni4android 生成
│   ├── ijkplayer               // ijkPlayer ネイティブ層コード
│   ├── 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 のクロスコンパイル環境を継承したツールセットであり、開発者が C/C++ の動的ライブラリを迅速に開発できる便利な MakeFile を提供し、so と Java プログラムを自動的に apk にパッケージ化して Android で実行できるようにします。

JNI 基礎知識#

JNI は Java Native Interface の略で、日本語では Java 本地呼び出しと呼ばれます。Java 1.1 から JNI 標準は Java プラットフォームの一部となり、Java コードと他の言語で書かれたコードの相互作用を可能にします。

JavaVM と JNIEnv#

JavaVM は Java 仮想マシンを表し、jni.h に定義されています。各プロセスには複数の JavaJVM が存在できますが、Android では 1 つのみ許可されています。この 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 関数を提供し、ネイティブ関数は常に JNIEnv を最初の引数として受け取ります。JNIEnv はスレッドローカルストレージに使用され、スレッド間で共有することはできません。コードの一部が他の方法で JNIEnv を取得できない場合は、共有 JavaVM を使用して GetEnv でスレッドの JNIEnv を取得できます。

JNI 登録方式#

JNI 関数の登録には主に静的登録方式と動的登録方式の 2 つがあります。典型的な例として音声動画オープンソースプロジェクト ijkPlayer は動的登録方式です。後続の文でさらに分析します。

  • 静的登録

静的登録方式は、ネイティブメソッドを含む .java ファイルを定義し、javah 関連コマンドを使用して対応する .h ヘッダーファイルを生成することです。

Activity 内でネイティブメソッドを次のように定義します:

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

便利のために、ディレクトリをプロジェクトの java ディレクトリに切り替え、次のコマンドを使用して C/C++ 用のヘッダーファイルを生成します:

javah -jni com.manu.ndksamples.MainActivity

クラスファイルが見つからないという例外が発生した場合は、-classpath パラメータを追加して対応するヘッダーファイルを生成することを試みてください。上記のネイティブメソッドで生成されたヘッダーファイルのコードは以下の通りです:

/* このファイルは編集しないでください - 機械生成されたものです */
#include <jni.h>
/* manu_com_iptvsamples_ndk_NDKSampleActivity クラスのヘッダー */

#ifndef _Included_com_manu_ndksamples_MainActivity
#define _Included_com_manu_ndksamples_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
 * クラス:     com_manu_ndksamples_MainActivity
 * メソッド:    sum
 * シグネチャ: (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 で定義されたネイティブメソッドを実装します。参考は以下の通りです:

#include "com_manu_ndksamples_MainActivity.h"

/*
 * クラス:     com_manu_ndksamples_MainActivity
 * メソッド:    sum
 * シグネチャ: (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 構造体を使用してネイティブ関数と 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 のネイティブメソッド sum に対応しており、この対応関係は RegisterNatives 関数を使用して登録されます。基本的な流れは、System.loadLibrary がライブラリをロードする際に JNI_OnLoad という関数を探し、その関数のコールバック内で登録を行います。同様に、JNI_OnUnload で破棄操作を行います。

まとめ#

本文では IjkPlayer のソースコードディレクトリ、IjkPlayer のコンパイル、およびいくつかの必須の JNI 関連の基礎知識について紹介しました。次回は正式に IjkPlayer ソースコードの読み始めます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。