性能优化之fragment的懒加载

你如果拿到公司的一个大型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保存的是一个大量数据的,肯定会很多内存,如果保存的是一个空白页面,那么我们保存的数据就会越少。

预加载可以避免吗?

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353

推荐阅读更多精彩内容