java - 如何为 Android 和 iOS 使用相同的 C++ 代码?

标签 java c++ java-native-interface cross-platform objective-c++

安卓带 NDK Objective-C++ 支持 C/C++ 代码和 iOS也有支持,那么如何使用在 Android 和 iOS 之间共享的 native C/C++ 代码编写应用程序?

最佳答案

更新。

这个答案在我写它四年后仍然很受欢迎,在这四年里很多事情都发生了变化,所以我决定更新我的答案以更好地适应我们当前的现实。答题思路不变;实现略有变化。我的英文也变了,进步了很多,所以现在答案大家都比较明白了。

请看the repo所以你可以下载并运行我将在下面展示的代码。

答案

在我展示代码之前,请仔细阅读下图。

Arch

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

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

iOS端的中间层很简单;您只需要配置您的项目以使用 Obj-c 的变体来构建,即 Objective-C++就是这样,您可以访问 C++ 代码。

事情在 Android 方面变得更难了,Java 和 Kotlin 这两种语言在 Android 上都在 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 类型和调用。一旦您可以在 Obj-C 上的任何文件上调用 CPP 代码,这不是强制性的,但它有助于保持组织,并且在您的包装文件之外,您维护完整的 Obj-C 样式代码,只有包装文件成为 CPP 样式.

一旦您的包装器连接到 CPP 代码,您就可以将其用作标准的 Obj-C 代码,例如 View Controller ”
#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++ 配置的更多信息,请 read it.

现在核心代码是我们应用程序的一部分,是时候创建桥接器了,为了使事情更简单和有条理,我们创建了一个名为 CoreWrapper 的特定类作为 JVM 和 CPP 之间的包装器:
public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

注意这个类有一个 native方法并加载名为 native-lib 的 native 库.这个库是我们创建的,最终CPP代码会变成共享对象.so文件嵌入到我们的 APK 中,以及 loadLibrary将加载它。最后,当您调用 native 方法时,JVM 会将调用委托(delegate)给加载的库。

现在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 一起工作的一些符号,如 JNIEXPORTJNICALL .要你明白那些东西的意思,有必要花点时间和read it ,出于本教程的目的,只需将这些内容视为样板。

一件重要的事情,通常也是很多问题的根源是方法的名称;它需要遵循“Java_package_class_method”模式。目前,Android studio 对它有很好的支持,所以它可以自动生成这个样板,并在它正确或未命名时显示给你。在我们的示例中,我们的方法名为“Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString”,因为“ademar.androidioscppexample”是我们的包,所以我们替换了“.”。通过“_”,CoreWrapper 是我们链接本地方法的类,“concatenateMyStringWithCppString”是方法名称本身。

由于我们正确声明了方法,现在是分析参数的时候了,第一个参数是 JNIEnv 的指针。这是我们访问 JNI 内容的方式,正如您将很快看到的那样,这对我们进行转换至关重要。第二个是 jobject它是您用来调用此方法的对象的实例。您可以将其视为java“this”,在我们的示例中我们不需要使用它,但我们仍然需要声明它。在此作业之后,我们将接收该方法的参数。因为我们的方法只有一个参数——一个字符串“myString”,我们只有一个同名的“jstring”。还要注意我们的返回类型也是一个 jstring。这是因为我们的 Java 方法返回一个 String,有关 Java/JNI 类型的更多信息请 read it.

最后一步是将 JNI 类型转换为我们在 CPP 端使用的类型。在我们的示例中,我们正在转换 jstringconst char *发送它转换为 CPP,获取结果并转换回 jstring .与 JNI 上的所有其他步骤一样,这并不难;它只是样板文件,所有工作都由 JNIEnv* 完成我们在调用 GetStringUTFChars 时收到的参数和 NewStringUTF .之后我们的代码就可以在 Android 设备上运行了,让我们来看看。

AndroidStudio
Android

关于java - 如何为 Android 和 iOS 使用相同的 C++ 代码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18334547/

相关文章:

c++ - 如何在不退出 do while 循环的情况下验证输入?

java - java处理word文档的问题

c++ - 读/写几个未知大小的结构到文件 C++

java - 我需要使用什么文件路径来查看 netbeans 上的 java 项目中的 html 页面?

c++ - 无需强制转换即可访问 C++ vector 元素

android - 如何通过 JNI 将图像从 Java 传递到 Cocos2d-x?

java - 在 JNI 中,如何从 native 代码访问静态 Java 变量

android - android 在停止服务时会杀死 JVM 吗?

java - session 事务消费者或生产者中的消息代理异常处理

java - 将 XML 绑定(bind)到 Java 中的类是否有意义?