2.10Fragment实践
目录介绍
- 01.什么是Fragment
- 02.Fragment使用
- 2.1 静态使用
- 2.2 动态使用
- 03.Fragment交互
- 3.1 最常用的方法
- 3.2 Fragment与Activity之间传值
- 3.3 Fragment与Fragment之间传值
- 3.4 为何不构造传值
- 04.Fragment回退栈
- 05.Fragment与Activity通信
- 06.Fragment旋转场景
- 07.Fragment使用建议
- 08.Fragment遇到的坑
- 8.1 getActivity()空指针
- 8.2 Fragment发生重叠
- 8.3 Fragment叠加点击穿透
- 8.4 commitAllowingStateLoss
- 09.Fragment一些疑惑
- 9.1 思考Fragment能否不依赖Activity
01.什么是Fragment
- 什么是Fragment
- 可以简单的理解为,Fragment是显示在Activity中的Activity。它可以显示在Activity中,然后它也可以显示出一些内容。因为它拥有自己的生命周期,可以接受处理用户的事件,并且你可以在一个Activity中动态的添加,替换,移除不同的Fragment,因此对于信息的展示具有很大的便利性。
- 作为 view 界面的一部分,Fragment 的存在必须依附于 FragmentActivity使用,并且与 FragmentActivity 一样,拥有自己的独立的生命周期,同时处理用户的交互动作。
- 同一个 FragmentActivity 可以有一个或多个 Fragment 作为界面内容,同样Fragment也可以拥有多个子Fragment,并且可以动态添加、删除 Fragment,让UI的重复利用率和易修改性得以提升,同样可以用来解决部分屏幕适配问题。
- Fragment是组件还是控件
- 严格意义上来说Fragment并不是一个显示控件,而只是一个显示组件。为什么这么说呢?其实像我们的Activity,Dialog,PopupWindow以及Toast类的内部都管理维护着一个Window对象,这个Window对象不但是一个View控件的集合管理对象,它也实现了组件的加载与绘制流程。
- 而我们的Fragment组件如果看过源码的话,严格意义上来说,只是一个View组件的集合并通过控制变量实现了其特定的生命周期,但是其由于并没有维护Window类型的成员变量,所以其不具备组件的加载与绘制功能,因此其不能单独的被绘制出来,这也是我把它称之为组件而不是控件的原因。
03.Fragment使用
- 使用条件
- 宿主Activity 必须继承自 FragmentActivity;
- 使用getSupportFragmentManager() 方法获取 FragmentManager 对象;
- Activity创建Fragment的方式是什么?
- 静态创建具体步骤
- 首先我们同样需要注册一个xml文件,然后创建与之对应的java文件,通过onCreateView()的返回方法进行关联,最后我们需要在Activity中进行配置相关参数即在Activity的xml文件中放上fragment的位置。
- 动态创建具体步骤
- (1)创建待添加的碎片实例
- (2)获取FragmentManager,在活动中可以直接通过调用 getSupportFragmentManager()方法得到。
- (3)开启一个事务,通过调用beginTransaction()方法开启。
- (4)向容器内添加或替换碎片,一般使用replace()方法实现,需要传入容器的id和待添加的碎片实例。
- (5)提交事务,调用commit()方法来完成。
- 静态创建具体步骤
3.1 静态使用
- 大概步骤:
- ① 创建一个类继承Fragment,重写onCreateView方法,来确定Fragment要显示的布局
- ② 在Activity中声明该类,与普通的View对象一样
- 代码演示
- Activity对应的布局文件
<LinearLayout > <fragment android:id="@+id/search_fragment" android:name="com.yczbj.fragment.MyFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
- Activity中的显示和隐藏fragment代码
phoneNumFragment = (PhoneNumFragment) getSupportFragmentManager() .findFragmentById(R.id.search_fragment); private void showSearch() { getSupportFragmentManager() .beginTransaction() .show(phoneNumFragment) .commit(); }
3.2 动态使用
- 代码如下所示,这种是平时开发最常用的
OrderStatesFragment fragment = new OrderStatesFragment(); Bundle bundle = new Bundle(); bundle.putSerializable("confirmOrderModel",confirmOrderModel); fragment.setArguments(bundle); //添加fragment FragmentManager fragmentManager = activity.getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.setCustomAnimations(R.anim.push_bottom_in, 0); transaction.add(android.R.id.content, fragment, "OrderStatesFragment"); transaction.commitAllowingStateLoss();
- replace()和hide()区别
- replace()和hide()都可以动态的在Activity中显示多个Fragment,并且可以来回灵活的切换,但是它们有很大的区别。
- replace() 方法不会保留 Fragment 的状态,也就是说诸如 EditText 内容输入等用户操作在 remove() 时会消失;
- 但是hide()却不会,能完整的保留用户的处理信息。
- addToBackStack()退栈
- 当用户按下返回键时,如果回退栈中保存有之前的事务,会先执行事务回退,然后再执行Activity的finish()方法 。
- 使用场景
- 如果你有一个很高的概率会再次使用当前的Fragment,建议使用show(),hide(),可以提高性能。
- 在我使用Fragment过程中,大部分情况下都是用show(),hide(),而不是replace
- 注意:如果你的app有大量图片,这时更好的方式可能是replace,配合你的图片框架在Fragment视图销毁时,回收其图片所占的内存。
03.Fragment交互
- 注意,fragment尽量用bundle传递数据。fragment.setArguments(args);
3.1 最常用的方法
- 对Fragment传递数据,建议使用setArguments(Bundle args),而后在onCreate中使用getArguments()取出
- 在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent恢复机制类似。
- 使用newInstance(参数) 创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。
3.2 Fragment与Activity之间传值
- 1.Activity向Fragment传值:
- 要传的值,放到bundle对象里;
- 在Activity中创建该Fragment的对象fragment,通过调用fragment.setArguments()传递到fragment中;
- 在该Fragment中通过调用getArguments()得到bundle对象,就能得到里面的值。
- 2.Fragment向Activity传值:
- 第一种:
- 在Activity中调用getFragmentManager()得到fragmentManager,,调用findFragmentByTag(tag)或者通过findFragmentById(id)
- FragmentManager fragmentManager = getFragmentManager();
- Fragment fragment = fragmentManager.findFragmentByTag(tag);
- 第二种:
- 通过回调的方式,定义一个接口(可以在Fragment类中定义),接口中有一个空的方法,在fragment中需要的时候调用接口的方法,值可以作为参数放在这个方法中,然后让Activity实现这个接口,必然会重写这个方法,这样值就传到了Activity中。
- 第一种:
3.3 Fragment与Fragment之间传值
- Fragment与Fragment之间是如何传值的?
- 第一种:
- 通过findFragmentByTag得到另一个的Fragment的对象,这样就可以调用另一个的方法了。
- 第二种:
- 通过接口回调的方式。
- 第三种:
- 通过setArguments,getArguments的方式。
- 第一种:
3.4 为何不构造传值
- 为什么fragment传递数据不用构造方法传递?
- activity给fragment传递数据一般不通过fragment的构造方法来传递,会通过setArguments来传递,因为当横竖屏会调用fragment的空参构造函数,数据丢失。
04.Fragment回退栈
- Fragment的回退栈是用来保存每一次Fragment事务发生的变化
- 如果你将Fragment任务添加到回退栈,当用户点击后退按钮时,将看到上一次的保存的Fragment。一旦Fragment完全从后退栈中弹出,用户再次点击后退键,则退出当前Activity。
- 在某个activity上添加fragment,如果不处理宿主activity中返回键逻辑,点击返回键,关闭了fragment同时也关闭了activity。
OrderStatesFragment fragment = new OrderStatesFragment(); FragmentManager fragmentManager = activity.getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.add(android.R.id.content, fragment, "OrderStatesFragment"); transaction.commitAllowingStateLoss();
- 在某个activity上添加fragment,如果不处理宿主activity中返回键逻辑,点击返回键,关闭了fragment,回到宿主activity页面。
transaction.addToBackStack(null); //添加这个,点击返回键,关闭fragment
- addToBackStack()方法的作用:
- 当移除或替换一个Fragment并向返回栈添加事务时,系统会停止(而非销毁)移除的Fragment。
- 如果用户执行回退操作进行Fragment的恢复,该Fragment将重新启动。如果不向返回栈添加事务,则系统会在移除或替换Fragment时将其销毁。
05.Fragment与Activity通信
- Fragment依附于Activity存在,因此与Activity之间的通信可以归纳为以下几点:
- 如果你Activity中包含自己管理的Fragment的引用,可以通过引用直接访问所有的Fragment的public方法
- 如果Activity中未保存任何Fragment的引用,那么没关系,每个Fragment都有一个唯一的TAG或者ID,可以通过getFragmentManager.findFragmentByTag()或者findFragmentById()获得任何Fragment实例,然后进行操作
- Fragment中可以通过getActivity()得到当前绑定的Activity的实例,然后进行操作。不过不建议这样获取activity的实例,后面会提到的,容易报空指针异常问题。
- Fragment与Activity通信的优化
- 因为要考虑Fragment的重复使用,所以必须降低Fragment与Activity的耦合,而且Fragment更不应该直接操作别的Fragment,毕竟Fragment操作应该由它的管理者Activity来决定。
- 针对有些频繁show或者hide的fragment,可以这样处理
/** * 展示页面 */ private void showPlayingFragment() { if (isPlayFragmentShow) { return; } FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.setCustomAnimations(R.anim.fragment_slide_up, 0); if (mPlayFragment == null) { mPlayFragment = PlayMusicFragment.newInstance("Main"); ft.replace(android.R.id.content, mPlayFragment); } else { ft.show(mPlayFragment); } ft.commitAllowingStateLoss(); isPlayFragmentShow = true; } /** * 隐藏页面 */ private void hidePlayingFragment() { if(mPlayFragment!=null){ FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); ft.setCustomAnimations(0, R.anim.fragment_slide_down); ft.hide(mPlayFragment); ft.commitAllowingStateLoss(); isPlayFragmentShow = false; } }
06.Fragment旋转场景
- 在Activity的学习中都知道,当屏幕旋转时,是对屏幕上的视图进行了重新绘制。
- 因为当屏幕发生旋转,Activity发生重新启动,默认的Activity中的Fragment也会跟着Activity重新创建,用脚趾头都明白...横屏和竖屏显示的不一样肯定是进行了重新绘制视图的操作。所以,不断的旋转就不断绘制,这是一种很耗费内存资源的操作,那么如何来进行优化?
- 首先看看Fragment代码
public class FragmentOne extends Fragment { private static final String TAG = "FragmentOne"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.e(TAG, "onCreateView"); View view = inflater.inflate(R.layout.fragment_one, container, false); return view; } @Override public void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); Log.e(TAG, "onCreate"); } @Override public void onDestroyView() { // TODO Auto-generated method stub super.onDestroyView(); Log.e(TAG, "onDestroyView"); } @Override public void onDestroy() { // TODO Auto-generated method stub super.onDestroy(); Log.e(TAG, "onDestroy"); } }
- 然后你多次翻转屏幕都会打印如下log
07-20 08:18:46.651: E/FragmentOne(1633): onCreate 07-20 08:18:46.651: E/FragmentOne(1633): onCreate 07-20 08:18:46.651: E/FragmentOne(1633): onCreate 07-20 08:18:46.681: E/FragmentOne(1633): onCreateView 07-20 08:18:46.831: E/FragmentOne(1633): onCreateView 07-20 08:18:46.891: E/FragmentOne(1633): onCreateView
- 因为当屏幕发生旋转,Activity发生重新启动,默认的Activity中的Fragment也会跟着Activity重新创建;这样造成当旋转的时候,本身存在的Fragment会重新启动,然后当执行Activity的onCreate时,又会再次实例化一个新的Fragment,这就是出现的原因。
- 如何解决
- 通过检查onCreate的参数Bundle savedInstanceState就可以判断,当前是否发生Activity的重新创建
- 默认的savedInstanceState会存储一些数据,包括Fragment的实例
- 简单改一下代码,判断只有在savedInstanceState==null时,才进行创建Fragment实例
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(savedInstanceState == null) { mFOne = new FragmentOne(); FragmentManager fm = getFragmentManager(); FragmentTransaction tx = fm.beginTransaction(); tx.add(R.id.id_content, mFOne, "ONE"); tx.commit(); } }
- 现在无论进行多次旋转都只会有一个Fragment实例在Activity中,现在还存在一个问题,就是重新绘制时,Fragment发生重建,原本的数据如何保持?
- 和Activity类似,Fragment也有onSaveInstanceState的方法,在此方法中进行保存数据,然后在onCreate或者onCreateView或者onActivityCreated进行恢复都可以。
07.Fragment使用建议
- 关于使用Fragment操作的使用建议
- 如果Fragment视图被频繁的使用,或者一会要再次使用,建议使用show/hide方法,这样可以提升响应速度和性能。
- 如果Fragment占用大量资源,使用完成后,可以使用replace方法,这样可以及时的释放资源。
- 传递参数建议
- Fragment的数据传递通过setArguments/getArguments进行,这样在Activity重启时,系统会帮你保存数据,这点和Activity很相似。
- FragmentPageAdapter和FragmentPageStateAdapter的区别?
- FragmentPageAdapter在每次切换页面时,只是将Fragment进行分离,适合页面较少的Fragment使用以保存一些内存,对系统内存不会多大影响
- FragmentPageStateAdapter在每次切换页面的时候,是将Fragment进行回收,适合页面较多的Fragment使用,这样就不会消耗更多的内存
7.3 Fragment高耦合性
- 那些场景体现fragment高耦合性
- 当子Fragment需要调用宿主Activity的方法时,比如子Fragment需要发送一个消息,但是Fragment没有改方法,所以需要借助宿主Activity去发送,这时候常常需要强制转换content对象,然后调用宿主Activity发方发送广播,这种直接使用的方式违背了高聚低耦的设计原则;
- 解决办法:通过接口抽象的方法,通过接口去调用宿主Activity的方法。
public interface SendBListener { void send(); } public class FirstFragment extends Fragment { SendBListener listener; public void setListener(SendBListener listener) { this.listener = listener; } @OnClick(value = R.id.tv) void onTvClick(View view) { listener.send(); } } public class MainActivity extends AppCompatActivity implements SendBListener{ @Override public void send() { } @Override protected void onCreate(Bundle savedInstanceState) { FirstFragment firstFragment=new FirstFragment(); firstFragment.setListener(this); } }
08.Fragment遇到的坑
8.1 getActivity()空指针
- 遇到的问题
- 可能你遇到过getActivity()返回null,或者平时运行完好的代码,在“内存重启”之后,调用getActivity()的地方却返回null,报了空指针异常。
- 出现的原因分析
- 大多数情况下的原因:你在调用了getActivity()时,当前的Fragment已经onDetach()了宿主Activity。
- 比如:你在出栈了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法,这样就会空指针。
- 解决的方法介绍
- 在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些),即:
@Override public void onAttach(Context context) { super.onAttach(context); activity = (MainActivity) context; } @Override public void onDetach() { super.onDetach(); activity = null; }
8.2 Fragment发生重叠
- 发生重叠的原因
- 发生了页面重启(旋转屏幕、内存不足等情况被强杀重启)。
- 由于采用创建对象的方式去初始化Fragment对象,当宿主Activity在界面销毁或者界面重新执行onCreate()方法时,就有可能再一次的执行Fragment的创建初始,而之前已经存在的 Fragment 实例也会销毁再次创建,这不就与 Activity 中 onCreate() 方法里面第二次创建的 Fragment 同时显示从而发生 UI 重叠的问题。
- 如果宿主界面Activity可以横竖屏切换,导致的生命周期重新刷新也同理可导致界面的重叠问题。
- 重复replace|add Fragment 或者 使用show , hide控制Fragment;
- 发生了页面重启(旋转屏幕、内存不足等情况被强杀重启)。
- 安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)
- 在系统要把app回收之前,系统会把Activity的状态保存下来,Activity的FragmentManager负责把Activity中的Fragment保存起来。在“内存重启”后,Activity的恢复是从栈顶逐步恢复,Fragment会在宿主Activity的onCreate方法调用后紧接着恢复(从onAttach生命周期开始)。
- 通过源码分析重叠原因
- Activity中有个onSaveInstanceState()方法,该方法会在Activity将要被kill的时候回调(例如进入后台、屏幕旋转前、跳转下一个Activity等情况下会被调用)。
- 当Activity只执行onPause方法时(透明Activity),这时候如果App设置的targetVersion大于11则不会执行onSaveInstanceState方法。
- 此时系统帮我们保存一个Bundle类型的数据,我们可以根据自己的需求,手动保存一些例如播放进度等数据,而后如果发生了页面重启,我们可以在onRestoreInstanceState()或onCreate()里get该数据,从而恢复播放进度等状态。
- 产生Fragment重叠的原因就与这个保存状态的机制有关,大致原因就是系统在页面重启前,帮我们保存了Fragment的状态,但是在重启后恢复时,视图的可见状态没帮我们保存,而Fragment默认的是show状态,所以产生了Fragment重叠现象。
- 如何模拟调试
- 当你不确定你的app是否存在该问题时,先检查fragment是否有背景,如果有,先删掉
- 手机的 “设置” - “开发者选项” - 打开”不保留活动”(主要用于模拟Activity被及时回收)
- 把 app 切换到后台,再重新打开,通过点按不同的 tab 来切换 Fragment,打开其他页面在回来,在切换tab
- 如果有重影,请接着看下面的解决方案
8.2.1 findFragmentByTag
- 即在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。
- 解决办法:推荐利用savedInstanceState判断
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity); TargetFragment targetFragment; HideFragment hideFragment; if (savedInstanceState != null) { // “内存重启”时调用 targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName); hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName); // 解决重叠问题 getFragmentManager().beginTransaction() .show(targetFragment) .hide(hideFragment) .commit(); }else{ // 正常时 targetFragment = TargetFragment.newInstance(); hideFragment = HideFragment.newInstance(); getFragmentManager().beginTransaction() .add(R.id.container, targetFragment, targetFragment.getClass().getName()) .add(R.id,container,hideFragment,hideFragment.getClass().getName()) .hide(hideFragment) .commit(); } }
- 这种方式存在很大的弊端
- 使用比较麻烦,代码量较多;
- 在Fragment嵌套的场景下,恢复会有问题,原因在于:页面重启后,在父Fragment没有初始化完成前,getChildFragmentManager()子栈内的子Fragment是空,只有父Fragment初始化完成后,子栈内的子Fragment才能正确获取到。
8.2.2 自己保存Fragment的Hidden状态
- 知道了发生Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态,即mHidden,导致页面重启后,该值为默认的false,即show状态,所以导致了Fragment的重叠。
- 根据这个原因,我想到我们手动维护一个mSupportHidden不就行了吗?
/** * https://github.com/yangchong211 * 异常崩溃后会再次走onCreate方法,这也就是为啥有时候fragment重叠,因为被创建多次 * 发生Fragment重叠的根本原因在于FragmentState没有保存Fragment的显示状态, * 即mHidden,导致页面重启后,该值为默认的false,即show状态,所以导致了Fragment的重叠。 * 两种方案:第一种在activity中处理,第二种在fragment中处理 * @param savedInstanceState bundle */ @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { //异常启动 boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN); FragmentTransaction ft = null; if (getFragmentManager() != null) { ft = getFragmentManager().beginTransaction(); if (isSupportHidden) { ft.hide(this); } else { ft.show(this); } ft.commit(); } } else { //正常启动 } } /** * 异常崩溃,但是没有完全杀死app,内存重启,保存状态 * @param outState bundle */ @Override public void onSaveInstanceState(@NotNull Bundle outState) { outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden()); }
- 优点:不管多深的嵌套Fragment、同级Fragment等场景,全都可以正常工作,不会发生重叠!
8.2.3 通过getFragment和putFragment方式
- 在首页MainActivity中的onSaveInstanceState(Bundle outState)方法里,判断当前所有Fragment,将已经加载的Fragment进行保存
- 复制代码这里需要注意的是,通过getSupportFragmentManager().putFragment();方法按Tag保存Fragment时,需要先确认该Fragment已经add到FragmentManager中了,否则会出现 IllegalStateException: Fragment is not currently in the FragmentManager 错误。
/** * 非法异常情况保存重要状态 * @param outState bundle */ @Override protected void onSaveInstanceState(Bundle outState) { LogUtil.d("MainActivity-----onSaveInstanceState"); //传入当前选中的tab值,在销毁重启后再定向到该tab if (outState!=null){ /*fragment不为空时 保存*/ for (int i = 0; i < TAB_SIZE; i++) { //确保fragment是否已经加入到fragment manager中 if (fragmentList.size()>i && fragmentList.get(i).isAdded() && fragmentList.get(i) != null) { //保存已加载的Fragment getSupportFragmentManager().putFragment(outState, mFragmentTags.get(i), fragmentList.get(i)); } } outState.putInt("position",position); } super.onSaveInstanceState(outState); }
- 在onCreate(Bundle savedInstanceState)中恢复保存的Fragment
- 复制代码在进入onCreate函数时,先判断savedInstanceState是否为null,逐步判断对应Tag的Fragment存不存在,存在则传入到存储Fragment的list中。
@Override public void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { /*获取保存的fragment 没有的话返回null*/ for (int i = 0; i < TAB_SIZE; i++) { Fragment fragment = getSupportFragmentManager().getFragment( savedInstanceState, mFragmentTags.get(i)); if (fragment != null && fragmentList.size()>i) { fragmentList.set(i, fragment); } } position = savedInstanceState.getInt("position",0); } initFragment(); }
- 初始化Fragment
- 这一步本来是第一步,不过加了前面的操作之后,本来为空的FragmentList现在就不一定为空了,所以在初始化各个Fragment时,记得先判断是否已经存在了,如果不存在才创新一个新的对象,否则就是已经添加了之前保存的Fragment:
private void initFragment() { //避免异常重启后重复add if (fragmentList.size()>=TAB_SIZE){ fragmentList.set(0, new IndexFragment()); fragmentList.set(1, new BaoFragment()); fragmentList.set(2, new MyFragment()); } else { fragmentList.add(0, new IndexFragment()); fragmentList.add(1, new BaoFragment()); fragmentList.add(2, new MyFragment()); } }
8.3 Fragment叠加点击穿透
- 项目的首页是一个MainActivity包含多个Fragment,通过hide&show来进行tab切换。
- 在刚开始就遇到了一个很恶心的问题:当前Fragment页,点击能跳转到其他Fragment页的内容。具体来说就是不应该被点击的位置,出现了其它Fragment页面对应位置的点击事件。这个问题不是100%的复现的,而且有些机型不会出现,有些又很频繁。
- 还有种情形,app异常重启出现fragment叠加的时候,也会出现点击穿透……
- 为何出现该问题?
- 实际上是点击事件分发的问题,当多个Fragment添加进Fragment栈时,栈底的Fragment的点击事件在上层Fragment出现后仍然有效。
- 解决问题办法
- 在BaseFragment中全局添加了view.setClickable(true); 问题再也没复现过了。
@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(this.getLayoutId(), container, false); rootView.setClickable(true); //把View的click属性设为true,截断点击时间段扩散 return super.onCreateView(inflater, container, savedInstanceState); }
8.4 commitAllowingStateLoss
- https://blog.csdn.net/tijjyire/article/details/75243285
- https://blog.csdn.net/u013718730/article/details/102672214
- 思考一下:commit 和 commitAllowingStateLoss 的区别?
- 可以明白commit是会对状态进行检测,并抛出异常;而commitAllowingStateLoss方法只是不进行状态检测,因此不会抛出异常。
- fragment 基本上是每个项目都会用到,一般我们会这么写:
getSupportFragmentManager() .beginTransaction() .add(R.id.fragment_container, new MyFragment()) .commit();
- 但是有时候会报如下错误信息:Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
- 意思就是说我们不能在调用onSaveInstanceState进行commit操作。
- 网上的解决办法是使用commitAllowingStateLoss替换commit。确实是不报错了,但是为什么呢?
- 首先找到FragmentTransaction类。
public abstract class FragmentTransaction { public abstract int commit(); public abstract int commitAllowingStateLoss(); }
- 原来commit和commitAllowingStateLoss是抽象方法。继续往下找。
final class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator { @Override public int commit() { return commitInternal(false); } @Override public int commitAllowingStateLoss() { return commitInternal(true); } }
- 发现BackStackRecord类继承了FragmentTransaction。可以看出,不同之处只有commitInternal的参数。感觉离真相又近了一步,继续往下看:
int commitInternal(boolean allowStateLoss) { // ...不显示无关代码 mManager.enqueueAction(this, allowStateLoss); return mIndex; }
- 在commitInternal方法内,只有mManager.enqueueAction(this,allowStateLoss);使用了该布尔值参数。离真相还差一点了,继续推测:
public void enqueueAction(OpGenerator action, boolean allowStateLoss) { if (!allowStateLoss) { checkStateLoss(); } synchronized (this) { if (mDestroyed || mHost == null) { if (allowStateLoss) { // This FragmentManager isn't attached, so drop the entire transaction. return; } throw new IllegalStateException("Activity has been destroyed"); } // ...无关代码 } }
- 有了突破性进展!此处对allowStateLoss值进行了判断。checkStateLoss按照命名意思是校验状态。离真相仅剩一步了!
private void checkStateLoss() { if (isStateSaved()) { throw new IllegalStateException( "Can not perform this action after onSaveInstanceState"); } if (mNoTransactionsBecause != null) { throw new IllegalStateException( "Can not perform this action inside of " + mNoTransactionsBecause); } } @Override public boolean isStateSaved() { // See saveAllState() for the explanation of this. We do this for // all platform versions, to keep our behavior more consistent between // them. return mStateSaved || mStopped; }
- 这里会抛出异常信息,明显就是文章开头碰到的异常错误信息!isStateSaved()方法也一同显示了。到了此时,可以明白commit是会对状态进行检测,并抛出异常;而commitAllowingStateLoss方法只是不进行状态检测,因此不会抛出异常。这明显是有点逃避问题,那么这个状态是什么判断而得出的呢?
Parcelable saveAllState() { // ...无关代码 mStateSaved = true; // ...无关代码 }
- 主要看mStateSaved变量。在saveAllState方法内,把mStateSaved赋值为true。有意思的是saveAllState是在Activity和FragmentActivity内的onSaveInstanceState方法调用的。
- commit 和 commitAllowingStateLoss使用建议?
- 谷歌在commitAllowingStateLoss方法的注释上也写明,调用此方法会有丢失页面状态信息的风险。
- 一般情况下,尽量提早调用 commit 方法,比如说onCreate();
- 异步回调中避免使用commit;
- 不得已的情况,可以使用commitAllowingStateLoss替代commit。毕竟报错奔溃,比页面状态信息丢失更严重;
09.Fragment一些疑惑
9.1 思考Fragment能否不依赖Activity
- Fragment能否不依赖于Activity存在?
- Fragment不能独立存在,它必须嵌入到activity中,而且Fragment的生命周期直接受所在的activity的影响。
- transaction只是记录了从一个状态到另一个状态的变化过程
- 即比如从FragmentA替换到FragmentB的过程,当通过函数transaction.addToBackStack(null)将这个事务添加到回退栈,则会记录这个事务的状态变化过程,
- 如从FragmentA —>FragmentB,当用户点击手机回退键时,因为transaction的状态变化过程被保存,则可以将事务的状态变化过程还原,即将FragmentB —> FragmentA.
// Create new fragment and transaction Fragment newFragment = new ExampleFragment(); FragmentTransaction transaction = getFragmentManager().beginTransaction(); // Replace whatever is in the fragment_container view with this fragment, // and add the transaction to the back stack transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // Commit the transaction transaction.commit();
其他介绍
01.关于博客汇总链接
02.关于我的博客
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 简书:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
- 开源中国:https://my.oschina.net/zbj1618/blog
- 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e