จะใช้รหัส C ++ เดียวกันสำหรับ Android และ iOS ได้อย่างไร?


119

Android ที่มีNDKรองรับโค้ด C / C ++ และ iOS ที่มีObjective-C ++ก็รองรับเช่นกันดังนั้นฉันจะเขียนแอปพลิเคชันด้วยโค้ด C / C ++ ที่แชร์ระหว่าง Android และ iOS ได้อย่างไร


1
ลอง cocos2d-x framework
glo

@glo ดูเหมือนจะดี แต่ฉันกำลังมองหาสิ่งที่ธรรมดากว่านี้โดยใช้ c ++ ที่ไม่มีกรอบ "ยกเว้น JNI ชัด ๆ "
ademar111190

คำตอบ:


274

ปรับปรุง

คำตอบนี้เป็นที่นิยมมากแม้สี่ปีหลังจากที่ฉันเขียนมันในสี่ปีนี้มีการเปลี่ยนแปลงมากมายดังนั้นฉันจึงตัดสินใจอัปเดตคำตอบของฉันเพื่อให้เข้ากับความเป็นจริงในปัจจุบันของเรามากขึ้น แนวคิดคำตอบไม่เปลี่ยนแปลง การใช้งานมีการเปลี่ยนแปลงเล็กน้อย ภาษาอังกฤษของฉันก็เปลี่ยนไปเช่นกันพัฒนาขึ้นมากดังนั้นตอนนี้ทุกคนก็เข้าใจคำตอบมากขึ้น

โปรดดูที่repoเพื่อให้คุณสามารถดาวน์โหลดและเรียกใช้รหัสที่ฉันจะแสดงด้านล่าง

คำตอบ

ก่อนที่จะแสดงรหัสโปรดใช้แผนภาพต่อไปนี้ให้มาก

โค้ง

แต่ละ OS มี UI และลักษณะเฉพาะดังนั้นเราจึงตั้งใจที่จะเขียนโค้ดเฉพาะสำหรับแต่ละแพลตฟอร์มในเรื่องนี้ ในทางกลับกันรหัสลอจิกกฎทางธุรกิจและสิ่งที่สามารถแบ่งปันได้ทั้งหมดเราตั้งใจจะเขียนโดยใช้ C ++ ดังนั้นเราจึงสามารถรวบรวมรหัสเดียวกันกับแต่ละแพลตฟอร์มได้

ในแผนภาพคุณจะเห็นเลเยอร์ C ++ ที่ระดับต่ำสุด รหัสที่ใช้ร่วมกันทั้งหมดอยู่ในส่วนนี้ ระดับสูงสุดคือรหัส Obj-C / Java / Kotlin ปกติไม่มีข่าวที่นี่ส่วนที่ยากคือชั้นกลาง

ชั้นกลางถึงฝั่ง iOS นั้นเรียบง่าย คุณจะต้องกำหนดค่าโครงการของคุณเพื่อสร้างโดยใช้ตัวแปรของ Obj-c ที่รู้จักกันในชื่อObjective-C ++และทั้งหมดนี้คุณสามารถเข้าถึงรหัส C ++ ได้

สิ่งนี้ยากขึ้นในฝั่ง Android ทั้งสองภาษา Java และ Kotlin บน Android ทำงานภายใต้ Java Virtual Machine ดังนั้นวิธีเดียวในการเข้าถึงรหัส 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;
}

ยูนิกซ์

โบนัสที่น่าสนใจคือเราสามารถใช้รหัสเดียวกันสำหรับ 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 และการโทร ไม่บังคับเมื่อคุณสามารถเรียกใช้รหัส CPP ในไฟล์ใด ๆ ที่คุณต้องการบน Obj-C ได้ แต่จะช่วยรักษาองค์กรและภายนอกไฟล์ Wrapper ของคุณคุณจะรักษารหัสสไตล์ Obj-C ที่สมบูรณ์มีเพียงไฟล์ Wrapper เท่านั้นที่จะกลายเป็นสไตล์ CPP .

เมื่อ Wrapper ของคุณเชื่อมต่อกับรหัส 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 iPhone

Android

ตอนนี้เป็นเวลาสำหรับการรวม Android Android ใช้ Gradle เป็นระบบสร้างและสำหรับโค้ด C / C ++ จะใช้ CMake ดังนั้นสิ่งแรกที่เราต้องทำคือกำหนดค่า CMake บนไฟล์ gradle:

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 ที่จะทำงานร่วมกับ JVM เป็นและJNIEXPORT 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 ตัวอย่างของเราเราจะเปลี่ยนjstringไปconst char *ส่งแปลงเป็น CPP jstringได้รับผลที่ตามมาและแปลงกลับไป เช่นเดียวกับขั้นตอนอื่น ๆ ใน JNI มันไม่ยาก มันเป็น boilerplated เท่านั้นทำงานทั้งหมดจะกระทำโดยJNIEnv*การโต้แย้งที่เราได้รับเมื่อเราเรียกและGetStringUTFChars NewStringUTFหลังจากที่โค้ดของเราพร้อมที่จะทำงานบนอุปกรณ์ Android แล้วมาดูกัน

AndroidStudio Android


7
คำอธิบายที่ยอดเยี่ยม
RED.Skull

9
ฉันไม่เข้าใจ - แต่ +1 สำหรับหนึ่งในคำตอบคุณภาพสูงสุดใน SO
Michael Rodrigues

16
@ ademar111190 โดยโพสต์ที่เป็นประโยชน์มากที่สุด สิ่งนี้ไม่ควรถูกปิด
Jared Burrows

6
@JaredBurrows ฉันเห็นด้วย โหวตให้เปิดอีกครั้ง
OmnipotentEntity

3
@KVISH คุณต้องติดตั้ง Wrapper ใน Objective-C ก่อนจากนั้นคุณจะเข้าถึง Objective-C wrapper ได้อย่างรวดเร็วโดยการเพิ่มส่วนหัวของ wrapper ในไฟล์ส่วนหัวของการเชื่อมโยงของคุณ ไม่มีวิธีเข้าถึง C ++ โดยตรงใน Swift ในตอนนี้ ดูข้อมูลเพิ่มเติมได้ที่stackoverflow.com/a/24042893/1853977
Chris

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});
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.