转载请附上原博客连接 https://www.jianshu.com/p/837cdd4cf371
懒加载的应用场景和作用:
在我们开发APP的过程中,经常会用到ViewPager+Fragment的UI结构(微信主界面就是这样组成的)。通过左右滑动,ViewPager加载不同的Fragment进行显示。ViewPager在加载当前应当显示的Fragment的时候,会同时将该Fragment前后相邻的Fragment也加载到内存中。这样,当几个Fragment中都有很多耗费资源的初始化操作时,可能会因网络阻塞造成当前页面初始化缓慢的问题。因此我们需要只在Fragment用户可见的时候才加载数据,这就是懒加载。
首先我们来证实一下在使用Fragment+ViewPager的时候。ViewPager会提前加载当前Fragment左右相邻的Fragment。
我们像往常一样,在主界面中使用ViewPager,如下面代码所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tinymonster.lazyloadtest.MainActivity">
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/viewPager"
/>
</LinearLayout>
自定义Fragment,在生命周期内打印信息。
public class MyFragment extends Fragment{
private String data;
public MyFragment(){
}
public MyFragment(String data){
this.data=data;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
Log.e(data,"onAttach");
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(data,"onCreate");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(data,"onCreateView");
View view=inflater.inflate(R.layout.my_fragment,null);
TextView textView=(TextView)view.findViewById(R.id.text);
textView.setText(data);
return view;
}
@Override
public void onStart() {
super.onStart();
Log.e(data,"onStart");
}
@Override
public void onResume() {
super.onResume();
Log.e(data,"onResume");
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(data,"setUserVisibleHint:"+isVisibleToUser);
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.e(data,"onDestroyView");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(data,"onDestroy");
}
}
在主界面代码中给ViewPager添加Fragment,并且设置首先显示第三个Fragment。
public class MainActivity extends AppCompatActivity {
private ViewPager viewPager;
private MyAdapter myAdapter;
List<Fragment> list=new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewPager=(ViewPager)findViewById(R.id.viewPager);
list.add(new MyFragment("Fragment_1"));
list.add(new MyFragment("Fragment_2"));
list.add(new MyFragment("Fragment_3"));
list.add(new MyFragment("Fragment_4"));
list.add(new MyFragment("Fragment_5"));
myAdapter=new MyAdapter(getSupportFragmentManager(),list);
viewPager.setAdapter(myAdapter);
viewPager.setCurrentItem(2);
}
}
运行程序,打印信息如下。
我们从打印信息中可以看出,ViewPager在加载Fragment_3的同时,也调用的Fragment_2和Fragment_4的生命周期(onAttach,onCreate,onCreateView,onStart,onResume)。因此,如果Fragment_2,Fragment_3和Fragment_4的初始化中有很多耗费网络资源的操作时,可能造成网络阻塞,使得需要显示的Fragment_3迟迟不能初始化。
ViewPager源码分析
为什么会在加载当前Fragment的时候也把他左右两个Fragment也加载到内存中呢?下面我们看一下ViewPager的源码。
首先我们看ViewPager中的onMeasure()。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 没有指定的情况下宽高为0 ;
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
final int measuredWidth = getMeasuredWidth();
final int maxGutterSize = measuredWidth / 10;
mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);
// 得到可用空间
int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
//计算每一个孩子所用空间
int size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp != null && lp.isDecor) {
final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
int widthMode = MeasureSpec.AT_MOST;
int heightMode = MeasureSpec.AT_MOST;
boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
if (consumeVertical) {
widthMode = MeasureSpec.EXACTLY;
} else if (consumeHorizontal) {
heightMode = MeasureSpec.EXACTLY;
}
int widthSize = childWidthSize;
int heightSize = childHeightSize;
if (lp.width != LayoutParams.WRAP_CONTENT) {
widthMode = MeasureSpec.EXACTLY;
if (lp.width != LayoutParams.MATCH_PARENT) {
widthSize = lp.width;
}
}
if (lp.height != LayoutParams.WRAP_CONTENT) {
heightMode = MeasureSpec.EXACTLY;
if (lp.height != LayoutParams.MATCH_PARENT) {
heightSize = lp.height;
}
}
final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
child.measure(widthSpec, heightSpec);
if (consumeVertical) {
childHeightSize -= child.getMeasuredHeight();
} else if (consumeHorizontal) {
childWidthSize -= child.getMeasuredWidth();
}
}
}
}
// 合成测量规格
mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);
// Make sure we have created all fragments that we need to have shown.
mInLayout = true;
//填充
populate();
mInLayout = false;
// 测量调用每一个孩子的measure()方法
size = getChildCount();
for (int i = 0; i < size; ++i) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
if (DEBUG) {
Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null || !lp.isDecor) {
final int widthSpec = MeasureSpec.makeMeasureSpec(
(int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
child.measure(widthSpec, mChildHeightMeasureSpec);
}
}
}
}
ViewPager的onMeasure()方法进行了如下操作:1.确定了自身可用空间,2.遍历每一个装饰view,测量他们的大小和计算剩余空间,3.调用populate()方法根据页面缓存显示进行页面销毁与重建,4.测量每一个孩子。
下面我们再看一下populate()方法。
void populate() {
// 将当前选中position放入
populate(mCurItem);
}
void populate(int newCurrentItem) {
// 旧的选中的 ItemInfo
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
//infoForPosition方法得到 旧 ItemInfo
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
//排序绘制view的顺序
sortChildDrawingOrder();
return;
}
if (mPopulatePending) {
if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
sortChildDrawingOrder();
return;
}
// 不在window中时
if (getWindowToken() == null) {
return;
}
// 调用 startUpdate 方法
mAdapter.startUpdate(this);
//页的限制
final int pageLimit = mOffscreenPageLimit;
//开始页
final int startPos = Math.max(0, mCurItem - pageLimit);
//总数
final int N = mAdapter.getCount();
// 结束页
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
// 如果预期数量不一致
if (N != mExpectedAdapterCount) {
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.
int curIndex = -1;
ItemInfo curItem = null;
for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
//得到当前 ItemInfo
final ItemInfo ii = mItems.get(curIndex);
//如果 position 大于 mCurItem
if (ii.position >= mCurItem) {
// cutlItem=ii
if (ii.position == mCurItem) curItem = ii;
break;
}
}
// 如果等于空 添加
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 如果 curItem 不为空
if (curItem != null) {
float extraWidthLeft = 0.f;
//-1
int itemIndex = curIndex - 1;
//ii 如果itemIndex 大于 0 取出,否则null
ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
//得到可用宽度
final int clientWidth = getClientWidth();
// 计算左边需要的宽度
final float leftWidthNeeded = clientWidth <= 0 ? 0 :
2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;
// 从'选中的下标-1' --> 0 遍历
for (int pos = mCurItem - 1; pos >= 0; pos--) {
//如果额外的 大于 需要的 和 pos 小于 startPos
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
// 移除
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((View) ii.object));
}
itemIndex--;
curIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
} else if (ii != null && pos == ii.position) { // 如果选中的上一个有 infoItem
// extraWidthLeft 加上 ii.widthFactor
extraWidthLeft += ii.widthFactor;
itemIndex--;
//拿上一个 infoItem
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
// 新增一个 INfoItem
ii = addNewItem(pos, itemIndex + 1);
// 将占用的宽度比例加进来
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
// 右边消耗的比例
float extraWidthRight = curItem.widthFactor;
//右边index
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++) {
// 如果 pos大于 endPos
if (extraWidthRight >= rightWidthNeeded && pos > endPos) {
if (ii == null) {
break;
}
//移除
if (pos == ii.position && !ii.scrolling) {
mItems.remove(itemIndex);
mAdapter.destroyItem(this, pos, ii.object);
if (DEBUG) {
Log.i(TAG, "populate() - destroyItem() with pos: " + pos
+ " view: " + ((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 {
// 否则 新建 infoItem
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
// 计算偏移量
calculatePageOffsets(curItem, curIndex, oldCurInfo);
}
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);
}
}
// 通知适配器 哪个个是主页
mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);
// 结束更新
mAdapter.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
// 遍历view
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
lp.childIndex = i;
if (!lp.isDecor && lp.widthFactor == 0.f) {
// 0 means requery the adapter for this, it doesn't have a valid width.
final ItemInfo ii = infoForChild(child);
if (ii != null) {
// 设置属性
lp.widthFactor = ii.widthFactor;
lp.position = ii.position;
}
}
}
//排序
sortChildDrawingOrder();
if (hasFocus()) {
View currentFocused = findFocus();
ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;
if (ii == null || ii.position != mCurItem) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
ii = infoForChild(child);
//如果拿到当前view
if (ii != null && ii.position == mCurItem) {
//设置聚焦
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
上面一段代码很长,主要内容就是首先根据mOffscreenPageLimit(页面限制,用于确定缓存Fragment的数量)和mCurItem(当前应显示页码)计算出了应加载的Fragment的页码数,
//页的限制
final int pageLimit = mOffscreenPageLimit;
//开始页
final int startPos = Math.max(0, mCurItem - pageLimit);
//总数
final int N = mAdapter.getCount();
// 结束页
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
然后根据计算得到的开始页和结束页调用Adapter移除或者加载Fragment。
原来ViewPager中有一个变量(mOffscreenPageLimit),用来确定加载的Fragment数量,那么我们修改这个变量不久行了吗?
继续往下看,ViewPager中有提供设置mOffscreenPageLimit的函数。
//默认的缓存页面数量(常量)
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();
}
}
根据上面代码我们可知,如果我们设置的mOffscreenPageLimit小于DEFAULT_OFFSCREEN_PAGE,就强制给mOffscreenPageLimit赋值为DEFAULT_OFFSCREEN_PAGE。这个DEFAULT_OFFSCREEN_PAGE的值就是1。
现在终于真相大白了,ViewPager通过一定的逻辑判断来确保至少会预加载左右两侧相邻的1个页面,我们不能通过简单设置mOffscreenPageLimit来达到懒加载的目的。
懒加载实现原理
还记得我们前面再Fragment中打印的生命周期吗?我还在这个函数setUserVisibleHint(boolean isVisibleToUser)中打印了信息。根据函数名我们可以猜出来这个函数与用户可见有关。查看这个函数的源码
/**
* 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.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
翻译上面一段注释:设置一个提示用来表示这个Fragment是否被用户可见。应用程序可以将此设置为false,以指示Fragment的UI被滚动到不可见的位置,或者用户无法直接看到。系统可以使用它来对诸如片段生命周期更新或加载器排序行为等操作进行优先级排序。这个函数可以在Fragment的生命周期以外调用。
总之,当Fragment从可见变为不可见是或者从不可见变为可见时都会自动调用这个函数,可见是输入值为TRUE,不可见时输入值为FALSE。因此我们可以根据这个函数的输入值来判断Fragment是否可见,从而进行懒加载。
因为Fragment在由可见变为不可见,和由不可见变为可见的时候都会调用setUserVisibleHint()函数,因此我们可以在这个函数实现懒加载的逻辑。在这个函数中,我们判断只有当Fragment可见 & Fragment的View已经加载完成 & Fragment没有加载过数据 的条件下才进行数据加载。
根据这样的思路,我们设置了几个标记位:
private boolean isViewInit; //view是否已经加载完成
private boolean isVisible; //是否可见
private boolean isFirstLoad = true;//是否第一次加载数据,默认为TURE,既默认为第一次加载数据
设置了一个函数来模拟从网络加载数据
/**
* 模拟加载数据
*/
protected void loadData(){
Log.e(data,"加载数据!");
}
然后将懒加载的逻辑写成下面一个函数
/**
* 懒加载逻辑
*/
private void lazyLoad() {
if (!isViewInit || !isVisible || !isFirstLoad) { //view加载完成&可见&第一次加载时才加载数据
return;
}
loadData(); //加载数据
isFirstLoad = false; //更新标志位
}
注意,别忘记了在onActivityCreated()中调用懒加载函数,因为第一个要显示的Fragment在由不可见变为可见调用setUserVisibleHint()函数时,该Fragment的View还没有初始化完成,导致第一个显示的Fragment无法第一次出现时无法加载数据,必须重新滑动一下才能加载数据。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInit=true; //设置标志位,view表示初始化完成
lazyLoad(); //懒加载
}
setUserVisibleHint()函数中调用懒加载。
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(data,"setUserVisibleHint:"+isVisibleToUser);
if(isVisibleToUser){
isVisible=true;
lazyLoad();
}else {
isVisible=false;
}
}
以上就是我对懒加载的理解。如果你觉得我写的有什么不对或者有什么补充的请留言。
代码已上传至GitHub https://github.com/Tiny-Monster/LazyLoadTest
参考博客
https://blog.csdn.net/chengkun_123/article/details/73694936
https://www.jianshu.com/p/b8fe093a9d4b