React Native Bridge Nedir? Nasıl Çalışır?
Bu yazıda sizlere native ve JavaScript tarafın birbiri ile haberleştiği bridge (köprü) yapısından bahsedeceğim.
React Native Bridge Nedir?
React Native Bridge, JavaScript ile Native taraf arasında iletişimi sağlamakla yükümlü olan bir mesaj taşıma katmanıdır. C/C++ ile yazılmıştır ve bu sayede Android ve IOS gibi birçok platformu destekler. İçerisinde Safari tarayıcısının motoru olan JavaScript Core VM’i barındırır. Bunun amacı, global değişkenler tanımlayarak ve native metotların JS tarafına enjekte edilerek bir kod arayüzünün sağlanması suretiyle gelen isteklerin JSON olarak native tarafa iletilmesidir. Native taraftan dönen değerler ise JSON olarak serialized şekilde gelir ve deserialize edilerek sonuç JavaScript tarafında işlenir.
React Native’deki Thread Yapıları
Bridge’in asıl implementasyonuna geçmeden önce RN’in thread yapısına değinmekte fayda var. 3 temel thread bulunuyor:
- JavaScript thread: JavaScript/React kodunun çalıştığı thread’dir.
- Main/UI thread: Android ve IOS’teki native işlemlerden sorumlu ve arayüz bileşenlerini düzenleyen thread’dir.
- Shadow thread: Arayüz bileşenlerin bir ağaç veri tipinde tutularak Yoga Layout Engine üzerinde Shadow DOM’un yönetilmesini gerçekleştirir ve NativeUI’a ilgili arayüz birimlerinin ekrana basılmasını bildirir.
Ayrıca react-native-camera gibi her bir native modülün kendine ait bir GCD kuyruğu bulunuyor. Bahsettiğimiz shadow thread de aslında bir thread’den ziyade bir GCD’yi teşkil etmektedir.
Native Modüller
Native modüller, Android veya iOS tarafında çalıştırdığımız kod parçacıklarıdır diyebiliriz. Aşağıdaki gibi “Merhaba <isim>” şeklinde log atan bir iOS native modülümüz olsun:
RCT ile başlayan sınıflar aslında ReaCT’in bir kısaltmasıdır. RCTBridgeModule’den extend edilen class’lar ise bir React Native modülü haline gelir ve otomatik olarak initialize edilir. RCTBridgeModule’ünü implement eden class’lar RCT_EXPORT_MODULE() macro’sunu da dahil etmek zorundadır. Ayrıca JavaScript tarafından çağrılması gereken metod RCT_EXPORT_METHOD fonksiyonu ile belirtilmelidir. Şimdi daha detaylı olarak bu terimlere değinelim.
RCT_EXPORT_MODULE(“ModulAdi”)
Adı üzerinde yazdığımız modülü export etmeye yarar. Yani ilgili modül, bridge tarafından keşfedilebilir hale gelmiş olur. React Native tarafında içeriği aşağıdaki gibi tanımlanmıştır:
Öncelikle RCTRegisterModule’ü bir RCT_EXTERN fonksiyonu olarak tanımlıyor. Bu sayede derleyici, fonksiyonu derleme esnasında görmezden geliyor. Böylece bağlama zamanında (link time) sadece JavaScript thread’i tarafından çağrılabilir hale getirilmiş oluyor.
Daha sonra moduleName metodu tanımlanarak, js_name
değişkeni null değilse ve bir fonksiyon adı verilmişse onun kullanılması sağlanıyor. Aksi halde, ObjectiveC class’ının adı varsayılan olarak kullanılıyor.
Son olarak, uygulama açıldığında her bir class için çağrılan load metodu tanımlanıyor. Bu metot, üstte tanımlanan RCTRegisterModule fonksiyonunun bridge tarafından görülebilmesini sağlıyor.
RCT_EXPORT_METHOD(“Param1”, “Param2”)
Bu metot aslında tanımladığımız greet metoduna yeni bir ekleme yapmıyor. Sadece farklı bir isimde yeni br metot oluşturuyor. Örneğin aşağıdaki gibi bir kod üretilebiliyor:
Kodu açıklayacak olursak, __rct_export__
ön eki ile bir metot oluşturuyor. Daha sonra eğer tanımlanmış ise js_name’i ekliyor (bizim örneğimizde yoktu o nedenle eklemedi). Sonrasında tanımlanan satır numarasını(12) __COUNTER__
macrosunu kullanarak ekliyor.
Bu metodun amacı, js_name ve satır numarası parametrelerini kullanarak benzersiz bir metot adı üretmek. Bu sayede metot isimlerinin çakışması (method clashing) önlenmiş oluyor. Aslında category kullanarak aynı isimli iki metodun kullanılabilmesi mümkün. Fakat category kullanıldığında XCode’un, “unexpected behavior” mesajlarını olmadığı halde varmış gibi gösteriyor. Asıl amaç bu uyarının da giderilmesidir.
Bridge’in çalışma zamanındaki işleyişi
Bu ana kadar oluşturulan modül için, bridge açısından gereken tüm ayarlamalar yapılmış oldu. Artık bridge, export edilen modül ve metotları görebilir. Bu zamana kadar anlattığımız bu süreç aslında yükleme zamanında (load time)’da gerçekleşmektedir. Şimdi ise çalışma zamanında nasıl işlediğine değinebiliriz.
Bridge’in işleyişindeki aşamalar aşağıdaki gibidir:
Her şeyden önce modüllerin başlatılması gerektiği için bu kısma değinelim.
Modüllerin Başlatılması
Bu adımda, daha önce bahsettiğimiz RCTRegisterModule fonksiyonu, modules array’ine oluşturduğumuz Person class’ını ekler. Bu sayede bridge oluşturulduğunda, array içerisinde gezinerek ilgili modüller yüklenir. Modülün bridge’deki referansı JavaScript için kaydedilir ve bridge’e bu referens tekrar geri verilir. Bu sayede hem native hem de JS tarafında ayrı ayrı çağrılabilir. Ayrıca hangi queue üzerinde çalışmak istediği kontrol edilir. Eğer herhangi bir kuyruk belirtilmemişse diğer modüllerin kuyruklarından bağımsız olarak, modüle yeni bir kuyruk (queue) verilir. Yaptığı iş aşağıdaki gibidir:
Modüllerin Yapılandırılması
Modüller register edildikten sonra, bir arkaplan thread’inde her modül’ün export ettiği metotlar listelenir ve isimleri __rct_export__
ile başlayan metotlar alınır. Böylece metot imzalarının string gösterimleri elde edilir. Bu aşama önemlidir. Çünkü bu aşama sayesinde parametrelerin gerçek tipleri bilinebilir hale gelmektedir. Örneğin runtime’da bu parametrenin sadece bir id parametresinin olduğunu bilebiliriz. Fakat bu şekilde tipinin de NSString* olduğunu bilebiliyoruz:
JavaScript Executor’ün Ayarlanması
JavaScript Executor’leri, -(void)setUp
metodu sayesinde, JavaScript Core’un başlatılması gibi performans açısından yoran işlemleri, arkaplan thread’inde gerçekleştirebilir. setUp
çağrısını, tüm executorler’in yerine sadece aktif olan executor birimi alabildiğinden dolayı performans açısından da kazanç sağlanmaktadır:
Modüle ait JSON kodunun enjekte edilmesi
Birçok modül barındıran bir uygulamada sadece bir modülün yapılandırma ayarlarını içeren JSON kodu aşağıdaki gibi olacaktır:
Bu kod, JavaScript Core VM’de global değişken olarak kaydedilir. Böylece bridge’in JS tarafı oluşturulurken, bu kodu kullanarak modüllerin üretilmesi sağlanır.
JavaScript Kodunun Yüklenmesi
Bu kısımda adından da anlaşılacağı üzere, oluşturulan JavaScript kodu yüklenir. Geliştirim esnasında genellikle metro bundler’ın sunduğu kod indirilerek yüklenir. Üretim ortamına sunulduğunda ise, diskten (telefon hafızasından) yüklenmektedir.
JavaScript Kodunun Çalıştırılması
Her şey hazır hale geldiğinde, JavaScript Core VM’e uygulamanın kaynak kodu yüklenir. JavaScript Core VM, kaynak kodu kopyalar, parse eder ve çalıştırır. Çalıştırmanın başlangıç aşamasında tüm CommonJS modüllerini register eder ve dosyanın ilk satırına yazar:
Üretilen modülün JS tarafında kullanımı
JSON konfigürasyon kodu kullanılarak üretilen modüller, JavaScript tarafında react-native
’in NativeModules
nesnesi aracılığıyla kullanılır:
Böylelikle JavaScript tarafında greet()
metodu çağrıldığında; modülün adı, metodun adı ve kullanılan parametreler modülün kuyruğuna (Person GCD queue) aktarılır. JavaScript kodun çalıştırması tamamlandığında queue, bu çağrıları gerçekleştirmek için native tarafa aktarılır.
Çalışma döngüsü
Bridge’in çalışma döngüsü aşağıdaki gibi olacaktır:
Native taraftan başlayan çağrılar, JS kodunu tetikler. Çalışma esnasında NativeModules
‘e her çağrı yapıldığı anda, native tarafta gelen çağrılar kuyruğa alınır. JS kodu çağrımı tamamlandığında, native tarafta kuyruklanmış çağrılar işlenmeye başlanır. Çağrılar işlendikçe, bridge üzerinden yapılan çağrılar ve callback’ler JS tarafına geri iletilir.
Parametre Tipleri
Native’den JS’e olan çağrıların kolay bir şekilde yapılabilmesi için, JSON olarak kodlanan fonksiyon parametreleri NSArray
olarak aktarılır. Ancak JS’den yapılan çağrıların native tarafta kullanılabilmesi için ilgili veri tiplerinin (int, float, char vb.) açık bir şekilde belirtilmesi gerekir. Fakat daha önce de belirttiğimiz gibi runtime bizeNSMethodSignature
‘den yeterli bilgi veremediğinden dolayı veri tiplerini string olarak kaydedilmektedir.
Bu nedenle metot imzasından parametre tiplerinin ayıklanması için regex kullanılır ve RCTConvert yardımcı sınıfı kullanılarak uygun veri tipine dönüştürülür. RCTConvert sınıfında, her bir veri tipi varsayılan olarak desteklenmekte ve JSON string’i alınarak istenilen tipe göre dönüştürülmektedir.
Metodun dinamik olarak çağrılması içinobjc_msgSend kullanılır. Fakat struct tipi gelmesi durumunda arm64 mimarisinde objc_msgSend_stret metodu bulunmadığından dolayı NSInvocation metodu çağrılır.
Tüm parametreler JSON’dan ilgili veri tiplerine dönüştürüldüğünde diğer bir NSInvocation çağrısı yapılır ve istenilen modülün metodu, üretilen parametreler ile çağrılır.
Örneğin MyModule şeklindeki bir modülümüz olsun. Metot çağrımı aşağıdaki gibi yapılacaktır:
// Native tarafta bu şekilde export edilsin
RCT_EXPORT_METHOD(methodWithArray:(NSArray *) size:(CGRect)size) {}// JavaScript tarafından çağrımı aşağıdaki gibi olsun
require('NativeModules').MyModule.method(['a', 1], {
x: 0,
y: 0,
width: 200,
height: 100
});// JS kuyruğundan native'e iletilen kod aşağıdaki gibi olacaktır:
// ** Çağrılar, bir kuyruk olarak tutulduğu için, tüm alanların array tipinde olduğunu unutmayalım **
@[
@[ @0 ], // module ID'leri
@[ @1 ], // method ID'leri
@[ // parametreler
@[
@[@"a", @1],
@{ @"x": @0, @"y": @0, @"width": @200, @"height": @100 }
]
]
];// Daha sonra aşağıdaki çağrılara dönüştürülür (pseudo kod)
NSInvocation call
call[args][0] = GetModuleForId(@0)
call[args][1] = GetMethodForId(@1)
call[args][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1])
call[args][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
call()
Thread işlemleri
Daha önce belirttiğimiz gibi, -methodQueue
ile belirtilmediği sürece her bir modülün çalışacağı kendine ait bir GCD kuyruğu bulunmaktadır. Bu bağlamda RCTViewManager’dan türetilen View Manager’lar ise bir istisnadır. Çünkü varsayılan olarak Shadow Queue kullanılan bu bileşenler, sadece özel durumlarda bir placeholder olan RCTJSThread’i kullanabilir. RCTJSThread bir queue değil thread niteliğinde olduğundan dolayı istisnai bir durum oluşturmaktadır.
Thread’ler ile ilgili mevcut kurallar aşağıdaki gibidir:
-init
ve-setBridge
metotlarının main thread’de çalıştırılır.- Export edilen tüm metotların, modül kuyruğunda çalıştırılır.
RCTInvalidating
protokolü implement edilirse, ilgili modül kuruğundainvalidate
metodu çalıştırılır-dealloc
metodunun hangi thread tarafından çağrılacağı ise garanti edilmemektedir.
Eğer JS tarafından birçok çağrı gerçekleştirilirse, bu çağrılar ilgili kuyruk tarafından gruplanır ve paralel olarak çalıştırılmak üzere dispatch edilir:
Sonuç olarak
React Native’deki bridge yapısı JS ve Native taraf arasındaki haberleşmeyi sağlıyor ve C/C++ JavaScript VM’i kullanıyor. Basit gibi görünse de çetrefilli bir çalışma mantığı var ve önümüzdeki aylarda bridge mantığı kalkacak, Fabric ve TurboModules kavramları gelecek. Sonraki yazımda bu kavramlardan bahsedeceğim.
Bu yazı hakkında soru ve görüşleriniz varsa yorum kısmından bana yazabilirsiniz. Görüşmek üzere…
Kaynaklar:
- https://hackernoon.com/understanding-react-native-bridge-concept-e9526066ddb8
- https://subscription.packtpub.com/book/application_development/9781787282537/1/01lvl1sec9/how-the-react-native-bridge-from-javascript-to-native-world-works
- https://tadeuzagallo.com/blog/react-native-bridge/
- https://facebook.github.io/react-native/docs/native-modules-ios.html