Android與JNI(二)


Android與JNI(二)

 

軟件版本:
  ubuntu10.04
  java version "1.6.0_30-ea"
  eclipse
  android-ndk-r5b

目錄:

  1. 簡介
  2. JNI 組件的入口函數
  3. 使用 registerNativeMethods 方法
  4. 測試
  5. JNI 幫助方法
  6. 參考資料

1. 簡介

  Android與JNI(一)已經簡單介紹了如何在 android  環境下使用 JNI 了。但是遵循 JNI 開發的基本步驟似乎有點死板,而且得到的本地函數名太丑了。所以非常有必要在這里介紹另外一種實現方法。

2. JNI 組件的入口函數

  前一篇文章說到 static {System.loadLibrary("HelloJNI");} 會在第一次使用該類的時候加載動態庫 libHelloJNI.so 。當 Android 的 VM 執行到 System.loadLibrary() 這個函數時,首先會執行 JNI_OnLoad() 這個函數。與此對應的是卸載時調用 JNI_OnUnLoad() 。

  首先調用 c 組件中的 JNI_OnLoad()  的意圖包括:

  (1) 告訴 VM 此 c 組件使用那一個 JNI 版本。如果動態庫中沒有提供 JNI_OnLoad(),VM 默認該動態庫使用最老的 JNI 1.1 版本。由於新版的 JNI 做了許多擴充,如果需要使用 JNI 的新版功能,例如 JNI 1.4的 java.nio.ByteBuffer,就必須藉由 JNI_OnLoad() 函數來告知 VM。
  (2) 另外一個作用就是初始化,例如預先申請資源等。

  現在我們先現實一個非常簡單的 JNI_OnLoad(),看是不是在真的有調用到它。在 HelloJNI.c 中加入代碼:

1 jint JNI_OnLoad(JavaVM* vm, void *reserved)
2 {
3     LOGD("%s called.\n", __FUNCTION__);
4 
5     return JNI_VERSION_1_4;  // 注意這里要返回 JNI 的版本,否則會出錯喔。
6 }

  編譯:

$ndk-build 
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
/home/eddy/workspace/HelloJNI/obj/local/armeabi/objs/HelloJNI/HelloJNI.o: In function `JNI_OnLoad':
/home/eddy/workspace/HelloJNI/jni/HelloJNI.c:35: undefined reference to `LOGD'
collect2: ld returned 1 exit status
make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/libHelloJNI.so] Error 1

  加入頭文件 #include <utils/Log.h> 之后再編譯:

$ndk-build 
Compile thumb  : HelloJNI <= HelloJNI.c
/home/eddy/workspace/HelloJNI/jni/HelloJNI.c:20:23: error: utils/Log.h: No such file or directory
make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/objs/HelloJNI/HelloJNI.o] Error 1

  提示找不到指定的頭文件。由於我們沒有把這個 jni 放到整個 android 的源碼中編譯,所以遇到這個錯誤是正常的,解決的方法是在 Android.mk 中加入源碼中頭文件的路徑。

LOCAL_CFLAGS += -Iyour_path/android4.0/include/frameworks/base/include
LOCAL_CFLAGS += -Iyour_path/android4.0/include/system/core/include
LOCAL_CFLAGS += -Iyour_path/android4.0/include/frameworks/base/opengl/include
LOCAL_CFLAGS += -Iyour_path/android4.0/include/system/core/include

  再編譯:

$ndk-build 
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
/home/eddy/workspace/HelloJNI/obj/local/armeabi/objs/HelloJNI/HelloJNI.o: In function `JNI_OnLoad':
/home/eddy/workspace/HelloJNI/jni/HelloJNI.c:36: undefined reference to `__android_log_print'
collect2: ld returned 1 exit status
make: *** [/home/eddy/workspace/HelloJNI/obj/local/armeabi/libHelloJNI.so] Error 1

  好吧,又出錯了。但已經不是編譯出錯了,是鏈接出錯,這個很簡單,只要鏈接 liblog.so 這個庫就可以了。在 Android.mk 中添加:

LOCAL_LDLIBS += -lc -lm -llog

  再編譯:

$ndk-build 
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
Install        : libHelloJNI.so => libs/armeabi/libHelloJNI.so

  OK,大功告成。如無意外,運行后你會在 logcat 中找到這樣的打印:

/dalvikvm( 1956): Trying to load lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x41345548
D/dalvikvm( 1956): Added shared lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x41345548
D/        ( 1956): JNI_OnLoad called.

  這說明了在加載 JNI 的動態庫時,確實調用了 JNI_OnLoad() 。調用了這個庫又有什么用呢?下面為你揭曉。

3. 使用 registerNativeMethods 方法

  說了這么多,終於來重點了。先把修改后的 HelloJNI.c 列出來,然后再慢慢分析。

  1 /*
  2  * Copyright (C) 2009 The Android Open Source Project
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7  *
  8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS,
 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15  *
 16  */
 17 #include <string.h>
 18 #include <jni.h>
 19 #include "com_example_hellojni_HelloJNI.h"
 20 #include <utils/Log.h>
 21 
 22 #ifdef __cplusplus
 23 extern "C" {
 24 #endif
 25 
 26 static const char* className = "com/example/hellojni/HelloJNI";
 27 
 28 /* This is a trivial JNI example where we use a native method
 29  * to return a new VM String. See the corresponding Java source
 30  * file located at:
 31  *
 32  *   apps/samples/hello-jni/project/src/com/example/HelloJni/HelloJni.java
 33  */
 34 //jstring Java_com_example_hellojni_HelloJNI_stringFromJNI(JNIEnv *env, jobject this)
 35 //{
 36 //    return (*env)->NewStringUTF(env, "Hello from JNI !");
 37 //    //return "Hello from Jni !";
 38 //}
 39 jstring stringFromJNI(JNIEnv* env, jobject this)
 40 {
 41     return (*env)->NewStringUTF(env, "Hello form JNI!");
 42 }
 43 
 44 static JNINativeMethod gMethods[] = {
 45     { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
 46 };
 47 
 48 // This function only registers the native methods, and is called from JNI_OnLoad
 49 int register_location_methods(JNIEnv *env)
 50 {
 51     jclass clazz;
 52 
 53     /* look up the class */
 54     clazz = (*env)->FindClass(env, className );
 55     //clazz = env->FindClass(env, className);
 56     if (clazz == NULL) {
 57         LOGE("Can't find class %s\n", className);
 58         return -1;
 59     }
 60 
 61     LOGD("register native methods");
 62 
 63     /* register all the methods */
 64     if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
 65     //if (env->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
 66     {
 67         LOGE("Failed registering methods for %s\n", className);
 68         return -1;
 69     }
 70 
 71     /* fill out the rest of the ID cache */
 72     return 0;
 73 }
 74 
 75 jint JNI_OnLoad(JavaVM* vm, void *reserved)
 76 {
 77     JNIEnv* env = NULL;
 78     jint result = -1;
 79 
 80     LOGD("%s: +", __FUNCTION__);
 81 
 82     // for c
 83     if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
 84     // for c++
 85     //if( vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
 86         LOGE("ERROR: GetEnv failed.\n");
 87         return result;
 88     }
 89 
 90     if( register_location_methods(env) < 0 )
 91     {
 92         LOGE("ERROR: register location methods failed.\n");
 93         return result;
 94     }
 95 
 96     return JNI_VERSION_1_4;
 97 }
 98 
 99 
100 
101 void JNI_OnUnload(JavaVM* vm, void *reserved)
102 {
103     return;
104 }
105 
106 #ifdef __cplusplus
107 }
108 #endif

  先說一說這段代碼實現了什么,他與 [1] 的結果完全一樣,實現了 JAVA 中聲明的 stringFromJNI 本地方法,返回一個字符串。至於為什么不需要以Java_com_example_hellojni_HelloJNI_stringFromJNI 命名,就要慢慢分析了。

  還是從 JNI_OnLoad() 這個函數開始說起。該函數的動作包括:獲取 JNI 環境對象,登記本地方法,最后返回 JNI 版本。值得引起注意的是第83和85行,在 c 環境下編譯,使用第83行,c++ 環境下,使用第85行,否則會編譯出錯。

  登記本地方法,作用是將 c/c++ 的函數映射到 JAVA 中,而在這里面起到關鍵作用的是結構體 JNINativeMethod 。他定義在 jni.h 中。

1 typedef struct {   
2    const char* name;     /* java 中聲明的本地方法名稱 */
3    const char* signature;  /* 描述了函數的參數和返回值 */
4    void*       fnPtr;    /* c/c++的函數指針 */
5 } JNINativeMethod; 

  聲明實例就是第44到46行。

1 static JNINativeMethod gMethods[] = {
2     { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
3 };

  參數分析:

  "stringFromJNI":Java 中聲明的本地方法名;
  (void *)stringFromJNI:映射對象,本地 c/c++ 函數,名字可以與 Java 中聲明的本地方法名不一致。
  "()Ljava/lang/String;":這個應該是最難理解的,也就是結構體中的 signature 。他描述了本地方法的參數和返回值。例如
  "()V"
  "(II)V"
  "(Ljava/lang/String;Ljava/lang/String;)V"
  實際上這些字符是與函數的參數類型一一對應的。
  "()" 中的字符表示參數,后面的則代表返回值。例如"()V" 就表示void Func();
  "(II)V" 表示 void Func(int, int);
  具體的每一個字符的對應關系如下
  字符   Java類型     C類型
  V      void         void
  Z      jboolean     boolean
  I       jint         int
  J       jlong        long
  D      jdouble       double
  F      jfloat            float
  B      jbyte            byte
  C      jchar           char
  S      jshort          short
  數組則以"["開始,用兩個字符表示
  [I     jintArray       int[]
  [F     jfloatArray     float[]
  [B     jbyteArray     byte[]
  [C    jcharArray      char[]
  [S    jshortArray      short[]
  [D    jdoubleArray    double[]
  [J     jlongArray      long[]
  [Z    jbooleanArray    boolean[]
  上面的都是基本類型,如果參數是 Java 類,則以"L"開頭,以";"結尾,中間是用"/"隔開包及類名,而其對應的 C 函數的參數則為 jobject,一個例外是 String 類,它對應的 c 類型為 jstring 。
  Ljava/lang/String;     String     jstring

  Ljava/net/Socket;      Socket    jobject
  如果 JAVA 函數位於一個嵌入類(也被稱為內部類),則用$作為類名間的分隔符,例如:"Landroid/os /FileUtils$FileStatus;"。

  最終是通過第64行代碼登記所有記錄在 JNINativeMethod 結構體中的 JNI 本地方法。

  使用 registerNativeMethods 方法不僅僅是為了改變那丑陋的長方法名,最重要的是可以提高效率,因為當 Java 類透過 VM 呼叫到本地函數時,通常是依靠 VM 去動態尋找動態庫中的本地函數(因此它們才需要特定規則的命名格式),如果某方法需要連續呼叫很多次,則每次都要尋找一遍,所以使用 RegisterNatives 將本地函數向 VM 進行登記,可以讓其更有效率的找到函數。

  registerNativeMethods 方法的另一個重要用途是,運行時動態調整本地函數與 Java 函數值之間的映射關系,只需要多次調用 registerNativeMethods 方法,並傳入不同的映射表參數即可。

4. 測試

  為了對 registerNativeMethods有更好的理解,我在 Java 中再添加一個本地方法。

 1 package com.example.hellojni;
 2 
 3 import android.os.Bundle;
 4 import android.app.Activity;
 5 import android.util.Log;
 6 import android.view.Menu;
 7 import android.view.MenuItem;
 8 import android.widget.TextView;
 9 import android.support.v4.app.NavUtils;
10 
11 public class HelloJNI extends Activity {
12 
13     @Override
14     public void onCreate(Bundle savedInstanceState) {
15         super.onCreate(savedInstanceState);
16         setContentView(R.layout.hello_jni);
17         getActionBar().setDisplayHomeAsUpEnabled(true);
18         
19         /* Create a TextView and set its content.
20          * the text is retrieved by calling a native
21          * function.
22          */
23         TextView  tv = new TextView(this);
24         tv.setText( stringFromJNI() );
25         setContentView(tv);
26         
27         Log.d("JNI", "max = " + max(10, 100));
28     }
29 
30     @Override
31     public boolean onCreateOptionsMenu(Menu menu) {
32         getMenuInflater().inflate(R.menu.hello_jni, menu);
33         return true;
34     }
35 
36     
37     @Override
38     public boolean onOptionsItemSelected(MenuItem item) {
39         switch (item.getItemId()) {
40             case android.R.id.home:
41                 NavUtils.navigateUpFromSameTask(this);
42                 return true;
43         }
44         return super.onOptionsItemSelected(item);
45     }
46 
47     /* A native method that is implemented by the
48      * 'HelloJNI' native library, which is packaged
49      * with this application.
50      */
51     public native String  stringFromJNI();
52 
53     /* This is another native method declaration that is *not*
54      * implemented by 'HelloJNI'. This is simply to show that
55      * you can declare as many native methods in your Java code
56      * as you want, their implementation is searched in the
57      * currently loaded native libraries only the first time
58      * you call them.
59      *
60      * Trying to call this function will result in a
61      * java.lang.UnsatisfiedLinkError exception !
62      */
63     public native String  unimplementedStringFromJNI();
64     
65     public native int max(int a,int b);
66 
67     /* this is used to load the 'HelloJNI' library on application
68      * startup. The library has already been unpacked into
69      * /data/data/com.example.HelloJni/lib/libHelloJNI.so at
70      * installation time by the package manager.
71      */
72     static {
73         System.loadLibrary("HelloJNI");
74     }
75 }

  在 HellocJNI.c 中添加 max 的實現方法。

1 int native_max(JNIEnv* env, jobject this, int a, int b)
2 {
3     return  (a > b ? a:b);
4 }
5 
6 static JNINativeMethod gMethods[] = {
7     { "stringFromJNI", "()Ljava/lang/String;", (void *)stringFromJNI },
8     { "max", "(II)I", (void *)native_max },
9 };

  用 ndk 編譯生成動態庫。

$ndk-build 
Compile thumb  : HelloJNI <= HelloJNI.c
SharedLibrary  : libHelloJNI.so
Install        : libHelloJNI.so => libs/armeabi/libHelloJNI.so

  在模擬器上 run as ---> Android Application 。可以看到打印:

D/dalvikvm( 2174): Trying to load lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x413488a8
D/dalvikvm( 2174): Added shared lib /data/data/com.example.hellojni/lib/libHelloJNI.so 0x413488a8
D/        ( 2174): JNI_OnLoad: +
D/        ( 2174): register native methods
D/JNI     ( 2174): max = 100

  證明 max 調用成功。

  通過 registerNativeMethods 這種方法,我們可以看到操作的過程中,不需要再使用 javah -jni 生成 jni 頭文件。c/c++ 的函數名也可以自由取名。

5. JNI 幫助方法

  在 Android 源碼中 your_path/dalvik/libnativehelper/include/nativehelper/JNIHelp.h 這個頭文件提供了一些關於 JNI 的幫助函數,包括本地方法注冊、異常處理等函數。但是使用這些函數需要鏈接動態庫 libnativehelper.so 。還提供了一個計算方法映射表長度的宏定義。

1 #ifndef NELEM
2 # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
3 #endif

  有了這個宏之后,我們就可以這樣子寫:

1 (*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods));

  6. 參考資料

  [1]. Android與JNI(一)
  [2]. Android JNI知識簡介

  [3]. Android中JNI編程的那些事兒
  [4]. Android 動態注冊JNI


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM