JNI 与 NDK 入门(一)

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战java

JNI

概念

JNI是Java Native Interface的简写,它可使Java与其余语言(如C、C++)进行交互。android

它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。程序员

为什么须要JNI

  • Java的源文件很是容易被反编译,而经过Native语言生成的.so库文件则不容易被反编译。
  • 有时咱们使用Java时须要使用到一些库来实现功能,但这些库仅仅提供了一些Native语言的接口。
  • 使用Native语言编写的代码运行效率高,尤为体如今音频视频图片的处理等须要大量复杂运算的操做上。充分利用了硬件的性能。

因为上述缘由,此时咱们就须要让Java与Native语言交互。而因为Java的特色,与Native语言的交互能力很弱。所以在此时,咱们就须要用到JNI特性加强Java与Native方法的交互能力。面试

实现的步骤

  1. 在Java中声明Native方法(须要调用的本地方法)
  2. 经过 javac 编译 Java源文件( 生成.class文件)
  3. 经过 javah 命令生成JNI头文件(生成.h文件)
  4. 经过Native语言实如今Java源码中声明的Native方法
  5. 编译成.so库文件
  6. 经过Java命令执行 Java程序,最终实现Java调用本地代码(借助so库文件)

NDK

概念

Native是Native Development Kit的简写,是Android的开发工具包,属于Android,与Java无关系。数组

它能够快速开发C/C++的动态库,自动将.so和应用一块儿打包为APK。所以咱们能够经过NDK来在Android开发中经过JNI与Native方法交互。markdown

使用方式

  1. 配置 Android NDK环境(在SDK Manager中下载NDK、CMake、LLDB)
  2. 建立 Android 项目,与 NDK进行关联(建立项目时选择C++ support)
  3. 在 Android 项目中声明所须要调用的 Native方法
  4. 用Native语言实如今Android中声明的Native方法
  5. 经过 ndk-bulid 命令编译产生.so库文件

将Android项目与NDK关联

配置NDK路径

local.properties中加入以下一行便可app

ndk.dir=<ndk路径>函数

添加配置

在Gradle的 gradle.properties中加入以下一行,目的是对旧版本的NDK支持工具

android.useDeprecatedNdk=trueoop

添加ndk节点

在build.gradle中的defaultConfigandroid中加入以下的externalNativeBuild节点

apply plugin: 'com.android.application'



android {

    compileSdkVersion 27

    defaultConfig {

        applicationId "com.n0texpecterr0r.ndkdemo"

        minSdkVersion 19

        targetSdkVersion 27

        versionCode 1

        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        externalNativeBuild {

            cmake {

                cppFlags ""

            }

        }

    }

    buildTypes {

        release {

            minifyEnabled false

            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

        }

    }

    externalNativeBuild {

        cmake {

            path "CMakeLists.txt"

        }

    }

}



dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support:appcompat-v7:27.1.1'

    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test<span class="emoji emoji-sizer" style="background-image:url(/emoji-data/img-apple-64/1f3c3.png)" data-codepoints="1f3c3"></span>1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

}
复制代码

开发Native代码

在Java文件中声明native方法

咱们首先须要在Java代码的类中经过static块来加载咱们的Native库。能够经过以下代码,其中loadLibrary的参数是在CMakeList.txt中定义的Native库的名称

static {

    System.loadLibrary("native-lib");

}
复制代码

以后,咱们即可以在这个类中声明Native方法

public native String getStringFromJNI();
复制代码

建立CMakeList.txt

咱们还须要在src中建立一个CMakeList.txt文件,这个文件约束了Native语言源文件的编译规则。好比下面

cmake_minimum_required(VERSION 3.4.1)



add_library(native-lib SHARED src/main/cpp/native-lib.cpp)



find_library(log-lib log)



target_link_libraries(native-lib ${log-lib})
复制代码

add_library方法中定义了一个so库,它的名称是native-lib,也就是咱们在Java文件中用到的字符串,然后面则跟着这个库对应的Native文件的路径

find_library则是定义了一个路径变量,通过了这个方法,log-lib这个变量中的值就是Android中log库的路径

target_link_libraries则是将native-lib这个库和log库链接了起来,这样咱们就能在native-lib中使用log库的方法。

建立Native方法文件

在前面的CMake文件中能够看到,咱们把文件放在了src/main/cpp/,所以咱们建立cpp这个目录,在里面建立C++源文件native-lib.cpp。

而后, 咱们即可以开始编写以下的代码:

#include <jni.h>

#include <string>



extern "C"{

  JNIEXPORT jstring JNICALL

  Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(

      JNIEnv* env,

      jobject) {

    std::string hello = "IG牛逼";

    return env->NewStringUTF(hello.c_str());

  }

}
复制代码

此处咱们使用的是C++语言,让咱们来看看具体的代码。

首先咱们引入了jni须要的jni.h,这个头文件中声明了各个jni须要用到的函数。同时咱们引入了C++中的string.h。

而后咱们看到extern "C"。为了了解这里为何使用了extern "C",咱们首先须要知道下面的知识:

在C中,编译时的函数签名仅仅是包含了函数的名称,所以不一样参数的函数都是一样的签名。这也就是为何C不支持重载。

而C++为了支持重载,在编译的时候函数的签名除了包含函数的名称,还携带了函数的参数及返回类型等等。

试想此时咱们有个C的函数库要给C++调用,会由于签名的不一样而找不到对应的函数。所以,咱们须要使用extern "C"来告诉编译器使用编译C的方式来链接。

接下来咱们看看JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的做用就是说明该函数为JNI函数。

而jstring则对应了Java中的String类,JNI中有不少相似jstring的类来对应Java中的类,下面是Java中的类与JNI类型的对照表

咱们继续看到函数名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI。其实函数名中的_至关于Java中的 . 也就是这个函数名表明了java.com.n0texpecterr0r.ndkdemo.MainActivity.java中的getStringFromJNI方法,也就是咱们以前定义的native方法。

格式大概以下:

Java_包名_类名_须要调用的方法名

其中,Java必须大写,包名里的.要改为__要改为_1

接下来咱们看到这个函数的两个参数:

  • JNIEnv* env:表明了JVM的环境,Native方法能够经过这个指针来调用Java代码
  • jobject obj:它就至关于定义了这个JNI方法的类 (MainActivity) 的this引用

而后能够看到后面咱们建立了一个string hello,以后经过env->NewStringUTF(hello.c_str())方法建立了一个jstring类型的变量并返回。

在Java代码中调用native方法

接着,咱们即可以在MainActivty中像调用Java方法同样调用这个native方法

TextView tv = findViewById(R.id.sample_text);

tv.setText(getStringFromJNI());
复制代码

咱们尝试运行,能够看到,咱们成功用C++构建了一个字符串并返回给Java调用:

image.png

CMake

咱们在NDK开发中使用CMake的语法来编写简单的代码描述编译的过程,因为这篇文章是讲NDK的,因此关于CMake的语法就再也不赘述了。。。若是想要了解CMake语法能够学习这本书《CMake Practice

JNI与Java代码交互

方法签名

概念

在咱们JNI层调用一个方法时,须要传递一个参数——方法签名。

为何要使用方法签名呢?由于在Java中的方法是能够重载的,两个方法可能名称相同而参数不一样。为了区分调用的方法,就引入了方法签名的概念。

签名规则

对于基本类型的参数,每一个类型对应了一个不一样的字母:

  • boolean Z
  • byte B
  • char C
  • short S
  • int I
  • long J
  • float F
  • double D
  • void V

对于类,则使用 L+类名 的方式,其中(.)用(/)代替,最后加上分号

好比 java.lang.String就是 Ljava/lang/String;

对于数组,则在前面加 [ ,而后加类型的签名,几维数组就加几个。

好比 int[]对应的就是[I , boolean[][]对应的则是[[Z,而java.lang.String[]就是[Ljava/lang/String;

打印方法签名

咱们能够经过 javap -s 命令来打印方法的签名。

例子

好比下面的方法

public native String getMessage();



public native String getMessage(String id,long i);
复制代码

对应的方法签名分别为:

()Ljava/lang/String;

(Ljava/long/String;J)Ljava/lang/String;
复制代码

能够看到,前面括号中表示的是方法的参数列表,后面表示的则是返回值。

今天就先肝这么多,明天再说

公众号:程序员喵大人(专一于Android各种学习笔记、面试题以及IT类资讯的分享。)

相关文章
相关标签/搜索