热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidNDK开发的一点尝试

写在前面笔者是一个“原始”的C++开发者,对Java编程虽说不上抵触但也没有C++那么顺手。而且,作为一个游戏引擎,不管是在什么地方,效率总是第一位的,尤其是在移动平台这样资源吃紧

《Android NDK开发的一点尝试》

写在前面

笔者是一个“原始”的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在整个系统中的位置:

《Android NDK开发的一点尝试》

什么是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,在弹出的对话框中按照下图进行设置:

《Android NDK开发的一点尝试》

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

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

《Android NDK开发的一点尝试》

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

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

打开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函数的:

《Android NDK开发的一点尝试》

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文件:

《Android NDK开发的一点尝试》

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

修改实现

先照葫芦画瓢,在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();

编译运行,一切正常:

《Android NDK开发的一点尝试》

接着,我们把这个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();
}

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

偷天换日

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

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

《Android NDK开发的一点尝试》

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

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

另一种使用NDK开发的方法

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

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

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

《Android NDK开发的一点尝试》

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

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

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

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

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

在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文件:

《Android NDK开发的一点尝试》

《Android NDK开发的一点尝试》

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

《Android NDK开发的一点尝试》

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

总结

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


推荐阅读
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
  • 在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的步骤和方法
    本文介绍了在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的详细步骤和方法。首先需要下载最新的Java SE Development Kit 9发行版,然后按照给出的Shell命令行方式进行安装。详细的步骤和方法请参考正文内容。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 如何在跨函数中使用内存?
    本文介绍了在跨函数中使用内存的方法,包括使用指针变量、动态分配内存和静态分配内存的区别。通过示例代码说明了如何正确地在不同函数中使用内存,并提醒程序员在使用动态分配内存时要手动释放内存,以防止内存泄漏。 ... [详细]
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 实现一个通讯录系统,可添加、删除、修改、查找、显示、清空、排序通讯录信息
    本文介绍了如何实现一个通讯录系统,该系统可以实现添加、删除、修改、查找、显示、清空、排序通讯录信息的功能。通过定义结构体LINK和PEOPLE来存储通讯录信息,使用相关函数来实现各项功能。详细介绍了每个功能的实现方法。 ... [详细]
  • 设计模式——模板方法模式的应用和优缺点
    本文介绍了设计模式中的模板方法模式,包括其定义、应用、优点、缺点和使用场景。模板方法模式是一种基于继承的代码复用技术,通过将复杂流程的实现步骤封装在基本方法中,并在抽象父类中定义模板方法的执行次序,子类可以覆盖某些步骤,实现相同的算法框架的不同功能。该模式在软件开发中具有广泛的应用价值。 ... [详细]
author-avatar
温温
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有