Android ที่มีNDKรองรับโค้ด C / C ++ และ iOS ที่มีObjective-C ++ก็รองรับเช่นกันดังนั้นฉันจะเขียนแอปพลิเคชันด้วยโค้ด C / C ++ ที่แชร์ระหว่าง Android และ iOS ได้อย่างไร
Android ที่มีNDKรองรับโค้ด C / C ++ และ iOS ที่มีObjective-C ++ก็รองรับเช่นกันดังนั้นฉันจะเขียนแอปพลิเคชันด้วยโค้ด C / C ++ ที่แชร์ระหว่าง Android และ iOS ได้อย่างไร
คำตอบ:
คำตอบนี้เป็นที่นิยมมากแม้สี่ปีหลังจากที่ฉันเขียนมันในสี่ปีนี้มีการเปลี่ยนแปลงมากมายดังนั้นฉันจึงตัดสินใจอัปเดตคำตอบของฉันเพื่อให้เข้ากับความเป็นจริงในปัจจุบันของเรามากขึ้น แนวคิดคำตอบไม่เปลี่ยนแปลง การใช้งานมีการเปลี่ยนแปลงเล็กน้อย ภาษาอังกฤษของฉันก็เปลี่ยนไปเช่นกันพัฒนาขึ้นมากดังนั้นตอนนี้ทุกคนก็เข้าใจคำตอบมากขึ้น
โปรดดูที่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 ที่ใช้ร่วมกันเรามีไฟล์ส่วนหัวที่เรียบง่ายพร้อมการประกาศวิธีการที่ได้รับข้อความที่ต้องการ:
#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 ของเราเป็นแอป 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
ดูลักษณะของแอป:
ตอนนี้เป็นเวลาสำหรับการรวม 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 แล้วมาดูกัน
วิธีการที่อธิบายไว้ในคำตอบที่ยอดเยี่ยมข้างต้นสามารถทำงานได้โดยอัตโนมัติโดย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});
}
}