NDK 开发(一) —— 如何在 Android Studio 下进行 NDK 开发

android studio 教程 | 2018-12-25 15:45

在 AS 中进行 NDK 开发之前,我们先来简单的介绍几个大家都容易搞懵的概念:

1. 到底什么是 JNI,什么是 NDK?

2. 何为“交叉编译”?

先看什么是 JNI?

JNI 的全称就是 Java Native Interface,即 java 本地开发接口。可能大家和我一样,一听到接口什么的就犯懵:“我也知道这是 java 本地开发接口的意思,但它具体是个什么意思我还是搞不明白。”

其实 JNI 它就是一种协议,一说协议,那它就是对某种东西的一个规范和约束,说的好听一点就是标准化。如果你想用我这个东西,那你必须要遵守我这边的规范。像 http 协议一样,http 作为超文本传输协议,它规范了我们上网时从客户端到服务器端等一系列的运作流程。正因为如此,我们才能畅通无阻的上网。

那么换做 JNI 也一样,只不过 JNI 这个协议是用来沟通 java 代码和外部的本地代码 (c/c++) 。也就是说有了 JNI 这个协议,我们才能够随意的让 java 代码调用 C/C++ 的代码,同样 C/C++ 的代码也可以调用 java 的代码。如果没有这个协议作为支撑,那么 java 和 C/C++ 代码想要相互调用是不可能的。

下面通过两个图简单看一下 JNI 协议在系统架构中处于什么位置:

在上图中,上层绿色的部分一般都是用 Java 代码写的,下层橘黄色的部分一般都是用 C/C++ 代码写的。可以看出,正式由于有了中间 JNI 的存在我们才可以在 Application 层通过 JNI 调用下层中的一些东西。

了解了 JNI 的概念后,我们再看看 NDK,NDK(Native Development Kit) 就比较好理解了,它就是一个本地开发的“工具包”。Java 开发要用到 JDK, Android 开发要用到 SDK,那我们在 Android 中要进行 native 开发,也要用到它对应的工具包,即 NDK 。通俗的来讲,NDK 就是帮助我们可以在 Android 应用中使用 C/C++ 来完成特定功能的一套工具。

NDK 的作用有很多,我们简单的列举两个,比如:

1. 首先 NDK 可以帮助开发者“快速”开发 C(或 C++) 的动态库。

2. 其次,NDK 集成了“交叉编译器”。使用 NDK,我们可以将要求高性能的应用逻辑使用 C 开发,从而提高应用程序的执行效率。

上面提到了“交叉编译”,我们最后再解释一下什么是交叉编译。大家都知道编译器在将中间代码连接成当前计算机可执行的二进制程序时,连接程序会根据当前计算机的 CPU、操作系统的类型来转换。而根据运行的设备的不同,CPU 的架构也是不同,大体有如下三种常见的 CUP 架构:

arm 架构:主要在移动手持、嵌入式设备上。我们的手机几乎都是使用的这种 CUP 架构。

x86 架构: 主要在台式机、笔记本上使用。如 Intel 和 AMD 的 CPU 。

MIPS 架构:多用在网关、猫、机顶盒等设备。

若想在使用了基于 x86 架构 CPU 的操作系统上编译出可以在基于 arm 架构 CPU 的操作系统上运行的代码,就必须使用交叉编译。所以综上所述:交叉编译就是在一个平台下(比如:CPU 架构为 X86,操作系统为 Windows)编译出在另一个平台上(比如:CPU 架构为 arm, 操作系统为 Linux)可以执行的二进制代码。Google 提供的 NDK 就可以完成交叉编译的工作。

好了,上面的基本概念介绍完以后,我们正式进入 AS 下 NDK 开发的讲解。

1. 首先,你需要把 NDK 下载下来。下载地址:

下载完成后解压到任意目录即可(路径中不要带有中文字符)。我的就直接放在 D 盘的 ndk 目录下:

2. 在 AS 中为你的项目配置 NDK。首先新建一个 Android 工程 JNIDemo, Ctrl + shift + alt + s 打开 Project Structrue 把我们刚才下载好的 NDK 配置进去,点击 OK。

3. 配置好 NDK 后,简单的为我们的项目布局文件添加一个 TextView 和一个 Button,当点击 Button 的时候,我们通过调用底层自己写好的 C/C++ 代码来返回一个字符串,最后呈现在 TextView 上。

activity_main.xml 布局代码:

<?xml version="1.0" encoding="utf-8"?><LinearLayout   xmlns:android=""   android:layout_width="match_parent"   android:layout_height="match_parent"   android:orientation="vertical">   <TextView       android:id="@+id/textview"       android:layout_width="wrap_content"       android:layout_height="wrap_content"       android:text="Hello World!"       />   <Button       android:id="@+id/button"       android:layout_width="match_parent"       android:layout_height="wrap_content"       android:text="button"/></LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {   @Override   protected void onCreate(Bundle savedInstanceState) {       super.onCreate(savedInstanceState);       setContentView(R.layout.activity_main);       final TextView textview = findViewById(R.id.textview);       Button button = findViewById(R.id.button);       button.setOnClickListener(new View.OnClickListener() {           @Override           public void onClick(View v) {               textview.setText(JNIUtils.sayHelloFromJNI());           }       });   }}

上面代码中的 JNIUtils.sayHelloFromeJNI() 就是我们在与 MainActivity 相同的包中新建 JNIUtils 类后在里面编写的 native 方法。如下所示:

可以看到我们上面的 sayHelloFromJNI() 方法显示的是警告红色。把鼠标放到上面,它会提示我们对应的 JNI 头文件没有查找到。那么接下来我们要做的就是去生成与这个 sayHelloFromJNI() 方法所对应的头文件。

4. 生成头文件。快捷键 alt + F12 调出 AS 下的 Terminal 窗口,在 Terminal 命令行窗口中输入如下几条指令,回车:

前面两个 cd 命令没什么好说的,就是先进入当前项目的 app 目录下,然后再进入 Java 目录下。我们重点说一下最后一条命令:

javah -d ../jni com.example.zhangxudong.jindemo.JNIUtils

首先,要生成 Java 类对应的头文件我们就必须要用到 javah 这个命令,其次 -d 表示生成一个目录,那生成一个什么样的目录,具体又在哪里去生成这个目录呢?后面的 ../jni 告示了我们。../ 表示在当前目录的上一层目录,我们当前在 Java 目录下,那么它的上层目录就是 main 目录了。而 jni 就表示我们生成的目录的名称。所以整个 ../jni 就表示在 main 目录下生成一个名为 jni 的目录。

最后一个

com.example.zhangxudong.jindemo.JNIUtils就是我们在上面新建的 JNIUtils 的完整类名了。执行完这几条指令后,刷新一下目录我们就可以在 main 目录下看到 jni 这个目录,并且在它里面生成了我们 JNIUtils 类所对应的头文件。进入头文件中,代码是如下这个样子:

/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_zhangxudong_jnidemo_JNIUtils */#ifndef _Included_com_example_zhangxudong_jnidemo_JNIUtils#define _Included_com_example_zhangxudong_jnidemo_JNIUtils#ifdef __cplusplusextern "C" {#endif/* * Class:     com_example_zhangxudong_jnidemo_JNIUtils * Method:    sayHelloFromJNI * Signature: ()Ljava/lang/String; */JNIEXPORT jstring JNICALL Java_com_example_zhangxudong_jnidemo_JNIUtils_sayHelloFromJNI  (JNIEnv *, jclass);#ifdef __cplusplus}#endif#endif

5. 头文件生成以后,我们就需要编写我们的 C/C++ 代码了。

右键 jni 目录 → new → C/C++ Source File

输入要新建的 C/C++ 文件名称 JNIHello,这里我们用 C++ 来编写,所以 Type 为 .cpp,如果你选择用 C 来编写,那么 Type 选为 .c,点击 ok 。这里说一下,在我们进行 NDK 开发的时候,选择用 C 还是 C++,在编写代码的时候除了 C 和 C++ 基本的语法不同外,还是有许多不同地方需要注意。我们后续会慢慢介绍。这里先默认跟着我的步骤来。

JNIHello.cpp 代码如下:

#include "com_example_zhangxudong_jnidemo_JNIUtils.h"JNIEXPORT jstring JNICALL Java_com_example_zhangxudong_jnidemo_JNIUtils_sayHelloFromJNI        (JNIEnv *env, jclass jclass){return env->NewStringUTF("Hello World From JNI!!!!!");}

可以看到我们首先需要把原来生成的 JNIUtlis 对应的头文件引入进来,下面的代码基本都是从 com_example_zhangxudong_jnidemo_JNIUtils.h 中复制粘贴过来的一部分,然后稍加修改。修改的地方主要有 sayHelloFromJNI 的两个参数和里面的简单实现,参数方面就是加了 env 和 jclass 两个字段。函数里面的实现呢,就是简单的返回一个字符串 “Hello World From JNI!!!!!”,至于为什么这么写,我会在下一篇文章进行讲解,大家现在就需要知道如果要在这里返回一个字符串就必须要通过 env->NewStringUTF("xxxxxx"); 这行代码。

6. 上面的搞定以后,我们需要在 app 的 build.gradle 中的 defaultConfig 中加入如下代码。它表示项目在编译时生成的动态库的名字。

最后,我们还需在 JNIUitls 中加载我们生成的动态库:

public class JNIUtils {    static {        System.loadLibrary("JNIHello");    }    public static native String sayHelloFromJNI();}

我们把加载动态库的代码放到静态代码块中,就是表示在 JNIUtils 这个类在加载的时候就去加载我们的动态库。

7. 经过上面的5步,关于如何在 AS 中进行简单的 NDK 所需要的步骤差不多就讲完了。不过还有最后一点需要注意。到这里我们基本就可以执行一下我们的项目了,现在运行一下项目试一试......不出意外的话项目是 build 不成功的,它会报如下的错误:

Error:Execution failed for task ':app:compileDebugNdk'.> Error: Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio.  Please switch to a supported build system.  Consider using CMake or ndk-build integration. For more information, go to:   -ui/add-native-code.html#ndkCompile   To get started, you can use the sample ndk-build script the Android   plugin generated for you at:   E:\JNIDemo\app\build\intermediates\ndk\debug\Android.mk  Alternatively, you can use the experimental plugin:   https://developer.android.com/r/tools/experimental-plugin.html  To continue using the deprecated NDK compile for another 60 days, set  android.deprecatedNdkCompileLease=90556 in gradle.properties

因为我这里用的是 Android Studio3.0,报出的这个错误很可能和原来版本的 AS 不同,以前出现类似错误的时候,我们的解决方案一般都是在 gradle.properties 中添加一行这样的代码:android.useDeprecatedNdk=true 就搞定了。但是 AS 换为 3.0 后你可以再试一下这种方案,肯定是不行的,它会提示你

“Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio.  Please switch to a supported build system.”

大体意思就是最新的 AS 已经不支持 useDeprecatedNdk 这个标记了,并且在后续版本的 AS 中,它将被移除。所以我们新的解决方案就是按照它的提示在 gradle.properties 中添加

android.deprecatedNdkCompileLease=90556

这行代码。

最后我们运行一下项目,点击 button ,效果如下。可以看到,我们成功的通过 java 代码调用了 C++ 的代码,并返回 Hello World From JNI!!!!! 这个字符串。

那我们生成的动态库 (.so 文件) 都在哪里呢?

app→build→intermediates→ndk→debug→libs

可以看到各个平台对应的动态库都已经生成了。

关于 NDK 在 Android Studio 下的开发先讲到这里,希望大家多多支   。