Android基础只是 Fragment相关

旧时模样 提交于 2019-12-02 09:17:16

https://www.jianshu.com/p/d9143a92ad94
https://www.jianshu.com/p/fd71d65f0ec6

一、Fragment生命周期 https://www.jianshu.com/p/927ca066120b
1、定义
Fragment 表示 FragmentActivity 中的行为或界面的一部分。
2、Fragment优点:
与Activity相比,Fragment占用内存降低,响应速度快
3、Fragment生命周期

	1)onAttach:
		onAttach()在fragment与Activity关联之后调用。
		初始化fragment参数可以从getArguments()获得,
		当Fragment附加到Activity之后(onAttach 之后),就无法再调用setArguments()。

	2)onCreate:
		fragment初次创建时调用。
		此时只是创建Fragment,此时的Activity还没有创建完成,此时getActivity()可能为null。
		
	3)onCreateView:
		在这个fragment构造布局时调用。

	4)onActivityCreated:
		在Activity的OnCreate()结束后,会调用此方法。
		此时,Activity已经创建完成,在这里才可以使用Activity的所有资源。

	5)onStart:
		当到OnStart()时,Fragment对用户就是可见的了。但用户还未开始与Fragment交互。
		在生命周期中也可以看到Fragment的OnStart()过程与Activity的OnStart()过程是绑定的。意义即是一样的。
		以前你写在Activity的OnStart()中来处理的代码,用Fragment来实现时,依然可以放在OnStart()中来处理。

	6)onResume:
		当这个fragment对用户可见并且正在运行时调用。这是Fragment与用户交互之前的最后一个回调。
		Fragment的OnResume与Activity的OnResume是相互绑定的,意义是一样的。
		它依赖于包含它的activity的Activity.onResume。

	7)onPause:
		此回调与Activity的OnPause()相绑定,与Activity的OnPause()意义一样。

	8)onStop:
		这个回调与Activity的OnStop()相绑定,意义一样。
		已停止的Fragment可以直接返回到OnStart()回调,然后调用OnResume()。

	9)onDestroyView:
		如果Fragment即将被结束或保存,将回调onDestoryView(),会将在onCreateView创建的视图与这个fragment分离。
		下次这个fragment若要显示,那么将会创建新视图。
		这会在onStop之后和onDestroy之前调用。
		这个方法的调用同onCreateView是否返回非null视图无关。
		它会潜在的在这个视图状态被保存之后以及它被它的父视图回收之前调用。

	10)onDestroy:
		当这个fragment不再使用时调用。
		fragment经过了onDestroy()阶段,但仍然能从Activity中找到,因为它还没有Detach。

	11)onDetach:
		Fragment生命周期中最后一个回调是onDetach()。
		调用它以后,Fragment就不再与Activity相绑定,它也不再拥有视图层次结构,它的所有资源都将被释放。

二、Fragment的懒加载 https://www.jianshu.com/p/5c5c778d862e
1、原理
利用fragment的 setUserVisibleHint(boolean isVisibleToUser)、onHiddenChanged(boolean hidden)

	public abstract class LazyFragment extends Fragment {
		
		public boolean isVisible;//是否可见状态

		/**
		 * 标志位,View已经初始化完成,且Activity已经创建。
		 * onActivityCreated
		 */
		public boolean isPrepared;

		public boolean isFirstLoad = true;//是否第一次加载

		/**
		 * 要实现延迟加载Fragment内容,需要在 onActivityCreated
		 * isPrepared = true;
		 */
		public boolean isReadyLazyLoad() {
			if (!isPrepared || !isVisible || !isFirstLoad) {
				return false;
			}
			isFirstLoad = false;
			return true;
		}

		@Override
		public void onActivityCreated(@Nullable Bundle savedInstanceState) {
			super.onActivityCreated(savedInstanceState);
			if (!isLazy()) {
				initData();
			} else {
				isPrepared = true;
				onVisible();
			}
		}

		/**
		 * 如果是与ViewPager一起使用,调用的是setUserVisibleHint
		 * @param isVisibleToUser 是否显示出来了
		 */
		@Override
		public void setUserVisibleHint(boolean isVisibleToUser) {
			super.setUserVisibleHint(isVisibleToUser);
			if (getUserVisibleHint()) {
				isVisible = true;
				onVisible();
			} else {
				isVisible = false;
				onInvisible();
			}
		}

		/**
		 * 如果是通过 FragmentTransaction 的 show 和 hide 的方法来控制显示,调用的是 onHiddenChanged.
		 * 若是初始就show的Fragment 为了触发该事件 需要先hide再show
		 * @param hidden hidden True if the fragment is now hidden, false if it is not visible.
		 */
		@Override
		public void onHiddenChanged(boolean hidden) {
			super.onHiddenChanged(hidden);
			if (!hidden) {
				isVisible = true;
				onVisible();
			} else {
				isVisible = false;
				onInvisible();
			}
		}

		/**
		 * 是否为懒加载模式
		 * 注意:仅仅适用于和viewpager搭配使用
		 * 否则无法触发回调方法
		 *
		 * @return true 懒加载模式
		 */
		protected boolean isLazy() {
			return false;
		}

		// 可见时回调
		protected void onVisible() {
			if (isReadyLazyLoad()) {
				initData();
			}
		}

		// 不可见时回调
		protected void onInvisible() {
		}

		protected abstract void initData();
	}

三、Fragment之间的通信 https://developer.android.google.cn/training/basics/fragments/communicating

1、Fragment与Activity通信,通过接口回调。
	1)定义一个接口  
	2)Activity实现接口  
	3)Fragment的 onAttach 方法,传入的 Activity 参数转为接口对象引用,
	  通过该引用把数据传给Activity
	4)Activity传入Fragment,在Fragment创建时可以通过Bundle传入数据
	  在Fragment已经attach到Activity后,只能通过Fragment的引用来传入数据
	  
2、Fragment之间通信
	一个Fragment可以得到和它相关联的Activity,然后通过Activity 去获取另一个Fragment的实例。

四、add(), show(), hide(), replace()

1、区别
	show(),hide()最终是让Fragment的View setVisibility(true还是false),不会调用生命周期;

	replace()会销毁视图,即调用onDestoryView、onCreateView等一系列生命周期;

	add()和 replace()不要在同一个阶级的 FragmentManager里混搭使用。
	
2、使用场景
	如果有很高的概率会再次使用当前的Fragment,建议使用show(),hide(),可以提高性能。

	如果app有大量图片,这时更好的方式可能是replace,配合图片框架在Fragment视图销毁时,回收其图片所占的内存
	
3、onHiddenChanged的回调时机
	当使用add()+show(),hide()跳转新的Fragment时,旧的Fragment回调onHiddenChanged(),
	不会回调onStop()等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged()。
4、Fragment重叠问题
	使用 support-v4 24.0.0+
	
5、同时pop多个Fragment的问题
	使用 support-v4 25.4.0+

五、FragmentManager 管理Fragment栈

1、 每个Fragment以及宿主Activity(继承自FragmentActivity)都会在创建时,初始化一个FragmentManager对象,
	处理好Fragment嵌套问题的关键,就是理清这些不同阶级的栈视图。

	FragmentActivity的FragmentManager栈,管理嵌套在该Activity中的Fragment
	
	Fragment 的 ChildFragmentManager栈,管理嵌套在该Fragment中的子Fragment

2、 
	1)宿主 Activity 中,getSupportFragmentManager()获取的是FragmentActivity的 FragmentManager对象
    
	2)Fragment中,getFragmentManager()获取的是父Fragment(如果没有,则是FragmentActivity)的FragmentManager对象
	  而 getChildFragmentManager()是获取自己的FragmentManager对象

六、FragmentPagerAdapter 与 FragmentStatePagerAdapter 的区别

1、 区别
	1)FragmentPagerAdapter 中每一个Fragment都长存在与内存中,适用于比较固定的少量的Fragment。
	  FragmentPagerAdapter 在我们切换Fragment过程中不会销毁Fragment,只是调用事务中的detach方法。
	  而在detach方法中只会销毁Fragment中的View,而不会销毁Fragment对象。
	
	2)FragmentStatePagerAdapter中实现将只保留当前页面。
	  页面切换时,调用add/remove,当页面不可见,就会被移除,释放其资源。而在页面需要显示时,生成新的页面。
	  
	  移除Fragment前会保存Fragment状态,重新展示时,会根据状态重新创建页面
	  
	  在较多的Fragment的时候为了减少内存可适用。
	  FragmentStatePagerAdapter在我们切换Fragment,会把前面的Fragment直接销毁掉。

2、 使用FragmentPagerAdapter+ViewPager的注意事项

	1)切换回上一个Fragment页面时(已经初始化完毕),不会回调任何生命周期方法以及onHiddenChanged(),
	  只有setUserVisibleHint(boolean isVisibleToUser)会被回调,所以如果你想进行一些懒加载,需要在这里处理
	
	2)在给ViewPager绑定FragmentPagerAdapter时,
		new FragmentPagerAdapter(fragmentManager)的FragmentManager,一定要保证正确,
		如果ViewPager是Activity内的控件,则传递getSupportFragmentManager(),
		如果是Fragment的控件中,则应该传递getChildFragmentManager()。
		只要记住【ViewPager内的Fragments是当前组件的子Fragment这个原则】

	3)不需要考虑在“内存重启”的情况下,去恢复的Fragments的问题,因为 FragmentPagerAdapter 已经帮我们处理了

七、为什么不建议直接通过使用new Fragment的方式传入数据

因为系统的低内存回收机制,内存不够时,有可能会回收后台页面。

使用setArguments(Bundle args)对Fragment传递数据,在 “内存重启”前,系统会保存数据,不会造成数据的丢失。
和Activity的Intent恢复机制类似。

八、Fragment坑点与解决方案

1、 getActivity()空指针
	1)原因
	  i. 后台Activity在低内存时被系统回收,回到前台时系统恢复Activity,但此时Fragment已经onDetach()了
	  ii.在pop了Fragment之后,该Fragment的异步任务仍然在执行,并且在执行完成后调用了getActivity()方法
	
	2)解决
	  i. 在Fragment的onDestroyView()中,结束不必要的异步任务、延时任务。
	  ii.如果你需要在Fragment中用到宿主Activity对象,建议在你的基类Fragment定义一个Activity的全局变量,
		 在onAttach中初始化,可以有效避免一些意外Crash。

2、 Can not perform this action after onSaveInstanceState
	1)原因
		后台Activity在异常(低内存)终止时,系统调用 onSaveInstanceState() 保存Activity状态和数据
		在该Activity onResume()之前,执行Fragment事务,抛出异常。
		(一般是其他Activity的回调让当前页面执行事务的情况,会引发该问题)
		
	2)解决
		i. 使用 commitAllowingStateLoss()方法提交,但是有可能导致该次提交无效!(宿主Activity被强杀时)
		
		ii.对于popBackStack()没有对应的popBackStackAllowingStateLoss()方法
		   利用onActivityForResult()/onNewIntent(),可以做到事务的完整性,不会丢失事务
		   
		   // ReceiverActivity 或 其子Fragment:
			void start(){
			   startActivityForResult(new Intent(this, SenderActivity.class), 100);
			}

			@Override
			protected void onActivityResult(int requestCode, int resultCode, Intent data) {
				 super.onActivityResult(requestCode, resultCode, data);
				 if (requestCode == 100 && resultCode == 100) {
					 // 执行Fragment事务
				 }
			 }

			// SenderActivity 或 其子Fragment:
			void do() { // 操作ReceiverActivity(或其子Fragment)执行事务
				setResult(100);
				finish();
			}
3、Fragment重叠异常-----正确使用hide、show的姿势【v4-24.0.0+ 开始,官方修复了上述 没有保存mHidden的问题】
	
	1)触发条件
		i. 发生了页面重启(旋转屏幕、内存不足等情况被强杀重启)。
			在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),
			导致重复加载了同一个Fragment导致重叠。
			
			(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)
		
		ii.重复replace|add Fragment 或者 使用show , hide控制Fragment;
		
			如果你add()了几个Fragment,使用show()、hide()方法控制,比如微信、QQ的底部tab等情景,
			如果你什么都不做的话,在“内存重启”后回到前台,app的这几个Fragment界面会重叠。				

		iii.重叠原因
			原因是FragmentManager帮我们管理Fragment,当发生“内存重启”,他会从栈底向栈顶的顺序一次性恢复Fragment;
			但是因为官方没有保存Fragment的mHidden属性,默认为false,即show状态,
			所以所有Fragment都是以show的形式恢复,我们看到了界面重叠。

			(如果是replace,恢复形式和Activity一致,只有当你pop之后上一个Fragment才开始重新恢复,
			所有使用replace不会造成重叠现象)

	2)解决
		i. findFragmentByTag()
			在add()或者replace()时绑定一个tag,一般我们是用fragment的类名作为tag,
			然后在发生“内存重启”时,通过findFragmentByTag找到对应的Fragment,并hide()需要隐藏的fragment。
			
			@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();
				}
			}
			
		ii.手动维护一个mSupportHidden
			public class BaseFragment extends Fragment {
				private static final String STATE_SAVE_IS_HIDDEN = "STATE_SAVE_IS_HIDDEN";

				@Override
				public void onCreate(@Nullable Bundle savedInstanceState) {
					if (savedInstanceState != null) {
						boolean isSupportHidden = savedInstanceState.getBoolean(STATE_SAVE_IS_HIDDEN);
						FragmentTransaction ft = getFragmentManager().beginTransaction();
						if (isSupportHidden) {
							ft.hide(this);
						} else {
							ft.show(this);
						}
						ft.commit();
					}
				}

				@Override
				public void onSaveInstanceState(Bundle outState) {
					outState.putBoolean(STATE_SAVE_IS_HIDDEN, isHidden());
				}
			}
			
		iii. support升级到 v4-24.0.0+

4、Fragment的嵌套问题
	
	
5、不靠谱的出栈方法remove()
	addToBackStack(name)将Fragment加入回退栈,使用remove()不能正常将Fragment从栈内移除
	依然可以返回到被remove()的Fragment,而且是空白页面。
	
	没有将Fragment加入回退栈,remove方法可以正常出栈。
	
	加入了回退栈,popBackStack()系列方法才能真正出栈
	
6、多个Fragment同时出栈的深坑BUG 【support-25.4.0 版本修复】
	
	在Fragment库中如下4个方法是可能产生BUG的:
		popBackStack(String tag,int flags)
		popBackStack(int id,int flags)
		popBackStackImmediate(String tag,int flags)
		popBackStackImmediate(int id,int flags)

	上面4个方法作用是,出栈到tag/id的fragment,即一次多个Fragment被出栈。

	1)FragmentManager栈中管理fragment下标位置的数组ArrayList<Integer> mAvailIndeices的BUG
		FragmentManagerImpl.makeActive(Fragment f)
		
		用ArrayList模拟的栈,多个Fragment同时出栈时,数组中对应位置变成了null
		新的Fragment再入栈时,栈内还存在null
		
		比如:
			栈中原始数据: [A,B,C,D]
			清除C以及其以上的所有Fragment:popBackStack(C, FragmentManager.POP_BACK_STACK_INCLUSIVE)
			清除后栈结构:[A,B,null,null]
			添加新的Fragment: addToBackStack(C)
			添加后的栈结构:[A,B,null,C]
			
			导致“内存重启”后,异常和Crash
			
	2)popBackStack的坑
		popBackStack是加入到主线队列的末尾,【等其它任务完成后才开始出栈】
		popBackStackImmediate是队列内的任务立即执行,再将出栈任务放到队列尾(可以理解为立即出栈)。
		
		如果你popBackStack多个Fragment后,紧接着beginTransaction() add新的一个Fragment,
		接着发生了“内存重启”后,你再执行 popBackStack(),app就会Crash,
		解决方案是postDelay出栈动画时间再执行其它事务,但是根据我的观察不是很稳定。
		
		如果你想【出栈多个Fragment,你应尽量使用 popBackStackImmediate(tag/id)】,而不是 popBackStack(tag/id),
		如果你想在【出栈后,立刻 beginTransaction()开始一项事务,应该把事务的代码 post/postDelay 到主线程的消息队列】

7、深坑 Fragment转场动画(仅分析v4包下的Fragment)
	getFragmentManager().beginTransaction()
						.setCustomAnimations(enter, exit)
    // 如果有通过tag/id同时出栈多个Fragment的情况时,
    // 请谨慎使用.setCustomAnimations(enter, exit, popEnter, popExit)  
    // 在support-25.4.0之前出栈多Fragment时,伴随出栈动画,会在某些情况下发生异常
    // 你需要搭配Fragment.onCreateAnimation() 临时取消出栈动画。
	
	注意:如果想给下一个Fragment设置进栈动画和出栈动画
	setCustomAnimations(enter, exit)只能设置进栈动画,第二个参数并不是设置出栈动画;
	使用.setCustomAnimations(enter, exit, popEnter, popExit),这个方法的第1个参数对应进栈动画,第4个参数对应出栈动画,
	所以是setCustomAnimations(进栈动画, exit, popEnter, 出栈动画)

	让出栈动画运作正常的话,需要使用Fragment的onCreateAnimation中控制动画
	
	1)pop多个Fragment时转场动画 带来的问题 【在support-25.4.0 版本修复】

		在使用 pop(tag/id)同时出栈多个Fragment的这种情况下,将转场动画临时取消或者延迟一个动画的时间再去执行其他事务;

		原因在于这种情景下,【可能会导致栈内顺序错乱】(上文有提到),
		同时如果发生“内存重启”后,因为 Fragment 转场动画没结束时再执行其他方法,
		会导致Fragment状态不会被FragmentManager正常保存下来。

	2)进入新的Fragment并立刻关闭当前Fragment 时的一些问题
		如果从当前Fragment进入一个新的Fragment,并且同时要关闭当前Fragment。
		由于数据结构是栈,所以正确做法是先pop,再add,但是转场动画会有覆盖的不正常现象,需要特殊处理,不然会闪屏!

	Tip:
		如果遇到Fragment的mNextAnim空指针的异常(通常是在你的Fragment被重启的情况下),
		那么首先需要检查是否操作的Fragment是否为null;
		其次在你的Fragment转场动画还没结束时,你是否就执行了其他事务等方法;
		解决思路就是延迟一个动画时间再执行事务,或者临时将该Fragment设为无动画

推荐阅读:

Fragment全解析系列(一):那些年踩过的坑 https://www.jianshu.com/p/d9143a92ad94
Google-Fragment概览 https://developer.android.google.cn/guide/components/fragments
Google-与其他Fragment通信 https://developer.android.google.cn/training/basics/fragments/communicating

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!