分享人生的问题



每个开发人员都提出了有关Activity生命周期的问题:什么是绑定服务,如何在旋转屏幕时保存界面状态以及Fragment与Activity有何不同。
我们在FunCorp积累了有关类似主题的问题清单,但有一些细微差别。 我想与您分享其中的一些。


1.每个人都知道,如果您在第一个活动的顶部打开第二个活动并旋转屏幕,那么生命周期调用链将如下所示:


开幕活动

FirstActivity:onPause
SecondActivity:onCreate
SecondActivity:onStart
SecondActivity:onResume
FirstActivity:onSaveInstanceState
FirstActivity:onStop


转弯

SecondActivity:onPause
SecondActivity:onSaveInstanceState
SecondActivity:onStop
SecondActivity:onCreate
SecondActivity:onStart
SecondActivity:onRestoreInstanceState
SecondActivity:onResume


回去

SecondActivity:onPause
FirstActivity:onCreate
FirstActivity:onStart
FirstActivity:onRestoreInstanceState
SecondActivity:onStop


如果第二项活动是透明的,将会发生什么?


解决方案


就透明的顶级活动而言,就逻辑而言,一切都略有不同。 正是因为它是透明的,所以在旋转之后,有必要恢复内容及其正下方的活动。 因此,呼叫顺序将略有不同:


发现活动

FirstActivity:onPause
SecondActivity:onCreate
SecondActivity:onStart
SecondActivity:onResume


转弯

SecondActivity:onPause
SecondActivity:onSaveInstanceState
SecondActivity:onStop
SecondActivity:onCreate
SecondActivity:onStart
SecondActivity:onRestoreInstanceState
SecondActivity:onResume
FirstActivity:onSaveInstanceState
FirstActivity:onStop
FirstActivity:onCreate
FirstActivity:onStart
FirstActivity:onRestoreInstanceState
FirstActivity:onResume
FirstActivity:onPause


2.没有动态添加视图,任何应用程序都无法做,但是有时您必须在不同屏幕之间移动同一视图。 是否可以将同一对象同时添加到两个不同的活动? 如果我使用“应用程序”上下文创建它,并希望同时将其添加到不同的活动,会发生什么情况?


为什么需要这个?
有“不太愉快”的库在自定义视图中保存重要的业务逻辑,并且在每个新活动中重新创建这些视图是一个错误的决定,因为 我想要一个数据集。



解决方案


没有什么可以阻止使用Application上下文创建视图的。 它将仅应用与任何活动都不相关的默认样式。 您也可以在不同的活动之间移动该视图而不会出现问题,但是您需要确保仅将其添加到一个父级中


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中分离与UI关联的所有控制器和类。 有必要将片段和视图的填充(列表等)所需的数据接收清楚地分开,因为视图的填充和破坏比接收数据(从数据库中读取一些数据)要频繁得多。


状态恢复也有细微差别:例如,onSaveInstanceState有时在onDestroyView之后。 此外,应该牢记的是,如果onViewStateRestored中传入null,则意味着您无需还原任何内容,也不必重置为默认状态。


如果我们谈论添加和替换之间的便利,那么如果您具有深度导航(我们的用户导航深度为产品KPI之一),则替换在内存中更经济。 将工具栏替换为replace也更加方便,因为可以在onCreateView中重新使用它。 从add的优点出发:生命周期中的问题更少,当您返回时,我不需要重新创建视图,也不需要重新填充任何内容。


4.有时您仍然必须直接使用服务,甚至使用绑定服务。 活动与这些服务之一(仅一项活动)进行交互。 它连接到服务并向其中传输数据。 当您打开屏幕时,我们的活动被破坏了,我们不得不从这项服务中反弹。 但是,如果没有连接,则该服务将被破坏,并且在绑定之后将绑定到完全不同的服务。 如何确保当您打开服务时仍保持活动状态?


解决方案


如果您知道一个漂亮的解决方案,请在评论中写。 我只想到类似的东西:


  @Override protected void onDestroy() { super.onDestroy(); ThreadsUtils.postOnUiThread(new Runnable() { @Override public void run() { unbindService(mConnection); } }); } 

5.最近,我们在Single Activity(使用可用库之一)上重新导航了应用程序内部的导航。 以前,每个应用程序屏幕都是一个单独的活动,现在,导航可在片段上进行。 通过栈中的意图标志解决了堆栈中间返回活动的问题。 如何返回堆栈中间的片段?


解决方案


是的,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并将所有必需的数据保存到Bundle。 当您重新创建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中,存储具有相同ID的元素列表。 但是,仍然存在一些细微差别:


  @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; } 

如果我们在层次结构中有具有相同ID的元素,则应该小心,因为 它始终是找到的第一个始终返回的元素,并且在findViewById调用的不同级别可以是不同的对象。


9.旋转屏幕时,您会从TooLargeTransaction跌落(是的,这里是我们的ViewPager仍然间接引起的)。 如何找到罪魁祸首?


解决方案


这很简单:将ActivityLifecycleCallbacks挂在Application上,捕获所有onActivitySaveInstanceState并解析Bundle中的所有内容。 在那里,您可以获取此活动中所有视图和所有片段的状态。


下面是一个如何从Bundle中获取片段状态的示例:


 /** * Tries to find saved [FragmentState] in bundle using 'android:support:fragments' key. */ 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) } ... } 

接下来,我们只计算Bundle的大小并记录它:


  fun Bundle.getSizeInBytes(): Int { val parcel = Parcel.obtain() return try { parcel.writeValue(this) parcel.dataSize() } finally { parcel.recycle() } } 

10.假设我们有一个活动和一组依赖项。 在某些条件下,我们需要重新创建一组这些依赖项(例如,通过单击具有另一个UI的特定实验)。 我们如何实现呢?


解决方案


当然,您可以修改标志,并通过启动Intent来使它某种“拐杖”地重新开始活动。 但实际上,一切都很简单-活动具有重新创建方法。


很可能,这些知识中的大多数将对您没有用,因为您并不是从美好的生活中走出来的。 但是,其中一些很好地展示了一个人如何知道如何推理和提出解决方案。 我们在访谈中使用类似的问题。 如果您有一些有趣的任务需要您在面试中解决,或者您自行设置,请在评论中写下它们-进行讨论会很有趣!

Source: https://habr.com/ru/post/zh-CN426215/


All Articles