NDK开发基本常识

重要的事情说3遍

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

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

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

下载安装NDK开发环境

对着这个说明一步一步搞,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文件。
配置完成后,直接执行Build > Build APK(s)

image

编译完成后执行Build > Analyze APK... 找到对应的apk,就能看到如下界面,就能找到.os文件。

image

编写/注册c/c++函数

静态注册

函数命名规则

Java + 包名 + 类名 + 函数名

如:

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

上面的JNIEXPORTJNICALL是JNI的宏,用来标识该函数可以被JNI调用。

  • JNIEnv结构体指向了JNI的函数表,这些函数可以完成和Java的交互。
  • jobject是当前与之链接的native方法隶属的类对象,即调用这个JNI方法的对象。

上面这两个参数由Java虚拟机调用的时候传入。

动态注册

  1. 由于是将函数映射表注册到JVM中,所以函数的调用速度更快。
  2. 不用使用静态注册那套繁琐的命名规则。

注册

//编写需要使用的函数
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)
}

// 提供一个函数映射表,注册给JVM,这样JVM就可以通过函数映射表来调用相应的函数
// 这样的效率比静态注册的效率高
/**
 * @param1 Java中的native方法名。可以自定义。
 * @param2 函数签名,描述函数的返回值和参数
 * @param3 函数指针,指向被调用的c++函数。名车需要和函数名一样。
 */
static JNINativeMethod nativeMethod[] = {
    {"JNITest", "()Ljava/lang/String;", (void *) nativeJNITest},
    {"computeDamage", "(II)I", (void *)nativeComputeDamage}
};

//该函数在执行System.loadLibrary()后会被调用,用于向JVM注册函数表。
//返回值是使用的JNI版本。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    //需要通过JVM动态的获取JNIEnv来提供Java介质
    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    
    //需要调用这些函数的类
    //一定要注意名称的正确性:包名 + 类名
    jclass clz = env->FindClass("com/example/xingxinyu/cppdemo/JNIHelper");
    if(clz == NULL){
        LOGE("类名不对");
    } else {
        LOGE("类加载成功");
    }
    
    jint method_size = sizeof(nativeMethod) / sizeof(nativeMethod[0]);
   /**
     * 注册函数表
     * @param1 需要关联到那个【Java】类,Kotlin类不行
     * @param2 方法数组
     * @param3 方法数
     */
    env->RegisterNatives(clz, nativeMethod, method_size);
    //返回使用的JNI版本                               
    return JNI_VERSION_1_6;
}

解注册

向JVM中注册函数映射表后,因该在JVM释放改JNI组件时把其释放,不然就是隐患。

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    //需要通过JVM动态的获取JNIEnv来提供Java介质
    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return;
    }
    jclass clz = env->FindClass("com/example/xingxinyu/cppdemo/JNIHelper");
    // 解注册函数表
    env->UnregisterNatives(clz);
}

JNI描述符

前面在动态注册时,需要生成函数映射表,其中需要一个是函数签名,它是由JNI描述符来描述的,写错了函数就会找不到。

基本类型对应关系

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方法所在的类是一个内部类,则格式为L + 外部类相对路径 + $ + 内部类名 + ;。如:

// FileStatus
Landroid/os/FileUtils$FileStatus;

数组描述符

数组描述符的格式为:对应维度个[ + 类型描述符。如:

// int[]
[I

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

方法描述符

就是一开始说的函数签名,它的格式为:(参数描述符) + 返回值描述符

需要注意一点,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的数据类型

在普通数据类型前+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对象

// 声明一个结构体,用来保存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;

接下来编写一个函数,它能够接收一个Hero对象。

void nativeInitHero(JNIEnv *env, jobject thiz, jobject hero, jstring name, jstring race) {
    if (hero == NULL){
        return;
    }
    // GetObjectClass()函数可以根据对象实例直接获取对象的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()函数,可以设置对象的属性值
        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()函数,其它引用类型通过SetObjectField()设置即可。

从上面的代码可以看出,在c++中,我们无法直接访问到Java类的属性,只能通过JNI获取类的属性的ID,然后再根据属性ID访问类的属性。

接下来注册该函数,这里会用到上面讲的动态注册。

// 一定要注意命名的精准性,否则就找不到这个函数了
{"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

创建并返回自定义对象

直接看怎么在c++中创建自定义对象并返回它。我们只看最好用的一种方式。

// 定义Hero的路径宏,方便后面使用
#define HERO_PATH "com/coorchice/cppdemo/entry/Hero"
jobject nativeCreateHero(JNIEnv *env, jobject thiz, jstring name, jstring race)
{
    // 根据路径获取class
    jclass clz = env->FindClass(HERO_PATH);
    if (clz != NULL){
        LOGE("Find %s class success!", "Hero");
        // 获取Hero类的默认构造方法的ID
        // 后面需要使用这个ID来调用构造方法
        // <init> 就表示构造方法的名称
        // 第三个参数是构造方法的签名,签名格式和上面讲的一样
        jmethodID hero_construct_id = env->GetMethodID(clz, "<init>", "()V");
        // NewObject() 函数可以根据构造方法ID创建一个新的对象
        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()函数,可以设置对象的属性值
        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++中创建的Java对象给调用native方法的地方
        return hero;
    } else {
        return NULL;
    }
}

注意,只要是引用类型的对象,我们只需要把返回类型设置为jobject就行,在native方法中再写成真实类型。

同样,使用上面的动态注册,注册该函数。

{"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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全称 Java Native Interface,Java...
    cfanr阅读 57,483评论 9 132
  • 注:原文地址 紧接上篇:Android NDK开发:JNI基础篇 | cfanr,这篇主要介绍 JNI Nativ...
    cfanr阅读 12,992评论 11 56
  • 什么是JNI? JNI 是java本地开发接口.JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(...
    a_tomcat阅读 2,802评论 0 54
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,119评论 9 118
  • 刚开始玩微博的时候 朋友玩的很少 所以我总会发一些自己有的没的 可是渐渐的 周围的朋友 玩的多了 自己的东西也被大...
    A澳代姑娘阅读 175评论 0 0