1. <div id="f8mbs"></div>
        当前位置£º首页 > 安卓源码 > 技术博客 >

        Android Fragment 懒加载实践

        时间£º2019-02-14 13:54 来源£º浜掕仈缃 作者£º源码搜藏 浏览£º 收藏 挑错 推荐 打印

        开发中£¬Fragment 最常见的两种使用方式就是 ViewPager 嵌套 Fragment £¬以及直接通过FragmentManager 来管理 Fragment£¬对应的交互场景相信大家心里?#21152;?#19968;个原型£¬没有的话也没关系£¬后边会有例子的¡£但这和懒加载有什么关?#30340;Ø£?#35797;想一下£¬如果每个 Fragmen

        开发中£¬Fragment 最常见的两种使用方式就是 ViewPager 嵌套 Fragment £¬以及直接通过FragmentManager 来管理 Fragment£¬对应的交互场景相信大家心里?#21152;?#19968;个原型£¬没有的话也没关系£¬后边会有例子的¡£但这和懒加载有什么关?#30340;Ø£?#35797;想一下£¬如果每个 Fragment ?#21152;心?#35748;的网络请求操作£¨也可能是其它耗时操作£¬这里以网络请求为例£©£¬那么多个在 Fragment创建过程中都会执?#24515;?#35748;网络请求£¬无论 Fragment 是否对用户可见£¬显然有些浪费流量¡¢影响性 App 性能¡¢用户体验不佳?#28909;?#28857;£¬这些自然不是我们想看到的£¬出于这些原因£¬让 Fragment 进行数据懒加载就有必要了¡£

        Android Fragment 懒加载实践

        先解释下为什么会出现多个 Fragment中的默认网络请求都会被执行£¬由于Fragment在创建的整个过程会走完从onAttach()onResume()的生命周期方法£¬然而一般情况我们无非在这里几个生命周期方法£¨例如 onActivityCreated()£©里发起默认的网络请求£¬所以问题的原因显而易见£¬?#28909;?#19981;能在 Fragment 生命周期方法直接请求数据£¬所以就要另谋它法¡£

        我们要做的事情就是让 Fragment 按需加载数据£¬即对用户可见时再请求数据£¬让数据的请求时机可控£¬而不是在初始化创建过程中直接请求数据£¬同时不受嵌套层级的影响£¡

        接下来我们结合文章开头提到的两种 Fragment 使用方式来实现 Fragment 懒加载的功能¡£

        一¡¢ViewPager 嵌套 Fragment

        Fragment 有一个非生命周期的setUserVisibleHint(boolean isVisibleToUser)回调方法£¬当 ViewPager 嵌套 Fragment 时会起作用£¬如果切换 ViewPager 则该方法也会被调用£¬参数isVisibleToUsertrue代表当前 Fragment 对用户可见£¬否则不可见¡£

        目测可以在这个方法中来判断是否请求数据£¬但在 Fragment 创建期间`setUserVisibleHint[BaseFragment]()£¬代码如下£º

        public abstract class LazyLoadFragment extends BaseFragment {
            private boolean isViewCreated; // 界面是否已创建完成
            private boolean isVisibleToUser; // 是否对用户可见
            private boolean isDataLoaded; // 数据是否已请求
        
            // 实现具体的数据请求逻辑
            protected abstract void loadData();
        
            @Override
            public void setUserVisibleHint(boolean isVisibleToUser) {
                super.setUserVisibleHint(isVisibleToUser);
                this.isVisibleToUser = isVisibleToUser;
                tryLoadData();
            }
        
            @Override
            public void onActivityCreated(@Nullable Bundle savedInstanceState) {
                super.onActivityCreated(savedInstanceState);
                isViewCreated = true;
                tryLoadData();
            }
        
            public void tryLoadData() {
                if (isViewCreated &amp;&amp; isVisibleToUser &amp;&amp; !isDataLoaded) {
                    loadData();
                    isDataLoaded = true;
                }
            }
        }

        ViewPager 中第一个可见的 Fragment 会走onActivityCreated()方法去请求数据£¬之后切换 Fragment 会走setUserVisibleHint()方法去尝试请求数据¡£这样我们的 Fragment 继承 LazyLoadFragment£¬然后实现loadData()方法去完成数据的请求即可£¬写一个简单的 ViewPager 嵌套 Fragment 的界面£¬测试效果如下£º

        Android Fragment 懒加载实践

        一切正常£¬符合我们的预期£¬由于现在只是一层嵌套£¬我们再加两层 ViewPager 嵌套 Fragment 试试£¬如下图£¨具体的实现可参考源码£©£º

        Android Fragment 懒加载实践

        1¡¢这里我们?#32423;?#29992; tab 标签上的编号指代对应的 Fragment£¬例如1-1代表最外层 ViewPager 的第一个 Fragment¡£
        2¡¢ViewPager 都设置setOffscreenPageLimit()为其包含的 Fragment 个数

        再次运行£¬只有1-1对用户可见£¬按照预期应该只有1-1请求了数据£¬但是2-1¡¢3-1也请求了数据£º

        Android Fragment 懒加载实践

        所以问题来了£¬虽然2-1¡¢3-1对用户不可见£¬但在创建过程中它们的setUserVisibleHint()isVisibleToUser参数最终为true£¬从而在onActivityCreated()方法中请求了数据¡£注意此时2-1¡¢3-1的父 Fragment 也是不可见的£¬所以要解决这个问题£¬可以在tryLoadData()方法中判?#31995;?#21069;要请求数据的 Fragment 的 父 Fragment 是否可见£¬不可见则不请求数据¡£

        但新的问题又来了£¬这个导致该 Fragment 失去了初次请求数据的机会£¬即便该 Fragment 初次对用户可见时也不会主动去请求数据£¬需要来回再切换一次才会请求数据£¬要解决这个问题£¬可以让该 Fragment 的父 Fragment 请求数据时通知子 Fragment 去请求数据£¬修改下代码£º

        public abstract class LazyLoadFragment extends BaseFragment {
            private boolean isViewCreated; // 界面是否已创建完成
            private boolean isVisibleToUser; // 是否对用户可见
            private boolean isDataLoaded; // 数据是否已请求, isNeedReload()返回false的时起作用
        
            // 实现具体的数据请求逻辑
            protected abstract void loadData();
        
            @Override
            public void setUserVisibleHint(boolean isVisibleToUser) {
                super.setUserVisibleHint(isVisibleToUser);
                this.isVisibleToUser = isVisibleToUser;
                tryLoadData();
            }
        
            @Override
            public void onActivityCreated(@Nullable Bundle savedInstanceState) {
                super.onActivityCreated(savedInstanceState);
                isViewCreated = true;
                tryLoadData();
            }
        
            /**
             * ViewPager场景下£¬判断父fragment是否可见
             *
             * @return
             */
            private boolean isParentVisible() {
                Fragment fragment = getParentFragment();
                return fragment == null || (fragment instanceof LazyLoadFragment &amp;&amp; ((LazyLoadFragment) fragment).isVisibleToUser);
            }
        
            /**
             * ViewPager场景下£¬当前fragment可见£¬如果其子fragment也可见£¬则尝试让子fragment请求数据
             */
            private void dispatchParentVisibleState() {
                FragmentManager fragmentManager = getChildFragmentManager();
                List<Fragment> fragments = fragmentManager.getFragments();
                if (fragments.isEmpty()) {
                    return;
                }
                for (Fragment child : fragments) {
                    if (child instanceof LazyLoadFragment &amp;&amp; ((LazyLoadFragment) child).isVisibleToUser) {
                        ((LazyLoadFragment) child).tryLoadData();
                    }
                }
            }
        
            public void tryLoadData() {
                if (isViewCreated &amp;&amp; isVisibleToUser &amp;&amp; isParentVisible() &amp;&amp;  !isDataLoaded) {
                    loadData();
                    isDataLoaded = true;
                    // 通知 子 Fragment 请求数据
                    dispatchParentVisibleState();
                }
            }
        }

        再次测试效果如下£º

        Android Fragment 懒加载实践

        效果符?#26174;?#26399;£¬由于1-2¡¢2-1同时可见£¬所以会几乎同时请求数据£¬2-2¡¢3-1也类似¡£

        至此 ViewPager 嵌套 Fragment 形式的懒加载就实现了¡£

        二¡¢FragmentManager 管理 Fragment

        FragmentManager 管理 Fragment 时£¬和 ViewPager 嵌套 Fragment 中的问题类似£¬但此时setUserVisibleHint()方法并不会被调用£¬所以要寻找新的途径了¡£

        当用 FragmentManager 来 add()¡¢hide()¡¢show() Fragment 时 Fragment 的onHiddenChanged(boolean hidden)方法会被调用£¬其中hidden参数为false时代表对应 Fragment 可见£¬否则不可见£¬注意三个操作里当执行 show()操作时hidden参数才为false£¬同时由于该方法在onActivityCreated()之后被调用¡£我们可以直接在onHiddenChanged()方法参数为false时发起数据请求即可¡£

        当存在多层嵌套的情况时£¬即 FragmentManager 管理的 Fragment 内部又使用 FragmentManager 管理新的 Fragment£¬这种情况和多层 ViewPager 嵌套 Fragment 时的处理方法类似£¬即判?#31995;?#21069; Fragment 的父 Fragment 是否可见¡¢以及 Fragment 可见时通知子 Fragment 去请求数据¡£

        主要的问题就这些£¬看下代码实现£º

        public abstract class LazyLoadFragment extends BaseFragment {
            private boolean isDataLoaded; // 数据是否已请求
            private boolean isHidden = true; // 记录当前fragment的是否隐藏
        
            // 实现具体的数据请求逻辑
            protected abstract void loadData();
        
            /**
             * 使用show()¡¢hide()控制fragment显示¡¢隐藏时回调该方法
             *
             * @param hidden
             */
            @Override
            public void onHiddenChanged(boolean hidden) {
                super.onHiddenChanged(hidden);
                isHidden = hidden;
                if (!hidden) {
                    tryLoadData1();
                }
            }
        
            /**
             * show()¡¢hide()场景下£¬当前fragment没隐藏£¬如果其子fragment也没隐藏£¬则尝试让子fragment请求数据
             */
            private void dispatchParentHiddenState() {
                FragmentManager fragmentManager = getChildFragmentManager();
                List<Fragment> fragments = fragmentManager.getFragments();
                if (fragments.isEmpty()) {
                    return;
                }
                for (Fragment child : fragments) {
                    if (child instanceof LazyLoadFragment &amp;&amp; !((LazyLoadFragment) child).isHidden) {
                        ((LazyLoadFragment) child).tryLoadData1();
                    }
                }
            }
        
            /**
             * show()¡¢hide()场景下£¬父fragment是否隐藏
             *
             * @return
             */
            private boolean isParentHidden() {
                Fragment fragment = getParentFragment();
                if (fragment == null) {
                    return false;
                } else if (fragment instanceof LazyLoadFragment &amp;&amp; !((LazyLoadFragment) fragment).isHidden) {
                    return false;
                }
                return true;
            }
        
            /**
             * show()¡¢hide()场景下£¬尝试请求数据
             */
            public void tryLoadData1() {
                if (!isParentHidden() &amp;&amp; !isDataLoaded) {
                    loadData();
                    isDataLoaded = true;
                    dispatchParentHiddenState();
                }
            }
        }

        实?#23454;?#27979;试效果如下£º

        Android Fragment 懒加载实践

        上边我们用isDataLoaded控制 Fragment 只请求一次数据£¬如果需要每次 Fragment 可见都请求数据£¬我们只需对LazyLoadFragment做如下修?#27169;?/p>

        public abstract class LazyLoadFragment extends BaseFragment {
            /**
             * fragment再次可见时£¬是否重新请求数据£¬默认为flase则只请求一次数据
             *
             * @return
             */
            protected boolean isNeedReload() {
                return false;
            }
        
            /**
             * ViewPager场景下£¬尝试请求数据
             */
            public void tryLoadData() {
                if (isViewCreated &amp;&amp; isVisibleToUser &amp;&amp; isParentVisible() &amp;&amp; (isNeedReload() || !isDataLoaded)) {
                    loadData();
                    isDataLoaded = true;
                    dispatchParentVisibleState();
                }
            }
        
            /**
             * show()¡¢hide()场景下£¬尝试请求数据
             */
            public void tryLoadData1() {
                if (!isParentHidden() &amp;&amp; (isNeedReload() || !isDataLoaded)) {
                    loadData();
                    isDataLoaded = true;
                    dispatchParentHiddenState();
                }
            }
        }

        添加了一个isNeedReload()方法£¬如果子类需要每次可见都请求数据£¬重写该方法返回true即可¡£

        1¡¢对于Activity£¬getSupportFragmentManager()得到的是FragmentActivity的FragmentManager对象
        2¡¢对于Fragment£¬getFragmentManager()得到的是父Fragment的FragmentManager对象£¬如果没有父Fragment£¬则是FragmentActivity的FragmentManager对象
        3¡¢getChildFragmentManager()得到是Fragment自身的FragmentManager对象

        至此£¬LazyLoadFragment 基类就完成了£¬只要继承它就可以轻松实现懒加载功能

        Android Fragment 懒加载实践转载<\/script>');
        ÁÉÄþʮһѡÎ嵥˫