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

前言

在我们的项目里经常会用到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的生命周期以外调用。

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

面试总结(5):Fragment的懒加载_第1张图片

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

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

这里写图片描述

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

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

/**
 * Created by li.zhipeng on 2017/5/7.
 * 

* 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.
 * 

* 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.
 * 

* 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下载地址

你可能感兴趣的:(Android)