Tiefgreifendes Verständnis der Kernprinzipien von React Native (Bridge of React Native)

Tiefgreifendes Verständnis der Kernprinzipien von React Native (Bridge of React Native)

In diesem Artikel gehen wir davon aus, dass Sie bereits mit den Grundlagen von React Native vertraut sind, und konzentrieren uns auf die Funktionsweise der Kommunikation zwischen nativer Version und JavaScript.

Hauptthread

Bevor wir beginnen, müssen wir wissen, dass es in React Native drei Hauptthreads gibt:

  • Schattenwarteschlange: verantwortlich für Layoutarbeiten
  • Hauptthread: UIKit arbeitet in diesem Thread (Anmerkung des Übersetzers: Der UI-Manager-Thread kann als Hauptthread angesehen werden, der hauptsächlich für die Logik der Seiteninteraktion und die Steuerungszeichnung verantwortlich ist.)
  • JavaScript-Thread: Thread, der JS-Code ausführt

Darüber hinaus verfügt jedes native Modul im Allgemeinen über eine eigene GCD-Warteschlange, sofern nicht anders angegeben (wird später erläutert).

*Die Schattenwarteschlange ähnelt eigentlich eher einer GCD-Warteschlange als einem Thread

Natives Modul

Wenn Sie nicht wissen, wie Sie ein natives Modul erstellen, empfehle ich Ihnen, die Dokumentation zu lesen.

Dies ist ein Beispiel für ein natives Modul „Person“, das sowohl von JavaScript aufgerufen wird als auch JavaScript aufrufen kann.

@interface Person : NSObject <RCTBridgeModul> 
@Ende 
@implementation Logger 
RCT_EXPORT_MODULE() 
RCT_EXPORT_METHOD(Begrüßung:(NSString *)Name) 
{ 
 NSLog(@"Hallo, %@!", Name); 
 [_bridge.eventDispatcher sendAppEventWithName:@"begrüßt" body:@{ @"name": name }];     
} 
@Ende

Wir konzentrieren uns auf die beiden Makros RCT_EXPORT_MODULE und RCT_EXPORT_METHOD, worauf sie sich erweitern, welche Rollen sie spielen und wie sie funktionieren.

RCT_EXPORT_MODULE([js_name])

Wie der Name dieser Methode schon andeutet, exportiert sie Ihr Modul. Aber was bedeutet „Exportieren“ in diesem speziellen Kontext? Es bedeutet, dass die Bridge über Ihr Modul Bescheid weiß.

Die Definition ist eigentlich ganz einfach:

#define RCT_EXPORT_MODULE(js_name) \ 
 RCT_EXTERN void RCTRegisterModule(Klasse); \ 
 + (NSString \*)Modulname { return @#js_name; } \ 
 + (void)load { RCTRegisterModule(self); }

Es hat folgende Funktion:

  • Deklarieren Sie zunächst RCTRegisterModule als externe Funktion, was bedeutet, dass die Implementierung dieser Funktion für den Compiler nicht sichtbar ist, aber in der Linkphase verfügbar ist.
  • Deklarieren Sie eine Methode moduleName, die einen optionalen Makroparameter js_name zurückgibt, sodass das Modul in JS einen anderen Klassennamen hat als in Objective-C
  • Deklarieren Sie eine Lademethode (wenn die App in den Speicher geladen wird, wird die Lademethode jeder Klasse aufgerufen), die Lademethode ruft RCTRegisterModule auf, und dann kennt die Bridge das freigegebene Modul

RCT_EXPORT_METHOD(Methode)

Dieses Makro ist interessanter, da es Ihrer Methode nichts hinzufügt. Es deklariert nicht nur die angegebene Methode, sondern erstellt auch eine neue Methode. Die neue Methode sieht folgendermaßen aus:

+ (NSArray *)__rct_export__120 
{ 
 zurück @[ @"", @"log:(NSString *)message" ];
}

Es wird durch die Kombination eines Präfixes (__rct_export__) und eines optionalen js_name (in diesem Fall leer) mit der Zeilennummer der Deklaration und dem Makro __COUNTER__ erstellt.

Der Zweck dieser Methode besteht darin, ein Array zurückzugeben, das einen optionalen js_name und eine Methodensignatur enthält. Die Rolle dieses js_name besteht darin, Konflikte bei der Methodenbenennung zu vermeiden.

Laufzeit

Dieses gesamte Setup dient lediglich dazu, Informationen für die Bridge bereitzustellen, damit sie alle exportierten Module und Methoden finden kann. Dies geschieht jedoch alles zur Ladezeit. Sehen wir uns nun an, wie es zur Laufzeit verwendet wird.

Hier ist das Abhängigkeitsdiagramm, wenn die Brücke initialisiert wird:

Initialisierungsmodul

RCTRegisterModule schiebt die Klasse lediglich in das Array, sodass sie beim Instanziieren einer neuen Brücke gefunden werden kann. Die Brücke durchläuft alle Module im Array, erstellt für jedes Modul eine Instanz, speichert auf der Brückenseite eine Referenz auf die Instanz, weist der Modulinstanz eine Referenz auf die Brücke zu (damit wir uns auf beiden Seiten gegenseitig aufrufen können), prüft dann, ob für die Modulinstanz eine Warteschlange zur Ausführung angegeben ist, und weist ihr andernfalls eine neue, von den anderen Modulen getrennte Warteschlange zu:

NSMutableDictionary *modulesByName; // = ... 
für (Klasse ModulKlasse in RCTGetModuleClasses()) { 
// ... 
 Modul = [Modulklasse neu]; 
 wenn ([Modul antwortet auf Selector:@selector(setBridge:)]) {
 modul.bridge = selbst;
 moduleByName[Modulname] = Modul; 
 // ... 
}

Konfigurationsmodul

Sobald wir diese Module haben, listen wir in einem Hintergrund-Thread alle Methoden jedes Moduls auf und rufen dann die Methode auf, die mit __rct__export__ beginnt. Wir erhalten eine Zeichenfolge mit der Methodensignatur. Dies ist wichtig, da wir nun den tatsächlichen Typ des Parameters kennen. Zur Laufzeit wussten wir nur, dass einer der Parameter eine ID war, aber auf diese Weise können wir wissen, dass die ID tatsächlich ein NSString ist *

vorzeichenlose int Methodenanzahl; 
Methode *Methoden = class_copyMethodList(Modulklasse, &Methodenanzahl);
für (unsigned int i = 0; i < methodCount; i++) {
 Methode Methode = Methoden[i];
 SEL-Selektor = method_getName(Methode);
 wenn ([NSStringFromSelector(Selektor) hatPräfix:@"__rct_export__"]) {
 IMP imp = method_getImplementation(Methode);
 NSArray *Einträge = ((NSArray *(*)(id, SEL))imp)(_moduleClass, Selektor);
 //...
 [moduleMethods addObject:/* Objekt, das die Methode darstellt */];
 }
}

Einrichten des JavaScript-Executors

Der JS-Executor verfügt über eine -setUp-Methode, mit der er komplexere Aufgaben ausführen kann, z. B. JS-Code in einem Hintergrundthread initialisieren kann. Dies spart auch einiges an Arbeit, da nur der aktive Executor den Aufruf der setUp-Methode erhält, nicht alle Executors:

JSGlobalContextRef ctx = JSGlobalContextCreate(NULL);
_context = [[RCTJavaScriptContext alloc] initWithJSContext:ctx];

Einfügen einer JSON-Konfiguration

Die JSON-Konfiguration enthält nur unser Modul, zum Beispiel:

Diese Konfigurationsinformationen werden als globale Variable in der virtuellen JavaScript-Maschine gespeichert, sodass die JS-Seitenbrücke bei ihrer Initialisierung diese Informationen zum Erstellen von Modulen verwenden kann.

JavaScript-Code wird geladen

Dies ist ziemlich unkompliziert und erfordert lediglich das Laden des Quellcodes von dem von Ihnen angegebenen Anbieter, normalerweise vom Packager während der Entwicklung und von der Festplatte während der Produktion.

Ausführen von JavaScript-Code

Sobald alles bereit ist, können wir den Quellcode der Anwendung in die virtuelle JS-Maschine laden, den Code kopieren, analysieren und ausführen. Alle CommonJS-Module müssen bei der ersten Ausführung registriert werden und die Eintragsdatei wird benötigt.

JSValueRef jsError = NULL; 
JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); 
JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); 
JSValueRef-Ergebnis = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); 
JSStringRelease(jsURL); 
JSStringRelease(execJSString);

Module in JavaScript

Auf der JS-Seite können wir jetzt das Modul, das aus den vorherigen JSON-Konfigurationsinformationen besteht, über die NativeModules von react-native abrufen:

Die Funktionsweise besteht darin, dass beim Aufruf einer Methode diese zusammen mit dem Namen des Moduls, dem Namen der Methode und allen Parametern in eine Warteschlange gestellt wird. Am Ende der JavaScript-Ausführung wird diese Warteschlange dem nativen Modul zur Ausführung übergeben.

Anrufzyklus

Wenn wir das Modul nun mit dem obigen Code aufrufen, sieht es folgendermaßen aus:

Der Aufruf muss nativ beginnen, und nativ ruft JS auf (dieses Bild erfasst nur einen bestimmten Moment, in dem JS ausgeführt wird). Während des Ausführungsprozesses ruft JS die Methode von NativeModules auf und stellt diesen Aufruf in die Warteschlange, da dieser Aufruf auf der nativen Seite ausgeführt werden muss. Wenn die Ausführung von JS abgeschlossen ist, durchläuft das native Modul alle in die Warteschlange gestellten Aufrufe. Wenn die Ausführung abgeschlossen ist, ruft es über die Brücke zurück (ein natives Modul kann enqueueJSCall:args: über die _bridge-Instanz aufrufen), um erneut JS aufzurufen.

(Wenn Sie das Projekt verfolgt haben, gab es früher auch eine Anrufwarteschlange von native->JS, die bei jedem vSYNC versendet wurde, aber das wurde entfernt, um die Startzeit zu verbessern)

Parametertyp

Native-zu-JS-Aufrufe sind einfach, die Parameter werden als NSArray übergeben, das wir als JSON-Daten kodieren, aber für JS-zu-Native-Aufrufe brauchen wir den nativen Typ, dafür prüfen wir die Basistypen (Ints, Floats, Chars...), aber wie oben erwähnt, bekommen wir für kein Objekt (keine Struktur) zur Laufzeit genügend Informationen von NSMthodSignature, also speichern wir den Typ als Zeichenfolge.

Wir verwenden reguläre Ausdrücke, um den Typ aus der Methodensignatur zu extrahieren, und verwenden die Klasse RCTConvert, um das Objekt tatsächlich zu konvertieren. Standardmäßig stellt sie Methoden für jeden Typ bereit und versucht, den JSON-Eingang in den erforderlichen Typ zu konvertieren.

Sofern es sich nicht um eine Struktur handelt, verwenden wir objc_msgSend, um die Methode dynamisch aufzurufen, da es auf arm64 keine Version von objc_msgSend_stret gibt. Daher verwenden wir NSInvocation.

Nachdem wir alle Parameter konvertiert haben, verwenden wir eine weitere NSInvocation, um das Zielmodul und die Zielmethode aufzurufen.

Beispiel:

// Wenn Sie die folgende Methode in einem bestimmten Modul hätten, z. B. „MyModule“
RCT_EXPORT_METHOD(MethodeMitArray:(NSArray *) Größe:(CGRect)Größe) {}
// Und habe es von JS aus aufgerufen, etwa: 
erfordern('NativeModules').MeinModul.Methode(['a', 1], {
 x: 0, 
 y: 0, 
 Breite: 200, 
 Höhe: 100 
});
// Die an das native System gesendete JS-Warteschlange würde dann wie folgt aussehen:
// ** Denken Sie daran, dass es sich um eine Warteschlange von Anrufen handelt, daher sind alle Felder Arrays ** 
@[ 
 @[ @0 ], // Modul-IDs 
 @[ @1 ], // Methoden-IDs 
 @[ // Argumente 
 @[ 
 @[@"ein", @1], 
 @{ @"x": @0, @"y": @0, @"Breite": @200, @"Höhe": @100 } 
 ] 
 ]
];
// Dies würde sich in folgende Aufrufe umwandeln (Pseudocode) 
NSInvocation-Aufruf 
Aufruf[Argumente][0] = GetModuleForId(@0) 
Aufruf[Argumente][1] = GetMethodForId(@1) 
Aufruf[Argumente][2] = obj_msgSend(RCTConvert, NSArray, @[@"a", @1]) 
Aufruf[Argumente][3] = NSInvocation(RCTConvert, CGRect, @{ @"x": @0, ... })
Anruf()

Themen

Wie oben erwähnt, verfügt jedes Modul standardmäßig über eine einzelne GCD-Warteschlange, es sei denn, es gibt durch Implementierung der Methode -methodQueue oder Zusammenführen der Eigenschaft methodQueue mit einer gültigen Warteschlange an, welche Warteschlange ausgeführt werden soll. Die Ausnahme sind ViewManager* (die RCTViewManager erweitern), die standardmäßig Shadow Queue verwenden, und das spezielle Ziel RCTJSThread ist nur ein Platzhalter, da es ein Thread und keine Warteschlange ist.

(Tatsächlich sind View-Manager keine wirkliche Ausnahme, da die Basisklasse die Shadow Queue ausdrücklich als Zielwarteschlange angibt.)

Die aktuellen Thread-Regeln lauten wie folgt:

  • -init und -setBridge: stellen die Ausführung im Hauptthread sicher
  • Es wird garantiert, dass alle exportierten Methoden in der Zielwarteschlange ausgeführt werden
  • Wenn Sie das RCTInvalidating-Protokoll implementieren, können Sie auch sicherstellen, dass Invalidate für die Zielwarteschlange aufgerufen wird.
  • Es gibt keine Garantie, für welchen Thread -dealloc aufgerufen wird

Wenn ein Stapel von JS-Aufrufen eingeht, werden diese Aufrufe nach Zielwarteschlange gruppiert und parallel aufgerufen:

// gruppiere „Anrufe“ nach „Warteschlange“ in „Buckets“ 
für (ID-Warteschlange in Buckets) { 
 dispatch_block_t block = ^{ 
 NSOrderedSet *Aufrufe = [Buckets Objekt für Schlüssel:Warteschlange]; 
 für (NSNumber *indexObj in Anrufen) { 
 // Tatsächlich anrufen 
 } 
 }; 
 wenn (Warteschlange == RCTJSThread) { 
 [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; 
 } sonst wenn (Warteschlange) { 
 dispatch_async(Warteschlange, Block); 
 } 
}

Abschluss

Das war unser ausführlicherer Überblick über die Funktionsweise von React Native Bridging. Ich hoffe, dies hilft Leuten, die komplexere Module erstellen oder zum Kern-Framework beitragen möchten.

Dies ist das Ende dieses Artikels über ein vertieftes Verständnis der Kernprinzipien von React Native (React Native Bridge). Weitere Inhalte zu den Prinzipien von React Native finden Sie in früheren Artikeln auf 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • Detaillierte Verwendung von React.Children
  • Anwendungsbeispiele für React Hooks
  • React+Koa-Beispiel zur Implementierung des Datei-Uploads
  • Beispielcode für die Entwicklung einer H5-Formularseite basierend auf React-Hooks und der Konfiguration der Zarm-Komponentenbibliothek
  • Das Umschalten zwischen React-Antd-Tabs führt zu wiederholter Aktualisierung von Unterkomponenten
  • ReactJs-Grundlagen-Tutorial - Essential Edition
  • ReactRouter-Implementierung
  • Detaillierte Erklärung zur Verwendung von React.cloneElement

<<:  Detailliertes Tutorial zur Installation von MySQL auf CentOS 6.9

>>:  Zusammenfassung der unbekannten Verwendung von "!" in Linux

Artikel empfehlen

21 Best Practices zur MySQL-Standardisierung und -Optimierung!

Vorwort Jede gute Angewohnheit ist ein Schatz. Di...

Detaillierte Erläuterung der benutzerdefinierten Vue-Anweisungen

Inhaltsverzeichnis Benutzerdefinierte Vue-Direkti...

js, um ein Wasserfall-Flusslayout zu erreichen (unendliches Laden)

In diesem Artikelbeispiel wird der spezifische Co...

Beispielcode zum Ändern des Textstils der Eingabeaufforderung in HTML

Auf vielen Websites wird im Eingabefeld Hinweiste...

Detaillierte Erklärung zur Installation von PHP7 unter Linux

Wie installiere ich PHP7 unter Linux? 1. Installi...

Was sind MySQL-Dirty-Pages?

Inhaltsverzeichnis Schmutzige Seiten (Speichersei...

So gestalten Sie das Frontend einer Website elegant und attraktiv für Benutzer

Das Temperament einer Web-Frontend-Website ist ein...

Einführung in die Verwendung gängiger XHTML-Tags

Es gibt viele Tags in XHTML, aber nur wenige werd...

Detaillierte Erklärung des dynamischen Weihnachtsbaums durch JavaScript

Inhaltsverzeichnis 1. Animierter Weihnachtsbaum, ...

So vereinfachen Sie Redux mit Redux Toolkit

Inhaltsverzeichnis Probleme, die Redux Toolkit lö...