写在前面笔者是一个“原始”的C++开发者,对Java编程虽说不上抵触但也没有C++那么顺手。而且,作为一个游戏引擎,不管是在什么地方,效率总是第一位的,尤其是在移动平台这样资源吃紧
写在前面
笔者是一个“原始”的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,因为这会增加开发过程的复杂性,得不偿失。但是如果需要进行下面的两项操作,那么它可能非常有用:
- 计算密集型应用,例如游戏或物理模拟
- 重复使用你或者其他开发者的C或C++库
游戏引擎绝对是一项计算密集型应用(不信?请百度一下全局光照),所以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。这要是每次都要输这么长的名字来调用,不的烦死啊。当然不是,我们来分析一下这个函数名的结构:
- Java:表示这个函数是在java目录下面
- com_example_administrator_hellondk:表示com.example_administrator.hellondk目录
- MainActivity:表示这个函数是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
#include
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
#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++的工程,并将它改成了我们自己的东西感觉非常好!