美文网首页
性能优化之fragment的懒加载

性能优化之fragment的懒加载

作者: GoLearning轻松学 | 来源:发表于2022-01-13 09:53 被阅读0次

    你如果拿到公司的一个大型app,首先肯定是研究一下它的技术,这个App到底用到什么框架,用什么方式来实现的,如果开发一款高性能app,考虑使用什么框架来构建这个app,现在大多数app都是在用MVVM,这里的MVVM是M-V-VM,UI上的框架大多使用viewPage+Fragment,头条、QQ等都是这么干,都是使用双层的ViewPage+Fragment,这个已经成为了app的标配,左右滑动,滑动切换界面,切换内容,符合我们用户的操作需求。
    认真观察头条的app,在它栏目里左右滑动的时候会出现空白,这是使用了懒加载,这是对性能的考虑。

    了解懒加载之前,先了解一下什么是预加载?

    预加载就是预先加载,先加载,除了当前页面,前后两个页面也要加载出来。让界面可以加快显示出来。
    数据结构中有两个知识点,空间复杂度、时间复杂度。
    那么这里让界面能加快展示到用户面前,是有代价的。有句话是“用空间换时间”,“用时间换空间”,虽然我只显示了一个页面,当前页面,但是我已经在背后默默地在当前页面左右都缓存了两个页面的数据,者就是预加载,将它加载到内存里面,当我们从当前页面滑到另外一个页面时候,直接那个页面的数据就显示出来了,它不需要再去进行渲染这方面的操作,这动作已经完成了,所以这就能提升我们用户左右滑动这个效果的感觉,但是这个代价在,预加载都要放到内存中,内存的使用增加,数据加载增加,本来只加载1页页面的数据,现在要加载5页的数据,内存本来是缓存一个页面的,现在要加载5页的数据,内存5M->25M ,数据加载0.1s->0.5s

    懒加载

    预加载在数据量少的时候,确实能满足app的需求,但是随着用户的需求,页面的展示越来越高要求,页面需要展示高清图,视频等占内存大,请求数据大的。预加载占的内存太大,而且一次性请求的数据量也会随之而几倍增长。
    懒加载,用一个空白页面,有页面没数据,节省内存,预加载一个空白页面只占5k内存,减少内存的使用,当滑动为当前页面时,加载数据。


    e7277b65bd4d0294bc353947e77ce80.jpg
    a82bb8d2773f035ccbfcb710d807031.jpg

    都知道方法tansaction.attach(fragment)到这里时,Fragment的生命周期是不会马上执行的,等事务执行commit的时候才会走生命周期,这和数据库的管理很类似,都是用触发器去管理的,触发器管理有一个特征,它需要commit,你只有提交了,你的事件才会执行。
    所以这里setUserVisibleHint会先于生命周期执行。
    懒加载跟UI可见有关系
    加载数据的操作要放到与UI相关的函数里面去,跟UI相关的函数是:
    onCreateView -- >创建UI fragment页面的创建
    onResume -- >界面可见的
    onPause --> 是界面可见,但是不可交互
    onDestroyView --> 界面的销毁

    
    public abstract class LazyFragment extends Fragment
    {
        
        private static final String TAG = "LazyFragment";
        
        /**
         * Fragment生命周期 onAttach -> onCreate -> onCreatedView -> onActivityCreated
         * -> onStart -> onResume -> onPause -> onStop -> onDestroyView -> onDestroy
         * -> onDetach 对于 ViewPager + Fragment 的实现我们需要关注的几个生命周期有: onCreatedView +
         * onActivityCreated + onResume + onPause + onDestroyView
         */
        
        protected View rootView = null;
        
        /**
         * 布局是否创建完成
         */
        protected boolean isViewCreated = false;
        
        /**
         * 当前可见状态
         */
        protected boolean currentVisibleState = false;
        
        /**
         * 是否第一次可见
         */
        protected boolean mIsFirstVisible = true;
        
        @Override
        public void onAttach(Context context)
        {
            super.onAttach(context);
        }
        
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
        }
        
        /**
         * 修改Fragment的可见性 setUserVisibleHint 被调用有两种情况:
         * 1)在切换tab的时候,会先于所有fragment的其他生命周期,先调用这个函数,可以看log 2)
         * 对于之前已经调用过setUserVisibleHint方法的fragment后,让fragment从可见到不可见之间状态的变化
         */
        @Override
        public void setUserVisibleHint(boolean isVisibleToUser)
        {
            super.setUserVisibleHint(isVisibleToUser);
            Logger.d("setUserVisibleHint: " + isVisibleToUser);
            // 对于情况1)不予处理,用 isViewCreated 进行判断,如果isViewCreated false,说明它没有被创建
            if (isViewCreated)
            {
                // 对于情况2,需要分情况考虑,如果是不可见 -> 可见 2.1
                // 如果是可见 -> 不可见 2.2
                // 对于2.1)我们需要如何判断呢?首先必须是可见的(isVisibleToUser
                // 为true)而且只有当可见状态进行改变的时候才需要切换,否则会出现反复调用的情况
                // 从而导致事件分发带来的多次更新
                if (isVisibleToUser && !currentVisibleState)
                {
                    // 从不可见 -> 可见
                    dispatchUserVisibleHint(true);
                }
                else if (!isVisibleToUser && currentVisibleState)
                {
                    dispatchUserVisibleHint(false);
                }
            }
        }
        
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
        {
            super.onCreateView(inflater, container, savedInstanceState);
            if (null == rootView)
            {
                rootView = inflater.inflate(getLayoutRes(), container, false);
            }
            initView(rootView);
            Logger.d("onCreateView: ");
            // 初始化的时候,判断当前Fragment可见状态
            // isHidden在使用FragmentTransaction的show/hidden时会调用,可见返回的是false
            if (!isHidden() && getUserVisibleHint())
            {
                // 可见状态,进行事件分发
                dispatchUserVisibleHint(true);
            }
            return rootView;
        }
        
        /**
         * 用FragmentTransaction来控制fragment的hide和show时,
         * 那么这个方法就会被调用。每当你对某个Fragment使用hide 或者是show的时候,那么这个Fragment就会自动调用这个方法。
         */
        
        @Override
        public void onHiddenChanged(boolean hidden)
        {
            Logger.d("onHiddenChanged: " + hidden);
            super.onHiddenChanged(hidden);
            // 这里的可见返回为false
            if (hidden)
            {
                dispatchUserVisibleHint(false);
            }
            else
            {
                dispatchUserVisibleHint(true);
            }
        }
        
        /**
         * 统一处理用户可见事件分发
         */
        private void dispatchUserVisibleHint(boolean isVisible)
        {
            Logger.d("dispatchUserVisibleHint: " + isVisible);
            
            // 首先考虑一下fragment嵌套fragment的情况(只考虑2层嵌套)
            if (isVisible && isParentInvisible())
            {
                // 父Fragmnet此时不可见,直接return不做处理
                return;
            }
            // 为了代码严谨,如果当前状态与需要设置的状态本来就一致了,就不处理了
            if (currentVisibleState == isVisible)
            {
                return;
            }
            currentVisibleState = isVisible;
            if (isVisible)
            {
                if (mIsFirstVisible)
                {
                    mIsFirstVisible = false;
                    // 第一次可见,进行全局初始化
                    onFragmentFirstVisible();
                }
                onFragmentResume();
                // 分发事件给内嵌的Fragment
                dispatchChildVisibleState(true);
            }
            else
            {
                onFragmentPause();
                dispatchChildVisibleState(false);
            }
            
        }
        
        /**
         * 在双重ViewPager嵌套的情况下,第一次滑到Frgment 嵌套ViewPager(fragment)的场景的时候
         * 此时只会加载外层Fragment的数据,而不会加载内嵌viewPager中的fragment的数据,因此,我们
         * 需要在此增加一个当外层Fragment可见的时候,分发可见事件给自己内嵌的所有Fragment显示
         */
        private void dispatchChildVisibleState(boolean visible)
        {
            FragmentManager fragmentManager = getChildFragmentManager();
            List<Fragment> fragments = fragmentManager.getFragments();
            if (null != fragments)
            {
                for (Fragment fragment : fragments)
                {
                    if (fragment instanceof LazyFragment && !fragment.isHidden()
                        && fragment.getUserVisibleHint())
                    {
                        ((LazyFragment)fragment).dispatchUserVisibleHint(visible);
                    }
                }
            }
        }
        
        /**
         * Fragment真正的Pause,暂停一切网络耗时操作
         */
        protected void onFragmentPause()
        {
            Logger.d("onFragmentResume " + " 真正的resume,开始相关操作耗时");
            
        }
        
        /**
         * Fragment真正的Resume,开始处理网络加载等耗时操作
         */
        protected void onFragmentResume()
        {
            Logger.d("onFragmentPause" + " 真正的Pause,结束相关操作耗时");
        }
        
        private boolean isParentInvisible()
        {
            Fragment parentFragment = getParentFragment();
            if (parentFragment instanceof LazyFragment)
            {
                LazyFragment fragment = (LazyFragment)parentFragment;
                return !fragment.isSupportVisible();
            }
            return false;
        }
        
        private boolean isSupportVisible()
        {
            return currentVisibleState;
        }
        
        /**
         * 在滑动或者跳转的过程中,第一次创建fragment的时候均会调用onResume方法
         */
        @Override
        public void onResume()
        {
            super.onResume();
            // 如果不是第一次可见
            if (!mIsFirstVisible)
            {
                // 如果此时进行Activity跳转,会将所有的缓存的fragment进行onResume生命周期的重复
                // 只需要对可见的fragment进行加载,
                if (!isHidden() && !currentVisibleState && getUserVisibleHint())
                {
                    dispatchUserVisibleHint(true);
                }
            }
            
        }
        
        /**
         * 只有当当前页面由可见状态转变到不可见状态时才需要调用 dispatchUserVisibleHint currentVisibleState &&
         * getUserVisibleHint() 能够限定是当前可见的 Fragment 当前 Fragment 包含子 Fragment 的时候
         * dispatchUserVisibleHint 内部本身就会通知子 Fragment 不可见 子 fragment 走到这里的时候自身又会调用一遍
         */
        @Override
        public void onPause()
        {
            super.onPause();
            if (currentVisibleState && getUserVisibleHint())
            {
                dispatchUserVisibleHint(false);
            }
        }
        
        @Override
        public void onDestroyView()
        {
            super.onDestroyView();
            Logger.d("onDestroyView");
            isViewCreated = false;
            mIsFirstVisible = false;
        }
        
        @Override
        public void onDestroy()
        {
            super.onDestroy();
        }
        
        @Override
        public void onDetach()
        {
            super.onDetach();
        }
        
        /**
         * 第一次可见,根据业务进行初始化操作
         */
        protected abstract void onFragmentFirstVisible();
        
        /**
         * 初始化页面,如果不需要进行懒加载则可直接在此处进行网络处理
         * 
         * @param rootView
         */
        protected abstract void initView(View rootView);
        
        /**
         * 获取布局资源
         */
        protected abstract int getLayoutRes();
        
    }
    

    initView()和getLayoutRes()都声明为抽象函数,让子类去实现,这里是布局和findViewbyId等操作,交给具体的fragment去实现

    为什么rootView要去判空创建呢?rootView不是一定会是空吗?

    它的生命周期肯定不会只调用一次,当它重复去走onCreateView的时候,这个生命周期我们是控制不了的,是Activity去回调给我们的。

    为什么onFragmentResume()和onFragmentPause()不是抽象方法?

    这个LazyFragment是一个抽象类,如果你把它做成一个模板类BaseFragment,你就可以把它当父类,你把它当父类又不需要实现懒加载,那你就不需要实现这两个函数,是没关系的没这是正常的fragment,为了扩展,这是一个需求,面向对象的思路,选择和不选择懒加载都可以共用 这个父类。

    加入标志位 isViewCreate

    这个值为true时,才会分发事件
    在首次加载的时候也要补发一句

           // 初始化的时候,判断当前Fragment可见状态
            // isHidden在使用FragmentTransaction的show/hidden时会调用,可见返回的是false
            if (!isHidden() && getUserVisibleHint())
            {
                // 可见状态,进行事件分发
                dispatchUserVisibleHint(true);
            }
    
    
    添加变量currentVisibleState

    为了代码严谨,如果当前状态与需要设置的状态本来就一致了,就不处理了

    fragment要显示数据,哪里来的?

    fragment要显示数据,哪里来的?一般情况下在onResume里面,在onResume里面做数据加载操作,网络操作,线程,启动线程,去viewmodel里面取数据,但是懒加载不允许你数据加载在onResume里面,因为onRsume有种情况不需要加载,那就是预加载的情况,如果预加载也在onRsume里面,那么懒加载就没意义了,所以懒加载是用于控制数据加载的时机,所以不能在onResume里面加载,应该在UI可见的时候去setUserVisibleHint传入true,说明当前可见,就加载数据调用dispathUserVisibleHint(true);否者停止加载,调用dispatchUserVisibleHint(false),根据传进去的visibleState去判断选择使用onFragmentLoad还是OnFragmentLoadStop。

    在onResume()和onPause()里面加处理

    当这个Activity跳到另一个Activity的时候,不会去修改可见性,只会走生命周期的onPause(),而回来的时候也只会走onResume(),所以我们要主动去调用可见方法。dispatchUserVisibleHint(XXX);

    什么是性能优化?

    性能优化的本质是,做法是节省内存,减少流量,目的是在软件放牧减少不必要的CPU的浪费,减少对内存的不合理使用,这是根本。

    懒加载是怎么优化的?

    懒加载是针对预加载进行优化的,对ArrayList的优化,它其实根本是对ArrayList,以前保存的是一个fragment,这个fragment中有大量的view,现在我们通过arrayList里面保存的也是一个fragment,只不过这个fragment是一个空白的fragment,这就减少了内存,那空白的fragment的数据哪里来的呢?所以就出现了可见的时候加载,在可见的时候将空白的数据,把它替换掉,形成一个可见的漂亮的界面,然后放到ArrayList里面去,这是一个延迟加载,懒加载,优化的根本是对ArrayList原理里面的优化,ArrayList保存的是一个大量数据的,肯定会很多内存,如果保存的是一个空白页面,那么我们保存的数据就会越少。

    预加载可以避免吗?

    当然不可以,这里虽然加载的是一个空白页面,但是预加载这个模式依旧是存在的。

    相关文章

      网友评论

          本文标题:性能优化之fragment的懒加载

          本文链接:https://www.haomeiwen.com/subject/nepwcrtx.html