Android NDK程序员

Android NDK开发的一点尝试

2018-04-06  本文已影响73人  闪电的蓝熊猫

写在前面

笔者是一个“原始”的C++开发者,对Java编程虽说不上抵触但也没有C++那么顺手。而且,作为一个游戏引擎,不管是在什么地方,效率总是第一位的,尤其是在移动平台这样资源吃紧的环境下。所以呢,也算是给自己的一点安慰,可以尝试在Android进行C++开发了。

一些基本却极为重要的概念

一般情况下,当你使用NDK开发的时候,你都不会只接触到NDK这一个名词。你会听到一系列的陌生词语,例如JNI、NDK、交叉编译等等。这些东西都是非常重要的概念,对我们理解NDK开发有重大帮助,所以,笔者在这里多啰嗦一些,讲讲这些概念:

什么是JNI?

JNI的全称是Java Native Interface,Java原生接口。一脸懵逼!“我知道JNI全称是Java原生接口,可我还是不能理解这东西到底有什么用。”你可能会这样大吼。别急,看到接口我们首先想到的是什么?没错,是API,应用程序接口,这是我们熟悉的东西,JNI和API本质上是一样的,它是供给别的代码调用的一个或一组函数。我们首先使用C++写出了一些函数,然后将这些函数在Java类中再声明一次(加上关键字native),这样Java类中的函数和C++中的函数就匹配(勾搭?)到一起了,我们使用Java类中的函数,其实就是使用C++中的函数。这个在Java类中声明的函数就是一个JNI。

下面这张图很好地展示了JNI在整个系统中的位置:


什么是NDK?

NDK的全称是Native Development Kit,原生开发工具包。这就很容易理解了,就是一套开发工具而已。

在谷歌官方的指南中,并不提倡大多数初学Android编程者使用NDK,因为这会增加开发过程的复杂性,得不偿失。但是如果需要进行下面的两项操作,那么它可能非常有用:

游戏引擎绝对是一项计算密集型应用(不信?请百度一下全局光照),所以NDK开发势在必行。

什么是交叉编译?

简单来说,交叉编译就是在一个平台(例如平常开发时的windows)上生成另一个平台(例如Android)上的可执行代码。你一定会觉得奇怪,编译就编译呗,为什么还要加一个交叉?其实,这就是一种称呼上的不同罢了。

假如我们要在Windows系统上编译在Windows系统上运行的程序,这叫做本地编译。在Windows系统上编译在Android系统上运行的程序就叫交叉编译。那么为什么不在Android系统上编译在Android系统上运行的程序呢?原因很简单,因为Android系统上不允许或者不能够安装我们需要的编译器,因为Android系统资源贫乏,不足以支持编译。所以,我们使用交叉编译的方法来弥补这点不足。

开始我们的开发之旅

假设你已经下载并完成Android Studio的安装,现在需要新建一个工程。打开Android,选择start a new project,在弹出的对话框中按照下图进行设置:



注意一定要勾选Include C++ support!

一路点击Next按钮,直到下面这个界面,选择Empty Activity:



继续点击Next,直到最后点击Finish完成创建。

创建完成之后,你的AS很可能会包如下的错误:



直接点击Install NDK and sync project,等待其下载完成。或者,你也可以像我一样,点击file->Project structure,打开Project Structure对话框:



按照上图中的步骤,手动配置NDK的路径。完成之后,等待Gradle解析项目工程。

Gradle解析完毕后,点击File->Settings,打开Settings对话框。在左侧栏中找到Android SDK,点击之后切换到SDK Tools标签页,在CMake和LLDB两项前面打钩,点击Apply按钮,等待AS将CMake和LLDB下载安装完毕。



这些步骤都完成之后,直接点击运行按钮,选择调试用的模拟器,我们一行代码都不用写就可以获得一个使用了NDK的APP:



一脸懵逼!怎么啥都没做就已经完成了?别急,我们来看看AS都替我们完成了哪些工作。首先展开左侧的目录,将native-lib.cpp和MainActivity两个文件暴露出来:

打开native-lib.cpp文件,我们赫然发现,APP中显示的Hello from C++文字居然在这个地方。

在看一下函数名,我天,这么长:Java_com_example_administrator_hellondk_MainActivity_stringFromJNI。这要是每次都要输这么长的名字来调用,不的烦死啊。当然不是,我们来分析一下这个函数名的结构:

可以看出来,为了区分C++的函数,AS在其函数名之前加上了很多定位方式,确保其命名的唯一性,打开MainActivity文件,我们可以看到在MainActivity类中是如何定义stringFromJNI函数的:



public native String stringFromJNI()这一句定义了在MainActivity类中的原生函数,这个函数对应了cpp文件中的Java_com_example_administrator_hellondk_MainActivity_stringFromJNI定义。

下面的代码:

    static {
        System.loadLibrary("native-lib");
    }

表示MainActivity类需要加载native-lib模块,就是我们native-lib.cpp文件,展开左侧的目录,你也可以看到编译之后的.so文件:


好,文件都看懂了,现在开始搞事情!

修改实现

先照葫芦画瓢,在native-lib.cpp中定义一个我们自己的函数,在MainActivity中声明并且调用:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_administrator_hellondk_MainActivity_stringFromMyJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from My C++";
    return env->NewStringUTF(hello.c_str());
}
...
tv.setText(stringFromMyJNI());
...
public native String stringFromMyJNI();

编译运行,一切正常:


接着,我们把这个stringFromMyJNI函数放到另一个cpp文件中,并且不在MainActivity类中声明原生函数,定义一个新类声明原生函数。

右击cpp文件夹,选择New->C/C++ Source File,将新文件命名为hellondk.cpp。把头文件和stringFromMyJNI函数复制过去,我们的hellondk.cpp就变成了这个样子:

//
// Created by Administrator on 2018/4/6.
//
#include <jni.h>
#include <string>

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_administrator_hellondk_NDKUtil_stringFromMyJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from My C++";
    return env->NewStringUTF(hello.c_str());
}

接着,右击com.example.administrator.hellondk文件夹,选择New->Java class,在弹出的对话框中将Java类命名为NDKUtil,点击确定。

然后,将static块和stringFromMyJNI函数的声明赋值到NDKUtil类中,在public native 之间添加一个static关键字,表明这是Java类的静态函数。完成之后,NDKUtil.java文件就像这个样子:

package com.example.administrator.hellondk;

/**
 * Created by Administrator on 2018/4/6.
 */

public class NDKUtil {
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    public static native String stringFromMyJNI();
}

编译运行,嗯?怎么报错了?



经过一阵仔细的排查,终于发现了问题,我们的hellondk.cpp文件没有编译,调用的时候无法找到这个函数,所以才崩溃。知道原因就好办了,AS使用的是CMake编译器,找到CMakeLists.txt文件打开,在里面添加一行:



点击右上角的Sync now,等待片刻之后,再次编译运行,发现这次运行成功了:

偷天换日

既然不用native-lib中的函数了,干脆把这个文件去掉,把生成的库名字也改掉,换成我们自己的库名(比如hellondk-lib),这样就神不知鬼不觉了!

说干就干,把native-lib.cpp文件删除,对CMakeLists.txt文件做如下修改:




最后,在MainActivity类中将stringFromJNI函数的相关内容删除,运行APP:


非常好,我们的偷天换日计划成功了!

不知不觉中,我们完成了NDK开发的一些初步尝试,想想还有点小兴奋,你是不是已经迫不及待想看后面的东西了?

另一种使用NDK开发的方法

另一种NDK开发的方法,说的自然就是之前一直用的ndk-build编译方法。与我们之前介绍的方法相比,本质的区别就是使用的编译器不同。Include C++ support方法使用的是CMake编译器,细心的读者肯定已经发现了。ndk-build使用的编译器是我们下载的ndk包里的,要使用它,我们还需要进行一些配置。

首先,创建一个普通的Android项目(不勾选Include C++ support),取名为NDKJni。打开工程之后,选择File->Settings,定位到下面的标签:



点击+按钮,打开设置框。完成设置:



图中,Program表示要调用的工具的位置,Parameters表示调用时传递给javah工具的参数。我们设置的参数表示javah生成的代码放在jni目录下面,采用UTF-8的字符格式。

完成之后再次点击+号,完成设置:



这是使用ndk-build工具的命令,Program要定位到我们的ndk-build工具,Working directory不用说,自然是当前的main目录下。

配置完成后,首先定位到activity_main.xml文件,加上android:id="@+id/sample_text"这一行代码:


然后,到MainActivity中添加JNI的声明:

public native String stringFromJNI();这一行代码用来声明Java原生函数。static{}这一块代码用来加载原生库,我们的库名取为ndkjni。

哦,别忘了配置NDK路径,参考上面的配置方式。

右击MainActivity,选择javah-jni工具:



成功之后,在main文件夹下会多一个jni文件夹,里面有javah生成的头文件,这个头文件唯一的作用就是帮助我们定义实际的函数(毕竟函数名实在太长了!):



在jni目录下新建一个C++文件,取名为ndkjni.cpp。文件内容如下:
//
// Created by Administrator on 2018/4/11.
//
#include <jni.h>

#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_administrator_ndkjni_MainActivity
 * Method:    stringFromNDKJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_administrator_ndkjni_MainActivity_stringFromNDKJNI (JNIEnv * env, jobject thiz) {
    return env ->NewStringUTF("This is NDKJNI");
}

#ifdef __cplusplus
}
#endif

从javah为我们生成的头文件中把函数声明拷贝出来,给参数取好名字,添上血肉,我们的函数就完成了。

这时候,打开MainActivity文件,发现我们的stringFromNDKJNI还是红色的,说明这个函数和C++文件中的那个函数还没有关联起来。怎么办呢?别急,我们的准备工作还没有做好。

右击jni目录,选择New->File,新建两个文件,取名为Android.mk和Application.mk。Android.mk中,添加如下代码:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ndkjni
LOCAL_SRC_FILES := ndkjni.cpp

include $(BUILD_SHARED_LIBRARY)

Application.mk中,只要添加一行就可以了:

APP_ABI := all

关于语法方面的内容,可以参考google官方文档,这里不多废话。完成上面两个文档之后,右击jni目录,选择Link C++ Project with Gradle标签。在弹出的对话框中,定位到我们刚刚创建的Android.mk文件:



操作完成之后,我们就可以看到,我们的stringFromNDKJNI()函数不再是红色的了。说明函数已经看对眼了!好,我们来尝试运行一下:

非常完美,一个错误都没有。

总结

本文中,我们首先了解了JNI、NDK、交叉编译这三个基本概念,这是NDK开发的基础,类似楼房地基一样的重要东西。然后,我们创建了一个包括C++的工程,并将它改成了我们自己的东西感觉非常好!

上一篇下一篇

猜你喜欢

热点阅读