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


119

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


1
尝试cocos2d-x框架
glo

@glo看起来不错,但是我正在寻找更通用的东西,使用不带框架的c ++,“显然排除了JNI”。
ademar111190 2013年

Answers:


273

更新。

即使在我写了四年后,这个答案还是很受欢迎,在这四年中,很多事情都发生了变化,所以我决定更新我的答案,以更好地适应当前的现实。答案是不变的。实施有所改变。我的英语也发生了变化,已经有了很大的提高,因此答案现在对于每个人来说都是可以理解的。

请查看存储库,以便您下载并运行下面将显示的代码。

答案

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

拱

每个OS都有其UI和特性,因此我们打算在这方面向每个平台编写特定的代码。换句话说,我们打算使用C ++编写所有逻辑代码,业务规则和可以共享的东西,因此我们可以将相同的代码编译到每个平台。

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

iOS的中间层很简单;您只需要配置项目就可以使用Obj-c的一个不同版本(称为Objective-C ++)进行构建,仅此而已,您就可以访问C ++代码。

在Android方面,事情变得更加困难,在Java虚拟机下运行的Android上的Java和Kotlin这两种语言。因此,访问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代码,例如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 苹果手机

安卓

现在是时候进行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 JNIEXPORT和一起用于JVM的一些符号JNICALL。为了理解这些东西的含义,有必要花一些时间阅读一下,对于本教程而言,只需将这些东西当作样板即可。

方法名称是一件重要的事情,通常是很多问题的根源。它需要遵循“ 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类型的更多信息,请阅读它。

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

AndroidStudio 安卓


7
很棒的解释
RED.Skull

9
我不明白-但+1是SO上质量最高的答案之一
Michael Rodrigues

16
@ ademar111190到目前为止最有用的帖子。这不应该被关闭。
Jared Burrows 2014年

6
@JaredBurrows,我同意。投票重新开放。
OmnipotentEntity 2014年

3
@KVISH,您必须首先在Objective-C中实现包装器,然后通过将包装器头添加到桥接头文件中来快速访问Objective-C包装器。到目前为止,尚无办法在Swift中直接访问C ++。有关更多信息,请参见stackoverflow.com/a/24042893/1853977
克里斯

3

上面的出色答案中描述的方法可以完全由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});
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.