编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1App启动流程梳理
  • 1.2ActivityThread分析
  • 1.3Context设计思想
  • 2.1Activity基础介绍
  • 2.2Activity启动流程
  • 2.3Activity布局创建
  • 2.4Activity布局绘制
  • 2.5Service基础介绍
  • 2.6Service启动流程
  • 2.7Receiver广播基础
  • 2.8Receiver深入原理
  • 2.9ContentProvider分析
  • 2.10Fragment实践
  • 2.11Intent深入思考
  • 3.1Paint和Canvas
  • 3.2View的绘制基础
  • 3.3onMeasure流程设计
  • 3.4onLayout流程设计
  • 3.5onDraw流程设计
  • 3.6View工作原理
  • 3.7View刷新设计流程
  • 3.8自定义View控件
  • 3.9自定义ViewGroup控件
  • 4.1Handler基础使用
  • 4.2消息机制流程分析
  • 4.3Handler深度解析
  • 4.4Message深度理解
  • 4.5MessageQueue解析
  • 4.6Looper深度解析
  • 4.7理解Handler同步屏障
  • 4.8ThreadLocal分析
  • 4.9ThreadLocal场景
  • 5.1View事件设计思考
  • 5.2View滑动冲突处理
  • 5.3View事件源码分析
  • 5.4View事件总结案例
  • 6.1DecorView设计思想
  • 6.2视图的载体View
  • 6.3视图管理者Window
  • 6.4窗口管理服务WMS
  • 6.5布局解析者Inflater
  • 7.1AsyncTask深入介绍
  • 7.2HandlerThread设计
  • 7.3IntentService设计
  • 8.1IPC通信方式介绍
  • 8.2AIDL进程间通信
  • 8.7Binder通信机制设计
  • /zh/android/basic/9.1注解设计思想和原理.html
  • 9.2APT技术设计详解
  • 9.3APT多种案例实践

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.关于博客汇总链接

  • 1.技术博客汇总
  • 2.开源项目汇总
  • 3.生活博客汇总
  • 4.喜马拉雅音频汇总
  • 5.其他汇总

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
贡献者: yangchong211
上一篇
2.9ContentProvider分析
下一篇
2.11Intent深入思考