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 プログラムを自動的にパッケージ化して 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 関数を提供し、Native 関数はすべて JNIEnv を最初のパラメータとして受け取ります。JNIEnv はスレッドローカルストレージに使用され、スレッド間で共有することはできません。コードの一部が他の方法で JNIEnv を取得できない場合は、共有 JavaVM を使用して GetEnv を介してスレッドの JNIEnv を取得できます。

JNI 登録方式#

JNI 関数の登録方法には主に静的登録方式と動的登録方式の 2 種類があります。典型的な例として音声動画オープンソースプロジェクト 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 で定義されたネイティブメソッドを実装します。参考は以下の通りです:

#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 構造体を使用してネイティブ関数と 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 ソースコードの読み始めます。

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