Am 10. Oktober 2018 veröffentlichte unser Team eine neue Version der Anwendung auf React Native. Wir freuen uns und sind stolz darauf.
Aber der Horror ist etwas: Nach ein paar Stunden steigt die Anzahl der Fehler für Android plötzlich an.
10.000 Abstürze für AndroidUnser
Sentry Crash Monitoring Tool wird verrückt.
In allen Fällen wird ein Fehler wie
JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView"
.
In React Native geschieht dies normalerweise, wenn Sie eine Eigenschaft mit dem falschen Typ festlegen. Aber warum ist der Fehler beim Testen nicht aufgetreten? Bei uns testet jeder Entwickler sorgfältig neue Versionen auf mehreren Geräten.
Fehler scheinen auch eher zufällig zu sein, sie scheinen auf jede Kombination von Eigenschaften und Typschattenknoten zu fallen. Hier sind zum Beispiel die ersten drei:
Error while updating property 'paddingTop' in shadow node of type: RCTView
Error while updating property 'height' in shadow node of type: RCTImageView
Error while updating property 'fill' of a view managed by: RNSVGPath
Es scheint, dass der Fehler auf jedem Gerät und in jeder Version von Android auftritt, gemessen am Sentry-Bericht.
Die meisten Abstürze für Android 8.0.0 stürzen ab, dies stimmt jedoch mit unserer Benutzerbasis übereinLass es uns wiedergeben!
Der erste Schritt vor dem Beheben des Fehlers besteht darin, ihn zu reproduzieren, oder? Glücklicherweise können wir dank Sentry-Protokollen herausfinden, was Benutzer tun, bevor ein Absturz auftritt.
Ta-a-ak, mal sehen ...

Hmm, in den allermeisten Fällen öffnen Benutzer einfach die Anwendung und - Boom, es kommt zu einem Absturz.
Ok, lass es uns noch einmal versuchen. Wir installieren die Anwendung auf sechs Android-Geräten, öffnen sie und beenden sie mehrmals. Keine Panne! Darüber hinaus ist es unmöglich, es lokal im Dev-Modus zu spielen.
Okay, das scheint sinnlos. Fehler sind immer noch recht zufällig und treten in 10% der Fälle auf. Es sieht so aus, als hätten Sie eine 1: 10-Chance, dass die Anwendung beim Start abstürzt.
Stapelverfolgungsanalyse
Um diesen Fehler zu reproduzieren, versuchen wir zu verstehen, woher er kommt ...
Wie bereits erwähnt, haben wir verschiedene Fehler. Und jeder hat ähnliche, aber leicht unterschiedliche Spuren.
Ok, nehmen wir den ersten:
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1 at android.support.v4.util.Pools$SimplePool.release(Pools.java:116) at com.facebook.react.bridge.DynamicFromMap.recycle(DynamicFromMap.java:40) at com.facebook.react.uimanager.LayoutShadowNode.setHeight(LayoutShadowNode.java:168) at java.lang.reflect.Method.invoke(Method.java) ... java.lang.reflect.InvocationTargetException: null at java.lang.reflect.Method.invoke(Method.java) ... com.facebook.react.bridge.JSApplicationIllegalArgumentException: Error while updating property 'height' in shadow node of type: RNSVGSvgView at com.facebook.react.uimanager.ViewManagersPropertyCache$PropSetter.updateShadowNodeProp(ViewManagersPropertyCache.java:113) ...
Das Problem liegt also in
android/support/v4/util/Pools.java
.
Hmm, wir sind sehr tief in der Android-Support-Bibliothek, es ist kaum möglich, hier einen Nutzen zu ziehen.
Finde einen anderen Weg
Eine andere Möglichkeit, die Hauptursache des Fehlers zu finden, besteht darin, nach neuen Änderungen an der neuesten Version zu suchen. Besonders diejenigen, die nativen Android-Code betreffen. Es entstehen zwei Hypothesen:
- Wir haben die native Navigation aktualisiert, bei der native Fragmente für Android für jeden Bildschirm verwendet werden.
- Wir haben react-native-svg aktualisiert. Es gab einige Ausnahmen in Bezug auf SVG-Komponenten, aber dies ist kaum der Fall.
Wir können den Fehler im Moment nicht reproduzieren, daher ist die beste Strategie:
- Setzen Sie eine der beiden Bibliotheken zurück und rollen Sie sie für 10% der Benutzer aus, was im Play Store trivial geschieht. Überprüfen Sie bei mehreren Benutzern, ob der Fehler weiterhin besteht. Somit bestätigen oder widerlegen wir die Hypothese.

Aber wie wählt man eine Bibliothek zum Zurücksetzen aus? Natürlich können Sie eine Münze werfen, aber ist dies die beste Option?
Komm auf den Punkt
Schauen wir uns die vorherige Spur genauer an. Vielleicht hilft dies bei der Bestimmung der Bibliothek.
public static class SimplePool implements Pool { private final Object[] mPool; private int mPoolSize; ... @Override public boolean release(T instance) { if (isInPool(instance)) { throw new IllegalStateException("Already in the pool!"); } if (mPoolSize < mPool.length) { mPool[mPoolSize] = instance; mPoolSize++; return true; } return false; }
Es gab einen Fehler. Fehler java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
bedeutet, dass mPool
ein Array der Größe 10 ist, mPoolSize=-1
.
Okay, wie mPoolSize=-1
? Zusätzlich zu der oben beschriebenen recycle
kann mPoolSize
nur die mPoolSize
der SimplePool
Klasse mPoolSize
:
public T acquire() { if (mPoolSize > 0) { final int lastPooledIndex = mPoolSize - 1; T instance = (T) mPool[lastPooledIndex]; mPool[lastPooledIndex] = null; mPoolSize--; return instance; } return null; }
Daher besteht die einzige Möglichkeit, einen negativen mPoolSize
Wert zu erhalten, darin, ihn mit mPoolSize=0
zu reduzieren. Aber wie ist das mit der Bedingung mPoolSize > 0
?
Wir werden Haltepunkte in Android Studio setzen und sehen, was passiert, wenn die Anwendung gestartet wird. Ich meine, hier ist die if
Bedingung, dieser Code sollte gut funktionieren!
Endlich eine Offenbarung!
Unter DynamicFromMap
statische Verknüpfung zu SimplePool
.
private static final Pools.SimplePool<DynamicFromMap> sPool = new Pools.SimplePool<>(10);
Nach mehreren zehn Klicks auf die Wiedergabetaste mit sorgfältig festgelegten Haltepunkten sehen wir, dass die Threads mqt_native_modules die Funktionen SimplePool.acquire
und SimplePool.release
mit React Native aufrufen, um die Stileigenschaften der React-Komponente zu steuern (unterhalb der Komponente width
Eigenschaft).

Sie sind aber auch über den Hauptstrom zugänglich!

Oben sehen wir, dass sie verwendet werden, um die fill
im Hauptstrom zu aktualisieren, normalerweise für die Komponente react-native-svg
! In der Tat begann die DynamicFromMap
react-native-svg
DynamicFromMap
react-native-svg
Bibliothek erst mit der siebten Version mit der Verwendung von DynamicFromMap
, um die Leistung nativer SVG-Animationen zu verbessern.
And-and-and ... Eine Funktion kann von zwei Threads aus aufgerufen werden, aber DynamicFromMap
verwendet SimplePool
nicht threadsicher. "Thread sicher", sagen?
Thread-Sicherheit, ein bisschen Theorie
In Single-Threaded-JavaScript müssen sich Entwickler normalerweise nicht mit der Thread-Sicherheit befassen.
Java hingegen unterstützt das Konzept paralleler oder Multithread-Programme. Innerhalb eines Programms können mehrere Threads ausgeführt werden und möglicherweise auf die allgemeine Datenstruktur zugreifen, was manchmal zu unerwarteten Ergebnissen führt.
Nehmen Sie ein einfaches Beispiel: Das folgende Bild zeigt, dass die Flüsse A und B parallel sind:
- eine ganze Zahl lesen;
- seinen Wert erhöhen;
- gib ihn zurück.
Stream B kann möglicherweise auf den Datenwert zugreifen, bevor Stream A ihn aktualisiert. Wir haben erwartet, dass zwei separate Schritte einen Endwert von 19
. Stattdessen können wir 18
bekommen. Eine solche Situation, in der der Endzustand der Daten von der relativen Reihenfolge der Flussoperationen abhängt, wird als Race-Bedingung bezeichnet. Das Problem ist, dass dieser Zustand nicht unbedingt immer auftritt. In dem obigen Fall hat Thread B möglicherweise einen anderen Job, bevor der Wert erhöht wird, wodurch Thread A genügend Zeit hat, den Wert zu aktualisieren. Dies erklärt die Zufälligkeit und Unfähigkeit, den Fehler zu reproduzieren.
Eine Datenstruktur gilt als threadsicher, wenn Operationen von mehreren Threads gleichzeitig ausgeführt werden können, ohne dass das Risiko einer Race-Bedingung besteht.
Wenn ein Thread für ein bestimmtes Datenelement liest, sollte ein anderer Thread nicht das Recht haben, dieses Element zu ändern oder zu löschen (dies wird als Atomizität bezeichnet). Im vorherigen Beispiel hätten die Rennbedingungen vermieden werden können, wenn die Aktualisierungszyklen atomar gewesen wären. Thread B wartet, bis Thread A den Vorgang abgeschlossen hat, und startet sich dann von selbst.
In unserem Fall kann dies passieren:
Da DynamicFromMap
eine statische Verknüpfung zu SimplePool
, stammen mehrere DynamicFromMap
Aufrufe aus verschiedenen Threads, während die DynamicFromMap
Methode in SimplePool
.
In der obigen Abbildung ruft Thread A die Methode auf und bewertet die Bedingung als wahr . Es ist jedoch noch nicht gelungen, den Wert von mPoolSize
(der in Verbindung mit Thread B verwendet wird) zu reduzieren, während Thread B diese Methode ebenfalls aufruft und die Bedingung ebenfalls als wahr bewertet. Anschließend reduziert jeder Aufruf den Wert von mPoolSize
, was zu dem Wert "unmöglich" führt.
Korrektur
Bei der Untersuchung der Korrekturoptionen haben wir eine Poolanforderung für React-Native gefunden , die dem Zweig noch nicht beigetreten ist - und in diesem Fall Thread-Sicherheit bietet.

Dann haben wir eine feste Version von React Native für Benutzer eingeführt. Der Absturz ist endlich behoben, Prost!
Dank der Hilfe von Jenick Duplessis (Mitwirkender am React Native-Kern) und Michael Sand (React-Native react-native-svg
Betreuer) ist der Patch in der nächsten Nebenversion von React Native 0.57 enthalten .
Es hat einige Mühe gekostet, diesen Fehler zu beheben, aber es war eine großartige Gelegenheit, sich eingehender mit React-Native und React-Native-SVG zu befassen. Ein guter Debugger und einige gut platzierte Haltepunkte sind wichtig. Ich hoffe, Sie haben aus dieser Geschichte auch etwas Nützliches gelernt!