في 10 أكتوبر 2018 ، أصدر فريقنا إصدارًا جديدًا من التطبيق على React Native. نحن سعداء وفخورون بذلك.
لكن الرعب شيء: بعد بضع ساعات ، يزداد عدد حالات الفشل في نظام Android فجأة.
10000 تعطل لنظام Androidأداة مراقبة تصادم
Sentry الخاصة بنا
تصبح مجنونة.
في جميع الحالات ، نرى خطأ مثل
JSApplicationIllegalArgumentException Error while updating property 'left' in shadow node of type: RCTView"
.
في React Native ، يحدث هذا عادةً إذا قمت بتعيين خاصية بنوع خاطئ. ولكن لماذا لم يظهر الخطأ أثناء الاختبار؟ في كل منا ، يختبر كل مطور بعناية الإصدارات الجديدة على العديد من الأجهزة.
تبدو الأخطاء أيضًا عشوائية إلى حد ما ، ويبدو أنها تقع على أي مجموعة من الخصائص وتكتب عقد الظل. على سبيل المثال ، فيما يلي الثلاثة الأولى:
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
يبدو أن الخطأ يحدث على أي جهاز وفي أي إصدار من نظام Android ، بناءً على تقرير سنتري.
معظم أعطال Android 8.0.0 تعطل ، ولكن هذا يتفق مع قاعدة مستخدمينالنلعبها مرة أخرى!
لذا ، فإن الخطوة الأولى قبل إصلاح الخلل هي إعادة إنتاجه ، أليس كذلك؟ لحسن الحظ ، بفضل سجلات Sentry ، يمكننا معرفة ما يفعله المستخدمون قبل وقوع الحادث.
Ta-a-ak ، دعنا نرى ...

حسنًا ، في الغالبية العظمى من الحالات ، يقوم المستخدمون ببساطة بفتح التطبيق و- الطفرة ، يحدث عطل.
حسنا ، دعونا نحاول مرة أخرى. نقوم بتثبيت التطبيق على ستة أجهزة تعمل بنظام Android ، وفتحه والخروج منه عدة مرات. لا خلل! علاوة على ذلك ، من المستحيل تشغيله محليًا في وضع dev.
حسنا ، هذا يبدو بلا معنى. لا تزال حالات الفشل عشوائية تمامًا وتحدث في 10٪ من الحالات. يبدو أن لديك فرصة 1 في 10 بأن يتعطل التطبيق عند بدء التشغيل.
تحليل تتبع المكدس
لإعادة إنتاج هذا الفشل ، دعونا نحاول أن نفهم من أين يأتي ...
كما ذكرنا سابقًا ، لدينا العديد من الأخطاء المختلفة. ولكل شخص آثار مماثلة ، لكن مختلفة قليلاً.
حسنًا ، لنأخذ أول واحد:
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) ...
وبالتالي فإن المشكلة هي في
android/support/v4/util/Pools.java
.
حسنًا ، نحن عميقون جدًا في مكتبة دعم Android ، ومن الصعب الحصول على أي فائدة هنا.
البحث عن طريقة أخرى
هناك طريقة أخرى للعثور على السبب الجذري للخطأ وهي البحث عن تغييرات جديدة على الإصدار الأخير. خاصة تلك التي تؤثر على كود Android الأصلي. تنشأ فرضيتان:
- قمنا بتحديث Native Navigation ، حيث يتم استخدام الأجزاء الأصلية لنظام Android لكل شاشة.
- قمنا بتحديث رد فعل الأم SVG . كانت هناك بعض الاستثناءات المتعلقة بمكونات SVG ، ولكن هذا هو الحال.
لا يمكننا إعادة إنتاج الخطأ في الوقت الحالي ، وبالتالي فإن أفضل استراتيجية هي:
- استرجع إحدى المكتبتين ، ثم قم بتدويره بنسبة 10٪ من المستخدمين ، وهو الأمر الذي يتم بشكل تافه في متجر Play. تحقق مع العديد من المستخدمين إذا استمر الفشل. وبالتالي ، فإننا نؤكد أو دحض الفرضية.

ولكن كيف تختار مكتبة لتتراجع؟ بالطبع ، يمكنك رمي عملة معدنية ، ولكن هل هذا هو الخيار الأفضل؟
وصول الى هذه النقطة
دعنا نلقي نظرة فاحصة على التتبع السابق. ربما هذا سوف يساعد في تحديد المكتبة.
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; }
كان هناك فشل. خطأ java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
يعني أن mPool
عبارة عن صفيف بحجم 10 ، لكن mPoolSize=-1
.
حسنًا ، كيف mPoolSize=-1
؟ بالإضافة إلى طريقة recycle
أعلاه ، فإن المكان الوحيد لتغيير mPoolSize
هو طريقة acquire
على فئة SimplePool
:
public T acquire() { if (mPoolSize > 0) { final int lastPooledIndex = mPoolSize - 1; T instance = (T) mPool[lastPooledIndex]; mPool[lastPooledIndex] = null; mPoolSize--; return instance; } return null; }
لذلك ، الطريقة الوحيدة للحصول على قيمة mPoolSize
سالبة هي تقليله باستخدام mPoolSize=0
. لكن كيف يكون هذا ممكنًا مع الحالة mPoolSize > 0
؟
سنضع نقاط توقف في Android Studio ونرى ما يحدث عند بدء تشغيل التطبيق. أعني ، هنا هو الشرط if
، هذا الرمز يجب أن تعمل بشكل جيد!
وأخيرا ، الوحي!
راجع DynamicFromMap
رابط ثابت إلى SimplePool
.
private static final Pools.SimplePool<DynamicFromMap> sPool = new Pools.SimplePool<>(10);
بعد عدة عشرات من النقرات على زر التشغيل مع نقاط توقف محددة بعناية ، نرى أن مؤشرات الترابط mqt_native_modules تستدعي SimplePool.acquire
و SimplePool.release
باستخدام SimplePool.release
Native للتحكم في خصائص نمط مكون React (أسفل خاصية width
المكون)

ولكن يتم الوصول إليها أيضا عن طريق التيار الرئيسي !

أعلاه ، نرى أنها تُستخدم لتحديث خاصية fill
في التدفق الرئيسي ، عادةً لمكون react-native-svg
! في الواقع ، بدأت مكتبة react-native-svg
استخدام DynamicFromMap
فقط مع الإصدار السابع لتحسين أداء الرسوم المتحركة svg الأصلية.
And-and-and ... يمكن استدعاء دالة من مؤشرات DynamicFromMap
اثنين ، ولكن DynamicFromMap
لا يستخدم SimplePool
بطريقة آمنة لمؤشر الترابط. "موضوع آمن ،" يقول؟
سلامة الموضوع ، قليلا من الناحية النظرية
في JavaScript واحد الخيوط ، لا يحتاج المطورون عادةً إلى التعامل مع أمان سلاسل الرسائل.
تدعم Java ، من ناحية أخرى ، مفهوم البرامج المتوازية أو متعددة مؤشرات الترابط. يمكن تشغيل عدة مؤشرات ترابط داخل نفس البرنامج ويمكن أن تصل إلى بنية البيانات العامة ، مما يؤدي في بعض الأحيان إلى نتائج غير متوقعة.
خذ مثالًا بسيطًا: الصورة أدناه توضح أن التدفقات A و B متوازية:
- قراءة عدد صحيح
- زيادة قيمتها.
- ارجعه.
يمكن للدفق B الوصول إلى قيمة البيانات قبل أن يقوم الدفق A بتحديثها. كنا نتوقع خطوتين منفصلتين لإعطاء القيمة النهائية لل 19
. بدلا من ذلك ، يمكننا الحصول على 18
. هذا الموقف حيث الحالة النهائية للبيانات تعتمد على الترتيب النسبي لعمليات التدفق يسمى حالة السباق. المشكلة هي أن هذا الشرط لا يحدث بالضرورة طوال الوقت. ربما ، في الحالة أعلاه ، يحتوي مؤشر الترابط B على وظيفة أخرى قبل المتابعة لزيادة القيمة ، مما يتيح وقتًا كافياً لمؤشر الترابط A لتحديث القيمة. هذا ما يفسر العشوائية وعدم القدرة على إعادة إنتاج الفشل.
تُعتبر بنية البيانات آمنة لمؤشر الترابط إذا كان من الممكن إجراء العمليات في وقت واحد بواسطة مؤشرات ترابط متعددة دون التعرض لخطر حدوث حالة سباق.
عند قراءة مؤشر ترابط واحد لعنصر بيانات معين ، لا ينبغي أن يكون لمؤشر ترابط آخر الحق في تعديل هذا العنصر أو حذفه (يُسمى هذا atomicity). في المثال السابق ، إذا كانت دورات التحديث ذرية ، فمن الممكن تجنب ظروف السباق. ينتظر مؤشر الترابط B حتى يكمل مؤشر الترابط A العملية ، ثم يبدأ تشغيل نفسه.
في حالتنا ، قد يحدث هذا:
نظرًا لأن DynamicFromMap
يحتوي على رابط ثابت إلى SimplePool
، فإن العديد من مكالمات DynamicFromMap
تأتي من مؤشرات DynamicFromMap
مختلفة ، بينما يتم استدعاء طريقة acquire
في SimplePool
.
في الرسم التوضيحي أعلاه ، يقوم مؤشر الترابط A باستدعاء الطريقة ، وتقييم الشرط على أنه صحيح ، لكنه لم يتمكن بعد من تقليل قيمة mPoolSize
(والذي يتم استخدامه بالاقتران مع مؤشر الترابط B) ، في حين يقوم مؤشر الترابط B أيضًا باستدعاء هذه الطريقة وتقييم الحالة على أنها صحيحة أيضًا . وبالتالي ، فإن كل مكالمة تقلل من قيمة mPoolSize
، مما يؤدي إلى القيمة "المستحيلة".
تصحيح
عند دراسة خيارات التصحيح ، وجدنا طلبًا خاصًا بتجميع رد الفعل الأصلي ، والذي لم ينضم بعد إلى الفرع - وهو يوفر أمانًا للخيوط في هذه الحالة.

ثم طرحنا إصدارًا ثابتًا من React Native للمستخدمين. تم إصلاح تحطم أخيرا ، هتافات!
لذا ، وبفضل مساعدة Jenick Duplessis (مساهم في مركز React Native) ومايكل ساند (صانعة react-native-svg
) ، يتم تضمين التصحيح في الإصدار الثانوي التالي من React Native 0.57 .
لقد استغرق الأمر بعض الجهد لإصلاح هذا الخطأ ، لكنها كانت فرصة رائعة للتعمق بشكل أكبر في رد الفعل الأصلي - رد الفعل الأصلي. يعد مصحح الأخطاء الجيد وعدد قليل من نقاط التوقف الموضوعة جيدًا أمرًا مهمًا. أتمنى أن تكون قد تعلمت شيئًا مفيدًا من هذه القصة!