Android z NDK obsługuje również kod C / C ++, a iOS z Objective-C ++ również obsługuje, więc jak mogę pisać aplikacje z natywnym kodem C / C ++ współdzielonym między systemami Android i iOS?
Android z NDK obsługuje również kod C / C ++, a iOS z Objective-C ++ również obsługuje, więc jak mogę pisać aplikacje z natywnym kodem C / C ++ współdzielonym między systemami Android i iOS?
Odpowiedzi:
Ta odpowiedź jest dość popularna nawet cztery lata po jej napisaniu, w ciągu tych czterech lat wiele się zmieniło, więc postanowiłem zaktualizować swoją odpowiedź, aby lepiej pasowała do naszej obecnej rzeczywistości. Pomysł odpowiedzi się nie zmienia; realizacja trochę się zmieniła. Mój angielski też się zmienił, bardzo się poprawił, więc odpowiedź jest teraz bardziej zrozumiała dla wszystkich.
Spójrz na repozytorium , abyś mógł pobrać i uruchomić kod, który pokażę poniżej.
Zanim pokażę kod, przeanalizuj poniższy diagram.
Każdy system operacyjny ma swój interfejs użytkownika i cechy szczególne, dlatego zamierzamy napisać określony kod do każdej platformy w tym zakresie. Z drugiej strony, cały kod logiczny, reguły biznesowe i rzeczy, które można udostępniać, zamierzamy napisać w C ++, abyśmy mogli skompilować ten sam kod na każdej platformie.
Na diagramie możesz zobaczyć warstwę C ++ na najniższym poziomie. Cały wspólny kod znajduje się w tym segmencie. Najwyższy poziom to zwykły kod Obj-C / Java / Kotlin, nie ma tu żadnych nowości, najtrudniejsza jest warstwa środkowa.
Środkowa warstwa po stronie iOS jest prosta; musisz tylko skonfigurować swój projekt do kompilacji przy użyciu wariantu Obj-c znanego jako Objective-C ++ i to wszystko, masz dostęp do kodu C ++.
Sytuacja stała się trudniejsza po stronie Androida, oba języki, Java i Kotlin, na Androida działają pod wirtualną maszyną Java. Tak więc jedynym sposobem uzyskania dostępu do kodu C ++ jest użycie JNI , poświęć trochę czasu na przeczytanie podstaw JNI. Na szczęście dzisiejsze IDE Android Studio ma ogromne ulepszenia po stronie JNI, a podczas edycji kodu wyświetlanych jest wiele problemów.
Nasza próbka to prosta aplikacja, w której wysyłasz tekst do CPP, a ona konwertuje ten tekst na coś innego i zwraca go. Pomysł jest taki, że iOS wyśle „Obj-C”, a Android wyśle „Java” z odpowiednich języków, a kod CPP utworzy następujący tekst: „cpp mówi cześć << otrzymano tekst >> ”.
Przede wszystkim utworzymy udostępniony kod CPP, robiąc to mamy prosty plik nagłówkowy z deklaracją metody, która otrzymuje żądany tekst:
#include <iostream>
const char *concatenateMyStringWithCppString(const char *myString);
Oraz wdrożenie 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;
}
Ciekawym bonusem jest to, że możemy użyć tego samego kodu dla Linuksa i Maca, a także innych systemów Unix. Ta możliwość jest szczególnie przydatna, ponieważ możemy szybciej przetestować udostępniony kod, więc utworzymy plik Main.cpp w następujący sposób, aby wykonać go z naszej maszyny i sprawdzić, czy współdzielony kod działa.
#include <iostream>
#include <string>
#include "../CPP/Core.h"
int main() {
std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
std::cout << textFromCppCore << '\n';
return 0;
}
Aby zbudować kod, musisz wykonać:
$ g++ Main.cpp Core.cpp -o main
$ ./main
cpp says hello to Unix
Czas na wdrożenie po stronie mobilnej. O ile iOS ma prostą integrację, zaczynamy od niego. Nasza aplikacja na iOS jest typową aplikacją Obj-c z tylko jedną różnicą; pliki są .mm
i nie .m
. tj. jest to aplikacja Obj-C ++, a nie aplikacja Obj-C.
Dla lepszej organizacji tworzymy plik CoreWrapper.mm w następujący sposób:
#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
Ta klasa jest odpowiedzialna za konwersję typów CPP i wywołań na typy i wywołania Obj-C. Nie jest to obowiązkowe, gdy możesz wywołać kod CPP w dowolnym pliku w Obj-C, ale pomaga to zachować organizację, a poza plikami opakowującymi utrzymujesz kompletny kod w stylu Obj-C, tylko plik opakowania otrzymuje styl CPP .
Po podłączeniu opakowania do kodu CPP możesz użyć go jako standardowego kodu Obj-C, np. 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
Zobacz, jak wygląda aplikacja:
Teraz przyszedł czas na integrację z Androidem. Android używa Gradle jako systemu kompilacji, a do kodu C / C ++ używa CMake. Więc pierwszą rzeczą, którą musimy zrobić, to skonfigurować CMake w pliku gradle:
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
...
}
Drugim krokiem jest dodanie pliku 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}
)
Plik CMake to miejsce, w którym musisz dodać pliki CPP i foldery nagłówkowe, których będziesz używać w projekcie, w naszym przykładzie dodajemy CPP
folder i pliki Core.h / .cpp. Aby dowiedzieć się więcej o konfiguracji C / C ++, przeczytaj to.
Teraz główny kod jest częścią naszej aplikacji, czas stworzyć mostek, aby uprościć i uporządkować rzeczy, tworzymy specjalną klasę o nazwie CoreWrapper, która będzie naszym opakowaniem między JVM i CPP:
public class CoreWrapper {
public native String concatenateMyStringWithCppString(String myString);
static {
System.loadLibrary("native-lib");
}
}
Zwróć uwagę, że ta klasa ma native
metodę i ładuje bibliotekę natywną o nazwie native-lib
. Ta biblioteka jest tą, którą tworzymy, w końcu kod CPP stanie się współdzielonym .so
plikiem obiektu osadzonym w naszym pliku APK i loadLibrary
załaduje go. Na koniec, po wywołaniu metody natywnej, JVM przekaże wywołanie do załadowanej biblioteki.
Teraz najbardziej dziwną częścią integracji Androida jest JNI; Potrzebujemy następującego pliku cpp, w naszym przypadku „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;
}
}
Pierwszą rzeczą, którą zauważysz, jest to, że extern "C"
ta część jest niezbędna do poprawnej pracy JNI z naszym kodem CPP i powiązaniami metod. Zobaczysz także symbole używane przez JNI do pracy z JVM jako JNIEXPORT
i JNICALL
. Abyś zrozumiał znaczenie tych rzeczy, musisz poświęcić trochę czasu i przeczytać to , dla celów tego samouczka potraktuj je jako szablon.
Jedną istotną rzeczą i zwykle źródłem wielu problemów jest nazwa metody; musi być zgodny ze wzorcem „Java_package_class_method”. Obecnie studio Android ma dla niego doskonałe wsparcie, więc może automatycznie wygenerować ten szablon i pokazać ci, kiedy jest poprawny lub nie został nazwany. W naszym przykładzie nasza metoda nosi nazwę „Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString”, ponieważ „ademar.androidioscppexample” to nasz pakiet, więc zastępujemy „.” przez „_”, CoreWrapper jest klasą, w której łączymy metodę natywną, a „concatenateMyStringWithCppString” jest samą nazwą metody.
Ponieważ mamy poprawnie zadeklarowaną metodę, czas na analizę argumentów, pierwszy parametr jest wskaźnikiem JNIEnv
, to sposób, w jaki mamy dostęp do rzeczy JNI, kluczowe jest, abyśmy dokonali naszych konwersji, jak wkrótce się przekonasz. Drugi to jobject
instancja obiektu, którego użyłeś do wywołania tej metody. Możesz pomyśleć, że to java " this ", na naszym przykładzie nie musimy go używać, ale nadal musimy to zadeklarować. Po tym zadaniu otrzymamy argumenty metody. Ponieważ nasza metoda ma tylko jeden argument - String „myString”, mamy tylko „jstring” o tej samej nazwie. Zauważ też, że nasz typ zwracany jest również jstringiem. Dzieje się tak, ponieważ nasza metoda Java zwraca ciąg znaków. Aby uzyskać więcej informacji na temat typów Java / JNI, przeczytaj ją.
Ostatnim krokiem jest konwersja typów JNI do typów, których używamy po stronie CPP. W naszym przykładzie przekształcamy jstring
go w const char *
wysyłanie przekonwertowanego na CPP, uzyskujemy wynik i konwertujemy z powrotem na jstring
. Jak wszystkie inne kroki na JNI, nie jest to trudne; jest to tylko kotłowe, cała praca jest wykonywana przez JNIEnv*
argument, który otrzymujemy, gdy wywołujemy GetStringUTFChars
i NewStringUTF
. Po tym nasz kod jest gotowy do uruchomienia na urządzeniach z Androidem, spójrzmy.
Podejście opisane w doskonałej odpowiedzi powyżej może być całkowicie zautomatyzowane przez Scapix Language Bridge, który generuje kod opakowujący w locie bezpośrednio z nagłówków C ++. Oto przykład :
Zdefiniuj swoją klasę w 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);
};
I zadzwoń od Swift:
class ViewController: UIViewController {
func send(friend: Contact) {
let c = Contact()
contact.sendMessage("Hello", friend)
contact.addTags(["a","b","c"])
contact.addFriends([friend])
}
}
A z Java:
class View {
private contact = new Contact;
public void send(Contact friend) {
contact.sendMessage("Hello", friend);
contact.addTags({"a","b","c"});
contact.addFriends({friend});
}
}