Android开发-从源码分析Fragment嵌套PagerAdapter生命周期,解决重建问题

本文主要是介绍Android开发-从源码分析Fragment嵌套PagerAdapter生命周期,解决重建问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

介绍

众所周知在Android开发中Fragment的生命周期非常复杂,复杂得甚至让Square公司提出了我为什么主张反对使用Android Fragment转而提倡使用自定义View组合替代Fragment。但是没办法公司项目还是使用了很多Fragment嵌套。遇到问题还是需要自己去处理的。

这里以Fragment的状态保存和恢复(即重建)来讨论一些关于Fragment的生命周期问题。

有隐患的代码

不知道各位有没有写过下面,类似的代码。

public class TabActivity extends AppCompatActivity {@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_tab);Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);setSupportActionBar(toolbar);SectionsPagerAdapter mSectionsPagerAdapter =new SectionsPagerAdapter(getSupportFragmentManager());ViewPager mViewPager = (ViewPager) findViewById(R.id.container);mViewPager.setAdapter(mSectionsPagerAdapter);TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);tabLayout.setupWithViewPager(mViewPager);}//交互的接口定义public interface OnInteractionListener {void action(String action);}public static class PlaceholderFragment extends Fragment {private static final String ARG_SECTION_NUMBER = "section_number";public PlaceholderFragment() {}private OnInteractionListener listener;public void setListener(OnInteractionListener listener) {this.listener = listener;}public static PlaceholderFragment newInstance(int sectionNumber) {PlaceholderFragment fragment = new PlaceholderFragment();Bundle args = new Bundle();args.putInt(ARG_SECTION_NUMBER, sectionNumber);fragment.setArguments(args);return fragment;}@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {View rootView = inflater.inflate(R.layout.fragment_tab, container, false);TextView textView = (TextView) rootView.findViewById(R.id.section_label);textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));//使用接口 和Activity交互listener.action("fragment response view is build");return rootView;}}public class SectionsPagerAdapter extends FragmentPagerAdapter {public SectionsPagerAdapter(FragmentManager fm) {super(fm);}/*** 在get方法中 返回Fragment实例 并设置接口回调* @param position* @return*/@Override public Fragment getItem(int position) {Logger.d("position = "+position);PlaceholderFragment fragment = PlaceholderFragment.newInstance(position + 1);//注入接口实例 打印输出fragment.setListener(new OnInteractionListener() {@Override public void action(String action) {Logger.d(action);}});return fragment;}@Override public int getCount() {return 3;}@Override public CharSequence getPageTitle(int position) {switch (position) {case 0:return "SECTION 1";case 1:return "SECTION 2";case 2:return "SECTION 3";}return null;}}
}

基本思路是使用ViewPager显示Fragment,使用FragmentPagerAdapter管理Fragment,并在创建的地方加入外部接口实例注入过程。
这段代码看起来没什么问题,跑起来也没有问题。但是有很大隐患。
首先代码是使用Android Studio自动生成的,操作如下。我在原有基础上加上了Fragment和Activity的通过接口交互的操作(有注释的部分代码)。
生成代码步骤

如果按上一篇提到的开发者选项->开启不保留活动测试切换后的Activity恢复重建。然后程序就崩溃了!!,原因竟然是NullPointerException空指针,因为交互的接口实例为空。

分析源码

因为接口实例的注入在FragmentPagerAdaptergetItem中完成,我们发现问题的入口就是FragmentPagerAdapter。

FragmentPagerAdapter:只贴出相关源码

public abstract class FragmentPagerAdapter extends PagerAdapter {private final FragmentManager mFragmentManager;public FragmentPagerAdapter(FragmentManager fm) {mFragmentManager = fm;}/*** Return the Fragment associated with a specified position.*/public abstract Fragment getItem(int position);@Overridepublic Object instantiateItem(ViewGroup container, int position) {if (mCurTransaction == null) {mCurTransaction = mFragmentManager.beginTransaction();}final long itemId = getItemId(position);// Do we already have this fragment?String name = makeFragmentName(container.getId(), itemId);Fragment fragment = mFragmentManager.findFragmentByTag(name);if (fragment != null) {if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);mCurTransaction.attach(fragment);} else {fragment = getItem(position);if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);mCurTransaction.add(container.getId(), fragment,makeFragmentName(container.getId(), itemId));}if (fragment != mCurrentPrimaryItem) {fragment.setMenuVisibility(false);fragment.setUserVisibleHint(false);}return fragment;}private static String makeFragmentName(int viewId, long id) {return "android:switcher:" + viewId + ":" + id;}}

从源码中发现instantiateItem中调用getItem(int
position)是有条件的。只有当上一步尝试在FragmentManager抽象类中查找不到特定的Fragment时才会调用,去子类中获取Fragment实例。
FragmentManager是由外部注入的,注入的是抽象定义没有实现代码,且findFragmentByTag方法是怎么样根据name去查找Fragment的。
我们得到以下两个问题:

1:FragmentManager的实例是什么?
2:findFragmentByTag查找的是什么?

FragmentActivity管理Fragment

查看源码我们来到FragmentActivity,它的主要功能就是对嵌套在他内部的Fragment进行管理。
FragmentActivity:

public class FragmentActivity {
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());//FragmentActivity对内部状态的保存操作
@Overrideprotected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);Parcelable p = mFragments.saveAllState();if (p != null) {outState.putParcelable(FRAGMENTS_TAG, p);}}}/*** Return the FragmentManager for interacting with fragments associated* with this activity.*/public FragmentManager getSupportFragmentManager() {return mFragments.getSupportFragmentManager();}

由以上这些代码,我们可以知道对于Fragment的控制,FragmentActivity其实是通过FragmentController实现的

FragmentController

public class FragmentController {/*** Returns a {@link FragmentManager} for this controller.*/public FragmentManager getSupportFragmentManager() {return mHost.getFragmentManagerImpl();}}

通过这行代码我们最终来到了目的地

FragmentManagerImpl:它是FragmentManager抽象类的具体实现类


final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {ArrayList<Fragment> mActive;ArrayList<Fragment> mAdded;@Overridepublic Fragment findFragmentByTag(String tag) {if (mAdded != null && tag != null) {// First look through added fragments.for (int i=mAdded.size()-1; i>=0; i--) {Fragment f = mAdded.get(i);if (f != null && tag.equals(f.mTag)) {return f;}}}if (mActive != null && tag != null) {// Now for any known fragment.for (int i=mActive.size()-1; i>=0; i--) {Fragment f = mActive.get(i);if (f != null && tag.equals(f.mTag)) {return f;}}}return null;}
}

findFragmentByTag:是从内部持有的Fragment集合中根据tag名称查找的。

Fragment的备忘录模式应用

我们通过源码回答了前面提出的两个问题,但是还是没有弄清楚崩溃的原因。
但是我们发现ArrayList<Fragment> mActive内部变量。根据对备忘录模式的理解,肯定存在对该变量的备忘录封装操作。
回到前面的部分FragmentActivity对内部状态的保存操作

我们看看FragmentManagerImpl关于备忘录模式的实现,篇幅有限只看保存操作。

FragmentManagerImpl:

Parcelable saveAllState() {//省略其他操作 只看关键代码// First collect all active fragments.int N = mActive.size();FragmentState[] active = new FragmentState[N];for (int i=0; i<N; i++) {Fragment f = mActive.get(i);FragmentState fs = new FragmentState(f);active[i] = fs;}FragmentManagerState fms = new FragmentManagerState();fms.mActive = active;fms.mAdded = added;fms.mBackStack = backStack;return fms;
}

Fragment的备忘录对象实现:

final class FragmentState implements Parcelable {final String mClassName;final int mIndex;final boolean mFromLayout;final int mFragmentId;final int mContainerId;final String mTag;final boolean mRetainInstance;final boolean mDetached;final Bundle mArguments;final boolean mHidden;Bundle mSavedFragmentState;Fragment mInstance;public FragmentState(Fragment frag) {mClassName = frag.getClass().getName();mIndex = frag.mIndex;mFromLayout = frag.mFromLayout;mFragmentId = frag.mFragmentId;mContainerId = frag.mContainerId;mTag = frag.mTag;mRetainInstance = frag.mRetainInstance;mDetached = frag.mDetached;mArguments = frag.mArguments;mHidden = frag.mHidden;}}

FragmentManager的备忘录实现:

final class FragmentManagerState implements Parcelable {FragmentState[] mActive;int[] mAdded;BackStackState[] mBackStack;public FragmentManagerState() {}}

所以基于以上的Fragment的备忘录模式的实现,Android系统能够保证当FragmentActivity被销毁后,重新返回时的重建。恢复到离开时的状态。

情景分析

在看了这么多源码之后,重新返回思考刚才的重建后崩溃NPE问题,分析情景如下:

  1. 第一次构建FragmentPagerAdapter,FragmetManager中Fragment集合为空,需要getItem执行返回Fragment实例,UI得以显示。
  2. 用户离开当前Activity,onSaveInstanceState保存操作被回调执行,Fragment被FragmetManager控制器调起保存操作,保存内部状态。
  3. 用户回到Activity,备忘录的数据恢复操作开始执行,就是onCreate(Bundle savedInstanceState)方法中Bundle有被保存下来的数据,FragmetManager被恢复成之前的状态,Fragment容器中有内容。
  4. 再次构建FragmentPagerAdapter,但是通过FragmetManager实例能够findFragmentByTag得到Fragment实例。
  5. 整个恢复过程由源码配合完成,我们的FragmentPagerAdapter子类的getItem方法没有被调起执行。
  6. 最后当所有得源码都执行完,执行到我们所写的Fragment子类,一个没有被恢复的对象也就是我们由外部注入的接口实例,肯定为空,然后就发生NullPointerException空指针崩溃。

问题的解决方案

知道了问题发生的原因,解决方案的思路就很清晰,因为数据的重建恢复完全由源码完成,我们所能做的就是配合源码的执行,在适当的生命周期添加适当的代码。

改变外部对象的注入方式

配合Fragment的生命周期,改变外部对象的注入方式,比如这样

Activity实现接口

public class TabActivity extends AppCompatActivity implements OnInteractionListener {
}

Fragment从生命周期中获取外部对象

public static class PlaceholderFragment extends Fragment {@Override public void onAttach(Context context) {super.onAttach(context);if (context instanceof OnInteractionListener){listener= (OnInteractionListener) context;}}

优点:可以很简单的实现解决问题,代码量比较少。
缺点:Fragmen和Activity的接口实现方式隐式绑定,不容易理解。且对于ViewPager这样的,还需要再添加代码集中控制Fragment集合。

继承FragmentPagerAdapter添加新方法

根据FragmentPagerAdapter的源码,重新定义创建和绑定过程

import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;/*** Created by LiCola on 2017/6/5. 按照FragmentActivity和FragmentPagerAdapter* 对子Fragment的生命周期和重建的顺序特性抽象的父类,* 建议项目中所有的有关FragmentPagerAdapter 都直接继承该抽象类,或按照该思路管理Fragment*/
public abstract class FragmentPagerRebuildAdapter<T extends Fragment> extends FragmentPagerAdapter {private final T PLACE_FRAGMENT = null;protected final int pageSize;protected List<T> fragments;public FragmentPagerRebuildAdapter(FragmentManager fm, int pageSize) {super(fm);this.pageSize = pageSize;fragments = loadPlaceFragment(pageSize);}/*** 根据位置参数创建并返回一个Fragment实例 该方法FragmentActivity在新建Fragment时调用,销毁后重建时不会调用** @param position 位置参数* @return 创建好的Fragment实例*/protected abstract T createFragment(int position);/*** 操作某个Fragment,设置或绑定操作或数据 该方法,新建或重建都调用** @param fragment 对某个位置的Fragment* @param position 某个位置的位置参数*/protected abstract void bindFragment(T fragment, int position);private void unbindFragment(T fragment, int position) {}public List<T> getFragmentList() {return fragments;}/*** 根据传入位置 得到Fragment*/@Nullablepublic T getFragmentByPosition(int position) {if (fragments == null || fragments.size() == 0 || position >= fragments.size()) {return null;}return fragments.get(position);}/*** 得到ViewPager当前页的Fragment*/@Nullablepublic T getFragmentByCurrentItem(ViewPager viewPager) {if (viewPager == null) {return null;}return getFragmentByPosition(viewPager.getCurrentItem());}/*** 获取实例方式 该方法的position可能会乱序输入,所以使用set方式*/@Overridepublic Object instantiateItem(ViewGroup container, int position) {Object object = super.instantiateItem(container, position);bindFragment((T) object, position);fragments.set(position, (T) object);return object;}@Overridepublic Fragment getItem(int position) {Fragment fragment = createFragment(position);if (fragment == null) {throw new UnsupportedOperationException("createFragment(position="+ position+ " 没有返回Fragment实例),检查代码确保createFragment方法覆盖所有position");}return fragment;}@Overridepublic void destroyItem(ViewGroup container, int position, Object object) {super.destroyItem(container, position, object);unbindFragment((T) object, position);}@Overridepublic int getCount() {return pageSize;}/*** 初始化List集合,并使用占位对象填充,否则无法使用ArrayList的set直接填充指定位置的数据* @param pageSize* @return*/private List<T> loadPlaceFragment(int pageSize) {if (pageSize <= 0) {throw new IllegalArgumentException("FragmentPagerRebuildAdapter pageSize<=0");}ArrayList<T> placeList = new ArrayList<>(pageSize);for (int i = 0; i < pageSize; i++) {placeList.add(PLACE_FRAGMENT);}return placeList;}
}

优点:逻辑清晰,子类只需要根据提示实现抽象方法,且提供了容器控制。
缺点:需要确定Fragment数量,不能改变数量,对现有代码修改比较大。

总结

  • 本文从问题出发,一步步探索源码,得到代码执行的内部逻辑。理解源码的后,能够清晰的情景分析,提出解决方案。
  • 对于解决方案,在理解问题发生的原因后,方案有多种,这里只是简单的讨论两种解决方案。
  • 讨论分析了备忘录模式应用在FragmentActivity和Fragment的应用。

水平有限,错误之处还望大家多多指正

这篇关于Android开发-从源码分析Fragment嵌套PagerAdapter生命周期,解决重建问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/576209

相关文章

Vue3绑定props默认值问题

《Vue3绑定props默认值问题》使用Vue3的defineProps配合TypeScript的interface定义props类型,并通过withDefaults设置默认值,使组件能安全访问传入的... 目录前言步骤步骤1:使用 defineProps 定义 Props步骤2:设置默认值总结前言使用T

SpringBoot 多环境开发实战(从配置、管理与控制)

《SpringBoot多环境开发实战(从配置、管理与控制)》本文详解SpringBoot多环境配置,涵盖单文件YAML、多文件模式、MavenProfile分组及激活策略,通过优先级控制灵活切换环境... 目录一、多环境开发基础(单文件 YAML 版)(一)配置原理与优势(二)实操示例二、多环境开发多文件版

使用docker搭建嵌入式Linux开发环境

《使用docker搭建嵌入式Linux开发环境》本文主要介绍了使用docker搭建嵌入式Linux开发环境,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录1、前言2、安装docker3、编写容器管理脚本4、创建容器1、前言在日常开发全志、rk等不同

504 Gateway Timeout网关超时的根源及完美解决方法

《504GatewayTimeout网关超时的根源及完美解决方法》在日常开发和运维过程中,504GatewayTimeout错误是常见的网络问题之一,尤其是在使用反向代理(如Nginx)或... 目录引言为什么会出现 504 错误?1. 探索 504 Gateway Timeout 错误的根源 1.1 后端

Web服务器-Nginx-高并发问题

《Web服务器-Nginx-高并发问题》Nginx通过事件驱动、I/O多路复用和异步非阻塞技术高效处理高并发,结合动静分离和限流策略,提升性能与稳定性... 目录前言一、架构1. 原生多进程架构2. 事件驱动模型3. IO多路复用4. 异步非阻塞 I/O5. Nginx高并发配置实战二、动静分离1. 职责2

解决升级JDK报错:module java.base does not“opens java.lang.reflect“to unnamed module问题

《解决升级JDK报错:modulejava.basedoesnot“opensjava.lang.reflect“tounnamedmodule问题》SpringBoot启动错误源于Jav... 目录问题描述原因分析解决方案总结问题描述启动sprintboot时报以下错误原因分析编程异js常是由Ja

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

Android协程高级用法大全

《Android协程高级用法大全》这篇文章给大家介绍Android协程高级用法大全,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友跟随小编一起学习吧... 目录1️⃣ 协程作用域(CoroutineScope)与生命周期绑定Activity/Fragment 中手

Python实战之SEO优化自动化工具开发指南

《Python实战之SEO优化自动化工具开发指南》在数字化营销时代,搜索引擎优化(SEO)已成为网站获取流量的重要手段,本文将带您使用Python开发一套完整的SEO自动化工具,需要的可以了解下... 目录前言项目概述技术栈选择核心模块实现1. 关键词研究模块2. 网站技术seo检测模块3. 内容优化分析模

深度剖析SpringBoot日志性能提升的原因与解决

《深度剖析SpringBoot日志性能提升的原因与解决》日志记录本该是辅助工具,却为何成了性能瓶颈,SpringBoot如何用代码彻底破解日志导致的高延迟问题,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言第一章:日志性能陷阱的底层原理1.1 日志级别的“双刃剑”效应1.2 同步日志的“吞吐量杀手”