Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android jni/ndk編程一:jni初級認識與實戰體驗

Android jni/ndk編程一:jni初級認識與實戰體驗

編輯:關於Android編程

Android平台很多地方都可以看到jni的身影,比如之前接觸到一個投屏的項目,主要的代碼是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源碼下編譯一個模塊使用mm命令很相似。之所以說他們相似是因為他們都需要一個Android.mk文件,而且文件的格式完全一樣,比如說有如下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文件之外,可能有必要添加一個Application.mk,這個文件通常是由一行:

APP_ABI := x86

這裡我們指明了需要編譯的二進制庫的格式。ABI(Application Binary Interface)與處理器相關,對於arm處理器,APP_ABI 可能要配置成 armeabi ,對於mips處理器,APP_ABI應該配置為mips,當然,我們還可以一次生成所有平台的庫,此時只需要給APP_ABI賦值ALL就可以了。

二.jni的初步認識

2.1 JNI的作用

JNI(Java Native Interface)它提供了若干的API實現了Java和其他語言的通信(主要是C&C++)。從Java1.1開始,JNI標准成為java平台的一部分,它允許Java代碼和其他語言寫的代碼進行交互。以上是百度百科上copy的話,也算是交代了下JNI的作用吧。

2.2 JNI使用流程

我們使用JNI的起點一般都是System.loadLibrary(“xxx”);開始的,xxx代表了需要加載的庫名。可以認為是它加載我們c/c++代碼到虛擬機中,這樣,我們的Java虛擬機就知道了c/c++中的函數了,之後,我們就可以調用它。
因此,使用jni只需兩步:
1.首先,我們要有一個動態庫,這個庫我們可以使用ndk-build來編譯生成。
2.其次,我們需要使用System.loadLibrary(“xxx”)來加載這個庫,加載完成後,就可以和本地代碼交互了。
2.3 查閱一個使用JNI的c文件
為了認識JNI,找一個使用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

這裡節選了其中的一些,我們會發現其中有很多奇怪的字段,比如JNIEXPORT 、JNICALL等,所以,接下來,我們得先搞清楚它們的意義。

2.4 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下編譯了。

2.4 extern “C”

extern “C”從字面上看有兩部分的內容:extern和“C”
extern是編程語言中的一種屬性,它表征了變量、函數等類型的作用域(可見性)屬性,是編程語言中的關鍵字。當進行編譯時,該關鍵字告訴編譯器它所聲明的函數和變量等可以在本模塊或者文件以及其他模塊或文件中使用。
“C”表明了一種編譯規約。
因此,extern “C”表明了一種編譯規約,其中extern是關鍵字屬性,“C”表征了編譯器鏈接規范。
使用extern “C” 聲明的函數將采用C語言的編譯方式編譯,也就是說只有在C++代碼中extern “C”才有意義,之所以這樣顯示聲明適應C語言的編譯方式編譯該代碼塊,是因為c和c++是有差異的,舉例來說,有如下函數:
void hello(int,int);
這個函數在C編譯器中,它的函數名師_hello,而在c++編譯器中它的函數名是hello_int_int,之所以這樣是因為c++支持函數重載,函數名可以相同,表征一個函數的除了函數名還有函數的參數列表。這因為有如此不同,因此我們可以想象如下情景:
加入我要在c++中調用一個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代碼的簡要過程

JVM查找native方法有兩種方式:
1.靜態方式:按照JNI規范的命名規則
2.動態方式:調用JNI提供的RegisterNatives函數,將本地函數注冊到JVM中。

3.1靜態方式

靜態方式使用的是按照JNI的命名規范來查找native函數,JNI函數命名規則為:
Java_類全路徑_方法名
比如我們打算向com.jinwei.hellotest包中的MainActivity類注冊名為sayHello的方法,那麼,我們的函數命名就應該為:Java_com_jinwei_jnitesthello_MainActivity_sayHello

3.2動態方式

了解動態注冊就要涉及到System.loadLibrary函數的工作流程了,這個函數打開一個動態庫後,會找到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函數進一步注冊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方法的注冊,這其中用到一個結構體: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 },
};

它是一個函數映射表,前邊是java層使用的函數名,後邊是native層使用的函數名,中間是函數簽名。
簽名是一種用參數個數和類型區分同名方法的手段,即解決方法重載問題。
假如有下面Java方法:
long f (int n, String s, int[] arr);
簽名後: “(ILjava/lang/String;[I)J”
其中要特別注意的是:
1. 類描述符開頭的’L’與結尾的’;’必須要有
2. 數組描述符,開頭的’[‘必須有.
3. 方法描述符規則: “(各參數描述符)返回值描述符”,其中參數描述符間沒有任何分隔
符號
簽名中使用的符號總結如下:
這裡寫圖片描述

四.實戰

4.1靜態方式

在隨便一個目錄下添加如下三個文件:
hello.c,Android.mk,Application.mk
hello.c

#include 
#include 
#include 
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的字樣了。
一下是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




    

4.2 動態方式

動態注冊的使用流程之前已經分析過,它和靜態的區別也只體現在hello.c文件上,這裡只把hello.c文件貼出來:

#include 
#include 
#include 



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.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) //這裡就是關鍵了,把本地函數和一個java類方法關聯起來。不管之前是否關聯過,一律把之前的替換掉!
    {
      printf("register native method failed!\n");
      return -1;
    } 

    return JNI_VERSION_1_4;  
}

還是一樣,使用ndk-build編譯,把編譯生成的庫文件拷貝到android studio工程src/main/jniLibs/armeabi目錄下,然後運行該項目即可。
注意:如果你的android設備或者虛擬機使用的x86等其他格式的鏡像,注意修改Application.mk文件,修改方法文章的第一小節已經介紹過了。

總結:通過以上基礎知識的介紹和兩個實戰的案例,我們應該初步理解了Jni工作的過程,對靜態方式和動態方式使用JNI有了直觀的體驗,但JNI畢竟非常復雜,我們還有很多的知識要學習,下一節主要介紹jni類型的轉換,就是怎麼把java層的String,int等轉換到c/c++層對應的類型。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved