手把手讲解ViewPager懒加载

文章详细分析了ViewPager如何通过缓存Fragment对象来提高性能,介绍了FragmentPagerAdapter的instantiateItem方法,并提出了一种利用自定义生命周期方法实现Fragment懒加载的策略。作者还提供了示例代码以展示如何在实际项目中应用这一优化技术。
摘要由CSDN通过智能技术生成

mCurItem = newCurrentItem;
}

if (mAdapter == null) { //adapter是空,就不进行后面的操作了
sortChildDrawingOrder();
return;
}

// Bail now if we are waiting to populate. This is to hold off
// on creating views from the time the user releases their finger to
// fling to a new position until we have finished the scroll to
// that position, avoiding glitches from happening at that point.
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, “populate is pending, skipping for now…”);
sortChildDrawingOrder();
return;
}

// Also, don’t populate until we are attached to a window. This is to
// avoid trying to populate before we have restored our view hierarchy
// state and conflicting with what is restored.
if (getWindowToken() == null) {
return;
}

mAdapter.startUpdate(this);//当一个改变发生在已经显示出来的页面时执行(貌似和缓存无关)

final int pageLimit = mOffscreenPageLimit;//当前缓存数
final int startPos = Math.max(0, mCurItem - pageLimit);//用当前页编号和缓存数的计算缓存开始的位置
final int N = mAdapter.getCount();//获得 adapter的子内容数量
final int endPos = Math.min(N-1, mCurItem + pageLimit);//用当前页编号和缓存数的计算缓存结束的位置

if (N != mExpectedAdapterCount) {//如果adapter的内容数量和期望值不同,就抛出异常
String resName;
try {
resName = getResources().getResourceName(getId());
} catch (Resources.NotFoundException e) {
resName = Integer.toHexString(getId());
}
throw new IllegalStateException(“The application’s PagerAdapter changed the adapter’s” +
" contents without calling PagerAdapter#notifyDataSetChanged!" +
" Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
" Pager id: " + resName +
" Pager class: " + getClass() +
" Problematic adapter: " + mAdapter.getClass());
}

// Locate the currently focused item or add it if needed.
// 本地化当前聚焦的item 或者 有必要的话将它加到list中去
int curIndex = -1;
ItemInfo curItem = null;//你就是当前正在显示的item
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
//这里在遍历一个mItems,它是ArrayList mItems
final ItemInfo ii = mItems.get(curIndex);// 获得当前位置item
if (ii.position >= mCurItem) {//
if (ii.position == mCurItem) curItem = ii;
//讲道理,上面两行代码我没看懂,先判定>= ,又判定==,最后把当前缓存的这个,赋值给curItem
break;
}
}

if (curItem == null && N > 0) {// 如果curItem经历了上面的遍历,还是null,并且adapter的内容数量大于0
curItem = addNewItem(mCurItem, curIndex);//那么就构建一个ItemInfo对象,并且赋值给curItem
}

// Fill 3x the available width or up to the number of offscreen
// pages requested to either side, whichever is larger.
// If we have no current item we have no work to do.
// 这段注释的意思是,填充三倍可见宽度 或者一直到 屏幕外页面的数量需要的宽度,无论它多大.
// 如果我们没有当前item,就不用做任何事情
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;//??
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//itemIndex已经是0了,所以,这里就是获取当前位置的item
final int clientWidth = getPaddedWidth();
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
for (int pos = mCurItem - 1; pos >= 0; pos–) {//从当前显示的item倒过来往前遍历
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {//如果遍历到了缓存item所在的位置,并且缓存的这个item没有处于滑动状态
mItems.remove(itemIndex);//就从缓存list中移除它
mAdapter.destroyItem(this, pos, ii.object);//并且 adapter执行destroyItem销毁item
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
" view: " + ii.object);
}
itemIndex–;
curIndex–;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {//如果遍历到了 当前显示的item
extraWidthLeft += ii.widthFactor;
itemIndex–;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {//其他情况
ii = addNewItem(pos, itemIndex + 1);// 加入缓存
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}

float extraWidthRight = curItem.widthFactor;
itemIndex = curIndex + 1;
if (extraWidthRight < 2.f) {
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
final float rightWidthNeeded = clientWidth <= 0 ? 0 :
(float) getPaddingRight() / (float) clientWidth + 2.f;
for (int pos = mCurItem + 1; pos < N; pos++) {//再遍历一次,从当前位置往后遍历
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
if (pos == ii.position && !ii.scrolling) {//
mItems.remove(itemIndex);//从缓存中移除掉当前item,因为正在显示中,所以无需缓存
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos +
" view: " + ii.object);
}
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) {
extraWidthRight += ii.widthFactor;
itemIndex++;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}

calculatePageOffsets(curItem, curIndex, oldCurInfo); Fix up offsets for later layout. 为了后面的布局,修复偏移量,没有涉及到缓存list的add和remove
}

if (DEBUG) {
Log.i(TAG, “Current page list:”);
for (int i=0; i<mItems.size(); i++) {
Log.i(TAG, “#” + i + ": page " + mItems.get(i).position);
}
}

上面的代码中提到了 addNewItem() ,它就是往缓存ArrayList<ItemInfo> mItems里面增加ItemInfo的 核心方法

ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}

看了这个方法,得出结论:

  1. 为什么至少缓存一个?

    答:当我们传参为0,它会强制设定为1.

if (limit < DEFAULT_OFFSCREEN_PAGES) {//如果入参是0
limit = DEFAULT_OFFSCREEN_PAGES;// 强制设定为1
}

  1. 缓存的是什么?

    答:缓存的是ItemInfo 对象, 而ItemInfoadapter.instantiateItem的一个封装,除了mAdapter.instantiateItem 返回的这个object之外,它还封装了position,还有 mAdapter.getPageWidth 页面宽度.

static class ItemInfo {
Object object;
boolean scrolling;
float widthFactor;

       /** Logical position of the item within the pager adapter. */
       int position;

       /** Offset between the starting edges of the item and its container. */
       float offset;
   }
  1. 存到哪里去了?

    答:存到了一个List,叫做 ArrayList<ItemInfo> mItems

private final ArrayList mItems = new ArrayList();

  1. 存起来之后用来干什么了?

    答: 追踪mItems.get方法,发现,源码中有30处出现,一共有 12 个方法使用了它:

setAdapter
setCurrentItemInternal
dataSetChanged
populate
calculatePageOffsets
infoForChild
infoForPosition
completeScroll
performDrag
infoForFirstVisiblePage
determineTargetPage
onDraw

可以发现 缓存,贯穿了几乎ViewPager的所有行为,包括适配器,数据变更,修正页面偏移量,计算滑动,拖拽,绘制等, 但是我想进一步探索的时候,发现这些方法,大部分没有注释,时间原因,只好作罢。

从上面的结论,我们总结一下,ViewPager缓存的,是PageAdapter mAdapter里面 instantiateItem 创建出来的object, 我们今天重点应该关注的是和Fragment有关的FragmentPagerAdapter,所以,我们看看它的instantiateItem到底返回了一个神马东西:

public Object instantiateItem(ViewGroup container, int position) {
//…省略非重点代码
return fragment;
}

看到了吧? return fragment !

所以,得出最终结论

ViewPager + Fragment (其实还应该加上 FragmentPagerAdapter), 缓存的是Fragment对象,该对象被封装成了ItemInfoadd到了ArrayList<ItemInfo> mItems 里面。当我们滑动ViewPager的时候,如果进入的是 已经缓存的Fragment,就不会去走 Fragment的重新创建生命周期(onCreate-onCreateView-onViewCreated-onStart-onResume),直接使用缓存中的Fragment,调用它的onStart-onResume.


#五 、ViewPager+Fragment懒加载机制的设计思路

既然ViewPager已经这么明确了,他就是要缓存至少一个Fragment,我们不太好去直接改源码,那怎么去 实现 懒加载呢?没办法了,曲线救国吧,改不了 ViewPager,那就 在Fragment的生命周期上做文章。

Fragment的生命周期也不太好去动刀,但是我们可以增加另外的方法,来结合生命周期,以及 上文提及的无关生命周期的两个函数 setUserVisibleHint / onHiddenChanged.

原本的Fragment生命周期函数不可信了,那么首先我们继承Fragment,增加3个我们自己的方法, 以及3bool标志位

public abstract class BaseLazyLoadingFragment extends Fragment {
//省略无关代码…
private boolean isViewCreated = false;//View是否已经被创建出来

private boolean isFirstVisible = true;//当前Fragment是否是首次可见

private boolean currentVisibleState = false;//当前真正的可见状态
/**

  • 当第一次可见的时候(此方法,在View的一次生命周期中只执行一次)
  • 如果Fragment经历了onDestroyView,那么整个方法会再次执行
  • 重写此方法时,对Fragment全局变量进行 初始化
  • 具体的参照github demo
    */
    protected void onFragmentFirstVisible() {
    Log.d(getCustomMethodTag(), “第一次可见,进行当前Fragment初始化操作”);
    }

/**

  • 当fragment变成可见的时候(可能会多次)
    */
    protected void onFragmentResume() {
    Log.d(getCustomMethodTag(), “onFragmentResume 执行网络请求以及,UI操作”);
    }

/**

  • 当fragment变成不可见的时候(可能会多次)
    */
    protected void onFragmentPause() {
    Log.d(getCustomMethodTag(), “onFragmentPause 中断网络请求,UI操作”);
    }
    }

我们设计了3个自定义的方法,那么这3个方法在什么情况下执行呢?

定义一个 visible状态分发的方法:

void dispatchVisibleState(boolean isVisible) {
//为了兼容内嵌ViewPager的情况,分发时,还要判断父Fragment是不是可见
if (isVisible && isParentInvisible()) {//如果当前可见,但是父容器不可见,那么也不必分发
return;
}
if (isVisible == currentVisibleState) return;//如果目标值,和当前值相同,那就别费劲了
currentVisibleState = isVisible;//更新状态值
if (isVisible) {//如果可见
//那就区分是第一次可见,还是非第一次可见
if (isFirstVisible) {
isFirstVisible = false;
onFragmentFirstVisible();
}
onFragmentResume();
dispatchChildVisibilityState(true);
} else {
onFragmentPause();
dispatchChildVisibilityState(false);
}
}

接着Fragment原有的生命周期函数内( 主要是OnCreateViewonResumeonPause),调用此方法

public abstract class BaseLazyLoadingFragment extends Fragment {

//省略无关代码…
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.d(getLifeCycleTag(), “onAttach”);
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
Log.d(getLifeCycleTag(), “onCreate”);
super.onCreate(savedInstanceState);
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mRoot = inflater.inflate(getLayoutId(), container, false);
Log.d(getLifeCycleTag(), “onCreateView”);
initView(mRoot);
//在View创建完毕之后,isViewCreate 要变为true
isViewCreated = true;
if (!isHidden() && getUserVisibleHint())
dispatchVisibleState(true);
return mRoot;
}

@Override
public void onDestroyView() {//相对应的,当View被销毁的时候,isViewCreated要变为false
super.onDestroyView();
Log.d(getLifeCycleTag(), “onDestroyView”);
reset();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Log.d(getLifeCycleTag(), “onViewCreated”);
}

@Override
public void onStart() {
super.onStart();
Log.d(getLifeCycleTag(), “onStart”);
}

@Override
public void onResume() {
super.onResume();
Log.d(getLifeCycleTag(), “onResume”);

if (!isFirstVisible) {
if (!isHidden() && !currentVisibleState && getUserVisibleHint())
dispatchVisibleState(true);
}

}

@Override
public void onPause() {
super.onPause();
Log.d(getLifeCycleTag(), “onPause”);
if (currentVisibleState && getUserVisibleHint()) {
dispatchVisibleState(false);
}
}

@Override
public void onStop() {
super.onStop();
Log.d(getLifeCycleTag(), “onStop”);
}

@Override
public void onDestroy() {
super.onDestroy();
Log.d(getLifeCycleTag(), “onDestroy”);
}

@Override
public void onDetach() {
super.onDetach();
Log.d(getLifeCycleTag(), “onDetach”);
}

//省略无关代码…
}

然而,能够表示当前Fragment是否可见的,并不止有生命周期函数,还有2个和生命周期无关的函数(setUserVisibleHint,onHiddenChanged),在其中也要调用dispatchVisibleState

/**

  • 此方法和生命周期无关,由外部调用,只是作为一个可见不可见的参考
    */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    Log.d(getLogTag(), “setUserVisibleHint:” + isVisibleToUser);

//因为只有在Fragment的View已经被创建的前提下,UI处理才有意义,所以
if (isViewCreated) {
//为了逻辑严谨,必须当目前状态值和目标相异的时候,才去执行UI可见分发
if (currentVisibleState && !isVisibleToUser) {
dispatchVisibleState(false);
} else if (!currentVisibleState && isVisibleToUser) {
dispatchVisibleState(true);
}
}

}

/**

  • 在Fragment被hide/show的时候被调用
  • @param hidden
    */
    @Override
    public void onHiddenChanged(boolean hidden) {
    super.onHiddenChanged(hidden);
    if (hidden) {
    dispatchVisibleState(false);
    } else {
    dispatchVisibleState(true);
    }
    }

最后,考虑到ViewPager+Fragment(ViewPager+Fragment)嵌套的问题:

设计一个 dispatchChildVisibilityState 方法,来控制 内嵌的Fragment的可见状态 :

private void dispatchChildVisibilityState(boolean isVisible) {
FragmentManager fragmentManager = getChildFragmentManager();
List list = fragmentManager.getFragments();
if (list != null) {
for (Fragment fg : list) {//遍历子
if (fg instanceof BaseLazyLoadingFragment
&& !fg.isHidden() && fg.getUserVisibleHint()) {
((BaseLazyLoadingFragment) fg).dispatchVisibleState(isVisible);
}
}
}
}

以及isParentInvisible方法来判定内嵌Fragment的父Fragment是否可见:

private boolean isParentInvisible() {
Fragment parent = getParentFragment();
Log.d(getLogTag(), “getParentFragment:” + parent + “”);
if (parent instanceof BaseLazyLoadingFragment) {
BaseLazyLoadingFragment lz = (BaseLazyLoadingFragment) parent;
return !lz.currentVisibleState;
}
return false;// 默认可见
}


#六 、案例演示(观察日志)

请下载源码,运行demo中的module:lazyloadingfragments

最简单的情况,单个ViewPager内滑动时

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

生命周期函数

刚启动app:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后从0滑动到1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然而,观察一下我们自定义的3个方法:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习分享

在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了

很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘

如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

image

七大模块学习资料:如NDK模块开发、Android框架体系架构…

image

2021大厂面试真题:

image

只有系统,有方向的学习,才能在短时间内迅速提高自己的技术,只有不断地学习,不懈的努力才能拥有更好的技术,才能在互联网行业中立于不败之地。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了

很多朋友不是没有资料,大多都是有几十上百个G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘

如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。

2021最新上万页的大厂面试真题

[外链图片转存中…(img-mrLYtx19-1713620609886)]

七大模块学习资料:如NDK模块开发、Android框架体系架构…

[外链图片转存中…(img-8m4bIwgl-1713620609887)]

2021大厂面试真题:

[外链图片转存中…(img-rIZZnvMx-1713620609888)]

只有系统,有方向的学习,才能在短时间内迅速提高自己的技术,只有不断地学习,不懈的努力才能拥有更好的技术,才能在互联网行业中立于不败之地。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值