热门标签 | 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++的工程,并将它改成了我们自己的东西感觉非常好!


推荐阅读
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
  • Mono为何能跨平台
    概念JIT编译(JITcompilation),运行时需要代码时,将Microsoft中间语言(MSIL)转换为机器码的编译。CLR(CommonLa ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • Java在运行已编译完成的类时,是通过java虚拟机来装载和执行的,java虚拟机通过操作系统命令JAVA_HOMEbinjava–option来启 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了在Mac上安装Xamarin并使用Windows上的VS开发iOS app的方法,包括所需的安装环境和软件,以及使用Xamarin.iOS进行开发的步骤。通过这种方法,即使没有Mac或者安装苹果系统,程序员们也能轻松开发iOS app。 ... [详细]
  • 本文介绍了在Android Studio中使用命令行build gradle的方法,并解决了一些常见问题,包括手动配置gradle环境变量和解决External Native Build Issues的方法。同时提供了相关参考文章链接。 ... [详细]
  • 初识java关于JDK、JRE、JVM 了解一下 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文详细介绍了Android中的坐标系以及与View相关的方法。首先介绍了Android坐标系和视图坐标系的概念,并通过图示进行了解释。接着提到了View的大小可以超过手机屏幕,并且只有在手机屏幕内才能看到。最后,作者表示将在后续文章中继续探讨与View相关的内容。 ... [详细]
  • Question该提问来源于开源项目:react-native-device-info/react-native-device-info ... [详细]
  • Java面试题系列:将面试题中比较经典和核心的内容写成系列文章持续在公众号更新,可巩固基础知识,可梳理底层原理,欢迎大家持续关注【程序新视界】。本篇为面试题系列第2篇。常见面试问 ... [详细]
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社区 版权所有