Làm thế nào để sử dụng cùng một mã C ++ cho Android và iOS?


119

Android với NDK có hỗ trợ mã C / C ++ và iOS với Objective-C ++ cũng có hỗ trợ, vậy làm cách nào tôi có thể viết ứng dụng với mã C / C ++ gốc được chia sẻ giữa Android và iOS?


1
thử cocos2d-x framework
Glo

@glo nó có vẻ tốt, nhưng tôi đang tìm kiếm một thứ chung chung hơn, sử dụng c ++ không có khuôn khổ, "rõ ràng là đã loại trừ JNI".
ademar111190

Câu trả lời:


273

Cập nhật.

Câu trả lời này khá phổ biến ngay cả bốn năm sau khi tôi viết nó, trong bốn năm này, rất nhiều thứ đã thay đổi, vì vậy tôi quyết định cập nhật câu trả lời của mình để phù hợp hơn với thực tế hiện tại của chúng tôi. Ý tưởng câu trả lời không thay đổi; việc triển khai đã thay đổi một chút. Tiếng Anh của tôi cũng đã thay đổi, nó đã được cải thiện rất nhiều, vì vậy câu trả lời là dễ hiểu hơn với mọi người.

Vui lòng xem qua repo để bạn có thể tải xuống và chạy mã mà tôi sẽ hiển thị bên dưới.

Câu trả lời

Trước khi tôi hiển thị mã, vui lòng xem sơ đồ sau.

Vòm

Mỗi hệ điều hành có giao diện người dùng và đặc thù của nó, vì vậy chúng tôi dự định viết mã cụ thể cho từng nền tảng về vấn đề này. Mặt khác, tất cả mã logic, quy tắc nghiệp vụ và những thứ có thể được chia sẻ mà chúng tôi dự định viết bằng C ++, vì vậy chúng tôi có thể biên dịch cùng một mã cho mỗi nền tảng.

Trong sơ đồ, bạn có thể thấy lớp C ++ ở mức thấp nhất. Tất cả mã được chia sẻ đều nằm trong phân đoạn này. Mức cao nhất là mã Obj-C / Java / Kotlin thông thường, không có tin tức ở đây, phần khó là lớp giữa.

Lớp giữa đối với phía iOS rất đơn giản; bạn chỉ cần định cấu hình dự án của mình để xây dựng bằng cách sử dụng một biến thể của Obj-c được gọi là Objective-C ++ và tất cả là bạn có quyền truy cập vào mã C ++.

Vấn đề trở nên khó khăn hơn ở phía Android, cả hai ngôn ngữ, Java và Kotlin, trên Android, đều chạy dưới Máy ảo Java. Vì vậy, cách duy nhất để truy cập mã C ++ là sử dụng JNI , hãy dành thời gian đọc những điều cơ bản về JNI. May mắn thay, Android Studio IDE ngày nay có những cải tiến lớn về mặt JNI và rất nhiều vấn đề được hiển thị cho bạn khi bạn chỉnh sửa mã của mình.

Mã theo từng bước

Mẫu của chúng tôi là một ứng dụng đơn giản mà bạn gửi một văn bản tới CPP và nó chuyển đổi văn bản đó sang một thứ khác và trả lại. Ý tưởng là, iOS sẽ gửi "obj-C" và Android sẽ gửi "Java" từ các ngôn ngữ tương ứng của họ và mã CPP sẽ tạo ra một văn bản dưới dạng "cpp nói xin chào với << văn bản đã nhận >> ".

Mã CPP được chia sẻ

Trước hết, chúng ta sẽ tạo mã CPP được chia sẻ, làm điều đó, chúng ta có một tệp tiêu đề đơn giản với khai báo phương thức nhận văn bản mong muốn:

#include <iostream>

const char *concatenateMyStringWithCppString(const char *myString);

Và việc thực hiện 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

Một phần thưởng thú vị là, chúng ta cũng có thể sử dụng cùng một mã cho Linux và Mac cũng như các hệ thống Unix khác. Khả năng này đặc biệt hữu ích vì chúng tôi có thể kiểm tra mã được chia sẻ của mình nhanh hơn, vì vậy chúng tôi sẽ tạo một Main.cpp như sau để thực thi nó từ máy của chúng tôi và xem mã được chia sẻ có hoạt động hay không.

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

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

Để tạo mã, bạn cần thực thi:

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

iOS

Đã đến lúc thực hiện ở mặt di động. Đối với iOS có một tích hợp đơn giản, chúng tôi đang bắt đầu với nó. Ứng dụng iOS của chúng tôi là một ứng dụng obj-c điển hình chỉ có một điểm khác biệt; các tệp được .mmvà không .m. tức là Nó là một ứng dụng obj-C ++, không phải là một ứng dụng obj-C.

Để tổ chức tốt hơn, chúng tôi tạo CoreWrapper.mm như sau:

#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

Lớp này có trách nhiệm chuyển đổi các loại CPP và lệnh gọi sang các loại và lệnh gọi obj-C. Không bắt buộc khi bạn có thể gọi mã CPP trên bất kỳ tệp nào bạn muốn trên obj-C, nhưng nó giúp giữ cho tổ chức và bên ngoài tệp trình bao bọc của bạn, bạn duy trì mã theo kiểu obj-C hoàn chỉnh, chỉ tệp trình bao bọc trở thành theo kiểu CPP .

Khi trình bao bọc của bạn được kết nối với mã CPP, bạn có thể sử dụng nó làm mã Obj-C tiêu chuẩn, ví dụ: 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

Hãy xem ứng dụng trông như thế nào:

Xcode điện thoại Iphone

Android

Bây giờ đã đến lúc tích hợp Android. Android sử dụng Gradle làm hệ thống xây dựng và mã C / C ++, nó sử dụng CMake. Vì vậy, điều đầu tiên chúng ta cần làm là định cấu hình CMake trên tệp gradle:

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

Và bước thứ hai là thêm tệp 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}
)

Tệp CMake là nơi bạn cần thêm tệp CPP và thư mục tiêu đề mà bạn sẽ sử dụng trong dự án, trong ví dụ của chúng tôi, chúng tôi đang thêm CPPthư mục và các tệp Core.h / .cpp. Để biết thêm về cấu hình C / C ++, vui lòng đọc nó.

Bây giờ mã lõi là một phần của ứng dụng của chúng tôi, đã đến lúc tạo cầu nối, để làm cho mọi thứ trở nên đơn giản và có tổ chức hơn, chúng tôi tạo một lớp cụ thể có tên CoreWrapper để làm trình bao bọc giữa JVM và CPP:

public class CoreWrapper {

    public native String concatenateMyStringWithCppString(String myString);

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

}

Lưu ý rằng lớp này có một nativephương thức và tải một thư viện gốc có tên native-lib. Thư viện này là thư viện chúng tôi tạo, cuối cùng, mã CPP sẽ trở thành một đối tượng được chia sẻ .soTệp được nhúng trong APK của chúng tôi và loadLibrarysẽ tải nó. Cuối cùng, khi bạn gọi phương thức gốc, JVM sẽ ủy quyền cuộc gọi đến thư viện đã tải.

Giờ đây, phần kỳ lạ nhất của tích hợp Android là JNI; Chúng tôi cần một tệp cpp như sau, trong trường hợp của chúng tôi là "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;
}

}

Điều đầu tiên bạn sẽ nhận thấy là extern "C"phần này cần thiết để JNI hoạt động chính xác với các liên kết mã CPP và phương pháp của chúng tôi. Bạn cũng sẽ thấy một số ký hiệu mà JNI sử dụng để làm việc với JVM như JNIEXPORTJNICALL. Để bạn hiểu được ý nghĩa của những điều đó, cần phải dành một thời gian và đọc nó , vì mục đích hướng dẫn này, chỉ cần coi những thứ này như bản ghi sẵn.

Một điều quan trọng và thường là gốc rễ của rất nhiều vấn đề là tên của phương pháp; nó cần tuân theo mẫu "Java_package_class_method". Hiện tại, Android studio có hỗ trợ tuyệt vời cho nó để nó có thể tự động tạo bảng soạn sẵn này và hiển thị cho bạn khi nó chính xác hoặc không được đặt tên. Trong ví dụ của chúng tôi, phương pháp của chúng tôi được đặt tên là "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString", đó là vì "ademar.androidioscppexample" là gói của chúng tôi, vì vậy chúng tôi thay thế "." bởi "_", CoreWrapper là lớp mà chúng ta đang liên kết phương thức gốc và "concatenateMyStringWithCppString" là chính tên phương thức.

Vì chúng tôi đã khai báo đúng phương thức, đã đến lúc phân tích các đối số, tham số đầu tiên là một con trỏ của JNIEnvnó là cách chúng tôi có quyền truy cập vào nội dung JNI, điều quan trọng là chúng tôi thực hiện chuyển đổi như bạn sẽ thấy ngay sau đây. Thứ hai là jobjectnó là thể hiện của đối tượng mà bạn đã sử dụng để gọi phương thức này. Bạn có thể nghĩ nó giống như java " this ", trong ví dụ của chúng ta, chúng ta không cần sử dụng nó, nhưng chúng ta vẫn cần khai báo nó. Sau đối tượng công việc này, chúng ta sẽ nhận được các đối số của phương thức. Bởi vì phương thức của chúng tôi chỉ có một đối số - một Chuỗi "myString", chúng tôi chỉ có một "jstring" có cùng tên. Cũng lưu ý rằng kiểu trả về của chúng ta cũng là một jstring. Đó là vì phương thức Java của chúng tôi trả về một Chuỗi, để biết thêm thông tin về các loại Java / JNI, vui lòng đọc nó.

Bước cuối cùng là chuyển đổi loại JNI sang loại chúng tôi sử dụng ở phía CPP. Trên ví dụ của chúng tôi, chúng tôi đang chuyển đổi jstringđến một const char *gửi nó chuyển đổi sang CPP, nhận được kết quả và chuyển đổi ngược lại jstring. Như tất cả các bước khác trên JNI, nó không khó; nó chỉ được soạn sẵn, tất cả công việc được thực hiện bởi JNIEnv*đối số mà chúng ta nhận được khi chúng ta gọi GetStringUTFCharsNewStringUTF. Sau khi mã của chúng tôi đã sẵn sàng để chạy trên các thiết bị Android, chúng ta hãy xem.

AndroidStudio Android


7
Lời giải thích tuyệt vời
RED.

9
Tôi không nhận được nó - nhưng 1 cho một trong những câu trả lời chất lượng cao nhất trên SO
Michael Rodrigues

16
@ ademar111190 Cho đến nay, bài viết hữu ích nhất. Điều này không nên được đóng lại.
Jared Burrows

6
@JaredBurrows, tôi đồng tình. Đã bỏ phiếu để mở lại.
OmnipotentEntity

3
@KVISH bạn phải triển khai trình bao bọc trong Objective-C trước, sau đó bạn sẽ nhanh chóng truy cập trình bao bọc Objective-C bằng cách thêm tiêu đề trình bao bọc vào tệp tiêu đề bắc cầu của bạn. Hiện tại, không có cách nào để truy cập trực tiếp C ++ trong Swift. Để biết thêm thông tin, hãy xem stackoverflow.com/a/24042893/1853977
Chris

3

Phương pháp tiếp cận được mô tả trong câu trả lời tuyệt vời ở trên có thể hoàn toàn tự động bởi Scapix Language Bridge tạo mã trình bao bọc trực tiếp từ các tiêu đề C ++. Đây là một ví dụ :

Xác định lớp của bạn trong 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);
};

Và gọi nó từ Swift:

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

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

Và từ Java:

class View {
    private contact = new Contact;

    public void send(Contact friend) {
        contact.sendMessage("Hello", friend);
        contact.addTags({"a","b","c"});
        contact.addFriends({friend});
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.