热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

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

前言

在我们的项目里经常会用到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.
*


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


推荐阅读
author-avatar
网络小爬虫
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有