本篇文章は IjkPlayer プレーヤーのソースコードを読むための第一篇です。以前の作業で IjkPlayer をコンパイルしたことを思い出し、今後のソースコードの読みやすさのために、以下に JNI 開発に関する基本知識を簡単にまとめます。本文の主な内容は以下の通りです:
- IjkPlayer コンパイル
- IjkPlayer ソースコードディレクトリ
- NDK 紹介
- JNI 基礎知識
- まとめ
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 ソースコードの読み始めます。