Android NDK开发——NDK与JNI简单介绍及NDK开发demo实现

https://rainmonth.github.io/posts/A191021.html

摘要

之前一直对NDK和JNI这两个玩意理解有点模糊,本文先分别介绍两者的相关概念,再介绍两者的使用的一般流程,最后介绍两者的联系。

Jni接口文档

本文主要介绍NDK开发时的一些环境配置,并提供了一个Java调用Native方法的简单实例。

JNI

简介

JNI,即Java Native Interface,是指Java本地接口。它定义Java与其他语言(如c、c++)之间的交互规范,依据这个规范就可以在Java代码中调用c、c++的方法,或者在c、c++代码中调用Java方法

JNI是Java调用本地其他语言(如c、c++)的一种特性,是Java的概念,与Android无关。

为什么要有JNI

Java具有跨平台的特性,所以Java就面临着与当前平台(本地环境)交互能力弱的这个痛点,为了解决这一痛点,JNI就应运而生了,JNI出现的目的就是为了增强Java与本地交互的能力。

JNI开发的一般步骤

  1. 在Java中声明native方法(即需要调用的本地方法)
  2. 通过javac命令编译java源文件(得到.class文件)
  3. 通过javah命令到处JNI的头文件(得到.h文件)
  4. 利用c、c++根据3中得到的.h文件生成.c(或.cpp)文件,即native实现;
  5. 编译生成.so库文件;
  6. 在Java文件中加载so库文件,并调用native方法。

后面会给出具体的Demo

参考文档

NDK

简介

NDK,即Native Development Kit,是Android提供的一个原生开发工具包,NDK是Android开发中的一个概念,与Java并无直接关系。

为什么要有NDK

通过JNI介绍可以看出,如果Android开发中想调用本地方法,步骤比较繁琐,NDK的出现,解决了这一问题,可以快速开发c、c++动态库,并将生成的so文件一起打包到apk中(可以理解为一种半自动化工具)

特点

关于NDK的特点,用一张图来表示。
NDK特点

关于NDK,还有一些需要注意的地方:

  • NDK可以直接将so文件打包至apk中,而JNI需要先生成so文件,然后将so文件放在指定的位置;
  • NDK提供的库优先,不要滥用,一般用于算法效率问题和安全敏感问题;
  • NDK提供了交叉编译其,用于生成不同CPU平台的动态库;

NDK开发的一般步骤

  1. 配置Android NDK开发环境;
  2. 创建Android项目,并与NDK进行关联;
  3. 在Android相关的类(Activity)中声明需要调用的native方法;
  4. 生成native的.h头文件;
  5. 依据头文件,编写其具体实现;
  6. 实现代码写好后,使用ndk build来编译生成so文件;
  7. 编译Android项目,实现Android调用本地代码。

参考文档

NDK中文官方文档

NDK和JNI的关系

NDK是Android平台上快速实现JNI的一种手段,JNI是Java实现的目的,即完成Java中调用本地代码。

JNI和NDK开发demo示例

由于NDK是达到JNI目的的一种方式,所以二者实现方式大体相同,区别就是生成.h文件以及so库的处理方式不同,所以这里主要给出NDK开发示例。
先说下我的环境配置

  • Mac系统
  • Android Studio 3.5
  • JDK 1.8.1,已将JDK 的bin目录加入到PATH;

NDK开发demo示例

具体步骤

  1. 配置NDK开发环境

下载NDK,打开Android Studio ->Preferences->Appearance & Behavior->Android SDK,切换到SDK Tools,下载NDK开发工具;

  1. 创建Android项目,新建一个NdkDemo 项目,在src/main下新建目录jin和jinLibs,并在jni目录下新建JniTest.java文件,内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package cn.rainonth.ndkdemo;

    /**
    * @author 张豪成
    * @date 2019-10-21 20:36
    */
    public class JniTest {
    public native static String get();

    public native static int add(int a, int b);
    }
  2. 根据JniTest.java 生成jni头文件,有两种方案:

    • 方法1:点击Build->Make Project,生成JniTest.class文件,然后找到生成的.class文件,利用javah命令生成;

    • 方法2:利用Android External tools来自动化上面的命令。具体方法如下:

      • Preferences->Tools->Exteranl tools,新建external tools,分别按下面的内容配置:
      1
      2
      3
      4
      5
      Name:Generate Jni Header(可以随便填)
      Description:生成JNI头文件(可以随便填)
      Program:javah
      Arguments:-v -jni -d $ModuleFileDir$/src/main/jni $FileClass$
      Working Directory:$SourcepathEntry$
      • 配置好后保存;
      • 找到刚新建的JniTest文件,右键->External tools->Generate Jni Header,即可在jin目录生成头文件了。

    上面生成的头文件如下(cn_rainmonth_ndkdemo_JniTest.h):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class cn_rainmonth_ndkdemo_JniTest */

    #ifndef _Included_cn_rainmonth_ndkdemo_JniTest
    #define _Included_cn_rainmonth_ndkdemo_JniTest
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
    * Class: cn_rainmonth_ndkdemo_JniTest
    * Method: get
    * Signature: ()Ljava/lang/String;
    */
    JNIEXPORT jstring JNICALL Java_cn_rainmonth_ndkdemo_JniTest_get
    (JNIEnv *, jobject);

    /*
    * Class: cn_rainmonth_ndkdemo_JniTest
    * Method: add
    * Signature: (II)I
    */
    JNIEXPORT jint JNICALL Java_cn_rainmonth_ndkdemo_JniTest_add
    (JNIEnv *, jobject, jint, jint);

    #ifdef __cplusplus
    }
    #endif
    #endif
  3. 编写JniTest.cpp文件
    在jin目录新建JniTest.cpp文件,引入上面生成的头文件,开始编写c++文件内容,如下:

    1
    2
    3
    4
    5
    //
    // Created by Randy on 2019-10-22.
    //
    #include <jni.h>
    #include "cn_rainmonth_ndkdemo_JniTest.h"

/*

  • Class: cn_rainonth_ndkdemo_JniTest
  • Method: get
  • Signature: ()Ljava/lang/String;
    */
    extern “C” JNIEXPORT jstring JNICALL Java_cn_rainmonth_ndkdemo_JniTest_get

     (JNIEnv *env, jobject clazz) {
    

    return env->NewStringUTF(“Hello World from C”);
    }

/*

  • Class: cn_rainonth_ndkdemo_JniTest

  • Method: add

  • Signature: (II)I
    */
    extern “C” JNIEXPORT jint JNICALL Java_cn_rainmonth_ndkdemo_JniTest_add

     (JNIEnv *, jobject clazz, jint a, jint b) {
    

    return a + b;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    我在编写的时候,发现没有代码补全,这里介绍下如何让代码补全,后面会介绍Android Studio 支持C++代码提示的方法,参见后面的补充。
    5. 使用ndk build来编译项目,并生成so库
    1. 编写Android.mk文件
    Android.mk文件中定义了c++项目编译的一些配置,具体配置及代码如下:

    ```makefile
    # 当前路径
    LOCAL_PATH := $(call my-dir)

    # 清除LOCAL_XXX变量
    include $(CLEAR_VARS)

    # 原生库名称
    LOCAL_MODULE := jnitest-lib

    # 原生代码文件
    LOCAL_SRC_FILES =: JniTest.cpp

    # 编译动态库
    include $(BUILD_SHARED_LIBRARY)
    1. 编写Application.mk

      Application.mk用来指定生成的.so库的名称,以及支持的ABI类型,代码如下:

      1
      2
      3
      4
      5
      # 原生库名称
      APP_MODULES := jnitest-lib

      # 指定机器指令集,armeabi mips mips64不再支持了
      APP_ABI := armeabi-v7a arm64-v8a x86 x86_64
    2. 生成so库
      在jni目录上右键,执行External Tools的ndk-build,就可以在main下面生成libs和obj目录了,libs目录下就是我们想要的so文件内容

      1. 使用

项目的MainActivity如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package cn.rainmonth.ndkdemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.TextView;

import cn.rainmonth.ndkdemo.R;

/**
* 参考:https://juejin.im/post/5a67dcdb518825732c53b338
*/
public class MainActivity extends AppCompatActivity {

TextView tvHello;
TextView tvAdd;
TextView tvResult;

// 加载so库
static {
System.loadLibrary("jnitest-lib");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

tvHello = findViewById(R.id.tv_hello);
tvAdd = findViewById(R.id.tv_add);
tvResult = findViewById(R.id.tv_result);

final JniTest jniTest = new JniTest();

tvHello.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 调用native的get方法
tvResult.setText(jniTest.get());
}
});

tvAdd.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 调用native的add方法
tvResult.setText(String.valueOf(jniTest.add(5, 3)));
}
});
}
}

补充

java 方法和Native方法关联方法

有静态关联和动态关联两种方法,详细内容参考:Android JNI初步Java方法和native方法关联

Android Studio开启c++代码补全

  • 先在jni目录编写Android.mk文件,内容同上面的Android.mk;
  • 然后Android Studio,File->Link C++ Project with Gradle,在弹出的配置框中,Build System选择ndkBuild,Project Path 选择刚才新建的Android.mk文件即可。这样就把C++代码和Gradle连接起来了。
  • 选择NDK编译,通过Android Studio的 Build菜单下的MakeProject即可完成C++和Java的关联;
  • 打包so文件方式,切换到jni目录,直接运行命令:ndk-build

注意:上面的操作其实相当于在app moudle的build.gradle的android闭包中添加如下代码:

1
2
3
4
5
6
// 相当于执行Link C++ Project with Gradle
externalNativeBuild {
ndkBuild {
path file('src/main/jni/Android.mk')
}
}

总结

这只是NDK开发的第一步,其中External Tools相关的操作能帮我们省去不少时间,后面讲通过实际项目来进行NDK开发的实践。