如何在 Android 和 iOS 上使用相同的 C + + 代码?

基于 NDK的 Android 支持 C/C + + 代码,基于 目标-C + + 的 iOS 也支持 C/C + + 代码,那么如何编写基于 Android 和 iOS 共享的原生 C/C + + 代码的应用程序呢?

40032 次浏览

更新。

这个答案在我写了四年之后仍然很受欢迎,在这四年里很多事情都改变了,所以我决定更新我的答案,以更好地适应我们当前的现实。答案的思想没有改变; 实现稍有改变。我的英语也发生了变化,它提高了很多,所以答案是更容易理解的大家现在。

请看一下 回购,这样您就可以下载并运行我将在下面展示的代码。

答案

在我展示代码之前,请在下面的图表中进行大量的说明。

Arch

每个操作系统都有其 UI 和特性,因此我们打算在这方面为每个平台编写特定的代码。另一方面,我们打算使用 C + + 编写所有可以共享的逻辑代码、业务规则和内容,这样我们就可以在每个平台上编译相同的代码。

在图中,您可以看到 C + + 层在最底层。所有共享代码都在这个段中。最高级别是常规的 Obj-C/Java/Kotlin 代码,这里没有新闻,最难的是中间层。

IOS 端的中间层非常简单; 您只需要配置项目,使用一个称为 目标-C + + 的 Obj-c 变体进行构建,就可以访问 C + + 代码了。

在安卓系统方面,这个问题变得更加困难了,Java 和 Kotlin 这两种语言在安卓系统上都是在 Java 虚拟机下运行的。因此,访问 C + + 代码的唯一方法是使用 JNI,请花时间阅读 JNI 的基础知识。幸运的是,今天的 Android Studio IDE 在 JNI 方面有了很大的改进,并且在编辑代码时会显示出很多问题。

代码按步骤执行

我们的示例是一个简单的应用程序,您发送一个文本到 CPP,它将该文本转换为其他内容并返回它。这个想法是,iOS 将发送“ Obj-C”和 Android 将发送“ Java”从各自的语言,和 CPP 代码将创建一个文本如下“ CPP 说你好 < 收到的文本 >”。

共享 CPP 代码

首先,我们将创建共享的 CPP 代码,这样我们就有了一个简单的头文件,带有接收所需文本的方法声明:

#include <iostream>


const char *concatenateMyStringWithCppString(const char *myString);

以及 CPP 的实施:

#include <string.h>
#include "Core.h"


const char *CPP_BASE_STRING = "cpp says hello to %s";


const char *concatenateMyStringWithCppString(const char *myString) {
char *concatenatedString = new char[strlen(CPP_BASE_STRING) + strlen(myString)];
sprintf(concatenatedString, CPP_BASE_STRING, myString);
return concatenatedString;
}

Unix

一个有趣的好处是,我们也可以在 Linux 和 Mac 以及其他 Unix 系统上使用相同的代码。这种可能性特别有用,因为我们可以更快地测试我们的共享代码,所以我们将创建一个 Main.cpp,如下所示,以便在计算机上执行它,并查看共享代码是否正常工作。

#include <iostream>
#include <string>
#include "../CPP/Core.h"


int main() {
std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
std::cout << textFromCppCore << '\n';
return 0;
}

要构建代码,您需要执行:

$ g++ Main.cpp Core.cpp -o main
$ ./main
cpp says hello to Unix

IOS

现在是在移动端实现的时候了。只要 iOS 有一个简单的集成,我们就从它开始。我们的 iOS 应用程序是一个典型的 Obj-c 应用程序,只有一个区别; 文件是 .mm而不是 .m。也就是说。它是一个 Obj-C + + 应用程序,而不是一个 Obj-C 应用程序。

对于一个更好的组织,我们创建 CoreWrapper.mm 如下:

#import "CoreWrapper.h"


@implementation CoreWrapper


+ (NSString*) concatenateMyStringWithCppString:(NSString*)myString {
const char *utfString = [myString UTF8String];
const char *textFromCppCore = concatenateMyStringWithCppString(utfString);
NSString *objcString = [NSString stringWithUTF8String:textFromCppCore];
return objcString;
}


@end

此类负责将 CPP 类型和调用转换为 Obj-C 类型和调用。一旦你可以调用任何文件的 CPP 代码,它不是强制性的,但它有助于保持组织,并在你的包装器文件之外,你维护一个完整的 Obj-C 样式的代码,只有包装器文件成为 CPP 样式。

一旦你的包装器连接到 CPP 代码,你可以使用它作为一个标准的 Obj-C 代码,例如 ViewController”

#import "ViewController.h"
#import "CoreWrapper.h"


@interface ViewController ()


@property (weak, nonatomic) IBOutlet UILabel *label;


@end


@implementation ViewController


- (void)viewDidLoad {
[super viewDidLoad];
NSString* textFromCppCore = [CoreWrapper concatenateMyStringWithCppString:@"Obj-C++"];
[_label setText:textFromCppCore];
}


@end

看看这个应用程序的外观:

Xcode iPhone

仿生人

现在是 Android 集成的时候了。Android 使用 Gradle 作为构建系统,对于 C/C + + 代码使用 CMake。因此,我们需要做的第一件事就是在 gradle 文件上配置 CMake:

android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
...
}

第二步是添加 CMakeLists.txt 文件:

cmake_minimum_required(VERSION 3.4.1)


include_directories (
../../CPP/
)


add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp
../../CPP/Core.h
../../CPP/Core.cpp
)


find_library(
log-lib
log
)


target_link_libraries(
native-lib
${log-lib}
)

CMake 文件是您需要添加 CPP 文件和头文件夹的地方,您将在项目中使用这些文件夹,在我们的示例中,我们将添加 CPP文件夹和 Core.h/。Cpp 文件。要了解更多关于 C/C + + 配置的信息,请参阅 看看吧。

现在核心代码是我们应用程序的一部分,是时候创建桥梁了,为了让事情更简单和有组织,我们创建了一个特定的类 CoreWrapper 作为 JVM 和 CPP 之间的包装器:

public class CoreWrapper {


public native String concatenateMyStringWithCppString(String myString);


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


}

注意,这个类有一个 native方法,并加载一个名为 native-lib的本机库。这个库是我们创建的,最后,CPP 代码将成为嵌入在 APK 中的共享对象 .so File,并且 loadLibrary将加载它。最后,当您调用本机方法时,JVM 将把调用委托给加载的库。

现在 Android 集成中最奇怪的部分是 JNI; 我们需要一个 cpp 文件,如下所示,在我们的示例中是“ nate-lib.cpp”:

extern "C" {


JNIEXPORT jstring JNICALL Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString(JNIEnv *env, jobject /* this */, jstring myString) {
const char *utfString = env->GetStringUTFChars(myString, 0);
const char *textFromCppCore = concatenateMyStringWithCppString(utfString);
jstring javaString = env->NewStringUTF(textFromCppCore);
return javaString;
}


}

您将注意到的第一件事是 extern "C",这部分是 JNI 正确使用 CPP 代码和方法链接所必需的。您还将看到 JNI 使用的一些符号作为 JNIEXPORTJNICALL与 JVM 协同工作。为了理解这些东西的含义,有必要花点时间和 看看吧,为了本教程的目的,只需将这些东西看作样板。

有一点很重要,通常很多问题的根源是方法的名称; 它需要遵循模式“ Java _ package _ class _ method”。目前,Android 工作室对它有很好的支持,所以它可以自动生成这个样板,并显示给你,当它是正确的或没有命名。在我们的示例中,我们的方法名为“ Java _ ademar _ androidioscppexample _ CoreWrapper _ concatenateMyStringWithCppString”,这是因为“ ademar.androidioscppexample”是我们的包,所以我们将“通过“ _”,CoreWrapper 是我们链接本机方法的类,而“ concatenateMyStringWithCppString”是方法名本身。

因为我们已经正确地声明了这是分析参数的时间,第一个参数是 JNIEnv的指针,这是我们访问 JNI 的方式,这是至关重要的,我们使我们的转换,你很快就会看到。第二个是 jobject,它是您用来调用此方法的对象的实例。你可以认为它是 java 的“ 这个”,在我们的例子中我们不需要使用它,但是我们仍然需要声明它。在这个 jobject 之后,我们将接收方法的参数。因为我们的方法只有一个参数—— String“ myString”,所以我们只有一个具有相同名称的“ jstring”。还要注意,我们的返回类型也是 jstring。这是因为我们的 Java 方法返回一个 String,关于 Java/JNI 类型的更多信息请 看看吧。

最后一步是将 JNI 类型转换为我们在 CPP 端使用的类型。在我们的示例中,我们将 jstring转换为 const char *,并将其转换为 CPP,得到结果并将其转换回 jstring。与 JNI 上的所有其他步骤一样,这并不难; 它只是一个样本,所有工作都是通过调用 GetStringUTFCharsNewStringUTF时接收到的 JNIEnv*参数完成的。在我们的代码可以在 Android 设备上运行之后,让我们来看一下。

AndroidStudio Android

上述优秀答案中描述的方法可以完全由 斯凯皮克斯语言桥自动化,它可以直接从 C + + 报头动态生成包装器代码。这是一个 例子:

用 C + + 定义你的类:

#include <scapix/bridge/object.h>


class contact : public scapix::bridge::object<contact>
{
public:
std::string name();
void send_message(const std::string& msg, std::shared_ptr<contact> from);
void add_tags(const std::vector<std::string>& tags);
void add_friends(std::vector<std::shared_ptr<contact>> friends);
};

斯威夫特说:

class ViewController: UIViewController {
func send(friend: Contact) {
let c = Contact()


contact.sendMessage("Hello", friend)
contact.addTags(["a","b","c"])
contact.addFriends([friend])
}
}

来自 Java 的:

class View {
private contact = new Contact;


public void send(Contact friend) {
contact.sendMessage("Hello", friend);
contact.addTags({"a","b","c"});
contact.addFriends({friend});
}
}