fragment有诸多的优势有点,但也存在着不足和一些隐藏的问题,以下是在开发过程中碰到的fragment各类问题,做以总结和分析:
目录:
* 1.fragment出栈以及同时多个出栈会导致的问题
* 2.Fragment回退监听问题
* 3.Fragment布局透明、布局重叠问题
* 4.Fragment页面动画跳转导致的问题
* 5.界面点击事件穿透问题
* 6.getActivity空指针问题
* 7.各个Fragment之间数据传递问题
* 8.Fragment的replace切换页面导致回退栈出现问题
* 9.Fragment嵌套内存泄漏问题
在源码中fragment的实例对象存储list集合中保存,在单次弹出操作过程如下图:
在单次弹出的时候通常不会出现问题,但是在pop栈顶的fragment后,然后又直接调用FragmentManager的getFragments()获取尾角标也就是栈顶的Fragment,会发现是null,原因就是在集合中被移除的fragemnt实质上被置为了null,而不是直接被移除
同样在多个fragment同时出栈的时候,这个问题更明显,如下图展示:
综上来说,整个队列的顺序是不存在问题的,只是在出栈进栈的过程中会导致有null的存在,所以,我们在操作存储fragment实例对象的list集合的时候,需要做如下操作:
// 将系统默认集合中的null进行剔除,然后调用重新存储的集合进行操作
public ArrayList getFragments() {
ArrayList fragments1 = new ArrayList<>();
@SuppressLint("RestrictedApi") List fragments = mActivity.getSupportFragmentManager().getFragments();
for (Fragment fragment : fragments) {
if (fragment != null) {
fragments1.add(fragment);
}
}
return fragments1;
}
在activity中我们可以很清晰的通过实现复写onBackPressed()而获取到当前activity的退出操作,在onBackPressed()函数中实现我们退出前需要执行的逻辑代码,但fragment源生是并没有对回退做监听的函数,尤其是在项目中高度使用fragment的情况下,fragment没有回退监听是非常的痛苦。
在各种调试下给出了以下的解决方案:
1. 监听activity的回退键监听
2. 通过FragmentManager获取所有的fragment实例对象的集合,获取集合尾部,也就是fragment回退栈栈顶的fragment实例对象
3. 执行fragment中对应的回退监听函数,实现对回退的监听。(整个封装,为了通用性,当前回调函数需要在所有调用的fragment的baseFragment中空实现一个回退需要执行的函数,由需要实现的监听回退的子类去复写实现)
**注意:需要说明的一点是,当前的解决方式仅适合于add方式且addToBackStack()添加入回退栈的加载,而不适合replace方式**
监听activity的回退键监听
首先我们需要知道,点击回退键执行的函数的调用流程顺序:
dispatchKeyEvent -> onUserInteraction -> onKeyDown -> onBackPressed
为了能够有最高的可控性,所以我们就选择dispatchKeyEvent
@SuppressLint("RestrictedApi")
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {//点击的是返回键
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
//按键的按下事件,就在此处获取栈顶fragment和监听fragment的回退执行的函数
}
return true;
}
return super.dispatchKeyEvent(event);
}
获取回退栈栈顶的fragment
执行fragment中对应的回退监听函数
//BaseFragment 中需要实现的回退监听的空函数
/**
* Explain : 后退键的监听
*@return : 当子类返回true,则认为当前后退由子类自己执行,不执行后退操作,全权交给子类自己去处理
* 当子类返回false,则认为当前子类不做处理,直接退出当前fragment
* @author LiXaing create at 2017/7/19 17:54
*/
protected boolean onBackPressed() {
return false;
}
/** Explain : Fragment的回退监听
* @author LiXiang create at 2018/1/30 19:03*/
@SuppressLint("RestrictedApi")
public void onBackPressedListener(){
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
// >1,当存在多个fragment的时候,就执行栈顶fragment的回退监听函数
if (getFragments() > 1) {
@SuppressLint("RestrictedApi")
BaseFragment fragment = (BaseFragment)
//获取当前子类的是否复写返回true有自己的操作,当返回false的时候就默认弹栈
getFragments().get(getFragments()-1);
if (!fragment.onBackPressed()) {
fm.popBackStack();
}
}
}else {
//当只有一个fragment的时候,也就是回到了主界面,所以直接关闭当前app
mActivity.finish();
}
}
//获取当前管理Fragment实例对象且不存在null的list集合
public ArrayList getFragments() {
ArrayList fragments1 = new ArrayList<>();
@SuppressLint("RestrictedApi") List fragments = getFragmentManager(false).getFragments();
for (Fragment fragment : fragments) {
if (fragment != null) {
fragments1.add(fragment);
}
}
return fragments1;
}
导致fragment重叠的问题主要是两方面导致的:
/**
* show一个Fragment,hide另一个/多个Fragment ; 主要用于类似微信主页那种 切换tab的情况
*
* @param showFragment 需要show的Fragment
* @param hideFragment 需要hide的Fragment
* @param isChildFragment 当时Fragment是否是在Fragment中的fragment
*/
public void showHideFragment(Fragment showFragment, Fragment hideFragment) {
if (showFragment == hideFragment) return;
FragmentManager fm = mActivity.getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
if (hideFragment == null) {
@SuppressLint("RestrictedApi") List fragmentList = fm.getFragments();
if (fragmentList != null) {
for (Fragment fragment : fragmentList) {
if (fragment != null && fragment != showFragment) {
ft.hide(fragment);
}
}
}
} else {
ft.hide(hideFragment);
}
ft.show(showFragment);
ft.commitAllowingStateLoss();
}
在fragmet页面跳转的时候,例如使用了进场动画,出场动画,在当前fragment在调用popBackStack()出栈,执行出场动画过程中,这时再popBackStack()会出现问题或者执行mActivity.getSupportFragmentManager().getFragments()会发现size的大小没有变动,获取尾角标的fragment会发现还是当前的fragment的实例对象。
解决方案:pop后的操作可以通过handler发送一个动画时长的延时操作。(另:在support-25.4.0版本,google对当前问题进行了修复。)
fragment在没有处理这一问题的时候,假如当前点击的位置并未有响应区,则点击事件会继续向下传递,假如在下面层的界面的当前位置刚好点击的响应区,则会去执行这一非当前界面的响应操作。
解决方案:
//1. 在XML布局的根布局中添加属性:
android:clickable="true"
//2. 在Fragment中获取布局的根视图,对其设定touch
mView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
在Fragemnt通常的使用中,getActivity空指针碰到的几率还是比较少,而导致这个问题的出现,更多的情况是不规范引起的。
例如网络请求等异步请求操作,当前Fragment执行了popBackStack()方法,并且代码运行已经经过onDetach()生命周期,fragment已经剥离了宿主activity,在这之后又调用到activity,就会导致空指针
解决方案:
1. 尽可能的避免在界面关闭后也中断相关的异步操作。
2. 在fragment的基类里,设定一个全局变量的mActivity,使用全局的mActivity代替getActivity(),即使Fragment在onDetach()后依旧可以获取到mActivity而不会导致空指针。
在fragment中,Fragment之间数据传源生支持setArguments(),getArguments()进行页面之间数据的相互传递,但这样的方式在实际的开发中具有很高的耦合和使用的不方便,在后期的维护中,业务逻辑也展示得非常的不清晰,时间久了可能就并不知道是从哪里来到哪里去;
例如假如有这么一个需求:
有五个Fragment需要连续跳转,Afragment中产生的数据需要在EFragment中使用,那么这部数据从A到E时如下图这样进行传递的:
也就是中间的Fragment需要去获取然后重新设定再传递给下一步,非常的不方便。
这里给出的解决方案原理是这样的:
创建一个专门存储数据的存储池,在任意Fragemnt中都可以向pool中存放数据,任意页面也可以从池中去获取数据。
这样一来,数据就由原来的逐级传递变为通过pool间接传递,解耦了中间层Fragemnt。
封装的工具类如下:
import android.support.v4.util.ArrayMap;
/**
* Created by lixiang on 2017/6/7.
*/
/** Explain : 在Fragment之间,总是会有数据的传递情况,总是会需要在初始化页面的时候获取argment,
* 所以导致Fragmnt的耦合度较高。
*
* 通过建造一个中间缓存池,可降低Fragment之间的耦合,由Fragment与Fragment之间的交互转换为
* Fragment与中间缓存池的交互。
*
* 所有的Fragment之间的数据传递统一面向当前中间缓存池。
* @author LiXaing create at 2017/6/7 16:27*/
public class ClassCachePool {
private static ArrayMap classObject = new ArrayMap();
private ClassCachePool(){};
private static ClassCachePool classCachePool = new ClassCachePool();
public static ClassCachePool getInstance() {
return classCachePool;
}
public void put(String key , Object object){
if (classObject.containsKey(key)) {
classObject.remove(key);
}
classObject.put(key,object);
}
public Object get(String key){
return classObject.get(key);
}
public synchronized Object remove(String key){
return classObject.remove(key);
}
}
replace实现的逻辑是:在add当前需要的fragment前,需要将之前的fragment都remove掉。
而这个问题的产生,就是在源码remove部分:
for (int i=0;i...
...
...
mManager.removeFragment(old, mTransition, mTransitionStyle);
}
假如当前有ABCDE五个Fragment,当i=0移除的是A,当前队列是BCDE,当i=1移除的是C。
可以发现,移除并不是顺序移除而是有跳跃才导致的这个问题
在support-25.0.0版本下发现remove这部分代码是有修改,对上一问题有修复:
通过倒叙的方式,每次remove掉的都是当前的列表的尾角标,从而避免了上面的问题,但对此没有去做测试,可能还会存在其他问题
for(; i >= 0; --i) {
Fragment old = (Fragment)added.get(i);
if(old.mContainerId == containerId) {
if(old == f) {
alreadyAdded = true;
} else {
...
...
....
added.remove(old);
++opNum;
}
}
}
如图:
在fragment创建childFragment的时候,需要注意的是:使用 getFragmentManager()会导致内存泄漏,导致泄漏的原因是在,Fragment在销毁时,会检查ChildFragmentManager,而被嵌套的Fragmetn隶属于当前fragment的childFragment,所以,被嵌套的fragment如果使用FragmentManager(),其实例对象是不会被销毁的,而外层的会被销毁;
所以:在嵌套的Fragment中,内部的fragment创建,需要使用getChildFragmentManager()