في 10 أكتوبر 2018 ، أصدر فريقنا إصدارًا جديدًا من التطبيق على React Native. نحن سعداء وفخورون بذلك.
لكن الرعب شيء: بعد بضع ساعات ، يزداد عدد حالات الفشل في نظام Android فجأة.
 10000 تعطل لنظام 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=-1java.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.releaseNative للتحكم في خصائص نمط مكون 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 .
 
 لقد استغرق الأمر بعض الجهد لإصلاح هذا الخطأ ، لكنها كانت فرصة رائعة للتعمق بشكل أكبر في رد الفعل الأصلي - رد الفعل الأصلي. يعد مصحح الأخطاء الجيد وعدد قليل من نقاط التوقف الموضوعة جيدًا أمرًا مهمًا. أتمنى أن تكون قد تعلمت شيئًا مفيدًا من هذه القصة!