带有NDK的Android支持 C/C++ 代码,带有Objective-C++ 的iOS也支持,那么我如何编写在 Android 和 iOS 之间共享的原生 C/C++ 代码的应用程序?
2 回答
更新。
这个答案在我写完四年后仍然很受欢迎,在这四年里很多事情都发生了变化,所以我决定更新我的答案以更好地适应我们当前的现实。答案思路不变;实施发生了一些变化。我的英语也变了,进步了很多,所以现在大家都比较容易理解了。
请查看repo,以便您可以下载并运行我将在下面显示的代码。
答案
在我展示代码之前,请先看一下下图。
每个操作系统都有其 UI 和特性,因此我们打算在这方面为每个平台编写特定的代码。另一方面,我们打算使用 C++ 编写所有可以共享的逻辑代码、业务规则和东西,因此我们可以将相同的代码编译到每个平台。
在图中,您可以看到最低级别的 C++ 层。所有共享代码都在此段中。最高层是常规的 Obj-C / Java / Kotlin 代码,这里没有消息,难的部分是中间层。
iOS端的中间层很简单;您只需要配置您的项目以使用称为Objective-C++的 Obj-c 变体进行构建,这就是全部,您可以访问 C++ 代码。
在 Android 方面,事情变得更加困难,Android 上的 Java 和 Kotlin 这两种语言都在 Java 虚拟机下运行。所以访问 C++ 代码的唯一方法是使用JNI,请花时间阅读 JNI 的基础知识。幸运的是,今天的 Android Studio IDE 在 JNI 方面有了很大的改进,并且在您编辑代码时会向您展示很多问题。
代码按步骤
我们的示例是一个简单的应用程序,您将文本发送到 CPP,然后它将该文本转换为其他内容并返回。这个想法是,iOS 将发送“Obj-C”,Android 将发送各自语言的“Java”,CPP 代码将创建如下文本“cpp 向<< text received >> ”。
共享 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 类型和调用。一旦你可以在 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
看看应用程序的外观:
安卓
现在是 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代码会变成一个共享对象.so
文件嵌入到我们的APK中,然后loadLibrary
加载它。最后,当您调用本机方法时,JVM 会将调用委托给加载的库。
现在Android集成最奇怪的部分是JNI;我们需要一个如下的 cpp 文件,在我们的例子中是“native-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 用于与 JVM 一起使用的一些符号,如JNIEXPORT
和JNICALL
。为了理解这些东西的含义,有必要花点时间阅读它,对于本教程的目的,只需将这些东西视为样板。
一件重要的事情,通常是很多问题的根源是方法的名称。它需要遵循“Java_package_class_method”模式。目前,Android Studio 对其有很好的支持,因此它可以自动生成此样板文件,并在正确或未命名时显示给您。在我们的示例中,我们的方法被命名为“Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString”,因为“ademar.androidioscppexample”是我们的包,所以我们替换了“.”。通过“_”,CoreWrapper 是我们链接本机方法的类,“concatenateMyStringWithCppString”是方法名称本身。
由于我们已经正确声明了方法,是时候分析参数了,第一个参数是一个指针,JNIEnv
它是我们访问 JNI 内容的方式,这对我们进行转换至关重要,正如您将看到的那样。第二个是jobject
您用来调用此方法的对象的实例。你可以认为它是java的“ this ”,在我们的例子中我们不需要使用它,但我们仍然需要声明它。在这个 jobject 之后,我们将接收该方法的参数。因为我们的方法只有一个参数——一个字符串“myString”,所以我们只有一个同名的“jstring”。另请注意,我们的返回类型也是一个 jstring。这是因为我们的 Java 方法返回一个字符串,有关 Java/JNI 类型的更多信息请阅读它。
最后一步是将 JNI 类型转换为我们在 CPP 端使用的类型。在我们的示例中,我们将 转换jstring
为const char *
将其转换为 CPP 的发送,获取结果并转换回jstring
. 与 JNI 上的所有其他步骤一样,这并不难;它只是样板文件,所有工作都是由JNIEnv*
我们调用GetStringUTFChars
and时收到的参数完成的NewStringUTF
。之后,我们的代码就可以在 Android 设备上运行了,让我们来看看。
Scapix Language Bridge可以完全自动化上面出色答案中描述的方法,它直接从 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);
};
并从 Swift 中调用它:
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});
}
}