为什么要使用懒加载?
我们经常会在项目中使用 ViewPager
+ Fragment
加载Tab数据,但是由于 ViewPager
有预加载的功能,所以当我们第一次进入时会加载不止第一页的数据。假如我们在其他页面做了一些耗时操作,就有可能导致页面不流畅,为了解决这种情况,所以就需要使用到懒加载。
什么是懒加载?
懒加载说白了就是当Fragment
可见时,才进行网络请求或者耗时操作;当Fragment
不可见时,将当前页面的网络请求或者耗时操作暂停(比如Handler
)。通过这种做法提高程序的性能和优化用户体验。
懒加载解决方式
1.设置setOffscreenPageLimit
// 默认的缓存页面数量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
// 缓存页面数量(变量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
public void setOffscreenPageLimit(int limit) {
// 当我们手动设置的limit数小于默认值1时,limit值会自动被赋值为默认值1(即DEFAULT_OFFSCREEN_PAGES)
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
// 经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit
mOffscreenPageLimit = limit;
populate();
}
}
通过源码可以看出 ViewPager
的 setOffscreenPageLimit
最小值为1,所以 ViewPager
在通常情况下都会加载左右各一个页面,也就是说无法通过这种系统的方法实现懒加载效果。
2.自己实现一个懒加载
1.首先创建 ViewPager
+ Fragment
的页面,适配器选择使用FragmentStatePagerAdapter
2.setUserVisibleHint()
在Fragment创建时会先被调用一次,传入 isVisibleToUser = false
。
如果当前 Fragment 可见,那么 setUserVisibleHint()
会再次被调用一次传入 isVisibleToUser = true
;
如果 Fragment 从可见->不可见,那么 setUserVisibleHint()
也会被调用,传入 isVisibleToUser = false
总结:setUserVisibleHint()
除了 Fragment
的可见状态发生变化时会被回调外,在 new Fragment()
时也会被回调,此方法先于生命周期方法执行。
如果我们想要在 Fragment
可见与不可见时做一些操作,就需要做一些判断重新封装。
3.先看一下正常启动时 setUserVisibleHint()
怎么执行的,先写一个BaseLazyLoadFragment
类继承自 Fragment
BaseLazyLoadFragment部分代码如下
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Log.d(getClass().getSimpleName(), "onCreateView: ");
if (view == null) {
view = inflater.inflate(getLayoutId(), null);
}
initView(view);
return view;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.d(getClass().getSimpleName(), "setUserVisibleHint: " + isVisibleToUser);
}
protected abstract void initView(View view);
protected abstract int getLayoutId();
Fragment代码如下
public class TestFragment3 extends BaseLazyLoadFragment {
private static final String TAG = "TestFragment3";
@Override
protected void initView(View view) {
}
@Override
protected int getLayoutId() {
return R.layout.fragment_test3;
}
}
进入程序依次打开三个页面运行结果如下
于此可见上面对 setUserVisibleHint()
总结是对的。
接下来分析一些这三屏的日志
1.除去第一页,第二页、第三页、第四页的 onCreateView
和 setUserVisibleHint()=false
都已经在前一个页面调用了,第一次加载时滑到当前页面(除第一页)实际调用了上一个页面的 setUserVisibleHint()=false
当前页面的setUserVisibleHint()=true
以及下一个页面的 onCreateView
。针对这种情况可以在onCreateView
中定义一个 isViewCreate
的 boolean
变量记录,然后在 setUserVisibleHint()
方法中,根据 isViewCreate
值,再去调用页面自己定义的可见和不可见方法
这个时候代码就是下面这样
private boolean isViewCreate = false;// View 是否创建
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(getClass().getSimpleName(), "onCreateView: ");
if (view == null) {
view = inflater.inflate(getLayoutId(), null);
}
initView(view);
isViewCreate = true;
return view;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isViewCreate) {
if (isVisibleToUser) {
dispatchVisibleHint(true);
} else {
dispatchVisibleHint(false);
}
}
}
public void dispatchVisibleHint(boolean isVisible) {
if (isVisible) {
onFragmentResume();
} else {
onFragmentPause();
}
}
public void onFragmentPause() {
}
public void onFragmentResume() {
}
2.上面的分析是在排除了第一页之后写的逻辑,所以需要对第一页加载做一些判断。第一页显示时的日志是,先调用 setUserVisibleHint()=false =>setUserVisibleHint()= true => onCreateView
, 其他页面显示当前页面的日志只是 setUserVisibleHint()=true
。也就是说在加载第一页的时候 setUserVisibleHint()=true
和 onCreateView
才都会调用,因此我们只需要在 onCreateView
中做一下判断。
这时onCreateView
中的代码就变成了
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (view == null) {
view = inflater.inflate(getLayoutId(), null);
}
initView(view);
isCreateView = true;
if (getUserVisibleHint() && !isHidden()) {
dispatchVisibleHint(true);
}
return view;
}
我们有时需要在页面第一次初始化时做一些加载动画,之后就不再显示的逻辑,这时我们也是可以使用一个 boolean
类型的变量 isFirstEnter
在页面可见的时候触发页面第一次可见的方法
这时候的完整代码就变成了
public abstract class BaseLazyLoadFragment extends Fragment {
private View view;
private boolean isCreateView = false;// View 是否创建
private boolean isFirstEnter = false;// View 第一次可见
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(getClass().getSimpleName(), "onCreateView: ");
if (view == null) {
view = inflater.inflate(getLayoutId(), null);
}
initView(view);
isCreateView = true;
isFirstEnter = true;
if (getUserVisibleHint() && !isHidden()) {
dispatchVisibleHint(true);
}
return view;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isCreateView) {
if (isVisibleToUser) {
dispatchVisibleHint(true);
} else {
dispatchVisibleHint(false);
}
}
Log.d(getClass().getSimpleName(), "setUserVisibleHint: " + isVisibleToUser);
}
public void dispatchVisibleHint(boolean isVisible) {
if (isVisible) {
onFragmentResume();
if (isFirstEnter) {
isFirstEnter = false;
onFragmentFirstVisible();
}
} else {
onFragmentPause();
}
}
// 页面首次可见
public void onFragmentFirstVisible() {
}
// 页面不可见
public void onFragmentPause() {
}
// 页面可见
public void onFragmentResume() {
}
protected abstract void initView(View view);
protected abstract int getLayoutId();
}
接下来我们看一些特殊情况
1.如果我们适配器使用FragmentPagerAdapter
加载时会怎么样:
如果对 FragmentPagerAdapter
和 FragmentStatePagerAdapter
不太清楚的可以看一下
FragmentPagerAdapter和FragmentStatePagerAdapter源码中的三宝
为了方便查看,我在 onFragmentPause
和 onFragmentResume
加了打印
// 页面不可见
public void onFragmentPause() {
Log.d(getClass().getSimpleName(), "onFragmentPause: ");
}
// 页面可见
public void onFragmentResume() {
Log.d(getClass().getSimpleName(), "onFragmentResume: ");
}
依次从1->4 可以看到此时日志都是正常的,如果跨页面跳转会怎么样?
分析一下4->1和1->3的日志:
正常情况下如果从 4->1 我们只需要第四页的 onFragmentPause
和第一页的 onFragmentResume
但是实际上多调用了第一页和第二页的 onFragmentPause
,1->3 也是同样多调用了第三页和第四页的 onFragmentPause
。
其实多调用的 onFragmentPause
页面都有2个共同点:
1.调用了页面的 setUserVisibleHint()=false
;
2.调用 onFragmentPause
的页面对用户不可见;
因此我们可以定义一个变量 currentFragmentVisibleState=false
记录 Fragment
的显示状态,根据 currentFragmentVisibleState
和 setUserVisibleHint
判断触发页面的可见和不可见方法 。
代码如下
// 保存 fragmet 显示状态(默认隐藏)
private boolean currentFragmentVisibleState = false;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isViewCreate) {
if (isVisibleToUser && !currentFragmentVisibleState) {
dispatchVisibleHint(true);
} else if (currentFragmentVisibleState && !isVisibleToUser) {
dispatchVisibleHint(false);
}
}
}
public void dispatchVisibleHint(boolean isVisible) {
if (currentFragmentVisibleState == isVisible) {
return;
}
currentFragmentVisibleState = isVisible;
if (isVisible) {
onFragmentResume();
if (isFirstEnter) {
isFirstEnter=true;
onFragmentFirstVisible();
}
} else {
onFragmentPause();
}
}
2.有时候我们会在 Fragment
中打开一个新的 Activity
或者我们点击 Home
键回到桌面了,这时候就需要调用 onFragmentPause()
做一些暂停操作;同样在回到当前页面时也需要调用 onFragmentResume()
做一些恢复操作。
@Override
public void onPause() {
super.onPause();
if (currentFragmentVisibleState && getUserVisibleHint() && !isHidden()) {
dispatchVisibleHint(false);
}
}
@Override
public void onResume() {
super.onResume();
if (!currentFragmentVisibleState && getUserVisibleHint() && !isHidden()) {
dispatchVisibleHint(true);
}
}
3.以上说的都是单个的 ViewPager
和 Fragment
,还有很多都会在嵌套一层,这个时候应该怎么处理呢?
我在 TestFragment2
中新增了3个 ChildFragment
,看一下日志打印
通过日志可以看出,相邻页面跳转和跨页面跳转都是没有问题的.如果我们切换外层的 Fragment
,这个时候会怎么样呢?
通过日志我们可以看出从 TestFragment2
->TestFragment3
,然后返回到 TestFragment2
,这时 ChildFragment1
的 onFragmentPause()
方法并没有调用,所以针对这种情况就需要在父类 显示/隐藏 的时候通知所有的子类。
public void dispatchVisibleHint(boolean isVisible) {
// 记录当前 Fragment 的显示状态
currentFragmentVisibleState = isVisible;
if (isVisible) {
if (isFirstEnter) {
// 第一次进入的标识改为false
isFirstEnter = false;
// 当前fragment首次可见
onFragmentFirstVisible();
}
onFragmentResume();
dispatchChildVisibleState(true);
} else {
onFragmentPause();
dispatchChildVisibleState(false);
}
}
public void dispatchChildVisibleState(boolean isChildVisible) {
List<Fragment> fragments = getChildFragmentManager().getFragments();
Log.d(getClass().getSimpleName(), "dispatchChildVisibleState: "
+ (fragments != null && fragments.size() > 0));
if (fragments != null && fragments.size() > 0) {
for (Fragment fragment : fragments) {
Log.d(getClass().getSimpleName(), "dispatchChildVisibleState: " +
fragment.getUserVisibleHint() + !fragment.isHidden());
if (fragment instanceof BaseLazyLoadFragment
&& fragment.getUserVisibleHint() && !fragment.isHidden()) {
((BaseLazyLoadFragment) fragment).dispatchVisibleHint(isChildVisible);
}
}
}
}
这时候我们在打印一下日志看一下是否有问题
日志是从第一页滑到有子 Fragment
的第二页,可以明显看到 ChildFragment1
被调用了两次,这样肯定是有问题的,所以需要我们解决。通过日志我们可以看到这样的流程 ChildFragment1
-> TestFragment2
-> ChildFragment1
,因为子ChildFragment1
调用是优先于父 Fragment
,所以在 父 Fragment
在显示的时候又会分发给子 Fragment
所以就会出现子 Fragment
被调用两次,但是此时子 Fragment
已经显示了所以我们只用在分发的之前判断当前页面的显示状态就可以了。
public void dispatchVisibleHint(boolean isVisible) {
// 如果当前的 Fragment 的分发状态和 isVisible 相同,此时就没有必要分发了
if (currentVisiableState == isVisible) {
return;
}
// 记录当前 Fragment 的显示状态
currentVisiableState = isVisible;
if (isVisible) {
if (isFirstEnter) {
isFirstEnter = false;
onFragmentFirstVisible();
}
dispatchChildVisibleStete(true);
onFragmentResume();
} else {
dispatchChildVisibleStete(false);
onFragmentPause();
}
}
至此懒加载逻辑就算是完整的,希望能对您理解懒加载有一定的帮助。如果有什么疏漏或者错误,欢迎留言讨论。
github地址