问题

Android withNDK支持C / C代码,iOS支持Objective-C++也支持,那么如何编写Android和iOS共享的本机C / C代码的应用程序?


#1 热门回答(215 赞)

##更新。

这个答案在我写完四年后很受欢迎,在这四年里,很多事情都发生了变化,所以我决定更新我的答案以更好地适应我们当前的现实。答案的想法不会改变;实施已经改变了一点。我的英语也发生了变化,它已经有了很大的改进,所以现在每个人的答案都更容易理解。

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

答案

在我展示代码之前,请仔细考虑下图。

Arch

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

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

iOS中间层很简单;你只需要使用Obj-c的变体将你的项目配置为Objective-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代码,例如视图控制器"

#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集成的时候了。 Android使用Gradle作为构建系统,而使用Cake 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");
    }

}

请注意,此类具有anative方法并加载名为native-lib的本机库。这个库是我们创建的,最后,CPP代码将成为一个共享对象.soFile嵌入我们的APK,而theloadLibrary将加载它。最后,当你调用本机方法时,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一起使用的一些符号asJNIEXPORTJNICALL。为了理解这些东西的含义,有必要花一点时间和read it,因为本教程的目的只是将这些东西视为样板。

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

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

最后一步是将JNI类型转换为我们在CPP端使用的类型。在我们的例子中,我们正在将jstring转换为aconst char *,将其转换为CPP,得到结果并转换回jstring。与JNI的所有其他步骤一样,并不难;它只是用于电镀,所有工作都是在我们拨打电话GetStringUTFCharsNewStringUTF时收到的JNIEnv*争议中完成的。在我们的代码准备好在Android设备上运行之后,让我们一起来看看吧。

AndroidStudio

Android


原文链接