Wie verwende ich denselben C ++ - Code für Android und iOS?

119

Android mit NDK unterstützt C / C ++ - Code und iOS mit Objective-C ++ unterstützt auch. Wie kann ich also Anwendungen mit nativem C / C ++ - Code schreiben, die von Android und iOS gemeinsam genutzt werden?

ademar111190
quelle
1
versuchen Sie cocos2d-x Framework
glo
@glo es scheint gut, aber ich bin auf der Suche nach einer allgemeineren Sache, die C ++ ohne Frameworks verwendet, "JNI natürlich ausgeschlossen".
Ademar111190

Antworten:

272

Aktualisieren.

Diese Antwort ist auch vier Jahre nach dem Schreiben sehr beliebt. In diesen vier Jahren haben sich viele Dinge geändert. Deshalb habe ich beschlossen, meine Antwort zu aktualisieren, um sie besser an unsere aktuelle Realität anzupassen. Die Antwortidee ändert sich nicht; Die Implementierung hat sich ein wenig geändert. Mein Englisch hat sich ebenfalls geändert, es hat sich stark verbessert, sodass die Antwort jetzt für alle verständlicher ist.

Bitte schauen Sie sich das Repo an, damit Sie den unten gezeigten Code herunterladen und ausführen können.

Die Antwort

Bevor ich den Code zeige, nehmen Sie bitte viel auf das folgende Diagramm.

Bogen

Jedes Betriebssystem hat seine Benutzeroberfläche und Besonderheiten, daher beabsichtigen wir, diesbezüglich spezifischen Code auf jede Plattform zu schreiben. Mit anderen Worten, alle Logikcodes, Geschäftsregeln und Dinge, die gemeinsam genutzt werden können, möchten wir mit C ++ schreiben, damit wir für jede Plattform denselben Code kompilieren können.

Im Diagramm sehen Sie die C ++ - Ebene auf der untersten Ebene. Der gesamte gemeinsam genutzte Code befindet sich in diesem Segment. Die höchste Ebene ist regulärer Obj-C / Java / Kotlin-Code, keine Neuigkeiten hier, der schwierige Teil ist die mittlere Ebene.

Die mittlere Ebene zur iOS-Seite ist einfach; Sie müssen Ihr Projekt nur so konfigurieren, dass es mit einer Variante von Obj-c namens erstellt wird Objective-C ++ bekannt ist, und Sie haben Zugriff auf C ++ - Code.

Auf der Android-Seite wurde es schwieriger, beide Sprachen, Java und Kotlin, auf Android, laufen unter einer Java Virtual Machine. Der einzige Weg, um auf C ++ - Code zuzugreifen, ist die Verwendung von JNI . Nehmen Sie sich bitte Zeit, um die Grundlagen von JNI zu lesen. Glücklicherweise weist die heutige Android Studio-IDE auf der JNI-Seite enorme Verbesserungen auf, und Ihnen werden beim Bearbeiten Ihres Codes viele Probleme angezeigt.

Der Code schrittweise

Unser Beispiel ist eine einfache App, mit der Sie einen Text an CPP senden. Dieser konvertiert diesen Text in etwas anderes und gibt ihn zurück. Die Idee ist, iOS sendet "Obj-C" und Android sendet "Java" aus ihren jeweiligen Sprachen, und der CPP-Code erstellt einen Text wie folgt "cpp sagt Hallo zu << empfangenem Text >> ".

Gemeinsamer CPP-Code

Zunächst erstellen wir den gemeinsam genutzten CPP-Code. Dabei haben wir eine einfache Header-Datei mit der Methodendeklaration, die den gewünschten Text erhält:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

Und die CPP-Implementierung:

#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

Ein interessanter Bonus ist, dass wir denselben Code auch für Linux und Mac sowie für andere Unix-Systeme verwenden können. Diese Möglichkeit ist besonders nützlich, da wir unseren freigegebenen Code schneller testen können. Daher erstellen wir eine Main.cpp wie folgt, um sie von unserem Computer aus auszuführen und zu überprüfen, ob der freigegebene Code funktioniert.

#include <iostream>
#include <string>
#include "../CPP/Core.h"

int main() {
  std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
  std::cout << textFromCppCore << '\n';
  return 0;
}

Um den Code zu erstellen, müssen Sie Folgendes ausführen:

$ g++ Main.cpp Core.cpp -o main
$ ./main 
cpp says hello to Unix

iOS

Es ist Zeit, auf der mobilen Seite zu implementieren. Soweit iOS eine einfache Integration hat, beginnen wir damit. Unsere iOS-App ist eine typische Obj-c-App mit nur einem Unterschied. Die Dateien sind .mmund nicht .m. dh es ist eine Obj-C ++ - App, keine Obj-C-App.

Für eine bessere Organisation erstellen wir CoreWrapper.mm wie folgt:

#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

Diese Klasse hat die Verantwortung, CPP-Typen und -Aufrufe in Obj-C-Typen und -Aufrufe zu konvertieren. Es ist nicht obligatorisch, wenn Sie CPP-Code für eine beliebige Datei auf Obj-C aufrufen können. Es hilft jedoch, die Organisation beizubehalten. Außerhalb Ihrer Wrapper-Dateien behalten Sie einen vollständigen Code im Obj-C-Stil bei. Nur die Wrapper-Datei wird im CPP-Stil erstellt .

Sobald Ihr Wrapper mit dem CPP-Code verbunden ist, können Sie ihn als Standard-Obj-C-Code verwenden, z. B. 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

Sehen Sie sich an, wie die App aussieht:

Xcode iPhone

Android

Jetzt ist es Zeit für die Android-Integration. Android verwendet Gradle als Build-System und für C / C ++ - Code CMake. Als erstes müssen wir also die CMake-on-Gradle-Datei konfigurieren:

android {
...
externalNativeBuild {
    cmake {
        path "CMakeLists.txt"
    }
}
...
defaultConfig {
    externalNativeBuild {
        cmake {
            cppFlags "-std=c++14"
        }
    }
...
}

Der zweite Schritt ist das Hinzufügen der Datei 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}
)

In der CMake-Datei müssen Sie die CPP-Dateien und Header-Ordner hinzufügen, die Sie für das Projekt verwenden. In unserem Beispiel fügen wir den CPPOrdner und die Core.h / .cpp-Dateien hinzu. Um mehr über die C / C ++ - Konfiguration zu erfahren, lesen Sie diese bitte .

Jetzt ist der Kerncode Teil unserer App. Es ist Zeit, die Brücke zu erstellen. Um die Dinge einfacher und organisierter zu gestalten, erstellen wir eine bestimmte Klasse namens CoreWrapper, die unser Wrapper zwischen JVM und CPP ist:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Beachten Sie, dass diese Klasse eine nativeMethode hat und eine native Bibliothek mit dem Namen lädt native-lib. Diese Bibliothek ist diejenige, die wir erstellen. Am Ende wird der CPP-Code zu einer gemeinsam genutzten Objektdatei, .sodie in unsere APK eingebettet istloadLibrary wird geladen. Wenn Sie die native Methode aufrufen, delegiert die JVM den Aufruf an die geladene Bibliothek.

Der seltsamste Teil der Android-Integration ist jetzt das JNI. Wir benötigen eine cpp-Datei wie folgt, in unserem Fall "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;
}

}

Das erste, was Sie bemerken werden, ist, dass extern "C"dieser Teil notwendig ist, damit JNI korrekt mit unseren CPP-Code- und Methodenverknüpfungen arbeitet. Sie werden auch einige Symbole sehen, die JNI verwendet, um mit JVM als JNIEXPORTund zu arbeiten JNICALL. Um die Bedeutung dieser Dinge zu verstehen, ist es notwendig, sich eine Zeit zu nehmen und sie zu lesen . Für dieses Tutorial betrachten Sie diese Dinge einfach als Boilerplate.

Eine wichtige Sache und normalerweise die Wurzel vieler Probleme ist der Name der Methode; Es muss dem Muster "Java_package_class_method" folgen. Derzeit bietet Android Studio eine hervorragende Unterstützung, sodass dieses Boilerplate automatisch generiert und Ihnen angezeigt werden kann, wenn es korrekt oder nicht benannt ist. In unserem Beispiel heißt unsere Methode "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString", weil "ademar.androidioscppexample" unser Paket ist, also ersetzen wir das "." Mit "_" ist CoreWrapper die Klasse, in der wir die native Methode verknüpfen, und "concatenateMyStringWithCppString" ist der Methodenname selbst.

Da wir die Methode korrekt deklariert haben, ist es Zeit, die Argumente zu analysieren. Der erste Parameter ist ein Zeiger JNIEnvdarauf, wie wir auf JNI-Inhalte zugreifen können. Es ist entscheidend, dass wir unsere Konvertierungen durchführen, wie Sie gleich sehen werden. Das zweite ist jobjectdie Instanz des Objekts, mit dem Sie diese Methode aufgerufen haben. Sie können es als Java " dies " betrachten. In unserem Beispiel müssen wir es nicht verwenden, aber wir müssen es trotzdem deklarieren. Nach diesem Jobobjekt erhalten wir die Argumente der Methode. Da unsere Methode nur ein Argument hat - einen String "myString", haben wir nur einen "jstring" mit demselben Namen. Beachten Sie auch, dass unser Rückgabetyp auch ein Jstring ist. Da unsere Java-Methode einen String zurückgibt, lesen Sie ihn bitte, um weitere Informationen zu Java / JNI-Typen zu erhalten .

Der letzte Schritt besteht darin, die JNI-Typen in die Typen zu konvertieren, die wir auf der CPP-Seite verwenden. In unserem Beispiel wandeln wir das jstringin ein const char *Senden um, konvertieren es in das CPP, erhalten das Ergebnis und konvertieren zurück zu jstring. Wie bei allen anderen Schritten auf JNI ist es nicht schwer; es ist nur kesselplattiert, die ganze Arbeit wird durch das JNIEnv*Argument erledigt, das wir erhalten, wenn wir das GetStringUTFCharsund aufrufen NewStringUTF. Nachdem unser Code auf Android-Geräten ausgeführt werden kann, werfen wir einen Blick darauf.

AndroidStudio Android

ademar111190
quelle
7
Tolle Erklärung
RED.Skull
9
Ich verstehe es nicht - aber +1 für eine der qualitativ hochwertigsten Antworten auf SO
Michael Rodrigues
16
@ ademar111190 Mit Abstand der hilfreichste Beitrag. Dies sollte nicht geschlossen worden sein.
Jared Burrows
6
@ JaredBurrows, ich stimme zu. Zur Wiedereröffnung gewählt.
Allmächtige
3
@KVISH Sie müssen den Wrapper zuerst in Objective-C implementieren, dann können Sie schnell auf den Objective-C-Wrapper zugreifen, indem Sie den Wrapper-Header zu Ihrer Bridging-Header-Datei hinzufügen. Ab sofort keine Möglichkeit mehr, direkt auf C ++ in Swift zuzugreifen. Weitere Informationen finden Sie unter stackoverflow.com/a/24042893/1853977
Chris
3

Der in der obigen hervorragenden Antwort beschriebene Ansatz kann von Scapix Language Bridge vollständig automatisiert werden, das Wrapper-Code im laufenden Betrieb direkt aus C ++ - Headern generiert. Hier ist ein Beispiel :

Definieren Sie Ihre Klasse in 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);
};

Und nenne es von Swift:

class ViewController: UIViewController {
    func send(friend: Contact) {
        let c = Contact()

        contact.sendMessage("Hello", friend)
        contact.addTags(["a","b","c"])
        contact.addFriends([friend])
    }
}

Und aus Java:

class View {
    private contact = new Contact;

    public void send(Contact friend) {
        contact.sendMessage("Hello", friend);
        contact.addTags({"a","b","c"});
        contact.addFriends({friend});
    }
}
Boris Rasin
quelle