轉(zhuǎn)載請附上原博客連接 http://www.reibang.com/p/837cdd4cf371
懶加載的應(yīng)用場景和作用:
在我們開發(fā)APP的過程中痪欲,經(jīng)常會用到ViewPager+Fragment的UI結(jié)構(gòu)(微信主界面就是這樣組成的)每界。通過左右滑動七兜,ViewPager加載不同的Fragment進(jìn)行顯示泥从。ViewPager在加載當(dāng)前應(yīng)當(dāng)顯示的Fragment的時候,會同時將該Fragment前后相鄰的Fragment也加載到內(nèi)存中缸濒。這樣妇拯,當(dāng)幾個Fragment中都有很多耗費資源的初始化操作時,可能會因網(wǎng)絡(luò)阻塞造成當(dāng)前頁面初始化緩慢的問題脓钾。因此我們需要只在Fragment用戶可見的時候才加載數(shù)據(jù)售睹,這就是懶加載。
首先我們來證實一下在使用Fragment+ViewPager的時候可训。ViewPager會提前加載當(dāng)前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捺宗,在生命周期內(nèi)打印信息。
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川蒙,并且設(shè)置首先顯示第三個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);
}
}
運(yùn)行程序,打印信息如下畜眨。
我們從打印信息中可以看出昼牛,ViewPager在加載Fragment_3的同時,也調(diào)用的Fragment_2和Fragment_4的生命周期(onAttach,onCreate,onCreateView,onStart,onResume)康聂。因此贰健,如果Fragment_2,F(xiàn)ragment_3和Fragment_4的初始化中有很多耗費網(wǎng)絡(luò)資源的操作時恬汁,可能造成網(wǎng)絡(luò)阻塞伶椿,使得需要顯示的Fragment_3遲遲不能初始化。
ViewPager源碼分析
為什么會在加載當(dāng)前Fragment的時候也把他左右兩個Fragment也加載到內(nèi)存中呢?下面我們看一下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();
}
}
}
}
// 合成測量規(guī)格
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;
// 測量調(diào)用每一個孩子的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()方法進(jìn)行了如下操作:1.確定了自身可用空間,2.遍歷每一個裝飾view偎痛,測量他們的大小和計算剩余空間旱捧,3.調(diào)用populate()方法根據(jù)頁面緩存顯示進(jìn)行頁面銷毀與重建,4.測量每一個孩子踩麦。
下面我們再看一下populate()方法枚赡。
void populate() {
// 將當(dāng)前選中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;
}
// 調(diào)用 startUpdate 方法
mAdapter.startUpdate(this);
//頁的限制
final int pageLimit = mOffscreenPageLimit;
//開始頁
final int startPos = Math.max(0, mCurItem - pageLimit);
//總數(shù)
final int N = mAdapter.getCount();
// 結(jié)束頁
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
// 如果預(yù)期數(shù)量不一致
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++) {
//得到當(dāng)前 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;
// 從'選中的下標(biāo)-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);
// 將占用的寬度比例加進(jìn)來
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) {
// 相等 那么取出數(shù)據(jù)
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);
// 結(jié)束更新
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) {
// 設(shè)置屬性
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);
//如果拿到當(dāng)前view
if (ii != null && ii.position == mCurItem) {
//設(shè)置聚焦
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
上面一段代碼很長谓谦,主要內(nèi)容就是首先根據(jù)mOffscreenPageLimit(頁面限制贫橙,用于確定緩存Fragment的數(shù)量)和mCurItem(當(dāng)前應(yīng)顯示頁碼)計算出了應(yīng)加載的Fragment的頁碼數(shù),
//頁的限制
final int pageLimit = mOffscreenPageLimit;
//開始頁
final int startPos = Math.max(0, mCurItem - pageLimit);
//總數(shù)
final int N = mAdapter.getCount();
// 結(jié)束頁
final int endPos = Math.min(N - 1, mCurItem + pageLimit);
然后根據(jù)計算得到的開始頁和結(jié)束頁調(diào)用Adapter移除或者加載Fragment反粥。
原來ViewPager中有一個變量(mOffscreenPageLimit)卢肃,用來確定加載的Fragment數(shù)量,那么我們修改這個變量不久行了嗎星压?
繼續(xù)往下看践剂,ViewPager中有提供設(shè)置mOffscreenPageLimit的函數(shù)鬼譬。
//默認(rèn)的緩存頁面數(shù)量(常量)
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
//緩存頁面數(shù)量(變量)
private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;
public void setOffscreenPageLimit(int limit) {
//當(dāng)我們手動設(shè)置的limit數(shù)小于默認(rèn)值1時,limit值會自動被賦值為默認(rèn)值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) {
//經(jīng)過前面的攔截判斷后,將limit的值設(shè)置給mOffscreenPageLimit,用于
mOffscreenPageLimit = limit;
populate();
}
}
根據(jù)上面代碼我們可知娜膘,如果我們設(shè)置的mOffscreenPageLimit小于DEFAULT_OFFSCREEN_PAGE,就強(qiáng)制給mOffscreenPageLimit賦值為DEFAULT_OFFSCREEN_PAGE优质。這個DEFAULT_OFFSCREEN_PAGE的值就是1竣贪。
現(xiàn)在終于真相大白了,ViewPager通過一定的邏輯判斷來確保至少會預(yù)加載左右兩側(cè)相鄰的1個頁面巩螃,我們不能通過簡單設(shè)置mOffscreenPageLimit來達(dá)到懶加載的目的演怎。
懶加載實現(xiàn)原理
還記得我們前面再Fragment中打印的生命周期嗎?我還在這個函數(shù)setUserVisibleHint(boolean isVisibleToUser)中打印了信息避乏。根據(jù)函數(shù)名我們可以猜出來這個函數(shù)與用戶可見有關(guān)爷耀。查看這個函數(shù)的源碼
/**
* 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;
}
翻譯上面一段注釋:設(shè)置一個提示用來表示這個Fragment是否被用戶可見。應(yīng)用程序可以將此設(shè)置為false拍皮,以指示Fragment的UI被滾動到不可見的位置歹叮,或者用戶無法直接看到。系統(tǒng)可以使用它來對諸如片段生命周期更新或加載器排序行為等操作進(jìn)行優(yōu)先級排序铆帽。這個函數(shù)可以在Fragment的生命周期以外調(diào)用咆耿。
總之,當(dāng)Fragment從可見變?yōu)椴豢梢娛腔蛘邚牟豢梢娮優(yōu)榭梢姇r都會自動調(diào)用這個函數(shù)爹橱,可見是輸入值為TRUE萨螺,不可見時輸入值為FALSE。因此我們可以根據(jù)這個函數(shù)的輸入值來判斷Fragment是否可見,從而進(jìn)行懶加載慰技。
因為Fragment在由可見變?yōu)椴豢梢娡终担陀刹豢梢娮優(yōu)榭梢姷臅r候都會調(diào)用setUserVisibleHint()函數(shù),因此我們可以在這個函數(shù)實現(xiàn)懶加載的邏輯惹盼。在這個函數(shù)中庸汗,我們判斷只有當(dāng)Fragment可見 & Fragment的View已經(jīng)加載完成 & Fragment沒有加載過數(shù)據(jù) 的條件下才進(jìn)行數(shù)據(jù)加載。
根據(jù)這樣的思路手报,我們設(shè)置了幾個標(biāo)記位:
private boolean isViewInit; //view是否已經(jīng)加載完成
private boolean isVisible; //是否可見
private boolean isFirstLoad = true;//是否第一次加載數(shù)據(jù)蚯舱,默認(rèn)為TURE,既默認(rèn)為第一次加載數(shù)據(jù)
設(shè)置了一個函數(shù)來模擬從網(wǎng)絡(luò)加載數(shù)據(jù)
/**
* 模擬加載數(shù)據(jù)
*/
protected void loadData(){
Log.e(data,"加載數(shù)據(jù)掩蛤!");
}
然后將懶加載的邏輯寫成下面一個函數(shù)
/**
* 懶加載邏輯
*/
private void lazyLoad() {
if (!isViewInit || !isVisible || !isFirstLoad) { //view加載完成&可見&第一次加載時才加載數(shù)據(jù)
return;
}
loadData(); //加載數(shù)據(jù)
isFirstLoad = false; //更新標(biāo)志位
}
注意枉昏,別忘記了在onActivityCreated()中調(diào)用懶加載函數(shù),因為第一個要顯示的Fragment在由不可見變?yōu)榭梢娬{(diào)用setUserVisibleHint()函數(shù)時揍鸟,該Fragment的View還沒有初始化完成兄裂,導(dǎo)致第一個顯示的Fragment無法第一次出現(xiàn)時無法加載數(shù)據(jù),必須重新滑動一下才能加載數(shù)據(jù)阳藻。
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
isViewInit=true; //設(shè)置標(biāo)志位晰奖,view表示初始化完成
lazyLoad(); //懶加載
}
setUserVisibleHint()函數(shù)中調(diào)用懶加載。
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(data,"setUserVisibleHint:"+isVisibleToUser);
if(isVisibleToUser){
isVisible=true;
lazyLoad();
}else {
isVisible=false;
}
}
以上就是我對懶加載的理解腥泥。如果你覺得我寫的有什么不對或者有什么補(bǔ)充的請留言匾南。
代碼已上傳至GitHub https://github.com/Tiny-Monster/LazyLoadTest
參考博客
https://blog.csdn.net/chengkun_123/article/details/73694936
http://www.reibang.com/p/b8fe093a9d4b