面试总结(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.
*
* <p>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.</p>
*
* <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>You should keep this limit low, especially if your pages have complex layouts.
* This setting defaults to 1.</p>
*
* @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下载地址

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

推荐阅读更多精彩内容