如何使用Android NDK和C++创建一个简单的路径推荐应用程序
在这个,教程中,我们将创建一个简单的应用程序来计算两点之间的最短路线。
我们将使用Dijkstra的最短路径优先(DSPF)算法来实现这一功能。
最终的应用程序将出现在下面的图片中。

为了实现用户界面的动态流量变化,我们将需要尽可能地实现本地化。这就是为什么我们要使用本地开发工具包。
请注意,这样的功能也可以集成到汽车仪表盘上(这里不需要执行延迟)。
我们将使用C++进行路径计算,然后用Kotlin向用户显示结果。
先决条件
要想继续学习,你需要具备以下技术的一些知识。
- C++
需要关注的关键领域包括:namespaces,pointers, 和string methods 。在使用JNI时,指针很重要,因为它们有助于内存管理和应用优化。
- Kotlin
我们将使用数据绑定来显示一个具有动态内容的简单文本小部件。
- Java (不是很需要)
这将需要了解JNI的工作。
此外,你需要Android Studio或IntellijIDEA(为Android开发配置)。
Android NDK和JNI的简要概述
JNI(Java Native Interface)是一种用于在Java字节码和其他本地语言(如C)之间进行通信的工具。
JNI允许我们用其他语言编写程序,并使其与可以在Java虚拟机上运行的语言(如Kotlin、Clojure)进行通信。
因此,我们可以实现使用基于JVM的语言不容易做到的复杂功能。这些功能包括与内存、硬件等低级组件进行通信。此外,使用本地语言编写的应用程序/产品运行速度更快。
NDK是一种工具,可以促进语言和JNI的连接。此外,它还允许我们调试和运行我们的应用程序。
关于这些技术的更多信息,请关注下面列出的。
- [MindOrks]
- [ProAndroidDev]
安装所需的组件
为了编写这些本地应用程序,我们必须安装下面列出的工具。
-
NDK
-
CMake是一个用于管理构建过程的工具。
安装这些组件很简单。在你的IDE(Android Studio或IntellijIDEA)中,访问SDK管理器,然后点击SDK工具标签。
接下来,选中NDK(并排)和CMake选项,然后点击OK 按钮开始安装。

要访问SDK管理器,请点击工具标签->Android->SDK管理器。
安装完成后,修改你的应用程序的Gradle文件defaultConfig ,指定你所使用的NDK版本。
defaultConfig {
applicationId "<your-package-name>"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
//add this line. this is what you will only modify
ndkVersion "<version-you-installed>"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ''
}
}
}
确保你在运行应用程序之前更新gradle文件。如果不这样做,可能会导致Android Studio下载另一个NDK版本
你可以在your-installation-path/Android/Sdk/ndk ,检查所安装的NDK版本。你会看到一个用版本号命名的文件夹。
创建一个示例应用程序。
开始一个新的项目创建过程。在手机和平板电脑选项中,选择Native C++ 。

在下一个屏幕上设置你喜欢的app name ,选择Kotlin 作为语言选项,然后点击下一步。

最后,选择C++ 标准版本,然后完成。在这篇文章中,让我们坚持使用工具链默认选项。

项目结构
我们看到两个文件夹(cpp和java),而不是通常只在src文件夹中的java文件夹。cpp文件夹存放着本地C++源代码文件和CMakeLists.txt文件。这个文件是CMake在管理编译过程中使用的一个配置文件。java文件夹是找到kotlin文件的地方。

打开cpp文件夹中的native-lib.cpp文件。你应该看到下面的代码。
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_terrence_aluda_nativetest_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
这是一个正常的C++文件,以预处理器指令开始。第一条包括处理我们的JNI代码的JNI库。
我们有一行extern "C" JNIEXPORT jstring JNICALL ,允许我们的函数被Kotlin/Java访问。该函数被称为stringFromJNI() 。
任何需要被外部JVM语言(Java/Kotlin)调用的函数都必须有这样的语法。
Java_packagename_ActivityName_functionName.
它有两个参数。
- JNIEnv* env - 这是一个指向我们将使用的环境的指针。
- jobject - 该函数将被调用的对象。在我们的例子中,是MainActivity。
然后我们使用从环境中访问的NewStringUTF() 方法返回要显示的字符串(hello)。
环境的属性和方法使用箭头(->)操作符访问。
NewStringUTF() 从作为参数传入的C++字符串中基于UTF-8字符创建一个字符串。
在Kotlin文件中,我们使用这段代码加载C++文件。
init {
System.loadLibrary("native-lib")
}
我们用这一行来声明这个函数。
external fun stringFromJNI(): String
我们调用它,然后将返回的字符串分配给TextView.text属性,使用。
binding.sampleText.text = stringFromJNI()
在运行应用程序时,你会看到一个屏幕,上面写着 "Hello from C++"。
我们将改变这两个文件中的代码以实现Dijkstra的最短路径优先算法。但首先,让我们看一下这个算法。
Dijkstra的最短路径优先算法
这种类型的贪婪算法在图的数据结构中寻找节点之间的最小权重。例如,寻找图中各点之间的最短距离。
图可以是现实生活中的任何东西,如公路和铁路网络、互联网路线等。
贪婪算法试图在每个阶段找到最优化的解决方案,然后再进入下一个阶段。
一个例子是在一个城市的某个区域找到机动时间最短的交通路线。
以此图为例。

我们想计算出0和6点之间的最短距离。它的工作原理是存储访问过的节点并计算它们与0之间的距离。
只有当它们与相邻的顶点相比距离最短时,它才会将它们添加到最短路径列表中。
这篇文章使用了相同的图和我们将在这里使用的相同的值。它也谈到了图。如果你想对算法有一个清晰的了解,请查看它。
最后,我们将得到这个图。

最后得到的最短路径列表将如图所示。
Node Distance from 0
0 0
1 2
2 6
3 7
4 17
5 22
6 19
我们将在我们的应用程序中使用相同的图和相同的值,但我们将假设这些值是在街道之间移动所需的时间而不是距离。
Node Time taken from 0
0 0
1 2
2 6
3 7
4 17
5 22
6 19
C++代码
它有五个函数。
streetName()printPath()minimumDistance()processPath()checkShortestRoute()
1. streetName()
//function to map street names to respective indices
string streetName(int index){
string street;
switch (index)
{
case 0:
street = "Georgia St.";
break;
case 1:
street = "Mitte St.";
break;
case 2:
street = "Lillies St.";
break;
case 3:
street = "Alexa St.";
break;
case 4:
street = "Quincy St.";
break;
case 5:
street = "Wood St.";
break;
case 6:
street = "Apple St.";
break;
default:
break;
}
return street;
}
这是不言自明的。它使用switch-case 语句将街道名称映射到各自的指数。
2. printPath()
//function for displaying the path
string printPath(int duration[]){
//the first street is where we are now, Georgia street
string pathDisplayed = "Georgia St.";
//start processing from the second street since we have the first one
int i = 1;
while (i < VERTICES) {
//display the next street if the time from the starting street is less and vice versa
if((duration[i+1])<(duration[i])){
pathDisplayed = pathDisplayed + " -> " + streetName(i+1);
i = i + 1;
}else if((duration[i+1])>(duration[i])){
pathDisplayed = pathDisplayed + " -> " + streetName(i);
//skip the next street since it has a greater distance
i = i + 2;
}
}
//remove the trailing " -> "
pathDisplayed = pathDisplayed.substr(0,pathDisplayed.size() - 4);
return pathDisplayed;
}
stringPath 方法接收duration[] 数组。它持有从起始位置到存储在各自顶点的位置的最短持续时间。
为了显示一个位置,我们检查下一条街道的时间是否小于当前街道的时间。如果下一条街道的时间更短,我们就显示它。否则,我们就显示当前街道。
在else 语句中,我们用2 增加索引,以跳过下一条街道,因为它的距离较大。
在最后一部分中,我们删除了尾部的*->*,以避免错误。
Georgia St. -> Mitte St. ->
当我们增加计数器时,我们可能会到达数组的末端,显示的字符串将与*->连接起来。*我们将在最后讨论这个函数的不足之处。
3. minimumDistance()
int minimumDistance(int duration[], bool shortestTimeSet[]){
int min_time = INT_MAX, min_time_index;
for (int v = 0; v < VERTICES; v++)
/*the minimum time is updated only if we don't have the node in the shortest time array
and the duration is less than the current minimum time*/
if (shortestTimeSet[v] == false && duration[v] <= min_time){
min_time = duration[v], min_time_index = v;
}
return min_time_index;
}
min_time 变量保存着当前的最小时间,它在代码中被更新。只有当我们在最短的时间数组(shortestTimeSet[])中没有顶点,并且持续时间小于当前的最短时间时,才会更新最小时间。
这个数组存储了所访问的streets(vertices) ,并且相对于相邻的streets(vertices) ,具有最短的时间。
4.processPath()
string processPath(int currentLocation){
//our graph represented in a matrix
int timeMatrix[VERTICES][VERTICES] = { { 0, 2, 6, 0, 0, 0, 0 },
{ 2, 0, 0, 5, 0, 0, 0 },
{ 6, 6, 0, 8, 0, 0, 0 },
{ 0, 0, 8, 0, 10, 15, 0 },
{ 0, 0, 0, 10, 0, 6, 2 },
{ 0, 0, 0, 15, 6, 0, 6 },
{ 0, 0, 0, 0, 2, 6, 0 } };
//The output array. It will hold the shortest distance from the starting location to the location in i
int duration[VERTICES];
//stores the streets(vertices) visited and have the shortest time relative to the adjacent streets(vertices)
bool shortestTimeSet[VERTICES];
// Initializing all distances as INFINITE and shortestTimeSet[] as false
for (int i = 0; i < VERTICES; i++)
duration[i] = INT_MAX, shortestTimeSet[i] = false;
// Distance of starting street(vertex) from itself is a 0
duration[currentLocation] = 0;
for (int count = 0; count < VERTICES - 1; count++) {
// Pick the street with the minimum time from the set of streets not yet processed. assign it to u
int u = minimumDistance(duration, shortestTimeSet);
// Mark the street as visited and having the shortest distance
shortestTimeSet[u] = true;
/* Update output array with the time to the chosen street only if it is in the shortestTimeSet[],
an edge exists where it is(it is not having a 0 value), its time is not INF, and the duration from the starting street
to where it is less than what is stored in duration[v] */
for (int v = 0; v < VERTICES; v++)
if (!shortestTimeSet[v] && timeMatrix[u][v] && duration[u] != INT_MAX
&& duration[u] + timeMatrix[u][v] < duration[v])
duration[v] = duration[u] + timeMatrix[u][v];
}
// print the constructed duration array
string path = printPath(duration);
return path;
}
在一堆数组的创建和初始化之后,我们转到for-loop ,由它来进行处理。
我们首先从尚未处理的街道集合中挑选出时间最短的街道,然后将其传递给minimumDistance() 函数,再将其分配给u 变量。
然后,我们通过将其在shortestTimeSet[] 数组中的值设置为true ,将其标记为已访问。只有在满足这些条件的情况下,我们才用顶点的信息更新duration[] 数组。
- 它在
shortestTimeSet[]阵列中。 - 存在一条没有零值的边。
- 它的时间不是INF(
INT_MAX)。 - 从源顶点到其当前位置的持续时间小于存储在
duration[v]。
最后,我们打印构建的持续时间数组。
5.checkShortestRoute()
//function that will be called from Kotlin
// we pass in the index of the street where the user is
extern "C" JNIEXPORT jstring JNICALL
Java_com_terrence_aluda_nativetest_MainActivity_checkShortestRoute(JNIEnv* env, jobject, jint currentLocation){
return env->NewStringUTF(processPath(currentLocation).c_str());
}
上述函数将从Kotlin文件中调用,我们将传入用户所处街道的索引。
以下是完整的代码。
#include <jni.h>
#include <limits.h>
#include <string>
using namespace std;
#define VERTICES 7
string streetName(int index){
string street;
switch (index)
{
case 0:
street = "Georgia St.";
break;
case 1:
street = "Mitte St.";
break;
case 2:
street = "Lillies St.";
break;
case 3:
street = "Alexa St.";
break;
case 4:
street = "Quincy St.";
break;
case 5:
street = "Wood St.";
break;
case 6:
street = "Apple St.";
break;
default:
break;
}
return street;
}
//function for displaying the path
string printPath(int duration[]){
//the first street is where we are now, Georgia street
string pathDisplayed = "Georgia St.";
//start processing from the second street since we have the first one
int i = 1;
while (i < VERTICES) {
//display the next street if the time from the starting street is less and vice versa
if((duration[i+1])<(duration[i])){
pathDisplayed = pathDisplayed + " -> " + streetName(i+1);
i = i + 1;
}else if((duration[i+1])>(duration[i])){
pathDisplayed = pathDisplayed + " -> " + streetName(i);
//skip the next street since it has a greater distance
i = i + 2;
}
}
//remove the trailing " -> "
pathDisplayed = pathDisplayed.substr(0,pathDisplayed.size() - 4);
return pathDisplayed;
}
int minimumDistance(int duration[], bool shortestTimeSet[]){
int min_time = INT_MAX, min_time_index;
for (int v = 0; v < VERTICES; v++)
/*the minimum time is updated only if we don't have the node in the shortest time array
and the duration is leass than the current minimum time*/
if (shortestTimeSet[v] == false && duration[v] <= min_time){
min_time = duration[v], min_time_index = v;
}
return min_time_index;
}
//computing the path using DSPF
//this is the heart of the algorithm
string processPath(int currentLocation){
//our graph represented in a matrix
int timeMatrix[VERTICES][VERTICES] = { { 0, 2, 6, 0, 0, 0, 0 },
{ 2, 0, 0, 5, 0, 0, 0 },
{ 6, 6, 0, 8, 0, 0, 0 },
{ 0, 0, 8, 0, 10, 15, 0 },
{ 0, 0, 0, 10, 0, 6, 2 },
{ 0, 0, 0, 15, 6, 0, 6 },
{ 0, 0, 0, 0, 2, 6, 0 } };
//The output array. It will hold the shortest distance from the starting location to the location in i
int duration[VERTICES];
//stores the streets(vertices) visited and have the shortest time relative to the adjacent streets(vertices)
bool shortestTimeSet[VERTICES];
// Initializing all distances as INFINITE and shortestTimeSet[] as false
for (int i = 0; i < VERTICES; i++)
duration[i] = INT_MAX, shortestTimeSet[i] = false;
// Distance of starting street(vertex) from itself is a 0
duration[currentLocation] = 0;
for (int count = 0; count < VERTICES - 1; count++) {
// Pick the street with the miinimum time from the set of streets not yet processed. assign it to u
int u = minimumDistance(duration, shortestTimeSet);
// Mark the street as visited and having the shortest distance
shortestTimeSet[u] = true;
/* Update output array with the time to the chosen street only if it is in the shortestTimeSet[],
an edge exists where it is(it is not 0), its time is not INF and the duration from the starting street
to where it is less than what is stored in duration[v] */
for (int v = 0; v < VERTICES; v++)
if (!shortestTimeSet[v] && timeMatrix[u][v] && duration[u] != INT_MAX
&& duration[u] + timeMatrix[u][v] < duration[v])
duration[v] = duration[u] + timeMatrix[u][v];
}
// print the constructed duration array
string path = printPath(duration);
return path;
}
//function that will be called from Kotlin
// we pass in the index of the street where the user is
extern "C" JNIEXPORT jstring JNICALL
Java_com_terrence_aluda_nativetest_MainActivity_checkShortestRoute(JNIEnv* env, jobject, jint currentLocation){
return env->NewStringUTF(processPath(currentLocation).c_str());
}
Kotlin和XML
在Kotlin代码中,我们声明本地函数,将0 作为第一个顶点,然后将返回值分配给TextView.text 属性。
package <replace-with-your-package-name>
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import <replace-with-your-package-name>.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
//display
binding.sampleText.text = checkShortestRoute(0)
}
/**
* run the shortest time algo
*/
external fun checkShortestRoute(x: Int): String
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
对于布局的XML,我们添加以下代码。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:text="Where you are now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textBanner"
android:layout_marginTop="100dp" android:textStyle="bold"
android:textSize="16sp" android:layout_alignParentLeft="true" android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"/>
<TextView
android:text="Georgia Street"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/currentPlaceTextView"
android:layout_marginTop="150dp"
android:layout_alignParentRight="false" android:layout_marginLeft="50dp" android:layout_marginStart="50dp"/>
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:layout_centerInParent="true" android:paddingLeft="30dp" android:paddingRight="20dp"
android:textSize="15sp"/>
<TextView
android:text="Destination"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textDestination"
android:layout_marginTop="100dp"
android:layout_marginRight="70dp"
android:layout_alignParentRight="true" android:layout_marginEnd="70dp" android:textStyle="bold"
android:textSize="16sp"/>
<TextView
android:text="Apple Street"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/destinationTextView"
android:layout_marginTop="150dp"
android:layout_marginRight="70dp"
android:layout_alignParentRight="true" android:layout_marginEnd="70dp"/>
<TextView
android:text="The shortest path:"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/destinationTextView"
android:layout_marginTop="75dp" android:textStyle="bold"
android:textSize="16sp" android:layout_alignParentLeft="false"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
运行该应用程序。
准确度
很明显,路径将是0 -> 1 -> 3 -> 4 -> 6 ,即Georgia -> Mitte St. -> Alexa St. -> Quincy St. -> Apple St. 。
然而,你会注意到,我们的路径打印功能错过了第四条街。这是因为条件检查的缘故。
当它检查持续时间时,它跳过了Quincy Street 来显示Georgia -> Mitte St. -> Alexa St. -> Apple St. 。这可以通过添加另一个嵌套的if 语句来操纵。
但是如果我们使用一个更复杂的图呢?如果相邻顶点之间的持续时间是相等的呢?如果我们通过Kotlin文件送入不同的位置,准确性会更加恶化。
所有这些让我们得出一个结论;这个应用程序需要一个视觉解决方案。一些动画的、基于画布的东西,对吗?让我们再看一下最终的图表。

它更直观,更吸引人,对用户检查路径来说是一个很好的用户体验。就像在谷歌地图中,地图上的路径被标记出来,这样使用它的人就只能按照标记的指南走。
记住,DSPF算法只比较权重,标记节点,并更新距离。它确实迎合了路径(这是一个无向图)。
我们不能使用有向图,因为用户可能向任何方向移动。另外,请注意,我们不是为许多用户创建应用程序。
总结
在这篇文章中,我们对JNI和NDK进行了简单的概述。我们还在我们的环境中安装了所需的组件。
现在你有了这个算法,你可以用Android画布画一个图或一个简单的地图,然后通过改变节点和边的颜色来模拟这个算法,创建一个路径。
你可以通过在运行时改变数值来进一步加强它,以便根据模拟的交通情况来改变路径。