
طرح كل مطور أسئلة حول دورة حياة النشاط: ما هي خدمة الربط ، وكيفية حفظ حالة الواجهة عند تدوير الشاشة ، وكيف تختلف الشظية عن النشاط.
قمنا في FunCorp بتجميع قائمة أسئلة حول مواضيع مشابهة ، ولكن مع بعض الفروق الدقيقة. أريد أن أشارك بعضهم معك.
1. يعلم الجميع أنه إذا قمت بفتح النشاط الثاني أعلى الأول وقمت بتدوير الشاشة ، فستبدو سلسلة الاتصال لدورة الحياة كما يلي:
النشاط الافتتاحيFirstActivity: onPause
النشاط الثاني: onCreate
SecondActivity: onStart
SecondActivity: onResume
FirstActivity: onSaveInstanceState
FirstActivity: onStop
إستدرSecondActivity: onPause
SecondActivity: onSaveInstanceState
النشاط الثاني: onStop
النشاط الثاني: onCreate
SecondActivity: onStart
SecondActivity: onRestoreInstanceState
SecondActivity: onResume
العودةSecondActivity: onPause
FirstActivity: onCreate
FirstActivity: onStart
FirstActivity: onRestoreInstanceState
النشاط الثاني: onStop
وماذا سيحدث إذا كان النشاط الثاني شفافًا؟
الحل
في حالة النشاط العلوي الشفاف ، من حيث المنطق ، كل شيء مختلف قليلاً. بالضبط لأنه شفاف ، بعد الدوران ، من الضروري استعادة المحتوى والنشاط الذي يقع تحته مباشرة. لذلك ، سيكون ترتيب المكالمات مختلفًا قليلاً:
نشاط الاكتشافFirstActivity: onPause
النشاط الثاني: onCreate
SecondActivity: onStart
SecondActivity: onResume
إستدرSecondActivity: onPause
SecondActivity: onSaveInstanceState
النشاط الثاني: onStop
النشاط الثاني: onCreate
SecondActivity: onStart
SecondActivity: onRestoreInstanceState
SecondActivity: onResume
FirstActivity: onSaveInstanceState
FirstActivity: onStop
FirstActivity: onCreate
FirstActivity: onStart
FirstActivity: onRestoreInstanceState
FirstActivity: onResume
FirstActivity: onPause
2. لا يمكن لأي تطبيق الاستغناء عن إضافة عرض ديناميكيًا ، ولكن في بعض الأحيان يجب عليك نقل العرض نفسه بين الشاشات المختلفة. هل يمكن إضافة نفس الكائن في وقت واحد إلى نشاطين مختلفين؟ ماذا يحدث إذا قمت بإنشائه باستخدام سياق التطبيق وأردت إضافته إلى أنشطة مختلفة في نفس الوقت؟
لماذا هذا مطلوب؟
هناك مكتبات "ليست ممتعة للغاية" تحمل منطقًا تجاريًا مهمًا داخل طرق العرض المخصصة ، وإعادة إنشاء طرق العرض هذه في كل نشاط جديد يعد قرارًا سيئًا ، لأن أريد الحصول على مجموعة بيانات واحدة.



الحل
لا شيء يمنع إنشاء طريقة عرض في سياق التطبيق. سيطبق ببساطة الأنماط الافتراضية التي لا تتعلق بأي نشاط. يمكنك أيضًا نقل هذا العرض بين الأنشطة المختلفة دون مشاكل ، ولكن عليك التأكد من إضافته إلى أحد الوالدين فقط
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { ... if (child.getParent() != null) { throw new IllegalStateException("The specified child already has a parent. " + "You must call removeView() on the child's parent first."); } ... }
يمكنك ، على سبيل المثال ، الاشتراك في ActivityLifecycleCallbacks ، وحذف onStop (removeView) من النشاط الحالي ، وإضافة onStart إلى الفتح التالي (addView).
3. يمكن إضافة جزء من خلال إضافة واستبدال. وما الفرق بين هذين الخيارين من حيث ترتيب استدعاء أساليب دورة الحياة؟ ما هي مميزات كل منهم؟
الحل
حتى إذا قمت بإضافة جزء من خلال الاستبدال ، فهذا لا يعني أنه تم استبداله بالكامل. هذا يعني أنه في هذه المرحلة من الحاوية سيتم استبدال وجهة نظره ، لذلك ، سيتم استدعاء onDestroyView للجزء الحالي ، وسيتم استدعاء onCreateView مرة أخرى عند العودة.
هذا يغير قواعد اللعبة إلى حد كبير. يجب عليك فصل جميع وحدات التحكم والفئات المرتبطة بواجهة المستخدم في onDestroyView. من الضروري الفصل بوضوح بين استلام البيانات اللازمة للجزء وملء العرض (القوائم ، وما إلى ذلك) ، حيث أن ملء وكسر العرض سيحدثان في كثير من الأحيان أكثر من تلقي البيانات (قراءة بعض البيانات من قاعدة البيانات).
هناك أيضًا فروق دقيقة في استرداد الحالة: على سبيل المثال ، يأتي onSaveInstanceState أحيانًا بعد onDestroyView. بالإضافة إلى ذلك ، يجب أن يوضع في الاعتبار أنه إذا ظهرت قيمة فارغة على onViewStateRestored ، فهذا يعني أنك لست بحاجة إلى استعادة أي شيء ، ولكن لا يمكنك إعادة تعيينه إلى الحالة الافتراضية.
إذا تحدثنا عن الراحة بين الإضافة والاستبدال ، فإن الاستبدال يكون أكثر اقتصادا في الذاكرة إذا كان لديك تنقل عميق (لدينا عمق تنقل للمستخدم لأحد مؤشرات الأداء الرئيسية للمنتج). من الملائم أيضًا استبدال شريط الأدوات باستبدال ، لأنه في onCreateView يمكنك إعادة استخدامه. من مزايا الإضافة: مشاكل أقل في دورة الحياة ، عندما تعود ، لا أعيد إنشاء العرض ولا أحتاج إلى إعادة ملء أي شيء.
4. في بعض الأحيان لا يزال يتعين عليك العمل مباشرة مع الخدمات وحتى مع خدمات الربط. يتفاعل النشاط مع إحدى هذه الخدمات (نشاط واحد فقط). يتصل بالخدمة وينقل البيانات إليها. عند تشغيل الشاشة ، يتم تدمير نشاطنا ، ونحن ملزمون بالانتعاش من هذه الخدمة. ولكن إذا لم يكن هناك اتصال ، فسيتم تدمير الخدمة ، وبعد تحول الربط إلى خدمة مختلفة تمامًا. كيف تتأكد من أنه عند تشغيل الخدمة تظل حية؟
الحل
إذا كنت تعرف حلاً جميلًا ، فاكتب في التعليقات. فقط يتبادر إلى الذهن شيء مماثل:
@Override protected void onDestroy() { super.onDestroy(); ThreadsUtils.postOnUiThread(new Runnable() { @Override public void run() { unbindService(mConnection); } }); }
5. في الآونة الأخيرة ، أعدنا التنقل داخل تطبيقنا على نشاط فردي (باستخدام إحدى المكتبات المتاحة). في السابق ، كانت كل شاشة تطبيق نشاطًا منفصلاً ، والآن أصبح التنقل يعمل على الأجزاء. تم حل مشكلة العودة إلى النشاط في منتصف المكدس بواسطة أعلام النية. كيف يمكنني العودة إلى الجزء الموجود في منتصف المكدس؟
الحل
نعم ، لا يوفر FragmentManager حلولًا خارج الصندوق. يقوم Cicerone بشيء مماثل في حد ذاته:
protected void backTo(BackTo command) { String key = command.getScreenKey(); if (key == null) { backToRoot(); } else { int index = localStackCopy.indexOf(key); int size = localStackCopy.size(); if (index != -1) { for (int i = 1; i < size - index; i++) { localStackCopy.pop(); } fragmentManager.popBackStack(key, 0); } else { backToUnexisting(command.getScreenKey()); } } }
6. أيضًا ، تخلصنا مؤخرًا من مكون غير فعال ومعقد مثل ViewPager ، لأن منطق التفاعل معه معقد للغاية ، ولا يمكن التنبؤ بسلوك الأجزاء في بعض الحالات. في بعض الأجزاء ، استخدمنا الأجزاء الداخلية. ماذا يحدث عند استخدام أجزاء داخل عناصر RecycleView؟
الحل
بشكل عام ، لن يكون هناك خطأ. ستتم إضافة الجزء وعرضه دون أي مشاكل. الشيء الوحيد الذي نواجهه هو التناقضات مع دورة حياتها. يدير التنفيذ على ViewPager دورة حياة الأجزاء من خلال setUserVisibleHint ، ويقوم RecycleView بكل شيء في الجبهة دون التفكير في الرؤية الفعلية وإمكانية الوصول إلى الأجزاء.
7. جميعًا لنفس السبب تحولنا من ViewPager ، واجهنا مشكلة استعادة الحالة. في حالة الأجزاء ، تم تنفيذ ذلك من خلال الإطار: في الأماكن المناسبة ، قمنا ببساطة بإعادة تعريف onSaveInstanceState وحفظنا جميع البيانات الضرورية في الحزمة. عندما تقوم بإعادة إنشاء ViewPager ، تمت استعادة جميع الأجزاء بواسطة FragmentManager وعادت حالتها. ماذا تفعل مع RecycleView و ViewHolder؟
الحل
تقول: "عليك أن تكتب كل شيء إلى قاعدة البيانات وأن تقرأ منه في كل مرة". أو يجب أن يكون منطق حفظ الحالة بالخارج ، والقائمة مجرد عرض. إنه في عالم مثالي. ولكن في حالتنا ، فإن كل عنصر من عناصر القائمة عبارة عن شاشة معقدة ذات منطق خاص بها. لذلك ، كان عليّ اختراع دراجتي بأسلوب "لنفعل نفس المنطق كما في ViewPager والجزء":
محول public class RecycleViewGalleryAdapter extends RecyclerView.Adapter<GalleryItemViewHolder> implements GalleryAdapter { private static final String RV_STATE_KEY = "RV_STATE"; @Nullable private Bundle mSavedState; @Override public void onBindViewHolder(GalleryItemViewHolder holder, int position) { if (holder.isAttached()) { holder.detach(); } holder.attach(createArgs(position, getItemViewType(position))); restoreItemState(holder); } @Override public void saveState(Bundle bundle) { Bundle adapterState = new Bundle(); saveItemsState(adapterState); bundle.putBundle(RV_STATE_KEY, adapterState); } @Override public void restoreState(@Nullable Bundle bundle) { if (bundle == null) { return; } mSavedState = bundle.getBundle(RV_STATE_KEY); } private void restoreItemState(GalleryItemViewHolder holder) { if (mSavedState == null) { holder.restoreState(null); return; } String stateKey = String.valueOf(holder.getGalleryItemId()); Bundle state = mSavedState.getBundle(stateKey); if (state == null) { holder.restoreState(null); mSavedState = null; return; } holder.restoreState(state); mSavedState.remove(stateKey); } private void saveItemsState(Bundle outState) { GalleryItemHolder holder = getCurrentGalleryViewItem(); saveItemState(outState, (GalleryItemViewHolder) holder); } private void saveItemState(Bundle bundle, GalleryItemViewHolder holder) { Bundle itemState = new Bundle(); holder.saveState(itemState); bundle.putBundle(String.valueOf(holder.getGalleryItemId()), itemState); } }
في موقع Fragment.onSaveInstanceState نقرأ حالة حاملي الأسهم التي نحتاجها ونضعها في الحزمة. عند إعادة إنشاء أصحاب ، نحصل على حزمة المحفوظة وعلى onBindViewHolder نقوم بتمرير الحالات الموجودة داخل أصحابها:
8. ما الذي يهددنا به هذا؟
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); ViewGroup root = findViewById(R.id.default_id); ViewGroup view1 = new LinearLayout(this); view1.setId(R.id.default_id); root.addView(view1); ViewGroup view2 = new FrameLayout(this); view2.setId(R.id.default_id); view1.addView(view2); ViewGroup view3 = new RelativeLayout(this); view3.setId(R.id.default_id); view2.addView(view3); }
الحل
في الواقع ، لا حرج في ذلك. في نفس RecycleView ، يتم تخزين قوائم العناصر ذات المعرف نفسه. ومع ذلك ، لا يزال هناك فارق بسيط:
@Override protected <T extends View> T findViewTraversal(@IdRes int id) { if (id == mID) { return (T) this; } final View[] where = mChildren; final int len = mChildrenCount; for (int i = 0; i < len; i++) { View v = where[i]; if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) { v = v.findViewById(id); if (v != null) { return (T) v; } } } return null; }
يجب أن يكون بعناية إذا كان لدينا عناصر بنفس المعرف في التسلسل الهرمي ، لأن دائمًا ما يكون العنصر الأول الذي تم إرجاعه دائمًا ، وعلى مستويات مختلفة من استدعاء findViewById يمكن أن يكون كائنات مختلفة.
9. تسقط من TooLargeTransaction عندما تقوم بتدوير الشاشة (نعم ، هنا لا يزال إلقاء اللوم بشكل غير مباشر على ViewPager). كيف تجد الجاني؟
الحل
الأمر بسيط للغاية: قم بتعليق ActivityLifecycleCallbacks عند التطبيق ، وامسك جميع onActivitySaveInstanceState وقم بتحليل كل ما يقع داخل الحزمة. هناك يمكنك الحصول على حالة جميع المشاهدات وجميع الأجزاء داخل هذا النشاط.
فيما يلي مثال لكيفية الحصول على حالة الأجزاء من الحزمة:
fun Bundle.getFragmentsStateList(): List<FragmentBundle>? { try { val fragmentManagerState: FragmentManagerState? = getParcelable("android:support:fragments") val active = fragmentManagerState?.mActive ?: return emptyList() return active.filter { it.mSavedFragmentState != null }.map { fragmentState -> FragmentBundle(fragmentState.mClassName, fragmentState.mSavedFragmentState) } } catch (throwable: Throwable) { Assert.fail(throwable) return null } } fun init() { application.registerActivityLifecycleCallbacks(object : SimpleActivityLifecycleCallback() { override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) { super.onActivitySaveInstanceState(activity, outState) outState?.let { ThreadsUtils.runOnMainThread { trackActivitySaveState(activity, outState) } } } }) } @MainThread private fun trackActivitySaveState(activity: Activity, outState: Bundle) { val sizeInBytes = outState.getSizeInBytes() val fragmentsInfos = outState.getFragmentsStateList() ?.map { mapFragmentsSaveInstanceSaveInfo(it) } ... }
بعد ذلك ، نقوم ببساطة بحساب حجم الحزمة وتسجيلها:
fun Bundle.getSizeInBytes(): Int { val parcel = Parcel.obtain() return try { parcel.writeValue(this) parcel.dataSize() } finally { parcel.recycle() } }
10. لنفترض أن لدينا نشاطًا ومجموعة من التبعيات عليه. في ظل ظروف معينة ، نحتاج إلى إعادة إنشاء مجموعة من هذه التبعيات (على سبيل المثال ، من خلال النقر على تجربة معينة مع واجهة مستخدم أخرى). كيف نطبق هذا؟
الحل
بالطبع ، يمكنك العبث بالأعلام وجعلها نوعًا من إعادة تشغيل النشاط "العكاز" من خلال إطلاق النية. ولكن في الواقع ، كل شيء بسيط للغاية - للنشاط طريقة إعادة إنشاء.
على الأرجح ، فإن معظم هذه المعرفة لن تكون مفيدة لك ، لأنك لا تأتي إلى كل منهم من حياة جيدة. ومع ذلك ، فإن بعضها يوضح جيدًا كيف يعرف الشخص كيفية التفكير واقتراح الحلول. نستخدم أسئلة مماثلة في المقابلات. إذا كانت لديك مهام مثيرة للاهتمام طُلب منك حلها في المقابلات ، أو قمت بتعيينها بنفسك ، اكتبها في التعليقات - سيكون من المثير للاهتمام مناقشتها!