面试总结(5):Fragment的懒加载

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011315960/article/details/71404177

前言

在我们的项目里经常会用到ViewPager+Fragment实现选项卡滑动切换的效果,ViewPager会预加载下一个Framgment的内容,这样的机制有优点也有缺点:

预加载让用户可以更快的看到接下来的内容,浏览起来连贯性更好,但是app在展示内容的同时还增加了额外的任务,这样可能影响界面的流畅度,并且可能造成流量的浪费。

目前大部分的app都使用Fragment懒加载机制,例如哔哩哔哩,360手机助手等等。

正文

实现Fragment懒加载也可以有很多种办法,可能有些朋友会第一时间想到通过监听滚动的位置,通过判断Fragment的加载状态实现懒加载,这种方法的缺点就是把实现暴露在Framgment之外,从封装的角度来说这不是一个好的方案。

最好的方案在网上已经随处可见了,那就是重写Fragment的setUserVisibleHint()方法,实现Fragment内部的懒加载机制。

首先我们看看这个方法的注释:

/**
* Set a hint to the system about whether this fragment’s UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
*

An app may set this to false to indicate that the fragment’s UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.


*
*

Note: This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.


*
* @param isVisibleToUser true if this fragment’s UI is currently visible to the user (default),
* false if it is not.
*/

英文有点多,简单的翻译总结一下就是,Framgent没有直接显示给用户(例如滚动出了屏幕)会返回false,值得注意的是这个方法可能在Fragment的生命周期以外调用。

官方已经提示这个方法可能在生命周期之外调用,先看看这个方法到底是在什么时候会被调用:

这里写图片描述

我在Fragment中打印了Fragment生命周期的几个比较重要的方法,从log上看setUserVisibleHint()的调用早于onCreateView,所以如果在setUserVisibleHint()要实现懒加载的话,就必须要确保View以及其他变量都已经初始化结束,避免空指针。

然后滑动ViewPager,看一下运行情况:

这里写图片描述

滑动的时候,两个Fragment的setUserVisibleHint()方法被调用,显示状态也发生了变化。

ok,那就可以直接写代码了,先定义我们的MyFragment:

/**
 * Created by li.zhipeng on 2017/5/7.
 * <p>
 * Fragment
 */

public class MyFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener{

    private SwipeRefreshLayout swipeRefreshLayout;
    private ListView listView;
    private Handler handler = new Handler();

    /**
     * 是否已经初始化完成
     * */
    private boolean isPrepare;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        Log.e("lzp", "onCreateView" + hashCode());
        return inflater.inflate(R.layout.fragment_my, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        Log.e("lzp", "onViewCreate" + hashCode());
        swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipeRefreshLayout);
        listView = (ListView) view.findViewById(R.id.listView);
        swipeRefreshLayout.setOnRefreshListener(this);
        // 初始化完成
        isPrepare = true;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.e("lzp", "onActivityCreated" + hashCode());
        // 创建时要判断是否已经显示给用户,加载数据
        onVisibleToUser();
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        Log.e("lzp", "setUserVisibleHint" + hashCode() + ":" + isVisibleToUser);
        // 显示状态发生变化
        onVisibleToUser();
    }

    /**
     * 显示给用户的操作
     * */
    private void onVisibleToUser(){
        if (isPrepare && getUserVisibleHint()){
            onRefresh();
        }
    }

    /**
     * 加载数据
     * */
    private void loadData() {
        listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
                "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
                "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
        }));
    }

    @Override
    public void onRefresh() {
       // 只加载一次数据,避免界面切换的时候,加载数据多次
        if (listView.getAdapter() == null){
            swipeRefreshLayout.setRefreshing(true);
            new Thread(){
                @Override
                public void run() {
                    // 延迟1秒
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            loadData();
                            swipeRefreshLayout.setRefreshing(false);
                        }
                    });
                }
            }.start();
        }
    }
}

效果图我就不贴出来了,最后会有源码链接,大家可以下载运行看看效果。

这样就结束了吗?那肯定不行,虽然了解了用法,但是不封装一下,以后就要写很多次,简直不要太low。

封装之后的BaseFragment的代码:

/**
 * Created by li.zhipeng on 2017/5/8.
 * <p>
 * Fragment 的基类
 */

public abstract class BaseFragment extends Fragment {

    /**
     * 布局id
     */
    private View contentView;

    /**
     * 是否启用懒加载,此属性仅对BaseLazyLoadFragment有效
     * */
    private boolean isLazyLoad;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        contentView = inflater.inflate(setLayoutId(), container, false);
        return contentView;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // 初始化
        init();
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 如果不是懒加载模式,创建就加载数据
        if (!isLazyLoad){
            loadData();
        }
    }

    /**
     * 设置加载的布局Id
     */
    protected abstract int setLayoutId();

    /**
     * 初始化操作
     */
    protected abstract void init();

    /**
     * findViewById
     */
    protected View findViewById(int id) {
        return contentView.findViewById(id);
    }

    /**
     * 懒加载数据
     */
    protected abstract void loadData();


    /**
     * 是否启用懒加载,此方法仅对BaseLazyLoadFragment有效
     * */
    public void setLazyLoad(boolean lazyLoad) {
        isLazyLoad = lazyLoad;
    }
}

接下来是BaseLazyLoadFragment:

/**
 * Created by li.zhipeng on 2017/5/8.
 *
 *  懒加载的Fragment
 */

public abstract class BaseLazyLoadFragment extends BaseFragment {

    /**
     * 是否已经初始化结束
     */
    private boolean isPrepare;

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setLazyLoad(true);
        isPrepare = true;
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // 创建时要判断是否已经显示给用户,加载数据
        onVisibleToUser();
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        // 显示状态发生变化
        onVisibleToUser();
    }

    /**
     * 判断是否需要加载数据
     */
    private void onVisibleToUser() {
        // 如果已经初始化完成,并且显示给用户
        if (isPrepare && getUserVisibleHint()) {
            loadData();
        }
    }
}

最后看看MyFragment的代码:

/**
 * Created by li.zhipeng on 2017/5/7.
 * <p>
 * Fragment
 */

public class MyFragment extends BaseLazyLoadFragment implements SwipeRefreshLayout.OnRefreshListener {

    private SwipeRefreshLayout swipeRefreshLayout;
    private ListView listView;
    private Handler handler = new Handler();

    @Override
    protected int setLayoutId() {
        return R.layout.fragment_my;
    }

    @Override
    protected void init() {
        swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipeRefreshLayout);
        listView = (ListView) findViewById(R.id.listView);
        swipeRefreshLayout.setOnRefreshListener(this);
    }

    @Override
    protected void loadData() {
        onRefresh();
    }

    @Override
    public void onRefresh() {
        // 只加载一次数据,避免界面切换的时候,加载数据多次
        if (listView.getAdapter() == null) {
            swipeRefreshLayout.setRefreshing(true);
            new Thread() {
                @Override
                public void run() {
                    // 延迟1秒
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            listView.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, new String[]{
                                    "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111",
                                    "111", "111", "111", "111", "111", "111", "111", "111", "111", "111", "111"
                            }));
                            swipeRefreshLayout.setRefreshing(false);
                        }
                    });
                }
            }.start();
        }
    }
}

ok,这样封装就结束了,以后不需要懒加载Framgent可以直接继承BaseFragment,需要懒加载的直接继承BaseLazyLoadFragment。

扩展

如果你也自己敲着代码去研究懒加载,并且Framgent的数量大于2个,你会发现等你翻到第三个,再重新返回第一个的时候,第一个又重新加载了,并且重新走了创建周期,我在这里给不明白的原因的朋友解释一下:

ViewPager只默认预加载下一页的Fragment,其他的Fragment会被移除并销毁。
下一次再添加的时候就需要重新创建Fragment。

那如果解决这个问题呢?

viewPager.setOffscreenPageLimit(2);

这个方法可以设置ViewPager预加载的页数,我们看一下方法的注释:

/**
* Set the number of pages that should be retained to either side of the
* current page in the view hierarchy in an idle state. Pages beyond this
* limit will be recreated from the adapter when needed.
*
*

This is offered as an optimization. If you know in advance the number
* of pages you will need to support or have lazy-loading mechanisms in place
* on your pages, tweaking this setting can have benefits in perceived smoothness
* of paging animations and interaction. If you have a small number of pages (3-4)
* that you can keep active all at once, less time will be spent in layout for
* newly created view subtrees as the user pages back and forth.


*
*

You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.


*
* @param limit How many pages will be kept offscreen in an idle state.
*/

英文注释有点长,简单翻译总结一下几点:

1、设置当前页的左右两边的预加载数量。

2、通过设置预加载的数量,有利于动画的显示和交互。(上面的英文提到了懒加载,个人感觉跟懒加载没什么关系,预加载数量少了,必然流畅度相对会越高)

3、如果页数比较少(例如3到4个),可以加载所有的页数,这样可以介绍页数创建的时间,滑动更流畅

4、保持预加载个数偏小,除非你的页数有多种复杂的布局,默认为1。(不可能小于1,方法里判断)

了解了这个方法的特性,我们只要viewPager.setOffscreenPageLimit(2)就可以了,这样就解决了刚才的问题。

另外要说的是setUserVisibleHint()只有在ViewPager+Fragment的时候才有效,单用Fragment的时候可以考虑使用onHiddenChanged(boolean hidden)方法。

总结

ok,懒加载就到此结束,这个问题一家国内大型的互联网上市公司问的问题,我们平时可能觉得懒加载什么的无所谓,产品就是矫情,然后开始抱怨这个那个的,现在看来这就是差别的所在,大公司对于性能的要求真是非常的严格,这一点我们都需要耐心的学习,才能真正的有所提高。

Demo下载地址

展开阅读全文

没有更多推荐了,返回首页