1. 程式人生 > >JNI開發基礎篇:C語言呼叫Java中的方法

JNI開發基礎篇:C語言呼叫Java中的方法

Life always has many things to bring you down.But what can really bring you down is just yourself.

這一篇來記錄如何在C中實現java方法的呼叫(最基本的原理:JAVA reflect 反射)

在JNI 中,java呼叫C的流程步驟:(個人的理解整理,如有誤,請包容也請指正)

 /**
    java  --->   C

    java--->System.loadLibrary()---->native method---->C method

     */

C呼叫Java

/**

    C ---->  Java

    java--->System.loadLibrary()---->native method ----> c method ----> java method

     */

所以,C語言呼叫java的時候,從android呼叫角度來講:

1:建立Java native method.

2:建立相應的JNI c method 

3:java 中呼叫 native method,然後native method 呼叫C method 

4:C呼叫java方法

接下來,我完成一個例項。要求是:

1:建立一個帶有整型引數的native方法,並建立C將要呼叫的java方法

2:然後呼叫C方法完成硬體中wife的關閉的操作或者啟動

3:最後C中完成硬體啟動後

4:最後呼叫java方法來輸出成功還是失敗。 

解釋:在C語言中不像java中那樣有Boolean和String型別,所以在自己建立方法的時候,也要符合C中的基本語法。(這個需要去另外學習的部分了。也是想要成為終端開發者的必經之路了。也會慢慢的靠近人工智慧的發展方向。純屬個人理解,努力探索永不止步!)

OK,Let’s beginning

1:建立一個帶有整型引數的native方法,並建立C將要呼叫的java方法

/*對於wifi的開啟或關閉的button*/
public native String WifiButton(); /*C將呼叫的java方法*/ public void returnTypeWife(int type){ Log.i("com.wedfrend.jni.JNI", "returnTypeWife: "+type); if(type == 0){ Log.i(TAG, "returnTypeWife: 開啟"); } Log.i(TAG, "returnTypeWife: 關閉"); }

2: 完成C中操作硬體功能,然後呼叫Java方法,最後輸出提示等

 /**
    C中方法宣告,這個在前面一篇說過
    *@param env   jni 提供萬能的二級指標
    *@param instance  可以理解為我們java中的上下文引數
    */
JNIEXPORT void JNICALL
Java_ltd_xiamenwelivetechnologyco_myapplication_JNI_WifiButton(JNIEnv * env, jobject instance) {

   /**
   1:現在首先完成C語言中處理硬體的方法

   由於具體操控硬體的相關程式碼複雜,而我目前並沒有完全掌握。

   所以這設定一個int 值,0 表示wifi開啟 ,1表示wifi關閉。

   (後續的進階篇會詳細的來聊聊硬體)
   */   

  int type = 0;//表示開啟wifi

/*2:C語言中呼叫Java中的方法,那麼具體的實現原理為Java的反射機制*/

/*
    2.1:獲得想要呼叫Java方法的類

    jclass      (*FindClass)(JNIEnv*, const char*);

    @param JNIEnv

    @param const char* 呼叫的具體類的路徑 這裡需要將java中的 "."改為"/",表示具體路徑下的檔案
*/

jclass jClass = (*env)->FindClass(env,"com/wedfrend/jni/JNI");

/*
   2.2:2:例項化該類

   jobject     (*AllocObject)(JNIEnv*, jclass);

   @param jclass 2.1得到的jclass
*/
jobject jObject = (*env)->AllocObject(env,jClass);

/*
  2.3:得到你將要呼叫java類中的方法

 jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); 

  @param const char* 第一個為呼叫java的方法名。
                     比如說要呼叫java中的returnTypeWife方法

  @param const char* 第二個表示該方法的函式簽名,對於這個函式簽名。


  穿插說明:函式簽名。如何獲得一個java函式的函式簽名。

  1:將自己的專案rebuild

  2:成功後,在專案中 `app/build/intermediates/classes/debug`目錄下生成相應的class檔案

  3:使用cmd命令進入相應的檔案目錄下

  4:執行命令  javap -s 全類名。如下是我的命令
    F:\"編譯專案成功之後的檔案路徑"\debug> javap -s com.wedfrend.jni.JNI

  5:會顯示這樣的結果 

      Compiled from "JNI.java"
      public class com.wedfrend.jni.JNI {
      public com.wedfrend.jni.JNI();
       descriptor: ()V   //這個便是簽名

     public native java.lang.String WifiButton();
     descriptor: ()Ljava/lang/String;



    public void returnTypeWife(int);
    descriptor: (I)I
}

 那麼我的呼叫那個方法,用那個的簽名,比如我們現在要用的是  

 public int returnTypeWife(int);
    descriptor: (I)I

那麼簽名自然是 (I)I
*/

jmethodID jMethodId = (*env)->GetMethodID(env,jClass,"returnTypeWife","(I)I");

/**
2.4:方法呼叫
void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);

相應的引數在上面已經得到,那麼最後的可變引數為我們方法中需要傳入的引數
*/
(**env).CallIntMethod(env,jObject,jMethodId,type);

//OK,It's done!

 LOGD("C called the java method !");

下面編譯執行,看看結果:

這裡寫圖片描述

點選按鈕後可以獲取相應的輸出:

這裡寫圖片描述

補充內容

1:C語言呼叫java中的static方法

以上便是C呼叫java中的方法。那麼C中對java中的靜態方法的呼叫,相比上面簡單一點:

// TODO
    //1:獲得想要呼叫Java方法的類
    // jclass      (*FindClass)(JNIEnv*, const char*);
    jclass jClass = (*env)->FindClass(env,"xx/xxx/JNI");

    //3:獲取我們想呼叫的方法 GetStaticMethodID
    //jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID jMethodId = (*env)->GetStaticMethodID(env,jClass,"AlarmClock","()V");

    // 呼叫靜態的方法 CallStaticVoidMethod
    //void        (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);
    (*env)->CallStaticVoidMethod(env,jClass,jMethodId);

只是將相應的方法改為呼叫靜態方法即可。

2:C語言處理後呼叫java方法改變UI介面

我們一直沒有使用生成方法中的一個引數:jobject instance

改變UI介面的時候,我們是需要在Activity中執行操作,那麼相應的方法,是需要放在具體的Activity中。

所以在需要修改的UI的Activity中,需要定義native方法,並定義C要掉用的方法。

之後在C中進行呼叫:

 //1:獲得想要呼叫Java方法的類
    // jclass      (*FindClass)(JNIEnv*, const char*);
    jclass jClass = (*env)->FindClass(env,"com/wedfrend/jni/MainActivity");

    //3:獲取我們想呼叫的方法
    //jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID jMethodId = (*env)->GetMethodID(env,jClass,"setUi","()V");

    //4: 方法呼叫
    /*
    這裡的jobject我們直接使用方法中的帶有引數  instance 便可
    */
    // jboolean    (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    (**env).CallVoidMethod(env,instance,jMethodId);

問題總結:

Q1:在普通呼叫的時候,是否也可以使用方法中的自帶引數 jobject,從而省略掉在C中例項化類。

A:可以這樣使用。
Q2:需要通過C語言呼叫java方法的時候,需要改變UI狀態,我的呼叫類寫在Activity中,但是native方法與其他的類寫在同一個檔案。

A:目前我自記得試驗是不可行的,如果有人成功,請指教。

這裡寫圖片描述

Q3:在C中獲取MethodId的時候,對於方法簽名,rebuild中是debug,那麼在正式簽名的時候,兩者之間的簽名結果是一樣的嗎?


A:1:在build.gradle中,我們設定一個release版本的keystore.android。並配置release版本,如下:

這裡寫圖片描述

2:確認是否debug和release中的簽名祕鑰不同,執行如圖所示的步驟

這裡寫圖片描述

3:檢視你的祕鑰,如圖:

這裡寫圖片描述

4:所以目前的debug與release的keystore不同,MD5加密也不同,所以現在執行如下:

這裡寫圖片描述

此時 專案中會生成兩個編譯版本,一個debug,一個release,如圖:

這裡寫圖片描述

然後我們按照檢視簽名的方式,在相應目錄下執行`javap -s 全類名`

如圖為Debug下的JNI.class

這裡寫圖片描述

下圖為Release下的JNI.class

這裡寫圖片描述

所以可以看到,對於編譯版本的keystore改變的情況下,對class中檔案的方法簽名沒有影響。

以上的問題也是我在實踐中跌跌撞撞,整理和實踐,希望對大家有用。目前的基礎入門篇告一段落。

在後面的延伸階段中,會直接結合複雜的邏輯進行使用。由於工作的原因,工作處於FrameWork層,所以深入在後續的學習中持續更新。敬請期待