NDKを使用するAndroid はC/C++ コードをサポートし、Objective-C++を使用する iOSもサポートしています。では、Android と iOS 間で共有されるネイティブ C/C++ コードを使用してアプリケーションを作成するにはどうすればよいでしょうか?
2 に答える
アップデート。
この回答は、私が書いてから 4 年経っても非常に人気があります。この 4 年間で多くのことが変化したため、現在の現実に合わせて回答を更新することにしました。答えのアイデアは変わりません。実装が少し変更されました。私の英語も変わりました。かなり上達したので、今では誰にとっても答えがより理解できるようになりました。
以下に示すコードをダウンロードして実行できるように、リポジトリをご覧ください。
答え
コードを示す前に、次の図をよく見てください。
各 OS には独自の UI と特性があるため、この点に関して、各プラットフォームに固有のコードを作成する予定です。一方、すべてのロジック コード、ビジネス ルール、および共有できるものは C++ を使用して作成する予定であるため、同じコードを各プラットフォームにコンパイルできます。
この図では、C++ レイヤーが最下位レベルにあることがわかります。すべての共有コードはこのセグメントにあります。最上位は通常の Obj-C / Java / Kotlin コードです。ここではニュースはありません。難しい部分は中間層です。
中間層から iOS 側までは単純です。Objective-C++として知られている Obj-c のバリアントを使用してビルドするようにプロジェクトを構成するだけで、C++ コードにアクセスできます。
Android 側では、Java と Kotlin の両方の言語が Java 仮想マシンの下で実行されるため、事態はさらに難しくなりました。したがって、C++ コードにアクセスする唯一の方法はJNIを使用することです。時間をかけて JNI の基本を読んでください。幸いなことに、現在の Android Studio IDE では JNI 側が大幅に改善されており、コードの編集中に多くの問題が表示されます。
手順ごとのコード
このサンプルは、テキストを CPP に送信し、そのテキストを別のものに変換して返す単純なアプリです。アイデアは、iOS が「Obj-C」を送信し、Android が「Java」をそれぞれの言語から送信し、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 アプリですが、違いは 1 つだけです。ファイルは.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 コードに接続されると、ViewController などの標準の 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
アプリの外観を見てみましょう。
アンドロイド
いよいよ Android の統合です。Android はビルド システムとして Gradle を使用し、C/C++ コードには CMake を使用します。したがって、最初に行う必要があるのは、gradle ファイルで CMake を構成することです。
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
...
}
2 番目のステップは、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 コードとメソッドのリンケージを正しく処理するために必要であるということです。JNIEXPORT
また、JNI が JVM を操作するためにおよびとして使用するいくつかのシンボルも表示されJNICALL
ます。これらの意味を理解するには、時間をかけて読む必要があります。このチュートリアルでは、これらのことをボイラープレートとして考えてください。
重要なことの 1 つであり、通常は多くの問題の原因となるのは、メソッドの名前です。パターン「Java_package_class_method」に従う必要があります。現在、Android Studio はこのボイラープレートを自動的に生成し、名前が正しいかどうかを示す優れたサポートを提供しています。この例では、メソッドの名前は「Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString」です。これは、「ademar.androidioscppexample」がパッケージであるため、「.」を置き換えるためです。「_」で、CoreWrapper はネイティブ メソッドをリンクしているクラスであり、「concatenateMyStringWithCppString」はメソッド名そのものです。
メソッドが正しく宣言されたので、引数を分析します。最初のパラメーターはそのポインターでありJNIEnv
、JNI にアクセスする方法です。すぐにわかるように、変換を行うことが重要です。2 つ目は、jobject
このメソッドを呼び出すために使用したオブジェクトのインスタンスです。これは Java の " this "と考えることができます。この例では使用する必要はありませんが、それでも宣言する必要があります。このジョブジェクトの後、メソッドの引数を受け取ります。このメソッドの引数は文字列 "myString" だけなので、同じ名前の "jstring" しかありません。また、戻り値の型も jstring であることに注意してください。これは、Java メソッドが文字列を返すためです。Java/JNI 型の詳細については、こちらをお読みください。
最後のステップは、JNI 型を CPP 側で使用する型に変換することです。この例では、 をCPP に変換されjstring
たconst char *
送信に変換し、結果を取得して に変換し直していjstring
ます。JNI の他のすべての手順と同様に、難しいことではありません。それはボイラープレートに過ぎず、すべての作業はandJNIEnv*
を呼び出したときに受け取った引数によって行われます。コードを Android デバイスで実行する準備ができたら、見てみましょう。GetStringUTFChars
NewStringUTF