Android平臺很多地方都可以看到jni的身影,比如之前接觸到1個投屏的項目,主要的代碼是c/c++寫的,然后通過Jni供java層調用;另外,就拿Android系統中的Service來講,很多的Service都有java層代碼和native層代碼組成,native層代碼會在android啟動的進程中完成向java層的注冊。總之,由于沒法甩開jni的身影,所以我打算花點時間系統的學習下Android下的jni開發。
所謂工欲善其事,必先利其器,在學習android系統的jni編程之前,先了解下jni編程使用的工具。1.1NDK(Native Development Kit)
NDK翻譯過來就是本地代碼開發工具集,本地代碼主要指c/c++,因此,我們的c/c++代碼可使用NDK中提供的工具完成編譯,我們可以把C/C++代碼編譯成動態庫,然后在java層訪問動態庫,這樣就是了java調用C/C++的功能。NDK眾多的工具中,ndk-build主要用來編譯native代碼,它在windows和linux平臺下均有響應的版本可使用。它的用法仿佛和在android源碼下編譯1個模塊使用mm命令很類似。之所以說他們類似是由于他們都需要1個Android.mk文件,而且文件的格式完全1樣,比如說有以下Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_PRELINK_MODULE := false
LOCAL_SRC_FILES := hello.c
LOCAL_MODULE := hello
include $(BUILD_EXECUTABLE)
我們在Android源碼目錄下使用mm命令編譯該模塊和在windows下使用ndk-build編譯該模塊都能產生libhello.so庫,表面上還真看不出差別。
使用ndk-build編譯native代碼時,除需要Android.mk文件以外,可能有必要添加1個Application.mk,這個文件通常是由1行:
APP_ABI := x86
這里我們指明了需要編譯的2進制庫的格式。ABI(Application Binary Interface)與處理器相干,對arm處理器,APP_ABI 可能要配置成 armeabi ,對mips處理器,APP_ABI應當配置為mips,固然,我們還可以1次生成所有平臺的庫,此時只需要給APP_ABI賦值ALL就能夠了。
JNI(Java Native Interface)它提供了若干的API實現了Java和其他語言的通訊(主要是C&C++)。從Java1.1開始,JNI標準成為java平臺的1部份,它允許Java代碼和其他語言寫的代碼進行交互。以上是百度百科上copy的話,也算是交代了下JNI的作用吧。
我們使用JNI的出發點1般都是System.loadLibrary(“xxx”);開始的,xxx代表了需要加載的庫名。可以認為是它加載我們c/c++代碼到虛擬機中,這樣,我們的Java虛擬機就知道了c/c++中的函數了,以后,我們就能夠調用它。
因此,使用jni只需兩步:
1.首先,我們要有1個動態庫,這個庫我們可使用ndk-build來編譯生成。
2.其次,我們需要使用System.loadLibrary(“xxx”)來加載這個庫,加載完成后,就能夠和本地代碼交互了。
2.3 查閱1個使用JNI的c文件
為了認識JNI,找1個使用JNI的文件,比如:android-ndk\android-ndk-r10\samples\hello-gl2\jni\gl_code.cpp:
...
extern "C" {
JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj, jint width, jint height);
JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj);
};
JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_init(JNIEnv * env, jobject obj, jint width, jint height)
{
setupGraphics(width, height);
}
JNIEXPORT void JNICALL Java_com_android_gl2jni_GL2JNILib_step(JNIEnv * env, jobject obj)
{
r
這里節選了其中的1些,我們會發現其中有很多奇怪的字段,比如JNIEXPORT 、JNICALL等,所以,接下來,我們得先弄清楚它們的意義。
這兩個字段定義在jni.h中,定義以下:
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
#define JNICALL __NDK_FPABI__
由于在在Windows中編譯dll動態庫時,如果動態庫中的函數要被外部調用,需要在函數聲明中添加 attribute ((visibility (“default”)))標識,表示將該函數導出在外部可以調用。在Linux/Unix系統中,這兩個宏可以省略不加。由于在Linux/Unix平臺下,這兩個宏為空,所以加不加都沒關系,固然還是建議加上哈,這樣linux下的代碼就能夠直接拿到linux下編譯了。
extern “C”從字面上看有兩部份的內容:extern和“C”
extern是編程語言中的1種屬性,它表征了變量、函數等類型的作用域(可見性)屬性,是編程語言中的關鍵字。當進行編譯時,該關鍵字告知編譯器它所聲明的函數和變量等可以在本模塊或文件和其他模塊或文件中使用。
“C”表明了1種編譯規約。
因此,extern “C”表明了1種編譯規約,其中extern是關鍵字屬性,“C”表征了編譯器鏈接規范。
使用extern “C” 聲明的函數將采取C語言的編譯方式編譯,也就是說只有在C++代碼中extern “C”才成心義,之所以這樣顯示聲明適應C語言的編譯方式編譯該代碼塊,是由于c和c++是有差異的,舉例來講,有以下函數:
void hello(int,int);
這個函數在C編譯器中,它的函數名師_hello,而在c++編譯器中它的函數名是hello_int_int,之所以這樣是由于c++支持函數重載,函數名可以相同,表征1個函數的除函數名還有函數的參數列表。這由于有如此不同,因此我們可以想象以下情形:
加入我要在c++中調用1個c函數
1.首先,我要在hello.h中聲明hello(int,int)函數,然后在對應的.c文件中實現它。
2.c++文件需要包括hello.h文件,然后履行hello(1,1);完成調用。
那末此時c編譯器生成的函數名為_hello。而c++編譯器會尋覓_hello_int_int的函數名,這不就找不到了嗎?
因此,extern “C”主要用于c++代碼調用c代碼時,避免出現函數找不到的問題。
JVM查找native方法有兩種方式:
1.靜態方式:依照JNI規范的命名規則
2.動態方式:調用JNI提供的RegisterNatives函數,將本地函數注冊到JVM中。
靜態方式使用的是依照JNI的命名規范來查找native函數,JNI函數命名規則為:
Java_類全路徑_方法名
比如我們打算向com.jinwei.hellotest包中的MainActivity類注冊名為sayHello的方法,那末,我們的函數命名就應當為:Java_com_jinwei_jnitesthello_MainActivity_sayHello
了解動態注冊就要觸及到System.loadLibrary函數的工作流程了,這個函數打開1個動態庫后,會找到JNI_OnLoad這個函數的地址,然后調用這個函數,因此我們可以在這個函數中完成向JVM注冊native方法的工作。
比如,Android源碼中有以下代碼片斷:
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
ALOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
if (register_android_media_ImageWriter(env) != JNI_OK) {
ALOGE("ERROR: ImageWriter native registration failed");
goto bail;
}
...
JNI_OnLoad調用register_android_media_ImageWriter函數進1步注冊native方法:
int register_android_media_ImageWriter(JNIEnv *env) {
...
int ret2 = AndroidRuntime::registerNativeMethods(env,
"android/media/ImageWriter$WriterSurfaceImage", gImageMethods, NELEM(gImageMethods));
return (ret1 || ret2);
}
該函數中使用AndroidRuntime::registerNativeMethods真正完成native方法的注冊,這其中用到1個結構體:gImageMethods,其定義以下:
static JNINativeMethod gImageMethods[] = {
{"nativeCreatePlanes", "(II)[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;",
(void*)Image_createSurfacePlanes },
{"nativeGetWidth", "()I", (void*)Image_getWidth },
{"nativeGetHeight", "()I", (void*)Image_getHeight },
{"nativeGetFormat", "()I", (void*)Image_getFormat },
};
它是1個函數映照表,前邊是java層使用的函數名,后邊是native層使用的函數名,中間是函數簽名。
簽名是1種用參數個數和類型辨別同名方法的手段,即解決方法重載問題。
假設有下面Java方法:
long f (int n, String s, int[] arr);
簽名后: “(ILjava/lang/String;[I)J”
其中要特別注意的是:
1. 類描寫符開頭的’L’與結尾的’;’必須要有
2. 數組描寫符,開頭的’[‘必須有.
3. 方法描寫符規則: “(各參數描寫符)返回值描寫符”,其中參數描寫符間沒有任何分隔
符號
簽名中使用的符號總結以下:
在隨意1個目錄下添加以下3個文件:
hello.c,Android.mk,Application.mk
hello.c
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
JNIEXPORT jstring JNICALL Java_com_jinwei_jnitesthello_MainActivity_sayHello(JNIEnv * env, jobject obj){
return (*env)->NewStringUTF(env,"jni say hello to you");
}
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := optional
LOCAL_PRELINK_MODULE := false
LOCAL_MODULE_PATH := hellolib
LOCAL_SRC_FILES := hello.c
LOCAL_MODULE := hello
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_ABI := armeabi
然后使用cmd進入到該目錄下,履行ndk-build。能履行ndk-build是由于我已把ndk-build工具所在的目錄添加到環境變量path中了。編譯成功后會在上級目錄的libs目錄的armeabi目錄下生成libhello.so文件。
在Android Studio工程的src/main下新建jniLibs目錄,在jniLibs目錄下新建armeabi目錄,然后把libhello.so拷貝到armeabi目錄下。這樣,就能夠在Android利用程序中訪問libhello.so庫了。關于jniLibs目錄的名字,這是Android gradle默許的jni庫目錄,我們是可以自定義的,這里就不啰嗦了,可以參考下我的詳細配置Android Studio中的Gradle
以后運行app就能夠看到jni say hello to you的字樣了。
1下是Android Studio中相干的文件:
MainActivity.java
package com.jinwei.jnitesthello;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView textView = null;
static {
System.loadLibrary("hello");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = (TextView) findViewById(R.id.text);
String hehe = this.sayHello();
textView.setText(hehe);
}
native public String sayHello();
}
activity_main.xml
<?xml version="1.0" encoding="utf⑻"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.jinwei.jnitesthello.MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
動態注冊的使用流程之前已分析過,它和靜態的區分也只體現在hello.c文件上,這里只把hello.c文件貼出來:
#include <stdio.h>
#include <jni.h>
#include <stdlib.h>
jstring native_sayHello(JNIEnv * env, jobject obj){
return (*env)->NewStringUTF(env,"jni say hello to you");
}
static JNINativeMethod gMethods[] = {
{"sayHello", "()Ljava/lang/String;", (void *)native_sayHello},
};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv* env = NULL; //注冊時在JNIEnv中實現的,所以必須首先獲得它
jint result = -1;
if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4) != JNI_OK) //從JavaVM獲得JNIEnv,1般使用1.4的版本
return -1;
jclass clazz;
static const char* const kClassName="com/jinwei/jnitesthello/MainActivity";
clazz = (*env)->FindClass(env, kClassName); //這里可以找到要注冊的類,條件是這個類已加載到java虛擬機中。 這里說明,動態庫和有native方法的類之間,沒有任何對應關系。
if(clazz == NULL)
{
printf("cannot get class:%s\n", kClassName);
return -1;
}
if((*env)->RegisterNatives(env,clazz,gMethods, sizeof(gMethods)/sizeof(gMethods[0]))!= JNI_OK) //這里就是關鍵了,把本地函數和1個java類方法關聯起來。不管之前是不是關聯過,1律把之前的替換掉!
{
printf("register native method failed!\n");
return -1;
}
return JNI_VERSION_1_4;
}
還是1樣,使用ndk-build編譯,把編譯生成的庫文件拷貝到android studio工程src/main/jniLibs/armeabi目錄下,然后運行該項目便可。
注意:如果你的android裝備或虛擬機使用的x86等其他格式的鏡像,注意修改Application.mk文件,修改方法文章的第1小節已介紹過了。
總結:通過以上基礎知識的介紹和兩個實戰的案例,我們應當初步理解了Jni工作的進程,對靜態方式和動態方式使用JNI有了直觀的體驗,但JNI畢竟非常復雜,我們還有很多的知識要學習,下1節主要介紹jni類型的轉換,就是怎樣把java層的String,int等轉換到c/c++層對應的類型。