相信很多小伙伴在面试的时候,都会被问到性能优化这一块,当然对于一个成熟的app,这一块是必不可少的。我们在开发app的时候,经常会有这样的需求:底部或者顶部几个栏目,中间内容跟着栏目的切换而变化,需要用到的控件就是TabLayout+ViewPager。做出来这样的效果其实并不难,跟着官方文档也能做出来,但是你真正了解过ViewPager的缓存机制吗?你知道在viewPager中Fragment的生命周期是怎样的? 这就是懒加载的由来。AndroidX版本,系统自己也实现了懒加载机制,使用的是LifeCycle进行管理,所以,本篇文章将带大家从原理到实现,从v4版本到Androidx版本拿下懒加载。
首先大家需要了解什么是懒加载?所谓的懒加载就是需要的时候(界面即将展示给用户)在加载,不需要的(界面即将销毁)暂停页面的一切操作。比如我页面中有很多fragment,并且都需要在初始化的时候执行网络等耗时操作,加载一个fragment的耗时是2s,那么完全加载所有的fragment就需要消耗大量的cpu资源,导致页面卡顿。以以下demo为例:
优化前:
下面让我们根据不同版本的ViewPager实现这种懒加载优化。
首先需要了解,为什么会出现上面默认加载两个fragment的问题???我们知道可以通过设置Viewpager.setOffscreenPageLimit(num) 来设置默认缓存多少个fragment。跟进源码:
确定limit的值最少为1,并且默认为1,所以系统会默认缓存1个fragment,加上第一页显示的fragment,总共需要初始化2个fragment。当我们设置limit的数量大于1时,会进入populate()方法,这里就是真正实现fragment缓存的方法。
void populate(int newCurrentItem) {
...
//1、开始
mAdapter.startUpdate(this);
...
// 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);
}
if (curItem != null) {
//2、计算左边需要加入缓存的item
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;
}
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 {
//2、1创建新的item
ii = addNewItem(pos, itemIndex + 1);
extraWidthLeft += ii.widthFactor;
curIndex++;
ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
}
}
//3、计算右边需要加入缓存的item
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);
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 {
//3、1创建新的item
ii = addNewItem(pos, itemIndex);
itemIndex++;
extraWidthRight += ii.widthFactor;
ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
}
}
}
calculatePageOffsets(curItem, curIndex, oldCurInfo);
//4、设置当前需要显示的item
mAdapter.setPrimaryItem(this, mCurItem, curItem.object);
}
//5、结束
mAdapter.finishUpdate(this);
...
}
这个方法大致分为5个步骤:
这里采用了适配器模式,第二步和第三步过程中会新建新的item,并保存在ArrayList中,代码中变量为mItems。其实从这里也可以看到PagerAdapter的调用生命周期。
private final ArrayList<ItemInfo> mItems = new ArrayList<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;
}
每一个item信息被存放在ItemInfo 中:
static class ItemInfo {
Object object;
int position;
boolean scrolling;
float widthFactor;
float offset;
}
这个类包含当前item在视图中的位置position,object就是fragment对象本身。
那么这个object是如何被创建出来的呢???回到**addNewItem()**方法,注意下面一句代码
ii.object = mAdapter.instantiateItem(this, position);
这个instantiateItem方法在源码中是个空方法,需要使用者在子类中自己实现具体。这就不得不引出FragmentPagerAdapter,我们看看他是如何实现的???
--------FragmentPagerAdapter.java--------
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//mFragmentManager管理事务
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// 通过tag找到fragment
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
//将fragment添加到事务中
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
//新增fragment不为当前的fragment
if (fragment != mCurrentPrimaryItem) {
//调用setUserVisibleHint方法
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
//返回fragment对象
return fragment;
}
从上面的代码可以看到,创建fragment的过程中首先会调用它的setUserVisibleHint(false) 方法,并且是默认不可见的。根据上面分析ViewPager的生命周期,接下来会进入**setPrimaryItem()**方法
@Override
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;
}
}
将上一界面设置为不可见,将当前界面设置为可见,再次调用 setUserVisibleHint(true)。
总结:
ok,了解了上面的原理,就可以进行懒加载处理了。就是处理setUserVisibleHint() 关键方法
public abstract class LazyFragment extends Fragment {
protected View root;
//当前view是否创建
private boolean isViewCreated = false;
//当前view状态变化
private boolean beforeVisibleState = false;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
if(root == null){
root = getLayoutInflater().inflate(getLayoutResource(),container,false);
}
initView(root);
isViewCreated = true;
if(getUserVisibleHint()){
setUserVisibleHint(true);
}
return root;
}
@Override
public void onResume() {
super.onResume();
if (getUserVisibleHint()) {
dispatchHintState(true);
}
}
@Override
public void onPause() {
super.onPause();
if (getUserVisibleHint()) {
dispatchHintState(false);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
isViewCreated = false;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isViewCreated){
if(!beforeVisibleState && isVisibleToUser){ //从不可见到可见
dispatchHintState(true);
}else if(beforeVisibleState && !isVisibleToUser){ //从可见到不可见
dispatchHintState(false);
}
}
}
private void dispatchHintState(boolean state){
//避免走两次,因为onResume会再次调用
if(beforeVisibleState == state){
return;
}
beforeVisibleState = state;
if(state){
startLoadData();
}else{
stopLoadData();
}
}
/**
* 停止加载数据
*/
protected void stopLoadData() {
}
/**
* 开始加载数据
*/
protected void startLoadData() {
}
protected abstract void initView(View root);
protected abstract int getLayoutResource();
}
所有的fragment继承这个LazyFragment,重写需要的方法就行了。
对于这个类需要注意几个点:
ok,以上就完成了旧版本的Viewpager嵌套fragment的懒加载机制。试试吧!!!
对于androidX,抛弃了setUserVisibleHint这个方法,底层使用LifeCycle来绑定fragment生命周期,让我们来看看原理吧。
同样进入到PagerAdapter的实现类FragmentPagerAdapter中,看他是如何创建一个Item对象的。
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
... 和之前代码一样
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
//当满足条件时,为事务添加最大走到的生命周期为onStart
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
//和v4的分析一致,做了兼容
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
所以合理重点分析
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment,
@NonNull Lifecycle.State state) {
addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
return this;
}
这里开始创建一个Op对象,走一下构造方法
Op(int cmd, Fragment fragment) {
this.mCmd = cmd;
this.mFragment = fragment;
this.mOldMaxState = Lifecycle.State.RESUMED;
this.mCurrentMaxState = Lifecycle.State.RESUMED;
}
Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) {
this.mCmd = cmd;
this.mFragment = fragment;
this.mOldMaxState = fragment.mMaxState;
this.mCurrentMaxState = state;
}
看到这里再对比之前,我们知道,v4版本的fragment在初始化的时候会走到onResume生命周期,而androidX则只会走到onStart()方法,所以我们可以判断是否执行onResume()方法来判断当前fragment是否可见。
public abstract class LazyFragment extends Fragment {
protected View root;
//当前view状态变化
private boolean beforeVisibleState = false;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
if(root == null){
root = getLayoutInflater().inflate(getLayoutResource(),container,false);
}
initView(root);
return root;
}
@Override
public void onResume() {
super.onResume();
if (!beforeVisibleState && isResumed()) {
dispatchHintState(true);
}
beforeVisibleState = true;
}
@Override
public void onPause() {
super.onPause();
if (beforeVisibleState && !isResumed()) {
dispatchHintState(false);
}
beforeVisibleState = false;
}
private void dispatchHintState(boolean state){
//避免走两次,因为onResume会再次调用
if(beforeVisibleState == state){
return;
}
beforeVisibleState = state;
if(state){
startLoadData();
}else{
stopLoadData();
}
}
/**
* 停止加载数据
*/
protected void stopLoadData() {
}
/**
* 开始加载数据
*/
protected void startLoadData() {
}
protected abstract void initView(View root);
protected abstract int getLayoutResource();
}
只需要在onResume和onPause方法中控制逻辑即可。在activity中需要这样使用FragmentPagerAdapter
FragmentPagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager(),BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
@NonNull
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
};
构造方法传入BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。这样就可以实现懒加载了。
懒加载源码