对于Fragment的处理,上一节中介绍了使用Navigation导航Fragment页面,当使用ViewPager + Fragment架构页面时,就会出现缓存问题,当然这不是因为Fragment有缓存功能。
真正让Fragment产生预加载的原因就是ViewPager的缓存机制,ViewPager默认情况下会有1页的缓存,但是这个1页的含义就是会缓存当前页面的左右1页的数据,也就是说,当前页面的左右1页有数据加载的操作,那么就会执行预加载。虽然我们没有切换到该页面,但是该页面已经进行了预加载,这就会造成不必要的资源浪费。
当Fragment界面进行预加载的时候,带来的性能问题就是UI卡顿。默认情况下,我们认为就是当前页面在网络请求加载数据,但是不知道的是,其他页面也在网络请求加载数据,这种情况下就导致当前页面加载很慢,页面渲染的时候拉长,就造成了UI的卡顿。
1、ViewPager的适配器模式
在Android中,提供了2种适配器,分别为FragmentPagerAdapter
和FragmentStatePagerAdapter
,这两种适配器适配对象都是Fragment。
在ViewPager
滑动的过程中,所经历的生命周期主要分为以下4步,就是populate
方法:
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (mAdapter == null) {
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();
#终点位置
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++) {
final ItemInfo ii = mItems.get(curIndex);
if (ii.position >= mCurItem) {
if (ii.position == mCurItem) curItem = ii;
break;
}
}
if (curItem == null && N > 0) {
curItem = addNewItem(mCurItem, curIndex);
}
// 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.
#第二步:开始适配数据
if (curItem != null) {
float extraWidthLeft = 0.f;
int itemIndex = curIndex - 1;
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;
for (int pos = mCurItem - 1; pos >= 0; pos--) {
if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
if (ii == null) {
break;
}
#for循环清除起始startPos之外的缓存数据
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) {
extraWidthLeft += ii.widthFactor;
itemIndex--;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
} else {
#添加Item
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;
}
#for循环清除endPos之外的缓存数据
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 {
#添加新的数据
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
#第3步:设置当前页面
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
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.finishUpdate(this);
// Check width measurement of current pages and drawing sort order.
// Update LayoutParams as needed.
final int childCount = getChildCount();
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);
if (ii != null && ii.position == mCurItem) {
if (child.requestFocus(View.FOCUS_FORWARD)) {
break;
}
}
}
}
}
}
(1)调用适配器的startUpdate
开始滑动更新;
(2)一开始根据PageLimit获取缓存量级,得到缓存的起始位置坐标和终点位置坐标,当滑动的时候,一部分会从缓存区移除,一部分会进入缓存区,因此左右两边都开始对缓存区以外的数据destoryItem
,对于新添加进来的addItem
。
(3)当滑动完成之后,设置一下当前页面setPrimaryItem
。
(4)调用适配器的finishUpdate
结束更新。
2、setUserVisiableHint的生命周期
假设当前页面有4个Tab界面,pageLimit = 1
。
如果当页面从Tab1跳转到Tab3,因为Tab2已经被初始化缓存过了,所以跳转到Tab4首先要缓存Tab4。
(1)缓存Tab4,setUserVisiableHint(false)
;
(2)Tab1从缓存区移除,setUserVisiableHint(false)
;
(3)Tab3设置为当前页面,setUserVisiableHint(true)
;
(4)最后才开始Fragment的生命周期。
@Override
public void finishUpdate(@NonNull ViewGroup container) {
if (mCurTransaction != null) {
// We drop any transactions that attempt to be committed
// from a re-entrant call to finishUpdate(). We need to
// do this as a workaround for Robolectric running measure/layout
// calls inline rather than allowing them to be posted
// as they would on a real device.
if (!mExecutingFinishUpdate) {
try {
mExecutingFinishUpdate = true;
#这个时候才commit提交,开始Fragment的生命周期
mCurTransaction.commitNowAllowingStateLoss();
} finally {
mExecutingFinishUpdate = false;
}
}
mCurTransaction = null;
}
}
也就是说在setUserVisiableHint
之后,才进行Fragment的布局加载和数据加载,这是懒加载的关键之处。
3、懒加载框架搭建
public abstract class LazyFragment extends Fragment {
private static final String LAZY_LOAD_TAG = "lazy_load";
private View rootView;
//View是否初始化完成
private boolean isViewCreated;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(LAZY_LOAD_TAG,"onCreateView");
if(rootView == null) {
rootView = LayoutInflater.from(getContext()).inflate(getLayout(), container, false);
}
isViewCreated = true;
initView(rootView);
if(getUserVisibleHint()){
//如果当前页面是可见的,那么就分发事件加载
dispatchUserVisibleHint(true);
}
return rootView;
}
protected abstract void initView(View rootView);
protected abstract int getLayout();
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
//判断当前Fragment是否可见
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(LAZY_LOAD_TAG,"setUserVisibleHint");
//当view初始化完成之后,才可以加载
if(isViewCreated) {
if (isVisibleToUser == true) {
//当前Fragment可见,分发事件加载数据
dispatchUserVisibleHint(true);
} else {
dispatchUserVisibleHint(false);
}
}
}
//分发事件:加载数据
private void dispatchUserVisibleHint(boolean isVisibleToUser) {
Log.e(LAZY_LOAD_TAG,"dispatchUserVisibleHint");
if(isVisibleToUser){
//数据加载
onFragmentLoad();
}else{
//停止数据加载
onFragmentStop();
}
}
protected void onFragmentStop() {
}
protected void onFragmentLoad() {
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
}
在进行懒加载的时候,需要注意的是,因为setUserVisibleHint
是在View初始化之前就执行的,所以懒加载的时候,必须要等待View初始化完成之后,才可以进行事件分发,因此设置一个标志位isViewCreated
,判断当前页面是否加载完成。
当默认加载首页的时候,因为只有在setUserVisibleHint
为可见的时候,才会分发事件,但是这个时候,主界面还没有初始化完成,也就是说主界面会一直卡在setUserVisibleHint
位置,因为isViewCreated
为false,所以就不会进行数据加载。
if(getUserVisibleHint()){
//如果当前页面是可见的,那么就分发事件加载
dispatchUserVisibleHint(true);
}
因此在当前界面需要判断该页面是否可见,如果可见,那么就可以分发事件加载数据。
目前来说,在相邻页面之间切换时,是没有问题的,但是在跨页面跳转的时候,比如从Tab4跳转到Tab2,那么会出现以下的场景。
2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.601 20974-20974/com.example.lazyload E/TAG: hisFragment数据停止加载
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/TAG: shopFragment数据停止加载
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:02:39.602 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.603 20974-20974/com.example.lazyload E/TAG: hisFragment数据加载
2020-05-07 20:02:39.606 20974-20974/com.example.lazyload E/lazy_load: onCreateView
2020-05-07 20:02:39.607 20974-20974/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:02:39.607 20974-20974/com.example.lazyload E/TAG: hisFragment数据加载
2020-05-07 20:02:39.614 20974-20974/com.example.lazyload E/lazy_load: onCreateView
当跳转到Tab2的时候,首先Tab2会销毁一次数据,然后又加载了2次数据,这是不允许的,这样就导致资源的浪费,所以这就涉及到了懒加载时机的问题。
懒加载的时机:从不可见到可见的那一刻;
停止加载数据:从可见到不可见的那一刻。
因此还要设置一个标志位,判断当前界面是不是可见的currentVisiableState = false
//判断当前Fragment是否可见
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(LAZY_LOAD_TAG,"setUserVisibleHint");
//当view初始化完成之后,才可以加载
if(isViewCreated) {
//当前可见状态从不可见----可见
if (isVisibleToUser == true && !currentVisiableState) {
//当前Fragment可见,分发事件加载数据
dispatchUserVisibleHint(true);
} else if(isVisibleToUser = false && currentVisiableState){ //可见变为不可见
dispatchUserVisibleHint(false);
}
}
}
//分发事件:加载数据
private void dispatchUserVisibleHint(boolean isVisibleToUser) {
Log.e(LAZY_LOAD_TAG,"dispatchUserVisibleHint");
//当前可见状态
currentVisiableState = isVisibleToUser;
if(isVisibleToUser){
//数据加载
onFragmentLoad();
}else{
//停止数据加载
onFragmentStop();
}
}
2020-05-07 20:31:10.164 22590-22590/com.example.lazyload E/lazy_load: setUserVisibleHint
2020-05-07 20:31:10.170 22590-22590/com.example.lazyload E/lazy_load: onCreateView
2020-05-07 20:31:10.171 22590-22590/com.example.lazyload E/lazy_load: dispatchUserVisibleHint
2020-05-07 20:31:10.171 22590-22590/com.example.lazyload E/TAG: AboutFragment数据加载
2020-05-07 20:31:10.178 22590-22590/com.example.lazyload E/lazy_load: onCreateView
此时还有一个问题就是,当从一个Fragment界面跳转到一个Activity界面的时候,要执行Fragment的onPause方法,当回到宿主Activity的时候,需要执行Fragment的onResume方法。
当离开宿主Activity的时候,需要停止加载当前页面的数据。
@Override
public void onResume() {
super.onResume();
//当
if(!currentVisiableState && getUserVisibleHint()){
dispatchUserVisibleHint(true);
}
}
@Override
public void onPause() {
super.onPause();
//当前页面可见的时候,发送停止加载指令
if(currentVisiableState && getUserVisibleHint()){
dispatchUserVisibleHint(false);
}
}
当DestoryView的时候,需要把所有的状态值复位。
@Override
public void onDestroyView() {
super.onDestroyView();
isViewCreated = false;
currentVisiableState = false;
}