NDK開發(fā)基本常識

重要的事情說3遍

請使用 Andorid Studio 2.2 及以上版本!

請使用 Andorid Studio 2.2 及以上版本!

請使用 Andorid Studio 2.2 及以上版本!

下載安裝NDK開發(fā)環(huán)境

對著這個說明一步一步搞,1分鐘妥妥的集成

看完這個鏈接再接著往下看啊!尤其是 CMake 的配置部分,需要認真看下。

將原生代碼編譯成.os

照著上一步一切順利的話,就可以嘗試開始這一步了。

首先在模塊的build.gradleandroid.defaultConfig.externalNativeBuild.cmake{}android.defaultConfig.ndk{}中添加這一句:

abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a'

看起來是這個樣子的:

externalNativeBuild {
    cmake {
        cppFlags ""
        abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a'
    }
}
ndk {
    abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'arm64-v8a'
}

沒有就自己寫上。這是用來配置將編譯哪幾種類型的ABI對應的.os文件。
配置完成后,直接執(zhí)行Build > Build APK(s)

image

編譯完成后執(zhí)行Build > Analyze APK... 找到對應的apk,就能看到如下界面,就能找到.os文件。

image

編寫/注冊c/c++函數(shù)

靜態(tài)注冊

函數(shù)命名規(guī)則

Java + 包名 + 類名 + 函數(shù)名

如:

JNIEXPORT void JNICALL
Java_com_example_cappdemo_helloCpp(JNIEnv* env, jobject)
{
    
}

上面的JNIEXPORTJNICALL是JNI的宏,用來標識該函數(shù)可以被JNI調(diào)用。

  • JNIEnv結(jié)構(gòu)體指向了JNI的函數(shù)表,這些函數(shù)可以完成和Java的交互。
  • jobject是當前與之鏈接的native方法隸屬的類對象,即調(diào)用這個JNI方法的對象。

上面這兩個參數(shù)由Java虛擬機調(diào)用的時候傳入。

動態(tài)注冊

  1. 由于是將函數(shù)映射表注冊到JVM中,所以函數(shù)的調(diào)用速度更快。
  2. 不用使用靜態(tài)注冊那套繁瑣的命名規(guī)則。

注冊

//編寫需要使用的函數(shù)
static jstring nativeJNITest(JNIEnv *env, jobject) 
{
    std::string test = "你好,c++";
    return env->NewStringUTF(test.c_str());
}

static jint nativeComputeDamage(JNIEnv *env, jobject thiz, jint attack, jint agility)
{
    return (jint)(attack + agility * 1.2)
}

// 提供一個函數(shù)映射表,注冊給JVM,這樣JVM就可以通過函數(shù)映射表來調(diào)用相應的函數(shù)
// 這樣的效率比靜態(tài)注冊的效率高
/**
 * @param1 Java中的native方法名。可以自定義。
 * @param2 函數(shù)簽名,描述函數(shù)的返回值和參數(shù)
 * @param3 函數(shù)指針,指向被調(diào)用的c++函數(shù)。名車需要和函數(shù)名一樣。
 */
static JNINativeMethod nativeMethod[] = {
    {"JNITest", "()Ljava/lang/String;", (void *) nativeJNITest},
    {"computeDamage", "(II)I", (void *)nativeComputeDamage}
};

//該函數(shù)在執(zhí)行System.loadLibrary()后會被調(diào)用,用于向JVM注冊函數(shù)表。
//返回值是使用的JNI版本。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    //需要通過JVM動態(tài)的獲取JNIEnv來提供Java介質(zhì)
    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    
    //需要調(diào)用這些函數(shù)的類
    //一定要注意名稱的正確性:包名 + 類名
    jclass clz = env->FindClass("com/example/xingxinyu/cppdemo/JNIHelper");
    if(clz == NULL){
        LOGE("類名不對");
    } else {
        LOGE("類加載成功");
    }
    
    jint method_size = sizeof(nativeMethod) / sizeof(nativeMethod[0]);
   /**
     * 注冊函數(shù)表
     * @param1 需要關(guān)聯(lián)到那個【Java】類,Kotlin類不行
     * @param2 方法數(shù)組
     * @param3 方法數(shù)
     */
    env->RegisterNatives(clz, nativeMethod, method_size);
    //返回使用的JNI版本                               
    return JNI_VERSION_1_6;
}

解注冊

向JVM中注冊函數(shù)映射表后,因該在JVM釋放改JNI組件時把其釋放,不然就是隱患。

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    //需要通過JVM動態(tài)的獲取JNIEnv來提供Java介質(zhì)
    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return;
    }
    jclass clz = env->FindClass("com/example/xingxinyu/cppdemo/JNIHelper");
    // 解注冊函數(shù)表
    env->UnregisterNatives(clz);
}

JNI描述符

前面在動態(tài)注冊時,需要生成函數(shù)映射表,其中需要一個是函數(shù)簽名,它是由JNI描述符來描述的,寫錯了函數(shù)就會找不到。

基本類型對應關(guān)系

Java JNI
byte B
char C
short S
int I
long J
float F
double D
boolean Z

引用類型描述符

引用類型的描述符各式為:L + 類相對路徑 + ;。如:

// String
Ljava/lang/String;

// Object
Ljava/lang/Object;

如果native方法所在的類是一個內(nèi)部類,則格式為L + 外部類相對路徑 + $ + 內(nèi)部類名 + ;。如:

// FileStatus
Landroid/os/FileUtils$FileStatus;

數(shù)組描述符

數(shù)組描述符的格式為:對應維度個[ + 類型描述符。如:

// int[]
[I

// Object[][]
[[Ljava/lang/Object;

方法描述符

就是一開始說的函數(shù)簽名,它的格式為:(參數(shù)描述符) + 返回值描述符

需要注意一點,void的描述符是V

// String fun()
()Ljava/lang/String;

// void fun(int a, int b)
(II)V

// File fun(byte[] bytes, int length)
([BI)Ljava/io/File;

JNI的數(shù)據(jù)類型

在普通數(shù)據(jù)類型前+j,比如:jobject對應Java的obejct

使用自定義對象

聲明以下Java對象

package com.coorchice.cppdemo.entry;

public class Hero {
  String race;
  String name;
  int attack;
  int agility;

  @Override
  public String toString() {
    return String.format("名字: %s\n種族: %s\n攻擊力: %d\n敏捷: %d", name, race, attack, agility);
  }
}

在c/c++中使用該對象的示例如下:

使用傳入的Java對象

// 聲明一個結(jié)構(gòu)體,用來保存Hero對象的信息
struct Hero {
    jclass clazz;           // Hero類
    jfieldID hero_name;     // name屬性的id
    jfieldID hero_race;     // race屬性的id
    jfieldID hero_attack;   // attack屬性的id
    jfieldID hero_agility;  // agility屬性的id
} hero_struct;

接下來編寫一個函數(shù),它能夠接收一個Hero對象。

void nativeInitHero(JNIEnv *env, jobject thiz, jobject hero, jstring name, jstring race) {
    if (hero == NULL){
        return;
    }
    // GetObjectClass()函數(shù)可以根據(jù)對象實例直接獲取對象的class
    // 比FindClass()方便很多
    hero_struct.clazz = env->GetObjectClass(hero);
    if (hero_struct.clazz != NULL) {
        LOGE("Find %s class success!", "Hero");
        // 通過GetFeildID()獲取Hero類的屬性ID
        hero_struct.hero_name = env->GetFieldID(hero_struct.clazz, "name", "Ljava/lang/String;");
        hero_struct.hero_race = env->GetFieldID(hero_struct.clazz, "race", "Ljava/lang/String;");
        hero_struct.hero_attack = env->GetFieldID(hero_struct.clazz, "attack", "I");
        hero_struct.hero_agility = env->GetFieldID(hero_struct.clazz, "agility", "I");

        // 通過SetXXXField()函數(shù),可以設置對象的屬性值
        env->SetObjectField(hero, hero_struct.hero_name,  name);
        env->SetObjectField(hero, hero_struct.hero_race,  race);
        env->SetIntField(hero, hero_struct.hero_attack, 10);
        env->SetIntField(hero, hero_struct.hero_agility, 7);
    } else {
        return;
    }
}

需要說明的是,JNI默認只提供了8種基本類型的SetXXXField()函數(shù),其它引用類型通過SetObjectField()設置即可。

從上面的代碼可以看出,在c++中,我們無法直接訪問到Java類的屬性,只能通過JNI獲取類的屬性的ID,然后再根據(jù)屬性ID訪問類的屬性。

接下來注冊該函數(shù),這里會用到上面講的動態(tài)注冊。

// 一定要注意命名的精準性,否則就找不到這個函數(shù)了
{"initHero", "(Lcom/coorchice/cppdemo/entry/Hero;Ljava/lang/String;Ljava/lang/String;)V", (void *) nativeInitHero}

在Java中使用:

// 在JniHelper中聲明native方法
public static native void initHero(Hero hero, String name, String race);

// 使用
Hero hero = new Hero();
initHero(hero, "惡魔獵手", "暗夜精靈");
hero.toString();

輸出:

名字: 惡魔獵手
種族: 暗夜精靈
攻擊力: 10
敏捷: 7

創(chuàng)建并返回自定義對象

直接看怎么在c++中創(chuàng)建自定義對象并返回它。我們只看最好用的一種方式。

// 定義Hero的路徑宏,方便后面使用
#define HERO_PATH "com/coorchice/cppdemo/entry/Hero"
jobject nativeCreateHero(JNIEnv *env, jobject thiz, jstring name, jstring race)
{
    // 根據(jù)路徑獲取class
    jclass clz = env->FindClass(HERO_PATH);
    if (clz != NULL){
        LOGE("Find %s class success!", "Hero");
        // 獲取Hero類的默認構(gòu)造方法的ID
        // 后面需要使用這個ID來調(diào)用構(gòu)造方法
        // <init> 就表示構(gòu)造方法的名稱
        // 第三個參數(shù)是構(gòu)造方法的簽名,簽名格式和上面講的一樣
        jmethodID hero_construct_id = env->GetMethodID(clz, "<init>", "()V");
        // NewObject() 函數(shù)可以根據(jù)構(gòu)造方法ID創(chuàng)建一個新的對象
        jobject hero = env->NewObject(clz, hero_construct_id);
        
        hero_struct.clazz = clz;
        // 通過GetFeildID()獲取Hero類的屬性ID
        hero_struct.hero_name = env->GetFieldID(hero_struct.clazz, "name", "Ljava/lang/String;");
        hero_struct.hero_race = env->GetFieldID(hero_struct.clazz, "race", "Ljava/lang/String;");
        hero_struct.hero_attack = env->GetFieldID(hero_struct.clazz, "attack", "I");
        hero_struct.hero_agility = env->GetFieldID(hero_struct.clazz, "agility", "I");
        
        // 通過SetXXXField()函數(shù),可以設置對象的屬性值
        env->SetObjectField(hero, hero_struct.hero_name,  name);
        env->SetObjectField(hero, hero_struct.hero_race,  race);
        env->SetIntField(hero, hero_struct.hero_attack, 99999);
        env->SetIntField(hero, hero_struct.hero_agility, 99999);
        
        // 返回一個在c++中創(chuàng)建的Java對象給調(diào)用native方法的地方
        return hero;
    } else {
        return NULL;
    }
}

注意,只要是引用類型的對象,我們只需要把返回類型設置為jobject就行,在native方法中再寫成真實類型。

同樣,使用上面的動態(tài)注冊,注冊該函數(shù)。

{"createHero", "(Ljava/lang/String;Ljava/lang/String;)Lcom/coorchice/cppdemo/entry/Hero;", (void *) nativeCreateHero}

再次提醒,一定要保證命名的精準。

看看如何在Java中使用吧。

// 在JniHelper中聲明相應的native方法
public static native Hero createHero(String name, String race);

// 使用
createHero("巫妖王", "人族").toString();

輸出:

名字: 巫妖王
種族: 人族
攻擊力: 99999
敏捷: 99999
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全稱 Java Native Interface,Java...
    cfanr閱讀 57,985評論 9 132
  • 注:原文地址 緊接上篇:Android NDK開發(fā):JNI基礎篇 | cfanr,這篇主要介紹 JNI Nativ...
    cfanr閱讀 13,142評論 11 56
  • 什么是JNI? JNI 是java本地開發(fā)接口.JNI 是一個協(xié)議,這個協(xié)議用來溝通java代碼和外部的本地代碼(...
    a_tomcat閱讀 2,854評論 0 54
  • 前言 人生苦多,快來 Kotlin ,快速學習Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,282評論 9 118
  • 剛開始玩微博的時候 朋友玩的很少 所以我總會發(fā)一些自己有的沒的 可是漸漸的 周圍的朋友 玩的多了 自己的東西也被大...
    A澳代姑娘閱讀 198評論 0 0