JNI入門チュートリアル:最小環境HelloWorld実戦


JNIはAndroidアプリケーション開発ではあまり扱われない技術ですが、Framework層では広く使われています.Androidアプリケーション開発者として、JNIの知識を学ぶことは、システム全体の原理を理解するのに役立ちます.
JNIを学ぶには多くの方法があります.
  • Frameworkソースを直接読むことができます.Frameworkコードを構成するコンパイル環境が複雑で、コンパイル後に直接テストを実行できないため、Rootシステムが必要
  • 次にNDKをダウンロードしてインストールし、Android Studioで直接プロジェクトを開発することもできます.この方式は環境構成の作業も多く,操作が面倒である.

  • そこでこの記事で検討したテーマは,最小環境下でのJNIの学習と検証である.JNIはJava自体が持つ能力なので、最小環境はJavaが実行する環境であり、Android開発ツールやIDEは必要ありません.デバッグとコンパイルを容易にするために、ここではUbuntuシステムを使用しています.インストールする必要がある開発ツールは次のとおりです.
  • openjdk:ここではopenjdk 11バージョンを採用
  • gcc:通過apt-get install build-essential取付
  • いずれかのテキストエディタ
  • JNI Hello World
    このJNIの例では、主にこれらのことをしています.
  • JavaでJNI関数を呼び出す
  • JNIでJavaの変数を印刷
  • JNIでJava中変数を修正
  • JNIでJavaメソッドを呼び出す
  • Javaコードの作成
    Javaレイヤのコードは簡単で、nativeコードの読み書きのためにString変数を宣言します.変数を印刷する方法も提供します
    public class JniStudy {
    
        private String msg = "Hello World from Java";
        
        private native void nativeChangeMsg();
        public native void nativeCallPrintMsg();
    
        public void getNativeMsg() {
            nativeChangeMsg();
        }
        
        public void printMsg() {
            System.out.println(this.msg);
        }
    
        public static void main(String[] args) {
            JniStudy test = new JniStudy();
            test.getNativeMsg();  //   native    msg  
            test.printMsg();
            System.out.println("####################");
            test.nativeCallPrintMsg();  //   native  ,native     printMsg  
        }
    }
    
    native識別の2つの方法は、いずれもJNIの方法であり、後でコマンドにより生成する必要がある.hヘッダファイル.その他のコードはすべてベースのJavaコードです.ファイルを保存したら、まずjavacコマンドで直接コンパイルし、コンパイルに成功するはずです.
    javac JniStudy.java
    

    ヘッダファイルの生成
    次にJavaコンパイルツールを用いてJNIモジュールの.hヘッダファイルを生成する必要がある.旧バージョンJavaでは、使用する必要がありますjavah、Java 11ではこのコマンドは削除されていますので、そのまま使用しますjavac
    javac JniStudy.java -h .
    
    -hパラメータはヘッダファイルを生成するために使用され、.現在のパスの下に生成されることを表す.生成されたヘッダファイルは次のとおりです.
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include 
    /* Header for class JniStudy */
    
    #ifndef _Included_JniStudy
    #define _Included_JniStudy
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     JniStudy
     * Method:    nativeChangeMsg
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_JniStudy_nativeChangeMsg
      (JNIEnv *, jobject);
    
    /*
     * Class:     JniStudy
     * Method:    nativeCallPrintMsg
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_JniStudy_nativeCallPrintMsg
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    Cコードの作成
    そして同じディレクトリの下にCソースファイルを作成するjniStudy.cnativeレイヤロジックを記述する
    #include 
    #include "JniStudy.h"  //          ,        
    
    void Java_JniStudy_nativeChangeMsg(JNIEnv *env, jobject obj) {
        puts("JNI : Java_JniStudy_nativeChangeMsg");
        //    Java Class
        jclass clazz = (*env)->FindClass(env, "JniStudy");
        if (clazz == NULL) {
            return;
        }
        
        //    Java Class      fieldID
        jfieldID fieldId = (*env) -> GetFieldID(env, clazz, "msg", "Ljava/lang/String;");
        if (fieldId == NULL) {
            return;
        }
        
        //    fieldID     Java        
        jstring msg_org = (*env) -> GetObjectField(env, obj, fieldId);
        if (msg_org == NULL) {
            return;
        }
    
        //    GetStringUTFChars   C  char   
        const char * msg_char = (*env)->GetStringUTFChars(env, msg_org, NULL);
        puts("Print Java String in JNI:");
        puts(msg_char);
        
        //    NewStringUTF  JNI    jstring
        jstring msg = (*env) -> NewStringUTF(env, "Hello World from JNI");
        //    SetObjectField   ,  jstring     Java        (   Java  msg)
        (*env)->SetObjectField(env, obj, fieldId, msg);
    }
    
    void Java_JniStudy_nativeCallPrintMsg(JNIEnv *env, jobject obj) {
        puts("JNI : Java_JniStudy_nativeCallPrintMsg");
        //    Java Class
        jclass clazz = (*env)->FindClass(env, "JniStudy");
        if (clazz == NULL) {
            return;
        }
        
        //    Java Class      methodID
        jmethodID printMethodId = (*env) -> GetMethodID(env, clazz, "printMsg", "()V");
        if (printMethodId == NULL) {
            return;
        }
        
        puts("JNI CallVoidMethod: printMsg");
        //    CallVoidMethod    Java    void   
        (*env)->CallVoidMethod(env, obj, printMethodId);
    }
    
    Java_JniStudy_nativeChangeMsgJavaオブジェクトのうちのmsgを読み込み、印刷する.そしてJNIインタフェースでmsgの内容を変更し、JavaレイヤでprintMsg新しい内容をプリントアウトします.Java_JniStudy_nativeCallPrintMsgJNIのインタフェースを介してJavaレイヤを直接呼び出すprintMsgメソッド.詳細な手順は、コードコメントを参照してください.
    Cコードのコンパイル
    nativeレイヤロジックを作成すれば、Cコードをコンパイルできます.コンパイルには2つのヘッダファイルが必要jni.h,jni_md.h.ヘッダファイルJniStudy.hでは、自動生成コードが参照されていることがわかるjni.hjni_md.hjni.hで参照されている.私たちのコードパスの下にはこの2つのファイルがないので、Ubuntuでは2つのヘッダファイルがそれぞれ/usr/lib/jvm/java-11-openjdk-amd64/include/ /usr/lib/jvm/java-11-openjdk-amd64/include/linux/
    この2つのパスをgccコンパイルパラメータに追加する必要があります.また、ライブラリファイルをコンパイルしていますのでmain関数は含まれていませんので、そのまま使用しますgcc -oエラーが発生します
    /usr/lib/gcc/x86_64-linux-gnu/7/…/…/…/x86_64-linux-gnu/scrt 1.o:関数'start'中:(.text+0 x 20):mainに対して定義されていない参照collect 2:error:ld returned 1 exit status
    追加する必要がある-sharedパラメータの最後の完全なコンパイルコマンドは以下の通りです.
    gcc -shared -o libjnistudy JniStudy.c \
     -I /usr/lib/jvm/java-11-openjdk-amd64/include/ \
     -I /usr/lib/jvm/java-11-openjdk-amd64/include/linux
    

    コンパイルが完了すると、同じディレクトリの下にlibjnistudyというライブラリファイルが生成されます.
    JNI Libにロード
    Javaコードを再度変更し、作成したライブラリファイルをロードします.デフォルトの環境ではjavaのjava.library.path現在のパスがないため、ここではSystem.loadメソッドを使用して、ライブラリファイルへの絶対パス
    public class JniStudy {
        ......
        static {
            System.load("/home/myname/spc-work/jni-test/libjnistudy");
        }
        ......
    }
    

    変更後再使用javacコマンドコンパイル生成.classファイル
    テスト
    Javaコマンドで直接実行:
    java JniStudy
    

    出力が見える
    JNI : Java_JniStudy_nativeChangeMsg Print Java String in JNI: Hello World from Java Hello World from JNI #################### JNI : Java_JniStudy_nativeCallPrintMsg JNI CallVoidMethod: printMsg Hello World from JNI
    Java->Native,Native->Javaの2つのリンクが通じていることを証明します.
    いくつかの問題
    以下は私がデバッグで出会ったいくつかの問題です.上に述べたものもありますが、ここでもう一度まとめます.
    1.ヒントjavahコマンドが見つからない
    これはJDKバージョンと関係があるかもしれません.旧バージョンJDKではjavahコマンドを使用できます.新版(JDK 10以上かも)javacコマンドにはヘッダファイル生成機能が統合されている.
    2.gccコンパイルエラー
    /usr/lib/gcc/x86_64-linux-gnu/7/…/…/…/x86_64-linux-gnu/scrt 1.o:関数'start'中:(.text+0 x 20):mainに対して定義されていない参照collect 2:error:ld returned 1 exit status
    JNIはライブラリファイルをロードしているため、mainメソッドがなく、コンパイル時に追加する必要がある-sharedパラメータ
    gcc -shared xxx.c -o xxx
    

    3.時報エラーFatel Errorを実行し、クラッシュ終了
    エラーメッセージ
    A fatal error has been detected by the Java Runtime Environment
    nativeレイヤメソッドSetXXXField,GetXXXFieldなどのパラメータを空にすることはできません.コード記述が間違っていると、入力パラメータが空になるとクラッシュします.情報をデバッグまたは印刷することで、パラメータのエラー位置を決定できます.
    4.nativeレイヤメソッドドキュメントはどこですか?
    native層の法例としてはFindClassGetFieldIDNewStringUTFなどがありますが、いずれも公式文書があります:JNI公式完全文書:Java Native Interface Specification Contents JNIメソッドリストおよび文書:Chapter 4:JNI Functions
    5.nativeのJavaタイプ署名の決定方法
    公式サイトドキュメント:Chapter 3:JNI Type-and Data Structures
    より簡単な方法はjavapコマンドによる表示例えばJniStudy.classファイル通過javap -s表示可能メソッドの署名
    Compiled from "JniStudy.java"
    public class JniStudy {
      public JniStudy();
        descriptor: ()V
    
      public native void nativeCallPrintMsg();
        descriptor: ()V
    
      public void getNativeMsg();
        descriptor: ()V
    
      public void printMsg();
        descriptor: ()V
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
    
      static {};
        descriptor: ()V
    }
    
    javap -vクラスファイルのコンパイル後のすべての情報を得ることもできますが、この内容は比較的多いです
    ......
      Compiled from "JniStudy.java"
    public class JniStudy
      minor version: 0
      major version: 55
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #7                          // JniStudy
      super_class: #15                        // java/lang/Object
      interfaces: 0, fields: 1, methods: 7, attributes: 1
    Constant pool:
       #1 = Methodref          #15.#31        // java/lang/Object."":()V
       #2 = String             #32            // Hello World from Java
       #3 = Fieldref           #7.#33         // JniStudy.msg:Ljava/lang/String;
       #4 = Methodref          #7.#34         // JniStudy.nativeChangeMsg:()V
       #5 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
       #6 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #7 = Class              #39            // JniStudy
       #8 = Methodref          #7.#31         // JniStudy."":()V
       #9 = Methodref          #7.#40         // JniStudy.getNativeMsg:()V
      #10 = Methodref          #7.#41         // JniStudy.printMsg:()V
      #11 = String             #42            // ####################
      #12 = Methodref          #7.#43         // JniStudy.nativeCallPrintMsg:()V
      #13 = String             #44            // /home/myname/spc-work/jni-test/libjnistudy
      #14 = Methodref          #35.#45        // java/lang/System.load:(Ljava/lang/String;)V
      #15 = Class              #46            // java/lang/Object
      
    ......
    

    の最後の部分
    この小さなDemoによって、JNIの最も基本的な流れが開通しました.プロジェクト学習の過程で、この方法を使用して迅速に検証し、効率を高め、複雑なエンジニアリングや環境構成の問題を回避することができます.もちろんJNIについては、C++言語、Android、Linuxシステムの下位APIの使用など、多くの知識があり、FrameworkソースコードとNDKを組み合わせて学ぶ必要があります.