NDK ile Android, C / C ++ kodunu desteklerken, Objective-C ++ ile iOS de desteğe sahiptir, bu nedenle Android ve iOS arasında paylaşılan yerel C / C ++ koduyla uygulamaları nasıl yazabilirim?
NDK ile Android, C / C ++ kodunu desteklerken, Objective-C ++ ile iOS de desteğe sahiptir, bu nedenle Android ve iOS arasında paylaşılan yerel C / C ++ koduyla uygulamaları nasıl yazabilirim?
Yanıtlar:
Bu cevap, yazdıktan dört yıl sonra bile oldukça popüler, bu dört yılda birçok şey değişti, bu yüzden cevabımı mevcut gerçekliğimize daha iyi uyacak şekilde güncellemeye karar verdim. Cevap fikri değişmez; uygulama biraz değişti. İngilizcem de değişti, çok gelişti, bu yüzden cevap artık herkes için daha anlaşılır.
Lütfen depoya bir göz atın , böylece aşağıda göstereceğim kodu indirip çalıştırabilirsiniz.
Kodu göstermeden önce, lütfen aşağıdaki diyagramdan çok şey alın.
Her işletim sisteminin kendi kullanıcı arayüzü ve özellikleri vardır, bu nedenle bu bağlamda her platforma belirli bir kod yazmayı planlıyoruz. Diğer taraftan, tüm mantık kodu, iş kuralları ve paylaşılabilecek şeyler C ++ kullanarak yazmayı amaçlıyoruz, böylece her platform için aynı kodu derleyebiliriz.
Diyagramda, en düşük seviyede C ++ katmanını görebilirsiniz. Tüm paylaşılan kodlar bu segmentte. En yüksek seviye normal Obj-C / Java / Kotlin kodudur, burada haber yok, zor kısım orta katmandır.
İOS tarafının orta katmanı basittir; Projenizi yalnızca Objective-C ++ olarak bilinen bir Obj-c varyantını kullanarak inşa etmek için yapılandırmanız gerekir ve hepsi bu, C ++ koduna erişiminiz vardır.
Android tarafında her iki dilde, Java ve Kotlin, Android'de Java Sanal Makinesi altında çalışmak daha da zorlaştı. Dolayısıyla, C ++ koduna erişmenin tek yolu JNI kullanmaktır , lütfen JNI'nin temellerini okumak için zaman ayırın. Neyse ki, bugünün Android Studio IDE'sinin JNI tarafında büyük iyileştirmeleri var ve kodunuzu düzenlerken size birçok sorun gösteriliyor.
Örneğimiz, CPP'ye bir metin gönderdiğiniz basit bir uygulamadır ve bu metni başka bir şeye dönüştürür ve döndürür. Fikir şu ki, iOS "Obj-C" gönderecek ve Android kendi dillerinden "Java" gönderecek ve CPP kodu aşağıdaki gibi bir metin oluşturacak "cpp, << metne merhaba >> " diyor .
Her şeyden önce, paylaşılan CPP kodunu oluşturacağız, bunu yaparak, istenen metni alan yöntem bildirimi ile basit bir başlık dosyamız var:
#include <iostream>
const char *concatenateMyStringWithCppString(const char *myString);
Ve CPP uygulaması:
#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;
}
İlginç bir bonus, aynı kodu Linux ve Mac ve diğer Unix sistemleri için de kullanabiliriz. Bu olasılık özellikle yararlıdır çünkü paylaşılan kodumuzu daha hızlı test edebiliriz, bu nedenle makinemizden çalıştırmak için aşağıdaki gibi bir Main.cpp oluşturacağız ve paylaşılan kodun çalışıp çalışmadığını göreceğiz.
#include <iostream>
#include <string>
#include "../CPP/Core.h"
int main() {
std::string textFromCppCore = concatenateMyStringWithCppString("Unix");
std::cout << textFromCppCore << '\n';
return 0;
}
Kodu oluşturmak için şunları yapmanız gerekir:
$ g++ Main.cpp Core.cpp -o main
$ ./main
cpp says hello to Unix
Mobil tarafta uygulama zamanı. İOS'un basit bir entegrasyonu olduğu için, onunla başlıyoruz. İOS uygulamamız, yalnızca bir farkla tipik bir Obj-c uygulamasıdır; dosyalar .mm
ve değil .m
. Yani bir Obj-C ++ uygulamasıdır, Obj-C uygulaması değildir.
Daha iyi bir organizasyon için CoreWrapper.mm'yi aşağıdaki gibi oluşturuyoruz:
#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
Bu sınıf, CPP türlerini ve çağrıları Obj-C türlerine ve çağrılarına dönüştürme sorumluluğuna sahiptir. Obj-C'de istediğiniz herhangi bir dosyada CPP kodunu çağırabildiğinizde zorunlu değildir, ancak organizasyonu korumaya yardımcı olur ve sarmalayıcı dosyalarınızın dışında tam bir Obj-C tarzı kod tutarsınız, yalnızca sarmalayıcılar dosyası CPP stili olur .
Sarıcınız CPP koduna bağlandıktan sonra, bunu standart bir Obj-C kodu olarak kullanabilirsiniz, örneğin 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
Uygulamanın nasıl göründüğüne bir göz atın:
Şimdi sıra Android entegrasyonuna geldi. Android, derleme sistemi olarak Gradle'ı kullanır ve C / C ++ kodu için CMake kullanır. Bu yüzden yapmamız gereken ilk şey CMake'i gradle dosyasında yapılandırmak:
android {
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
...
}
İkinci adım ise CMakeLists.txt dosyasını eklemektir:
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 dosyası, projede kullanacağınız CPP dosyalarını ve başlık klasörlerini eklemeniz CPP
gereken yerdir, örneğimizde klasörü ve Core.h / .cpp dosyalarını ekliyoruz. C / C ++ yapılandırması hakkında daha fazla bilgi edinmek için lütfen okuyun.
Artık temel kod uygulamamızın bir parçası, köprüyü oluşturma zamanı, işleri daha basit ve organize hale getirmek için JVM ve CPP arasında sarmalayıcımız olacak CoreWrapper adında belirli bir sınıf oluşturuyoruz:
public class CoreWrapper {
public native String concatenateMyStringWithCppString(String myString);
static {
System.loadLibrary("native-lib");
}
}
Bu sınıfın bir native
yöntemi olduğunu ve adlı yerel bir kitaplık yüklediğini unutmayın native-lib
. Bu kütüphane oluşturduğumuz kütüphanedir, sonunda CPP kodu APK'mıza .so
yerleştirilen paylaşılan bir nesne dosyası haline gelecek ve loadLibrary
yükleyecektir. Son olarak, yerel yöntemi çağırdığınızda, JVM çağrıyı yüklenen kitaplığa devredecektir.
Şimdi Android entegrasyonunun en garip kısmı JNI; Aşağıdaki gibi bir cpp dosyasına ihtiyacımız var, bizim durumumuzda "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;
}
}
Fark edeceğiniz ilk şey extern "C"
, JNI'nin CPP kodumuz ve yöntem bağlantılarımızla doğru çalışması için bu bölümün gerekli olduğudur. JNI'nin JVM ile çalışmak için kullandığı bazı sembolleri JNIEXPORT
ve olarak da göreceksiniz JNICALL
. Bu şeylerin anlamını anlamak için, biraz zaman ayırıp okumak gerekir , bu eğitim amaçları için bunları sadece şablon olarak düşünün.
Önemli bir şey ve genellikle pek çok sorunun kökü, yöntemin adıdır; "Java_package_class_method" modelini izlemesi gerekiyor. Şu anda, Android stüdyosu bunun için mükemmel bir desteğe sahiptir, bu nedenle bu ortak metini otomatik olarak oluşturabilir ve doğru olup olmadığını size gösterebilir. Örneğimizde yöntemimiz "Java_ademar_androidioscppexample_CoreWrapper_concatenateMyStringWithCppString" olarak adlandırılmıştır, çünkü "ademar.androidioscppexample" bizim paketimizdir, bu yüzden "." "_" ile CoreWrapper, yerel yöntemi bağladığımız sınıftır ve "concatenateMyStringWithCppString", yöntemin adının kendisidir.
Yöntemi doğru bir şekilde ilan ettiğimiz için, argümanları analiz etmenin zamanı geldi, ilk parametre bunun bir göstergesidir, JNIEnv
JNI öğelerine erişim şeklimizdir, yakında göreceğiniz gibi dönüşümlerimizi yapmamız çok önemlidir. İkincisi, jobject
bu yöntemi çağırmak için kullandığınız nesnenin örneğidir. Bunu java " bu " olarak düşünebilirsiniz , örneğimizde onu kullanmamıza gerek yok ama yine de beyan etmemiz gerekiyor. Bu iş nesnesinden sonra yöntemin argümanlarını alacağız. Metodumuzun sadece bir argümanı olduğundan - "myString" String, aynı ada sahip sadece "jstring" e sahibiz. Ayrıca dönüş tipimizin de bir jstring olduğuna dikkat edin. Java yöntemimiz bir String döndürdüğü için, Java / JNI türleri hakkında daha fazla bilgi için lütfen okuyun.
Son adım, JNI türlerini CPP tarafında kullandığımız türlere dönüştürmektir. Örneğimizde, CPP'ye dönüştürülmüş jstring
bir const char *
göndermeye dönüştürüyoruz, sonucu alıyoruz ve geri dönüştürüyoruz jstring
. JNI'deki diğer tüm adımlar gibi zor değil; sadece kaynatılır, tüm iş ve JNIEnv*
dediğimizde aldığımız argüman tarafından yapılır . Kodumuz Android cihazlarda çalışmaya hazır olduktan sonra, bir göz atalım.GetStringUTFChars
NewStringUTF
Yukarıdaki mükemmel yanıtta açıklanan yaklaşım, doğrudan C ++ başlıklarından anında sarmalayıcı kodu üreten Scapix Language Bridge tarafından tamamen otomatikleştirilebilir . İşte bir örnek :
Sınıfınızı C ++ 'da tanımlayın:
#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);
};
Ve Swift'den ara:
class ViewController: UIViewController {
func send(friend: Contact) {
let c = Contact()
contact.sendMessage("Hello", friend)
contact.addTags(["a","b","c"])
contact.addFriends([friend])
}
}
Ve Java'dan:
class View {
private contact = new Contact;
public void send(Contact friend) {
contact.sendMessage("Hello", friend);
contact.addTags({"a","b","c"});
contact.addFriends({friend});
}
}