NDK 与 JNI

引言

Android 开发应用程序, Google 提供了两种开发包:SDK 与 NDK。 相较于 SDK 拥有了很多的参考资料和文章,NDK 的资料要少很多。

Android 系统在一开始就支持使用 C/C++ 进行开发,因为 Android 的 SDK 主要是基于 Java 的,所以导致了在用 Android SDK 进行开发的工程师都必须使用 Java 进行开发。但是 Android 一开始就支持使用 JNI 的编程方式,也就是第三方应用可以通过 JNI 调用自己的 C 动态库。于是诞生了 NDK。

什么是 NDK

NDK 的全称是 Native Develop Kit。 官网的解释如下。

The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.

Android 的开发语言是 Java ,同时 Android 也是基于 Linux 系统的,因此其很多核心库是 C/C++ 开发的。 NDK 本身其实是一个交叉工作链,包含了 Android 上的一些库文件,同时为了方便编译 C/C++, NDK 提供了一些脚本。一般情况下,NDK 会把 C/C++ 编译成 .so 文件,然后在 Java 中调用。相对于直接使用 Java SDK 进行开发,使用 NDK 就会更麻烦复杂些。

为何要用 NDK

使用 NDK 来进行开发都比直接使用 SDK 开发要来的麻烦,但是其仍然被提供,且一直存在,必然是有一定的原因的。如:

  1. 重复使用现存的库,可以降低耦合度。
  2. 某些情况下,性能会更好。
  3. 能直接使用很多第三方使用 C/C++ 提供的库。
  4. 不依赖于 Dalvik Java 虚拟机的设计。
  5. 代码的保护。使用编译后的 C/C++ 比使用 Java 的 APK 反编译难度大。

NDK 到 so

一个 Java 应用程序往下走有两条路:

  1. 是通过 SDK API 直接进入 Java framework。
  2. 是通过 JNI 调用 Native 方法,再通过 NDK API 成为 C Framework。

什么是 JNI?

Java Native Interface

翻译过来好像是 Java 本地接口

看了这个全称和翻译也还是不懂,那么先解释一下其中的一个关键字 Native 。什么是 Native

什么是 Native ?

Native 方法是一类方法的总称,如果一个方法是由非 Java 的其他语言(如 C/C++)实现的,同时 Java 的代码又去调用此方法,则这个方法就是一个 Native 方法。在 Java 中定义一个 Native 方法时,并不会实现这个方法,就像定义一个接口,因为他们是用其他语言来实现的。

JNI 的作用?

为什么会出现 JNI 呢?

那么在 Java 发明之前,写代码基本上就是用 C/C++ 。因此那时候已经有很多现成的写好的代码,而当 Java 来了的时候,程序员就不想重复造轮子,因此他们就想直接利用之前的轮子。

于是就需要出现 JNI 这么一个玩意,通过这个中间件, Java 程序员就可以在 Java 的程序中直接调用一些其他语言实现的方法(这就是前面说的 Native 方法)。即可以在 Java 代码中调用 C/C++ 等语言的代码或者在 C/C++ 代码中调用 Java 代码。

同时,有些功能使用 Java 代码编写可能会有性能问题,并且反编译代码的安全性问题,因此在某些情况下就需要使用 C/C++ 来配合 Java 来开发了。

Java 调用 C/C++Java 语言里面本来就有的,并非 Android 自创的,一般的 Java 程序使用的 JNI 标准可能和 Android 不一样,AndroidJNI 更简单。由于 JNIJVM 规范的一部分,因此可以将我们写的 JNI 的程序在任何实现了 JNI 规范的 Java 虚拟机中运行。

开发 JNI 程序会受到系统环境限制,因为用 C/C++ 语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和 CPU 指令集,而且各个平台对标准 C/C++ 的规范和标准库函数实现方式也有所区别。这就造成了各个平台使用 JNI 接口的 Java 程序,不再像以前那样自由的跨平台。如果要实现跨平台, 就必须将本地代码在不同的操作系统平台下编译出相应的动态库。

Java 层如何使用 JNI

Java 要使用 JNI 需要两个步骤

  1. 加载动态库。
  2. 使用 Native 关键字声明与 JNI 层对应的函数。

加载动态库

1
System.loadLibrary("动态库的名称")

直接写名称就好,不用带后缀。如 media_jni 系统会自动根据不同平台进行转化,如 Windows 下就是 media_jni.dll 。Linux 就是 media_jni.so

JNI 层调用 Java 层

调用方式

JNI 层调用 Java 层函数时,需要根据 Java 层的函数来确定调用的函数的名字和签名

例如,下面的 Java 层的函数如下:

1
2
3
public void onSuccess(String msg){
......;
}

则 JNI 层在调用 onSuccess 函数时的代码如下:

1
2
3
4
5
6
7
8
9
10
11
extern "C" JNIEXPORT void JNICALL
Java_com_example_lib_JniThreadDemo_callJavaMethodOnCPPMainThread(JNIEnv *env, jobject jobj) {
jclass jclz = env->GetObjectClass(jobj);
//确定需要调用的函数
jmethodID jmethod = env->GetMethodID(jclz, "onSuccess", "(Ljava/lang/String;)V");
char *msg = "Msg From C++ Thread";
jstring jmsg = env->NewStringUTF(msg);
//调用 Java 层函数
env->CallVoidMethod(instance, jmethod, jmsg);
env->DeleteLocalRef(jmsg);
}

如上的代码中,有下面两点就可以进行调用了。

  1. 函数的名字为:“onSuccess ”。
  2. 函数的签名为: “(Ljava/lang/String;)V”。

签名获取

获取函数的名字固然比较简单,而获取函数的签名就有点点难写。有两种方法可以得到。

方法1,通过 “javap -s” 命令获取

要想获取某个字节码下面的所有的函数的签名信息,可以输入如下命令。

1
javap -s [path]/xxx.class

然后终端中就会打印其中所有的函数的情况, descriptor 关键字后面的字符串就是。

方法2,查询函数映射表

签名方法参数类型对应表

Java 类型 类型签名
boolean Z
byte B
char C
short S
int I
long L
float F
double D
void V
L全限定名; 如 String —-> Ljava/lang/util/String;(有分号)
数组 [类型签名, 比如 byte[] —-> [B

参数就是用 () 括起来,返回值就是跟在 () 外面。注意类的签名后面都是有分号的。此外并没有其他的任何分割符或者空格。

JNI 原理

Java 语言的执行环境是 Java 虚拟机(JVM),JVM 其实是主机环境中的一个进程,每个 JVM 都在本地环境中有一个 JavaVM 结构体,该结构体在创建 Java 虚拟机时被返回,在 JNI 环境中创建 JVM 的函数为 JNI_CreateJavaVM。

太复杂了,我暂时选择放弃。

第一个 NDK 程序

鉴于网上找到的 第一个 NDK 程序 教程都比较老旧了,要么没有使用 Android Studio,要么使用的版本比较旧,一些配置和界面早已经过时了,所以这里记录一下自己搭建的过程。如果有机会的话,也可以帮助有需要的人。

新建 demo 项目

配置 Android Studio NDK 和 CMake

参照官方教程,完成此处的配置。打开一个普通的(非 NDK) Android 项目。然后参考下面的步骤。

  1. 在项目中选择 Tools > SDK Manager
    SDK Manager
  2. 选择 NDK(Side by Side) 和 CMake 。
    SDK Tools 选择 NDK 和 CMake
  3. 点击下方的 OK , 此时系统会显示一个对话框,告诉您 NDK 软件包占用了多少磁盘空间。再次点击弹出来的 OK 。然后就会开始安装,等待其安装完毕,点击 Finish

至此,完成 Android Studio 的 NDK 和 CMake 配置。

创建项目

首先点击创建新项目(File > New > New Project),选择如图所示的 Native C++ 项目,点击下一步 Next

创建新的 Native C++项目

然后设置项目名字和包名。点击下一步 Next

设置项目名字和包名

然后 C++ 标准我们保持默认,直接 Finish

运行 demo

debug1

有些同学这个时候可能会看到下方的 console 里面有这么一个报错。

Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

SunCertPathBuilderException 报错

但是没关系,我们打开项目下面的 build.gradle (项目下面的那个哈,就是 Android Studio 打开后面会显示成 build.gradle(NDK_TEST) 而不是 build.gradle(:app)) ,然后添加下面的源,并修改 "com.android.tools.build:gradle:4.1.2"3.5.0

修改 gradle

代码如下, build.gradle

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
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()

maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'}

}
dependencies {
// classpath "com.android.tools.build:gradle:4.1.2"
classpath "com.android.tools.build:gradle:3.5.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

然后点击右上角的同步 (Sync Project With Gradle File) 。

sync grade

然后控制台终端就会开始同步。

开始同步

debug2

可能有些同学还会遇到下面的问题,报错 CMake 的版本不对或者没找到对应版本,这个是小问题,直接点击终端中的蓝色字体的部分, Android Studio 就会自动帮我们装,等待装好后就点击 Finish 就好了。然后再次点击右上角的同步 (Sync Project With Gradle File) 。可能还是会报错版本不一样,那就再次点击终端中的蓝色字体等 Android Studio 给切换了版本后,再次点击右上角的同步 (Sync Project With Gradle File) 。

CMake 报错

直到最后,终端中显示 build 成功。

build 成功

展示 demo

由于新建的 demo 程序,Android Studio 已经自动生成了 demo 代码。我们直接跑就是了。

然后连接上自己的设备,点击 build 编译运行。就可以在设备上看到 demo 打开运行了。如果还是报错不得行的话,那就试试切换一下 Android Studio 的 jdk 的版本吧。修改方法的想法来自于此博客的最后一个方法。修改的方法来自与官方文档 Android Studio 切换 jdk 版本

NDK demo

最后,祝你顺利。

编写自己的 NDK 程序

想要学会还是得自己动手整一个自己的 demo 出来,才能够记忆深刻,才能够理解。在上面的新建 demo 中进行修改。

修改 activity_main.xml 文件

将原来的内容完全删掉,直接替换为下面的内容。

activity_main.xml 可以根据自己的需要进行修改。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.gebilaolitou.jni.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<EditText
android:id="@+id/inputa"
android:hint="请输入a"
android:inputType="number"
android:layout_weight="1.0"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<EditText
android:inputType="number"
android:id="@+id/inputb"
android:hint="请输入b"
android:layout_weight="1.0"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

<TextView
android:text="请选择符号"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:text="@string/add" />

<Button
android:gravity="center"
android:layout_weight="1.0"
android:text="@string/sub"
android:id="@+id/sub"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<Button
android:gravity="center"
android:layout_weight="1.0"
android:text="@string/mul"
android:id="@+id/mul"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<Button
android:gravity="center"
android:layout_weight="1.0"
android:text="@string/div"
android:id="@+id/div"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

<Button
android:gravity="center"
android:layout_weight="1.0"
android:text="@string/pow"
android:id="@+id/pow"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

<TextView
android:id="@+id/result"
android:text="计算结果"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:textSize="20sp"
android:layout_height="wrap_content" />

<TextView
android:id="@+id/showString"
android:text="展示 使用 JNI String"
android:layout_marginTop="40dp"
android:layout_width="wrap_content"
android:textSize="20sp"
android:layout_height="wrap_content" />

</LinearLayout>

预览页面大概是这个样儿。

demo 界面

修改 MainActivity.java 文件

MainActivity.java

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.example.ndk_test;

import androidx.appcompat.app.AppCompatActivity;

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

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

// Used to load the 'native-lib' library on application startup.
static {
// System.loadLibrary("native-lib"); // 注释掉原来的这个导入 不然修改 makeflie 后,对于项目而言这个就不存在了,就会报错。
System.loadLibrary("calc"); // 导入自己的链接库,和 makefile 中要对应(后面会讲到)
}

private Button addBtn, subBtn, mulBtn, divBtn, powBtn; //定义 5 个按钮
private EditText inputA, inputB; // 两个输入框
private TextView resView; // 一个输出框
private TextView stringView; // 展示 JNI String

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

setupView();
addListener();

// // Example of a call to a native method
// TextView tv = findViewById(R.id.sample_text);
// tv.setText(stringFromJNI());
}

private void addListener(){ // 绑定监听事件
addBtn.setOnClickListener(this);
subBtn.setOnClickListener(this);
mulBtn.setOnClickListener(this);
divBtn.setOnClickListener(this);
powBtn.setOnClickListener(this);
}

private void setupView(){
addBtn=this.findViewById(R.id.add); // 绑定 5 个按钮 和 三个文本框
subBtn=this.findViewById(R.id.sub);
mulBtn=this.findViewById(R.id.mul);
divBtn=this.findViewById(R.id.div);
powBtn=this.findViewById(R.id.pow);

inputA=this.findViewById(R.id.inputa);
inputB=this.findViewById(R.id.inputb);

resView=this.findViewById(R.id.result);

stringView=this.findViewById(R.id.showString);
}

@Override
public void onClick(View v){
double res = 0;
String stringFromJNI;

String strA = inputA.getText().toString();
String strB = inputB.getText().toString();

int a = Integer.parseInt(strA);
int b = Integer.parseInt(strB);

switch (v.getId()){ // 通过 id 判断点击事件
case R.id.add:
res = add(a, b);
break;
case R.id.sub:
res = sub(a, b);
break;
case R.id.mul:
res = mul(a, b);
break;
case R.id.div:
res = div(a, b);
break;
case R.id.pow:
res = pow(a, b);
break;
}
resView.setText(""+res);

// 修改 stringView
stringFromJNI = getString(strA);
stringView.setText(""+stringFromJNI);
}

/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
// public native String stringFromJNI();

// 在这里调用 native 方法,注意这里都是 static 的,JNI 中有点小区别。
// 加法
public static native int add(int a, int b);

// 减法
public native int sub(int a, int b); // 注意这里的没有 static 关键字的 其他都有,注意看 cpp 文件中调用这个函数和其他函数的区别

// 乘法
public static native int mul(int a, int b);

// 除法
public static native double div(int a, int b);

// 乘方
public static native int pow(int a, int b);

// 展示 String
public static native String getString(String s);
}

创建 calc.cpp 文件

右键项目中的 cpp 文件夹,选择 NEW > C/C++ Source File ,创建一个自己的源文件,这里我创建的名字叫做 calc 。要和 Java 代码中导入的一致。

然后就可以开始编写代码了。

calc.cpp

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <jni.h>
#include <string>
#include <cstdio>
#include <iostream>
#include <android/log.h> // 引入 log 头文件

// 定义 tag
#define TAG "Ron_calc_cpp"
// 定义info信息
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
// 定义debug信息
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
// 定义error信息
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__)


using namespace std;

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jint JNICALL
Java_com_example_ndk_1test_MainActivity_add(JNIEnv *env, jclass obj, jint a, jint b){
LOGI("log I from add in c++");
jint c = a + b;
return c;
}

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jint JNICALL
Java_com_example_ndk_1test_MainActivity_sub(JNIEnv *env, jobject obj, jint a, jint b){ // 这里第二个参数是 jobject 类型
LOGD("log D from sub in c++");
jint c = a - b;
return c;
}

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jint JNICALL
Java_com_example_ndk_1test_MainActivity_mul(JNIEnv *env, jclass obj, jint a, jint b){ // 这里的 第二个参数 是 jclass 类型
jint c = a * b;
return c;
}

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jdouble JNICALL
Java_com_example_ndk_1test_MainActivity_div(JNIEnv *env, jclass obj, jint a, jint b){
if(b == 0){
LOGE("log E from div in c++, the divisor cannot be zero!");
return 0;
}

jdouble c = a * 1.0 / b;
return c;
}

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jint JNICALL
Java_com_example_ndk_1test_MainActivity_pow(JNIEnv *env, jclass obj, jint a, jint b){

LOGI("log I from pow in c++");

jint c = 1;
jint i = 0;
while(i < b){
c *= a;
i +=1 ;
}
return c;
}

extern "C" // 按照 C 的规则翻译函数名字
JNIEXPORT jstring JNICALL
Java_com_example_ndk_1test_MainActivity_getString(JNIEnv *env, jclass obj, jstring a){
const char* str = (env)->GetStringUTFChars(a, 0); // 将 jstring 转化为 char 数组,因为 jstring 和 c++ 的 string 有区别

if(str == NULL) {
return NULL;
}

std::cout << str << std::endl;

// 返回一个字符串
char tmpstr[100] = "return from C++, the input a = ";
jint i = 0;
jint last = 31;

while (1){
if (str[i] == '\0'){
break;
}
tmpstr[last++] = str[i++];
}
tmpstr[last++] = '\0';
jstring rtstr = env->NewStringUTF(tmpstr);

//释放资源
env->ReleaseStringUTFChars(a, str);

return rtstr;
}


解释一下

  1. JNIEXPORTJNICALL 是关键字,不需要修改。 这两个关键字中的 jstingjint 等是该方法的返回值类型,具体有个对照表,可以查看这里
  2. extern "C" 按照 C 的规则翻译函数名字。
  3. Java_com_example_ndk_1test_MainActivity_add 这类函数名,命名的规则是:
    Java_ + 包名 (以下划线分割) + 类名 (在这里类就是 Activity) + 方法名
  4. 方法的第一个参数都是 JNIEnv* 型,而第二个参数取决于 Java 中此方法是否是静态方法(有无 static 关键字),有的话就是 jobject 类型,否则就是 jclass 类型。
  5. 然后,此函数实际有哪些参数就一一写在后面,然后将类型按照对应表进行对照就行。
  6. 由于有些高级类的对应并不是一一对应的,存在些许的差异,这里就简单展示了 string 的区别,直接放进去的 jstring 类型和 c++ 中的 string 并不是同一个东西,因此需要进行一个转化,就如代码中 72 行所示,可以转化为一个 const char 而要从 c++ 构建一个 jstring 可以像 92 行所示,使用 env->NewStringUTF
  7. 使用 log 功能,没啥特别的,和 Java 中使用是一样的,直接调用就好。

修改 CMakeList.txt 文件

这个的修改也比较简单,只有两个地方需要修改。

第一处是修改 add_library

1
add_library( # Sets the name of the library. # 将我们自己的 cpp 名字写在这个地方,我注释了原来的 native-lib             # native-lib # 原来的             calc # 新的             # Sets the library as a shared library.             SHARED # 不变             # Provides a relative path to your source file(s). # 写自己的 cpp 的相对路径,这里就在默认路径下 我就直接写了名字             # native-lib.cpp # 原来的             calc.cpp ) # 新的

第二处是修改 target_link_libraries

1
target_link_libraries( # Specifies the target library. # 写自己的目标链接库的名字 需要和 java 中对应起来                       # native-lib # 原来的                       calc # 新的                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )

最后,修改完毕后的 CMakeList.txt 如下:

1
# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html# Sets the minimum version of CMake required to build the native library.cmake_minimum_required(VERSION 3.10.2)# Declares and names the project.project("ndk_test")# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library. # 将我们自己的 cpp 名字写在这个地方,我注释了原来的 native-lib             # native-lib             calc             # Sets the library as a shared library.             SHARED             # Provides a relative path to your source file(s). # 这里是一样的,写自己的 cpp 的相对路径,这里就在默认路径下 我就直接写了名字             # native-lib.cpp              calc.cpp )# Searches for a specified prebuilt library and stores the path as a# variable. Because CMake includes system libraries in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable.              log-lib              # Specifies the name of the NDK library that              # you want CMake to locate.              log )# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in this# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library. # 写自己的目标链接库的名字 需要和 java 中对应起来                       # native-lib                       calc                       # Links the target library to the log library                       # included in the NDK.                       ${log-lib} )

运行测试

在上述的所有代码都 ok 的情况下,就可以开始进行测试。链接自己的模拟器,点击 Run 。就可以看到大概下面这样的界面。

测试程序

然后就可以开始进行计算,这里输入 A=15B=0 ,然后点击除法,看看界面上返回的结果是 0.0 程序没有崩溃,这是因为我们在 calc.cpp 中对除数进行了判断。为 0 就直接返回 0。但是我们会打印一个 error 的日志。同时下方还显示了一组从 calc.cpp 中返回的关于第一个参数的信息。

除数为 0 测试

在 Android Studio 中按照我们的 TAG=Ron_calc_cpp 过滤一下日志,可以看到成功打印了如下的,除数不能为零的 error 信息。

日志查看

然后再随便测试一组数据,可以看到也是可以正常计算和显示日志的。

加法测试

日志查看

至此,第一个 NDK 程序算是成功跑通了。

参考链接

参考链接1:JNI 入门了解 https://www.jianshu.com/p/102b2d9db167

参考链接2:native 是啥 https://blog.csdn.net/qq_23994787/article/details/79066336

参考链接3:JNI 调用 Java https://www.jianshu.com/p/ec90ac9cc8d8

参考链接4:NDK 与 JNI 基础 https://www.jianshu.com/p/87ce6f565d37

参考链接5:第一个 NDK 程序 https://www.jianshu.com/p/2096fdd244b3

参考链接6:Android Studio 配置 NDK 和 CMake https://developer.android.com/studio/projects/install-ndk?hl=zh-cn

参考链接7: 解决报错方法一 https://blog.csdn.net/weixin_43766753/article/details/102527228

参考链接8: JNI 数据类型对照表 https://blog.csdn.net/afei__/article/details/80899758

参考链接9:Android Studio 创建自己的源文件 https://developer.android.com/studio/projects/add-native-code?hl=zh-cn#create-sources

参考链接10: CMakeList.txt 文件修改方式 https://blog.csdn.net/u010041075/article/details/68946334

参考链接11:添加 JNI log 信息 https://www.jianshu.com/p/3c1aff6f100f