ViewPager+Fragment预加载和懒加载分析
1 什么是fragment的预加载和懒加载?
预加载:viewpager显示当前fragment的时候,viewpager还会去预加载其他fragment的数据。预加载的Fragment
懒加载:加载的内容是否需要优化,网络数据的优化。即懒加载的是数据。
2 为什么要进行懒加载?
fragment的懒加载是指Fragment与ViewPager结合使用的使用,用到的一种优化方案。
因为缓存的存在,我觉得应该是因为预加载的存在,之所以要懒加载,就是因为预加载。这里的预加载指的是预加载ViewPager对Fragment的预加载,懒加载是指Fragment对数据的懒加载
viewpager显示当前fragment的时候,viewpager还会去预加载其他fragment的数据。进而导致界面卡顿,影响用户体验。
界面卡顿优化:
1、检查界面是否有过多的渲染;
2、加载的内容是否需要优化,网络数据的优化。此处可以使用懒加载来解决。
3 ViewPager预加载分析
实现fragment懒加载的原理,首先要了解ViewPager
预加载Fragment
的原理,在viewpager预加载fragment的基础之上,实现fragment的数据的懒加载。
3.1 设置预加载个数的函数
setOffscreenPageLimit(int limit)
设置视图层次结构中处于空闲状态时,应该保留在当前页面两侧的页面数量。超过此限制的页面将在需要时从适配器重新创建。这是一个优化。
如果预先知道需要支持的页面数量,或者在页面上设置了延迟加载机制,那么调整此设置将有利于页面动画和交互的流畅性。
如果您有少量的页面(3-4),您可以同时保持活动状态,那么在布局中为新创建的视图子树来回切换用户页面所花费的时间就会更少。您应该将这个限制保持在较低的水平,特别是如果您的页面具有复杂的布局。此设置默认为1。
ViewPager.java
public void setOffscreenPageLimit(int limit) {
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) {
mOffscreenPageLimit = limit;
populate();
}
}
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
private int mOffscreenPageLimit = 1;
从源码中可以发现,limit最小值为默认值1.设置缓存个数示例:
ViewPager.java
示例1:
mViewPager.setAdapter(new TestPagerAdapter(getSupportFragmentManager(),mFragments));
mViewPager.setOffscreenPageLimit(3);
示例2:
mViewPager.setOffscreenPageLimit(3);
mViewPager.setAdapter(new TestPagerAdapter(getSupportFragmentManager(),mFragments));
分析一下populate()函数:
void populate() {
this.populate(this.mCurItem);
}
从源码中可以看到,该函数直接调用void populate(int newCurrentItem){}
方法。通过调试发现上述两个示例均未触发void populate(int newCurrentItem){}
中的核心代码。
针对示例1的调试mViewPager.setOffscreenPageLimit(3);
:
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
//newCurrentItem=0,this.mCurItem=0
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
// this.mAdapter!=null
if (mAdapter == null) {
sortChildDrawingOrder();
return;
}
// this.mPopulatePending=false
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// this.getWindowToken() == null
if (getWindowToken() == null) {
return;
}
//省略后面代码
}
针对示例2的调试mViewPager.setOffscreenPageLimit(3);
:
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
//newCurrentItem=0,this.mCurItem=0
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
// this.mAdapter == null
if (mAdapter == null) {
sortChildDrawingOrder();//会走这一步
return;
}
//省略此部分代码
}
看一下sortChildDrawingOrder()的源码实现:
private void sortChildDrawingOrder() {
if (this.mDrawingOrder != 0) {
//省略此部分代码
}
}
此时的this.mDrawingOrder = 0
,所以直接返回函数调用处,接着退出void populate(int newCurrentItem)函数,返回到populate()
里面的调用处,进而回到setOffscreenPageLimit(int limit)
里面.
实例1、实例2的整个过程中没有触发任何对Item
的操作。所以说,设置适配器和设置预加载的数量的先后顺序对缓存的添加没有影响。一种数学关系:加载的Fragment的数量等于预加载的Fragment数量加1
3.2 设置适配器
/**
* Set a PagerAdapter that will supply views for this pager as needed.
*
* @param adapter Adapter to use
*/
public void setAdapter(PagerAdapter adapter) {
if (mAdapter != null) {
mAdapter.setViewPagerObserver(null);
mAdapter.startUpdate(this);
for (int i = 0; i < mItems.size(); i++) {
final ItemInfo ii = mItems.get(i);
mAdapter.destroyItem(this, ii.position, ii.object);
}
mAdapter.finishUpdate(this);
mItems.clear();
removeNonDecorViews();
mCurItem = 0;
scrollTo(0, 0);
}
final PagerAdapter oldAdapter = mAdapter;
mAdapter = adapter;
mExpectedAdapterCount = 0;
if (mAdapter != null) {
if (mObserver == null) {
mObserver = new PagerObserver();
}
mAdapter.setViewPagerObserver(mObserver);
mPopulatePending = false;
final boolean wasFirstLayout = mFirstLayout;
mFirstLayout = true;
mExpectedAdapterCount = mAdapter.getCount();
if (mRestoredCurItem >= 0) {
mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
setCurrentItemInternal(mRestoredCurItem, false, true);
mRestoredCurItem = -1;
mRestoredAdapterState = null;
mRestoredClassLoader = null;
} else if (!wasFirstLayout) {
populate();
} else {
requestLayout();
}
}
// Dispatch the change to any listeners
if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) {
for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) {
mAdapterChangeListeners.get(i).onAdapterChanged(this, oldAdapter, adapter);
}
}
}
3.3 populate()调用分析
1、查看源码可知:一共出现了9次被调用时机。
1、在ViewPager(@NonNull Context context, @Nullable AttributeSet attrs)
方法的局部内部类中被调用。
2、在setAdapter(@Nullable PagerAdapter adapter)
方法中被调用。
3、在setCurrentItemInternal(int , boolean , boolean , int )
方法中被调用。
4、在setPageTransformer(boolean , @Nullable, int)
方法中被调用。
5、在setOffscreenPageLimit(int limit)
方法中被调用。
6、在smoothScrollTo(int x, int y, int velocity)
方法中被调用。
7、在onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法中被调用。
8、在onInterceptTouchEvent(MotionEvent ev)
方法中被调用,点击(按下)可以触发。
9、在onTouchEvent(MotionEvent ev)
方法中被调用,点击(按下)可以触发。
2、预加载Fragment实例源码分析
从APP启动开始调试,先是调用了setOffscreenPageLimit(int limit)
函数中,接着走到了onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,在这个方法中的调用时候,真正的实现了预加载Fragment实例。即调用了addNewItem(int,int)方法实现预加载。
第一次调用addNewItem(int position ,int index)
,将position=0
和index=0
的那个对象添加了进来,
if (curItem == null && N > 0) {
curItem = this.addNewItem(this.mCurItem, curIndex);
}
执行完上述条件语句,此时的curItem!=null
,mCurItem==0
,curIndex==0
,mItems.size()==1
,进而使后 N-1 次调用在一个循环中得以执行:
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;//itemIndex == -1;
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//ii==null;
final int clientWidth = getClientWidth();// 1080
final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;//1.0
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//此处代码在 mCurItem=0 时不会执行。
}
float extraWidthRight = curItem.widthFactor;//1.0
itemIndex = curIndex + 1;//1
if (extraWidthRight < 2.f) {
//extraWidthRight=1.0
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
// ii = 1<[1,2,3]?mItems.get([1,2,3])?mItems.get(itemIndex) : null;
//ii == 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) {
// final int endPos = Math.min(N - 1, mCurItem + pageLimit);
//N = 4,mCurItem=0, pageLimit=3;==>endPos=3,不满足条件
} else if (ii != null && pos == ii.position) {
// 不满足条件 ii
} else {
ii = addNewItem(pos, itemIndex);
itemIndex++;//从 1 开始,而mItems中的元素个数也是从 1 开始递增
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
//ii==null
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
启动时的测量函数
同事完成了Fragment的预加载,也就是网上说的缓存,个人觉得叫预缓存或者预加载更贴切些。
3.4 addNewItem(int,int)函数
此函数为ViewPager中的函数
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;
}
从这个方法的代码逻辑可知,
ItemInfo为ViewPager中的一个静态内部类,封装了ViewPager的Item的信息。
static class ItemInfo {
Object object;
int position;
boolean scrolling;
float widthFactor;
float offset;
}
- 属性分析:
- object :指向新建的item对象
- position:item的位置
- scrolling:是否正在滑动
- widthFactor:宽度因子
- offset:偏移量
ii.object = mAdapter.instantiateItem(this, position);
每次创建一个item实例,都会被缓存到ItemInfo的对象中。而ii又被添加到了mItems列表中,被缓存起来。
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();
综上所述,设置Fragment的缓存数量或者设置适配器的先后顺序,对预缓存是没有影响的。真正完成预加载的逻辑是onMeasure()
中实现的。
4 PagerAdapter实现数据懒加载
4.1 setUseVisibleHint(boolean)
此函数是Fragment中的函数,专门被PagerAdapter调用。
日志截图
- 该截图是在
mViewPager.setOffscreenPageLimit(int limit);
的参数为1的情况下进行的。只缓存一个BFragment。 - 此方法在PagerAdapter的子类FragmentStatePagerAdapter中被调用,也就是只有在在viewpager、PagerAdapter和fragment结合使用的时候,会触发,触发时机下面分析。
- 该方法优先于
onCreateView()
被调用。
日志截图只是反映了,setUseVisibleHint(boolean)
发生了3次调用,且优先于fragment
的onCreateView()
方法。更详细的信息,需要看源码
先查看源码,看这个函数在哪里被调用,然后通过调试得到调用时机分析调用逻辑:
定位定时分析法:源码定位,调试源码定时。
1、在instantiateItem()
中调用了两次,应该就是先去获取预加载的Fragment的实例,此时对用户不可见。
2、在setPrimaryItem()
中调用了一次,对用户可见。
看一下预加载两个的情况
mViewPager.setOffscreenPageLimit(2);
日志截图-设置预加载数为2
尾部多出来3条日志,打印的都是AFragment,这是因为mCurItem==0
和Fragment还是AFragment的缘故。调用了4次,是因为发生了四次onMeasure()
.为什么调用了四次,留在以后探讨吧。
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
详细分析,见后面的setPrimaryItem()
4.2 instantiateItem()
在FragmentStatePagerAdapter.java的instantiateItem()
中被调用1次。此时虽然获得了Fragment对象,但是对用户还是不可见状态。
1、代码如下:
@Override
public Object instantiateItem(ViewGroup container, int position) {
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);//获取Fragment
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
}
2、代码片段分析
先是判断mFragments
里面有没有Fragment
,有的话,直接取出并返回。
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
看一下mFragment
的定义:
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
查看源码,发现mFragment
在初始化Item的函数instantiateItem()中添加了元素。
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment
由于是启动初始化,所有代码走到这个判断处,不满足条件,直接跳过这段代码,走其下面的代码。
获取Fragment:
Fragment fragment = getItem(position);//获取Fragment
看这一段代码:
// If we already have this item instantiated, there is nothing
// to do. This can happen when we are restoring the entire pager
// from its saved state, where the fragment manager has already
// taken care of restoring the fragments we previously had instantiated.
if (mFragments.size() > position) {
Fragment f = mFragments.get(position);
if (f != null) {
return f;
}
}
app启动时,position = 0
,mFragments.size()==0
(这个地方看源码可以知道),所以会直接跳过这段代码,走下面的:
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
Fragment fragment = getItem(position);//我们自己实现
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
while (mFragments.size() <= position) {
mFragments.add(null);
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
mFragments.set(position, fragment);
mCurTransaction.add(container.getId(), fragment);
return fragment;
此时的FragmentTransaction
也是null
,会走这段代码:
this.mCurTransaction = this.mFragmentManager.beginTransaction();
然后是,通过getItem(0)
取出一个fragment
,getItem(int)
由我们自己实现:
下面这段代码用于恢复状态
if (mSavedState.size() > position) {
Fragment.SavedState fss = mSavedState.get(position);
if (fss != null) {
fragment.setInitialSavedState(fss);
}
}
剩下的代码中,我们直接看fragment.setUserVisibleHint(false);
fragment.setUserVisibleHint(false);
就是这段代码,将isVisibleToUser
赋值为false
,这就是我们懒加载的依据。其实这个时候,我们应该是不加载数据,因为这个时候,视图还没创建,这里写懒加载的逻辑,首先要判断视图是否已创建,即视图不为空。
注意前面的说的APP启动,也就是viewpager
为fragment
的呈现做的准备工作,即实例化viewpager
的item
阶段,比如开启事务
。
下图是FragmentPagerAdapter.java中代码,分析思路是一样的:
接下来看另一个调用fragment.setUserVisibleHint(boolean)
的函数。
4.3 setPrimaryItem()
这个函数,最主要的调用就是被用户滑动切换fragment
的时候。
日志截图的另外一次调用在setPrimaryItem()
中。看一下这个函数合适何处被调用。查看源码,发现在populate(int newCurrentItem)
函数中被调用,而这个函数被这个populate()
调用。
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
一次是给BFragment
FragmentPagerAdapter.java和FragmentStatePagerAdapter.java一样的实现逻辑:
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
参数object
说明一下:要切换到的那个fragment
,比如从AFragment
切换到BFragment
,那么object
就是BFragment
。
这段代码触发的触发有两种:一是系统自动调用:发生在onMeasure()
阶段,二是手动
:包括点击和切换。
系统触发
系统触发显示默认的要展示的fragment
,这里指的是AFragment
。
调用发生在AFragment
的onCreateView()
方法之前,对于AFragment
:走红框部分,而不走白框。因为这个时候变量fragment
就是AFragment
,但是this.mCurrentPrimaryItem
此时为初始值null
,所以会走红框部分。然后,this.mCurrentPrimaryItem = fragment;
第一次被赋值,且指向即将可见的AFragment
。
- 白框部分:系统的调用时候,
this.mCurrentPrimaryItem
是初始值null
,不会走。其他情况出发都会走。并且走白框的时候,就是发生切换的时候,切换是走进这个if语句
的前提,当然是除了系统第一次调用这个函数的时候
。 - 红框部分:是的
this.mCurrentPrimaryItem
指向即将可见的fragment
。
但是,这个时候,依然没有去创建视图。所以会出现这样的情况:
image.png点击
点击的时候,只走一行代码,Fragment fragment = (Fragment)object;
;
因为点击的时候,object
没有发生改变,指向当前fragment
,this.mCurrentPrimaryItem
也指向当前fragment
。
切换
切换的时候,会走完这代码块的所有代码:
image.png- 白框部分将即将被切换掉的
fragment
设置为对用户不可见。 - 红框部分将即将可见的
fragment
设置为对用户可见。同时,将this.mCurrentPrimaryItem
指向即将可见的fragment
。
最终的日志
image.png