如何应对Android面试官-> 玩转 ViewPager 懒加载

前言

ViewPager 缓存页面与预加载机制

通常我们 ViewPager 在使用的是一般都是结合 Fragment 一起使用,我们先来搭一个简单的使用界面,最终搭建出来的效果如下:

简单的 ViewPager + Fragment 的实现,比较简单,就不贴代码的实现了,我们接下来内容的讲解;

如何给 ViewPager 设置缓存?

ini 复制代码
mViewPager.setOffscreenPageLimit(2);

传递几就缓存几个页面?;那么如果我们传递 0,就不会进行缓存了吗?以及它到底是怎么缓存的呢?又是缓存了几个呢?假设我们传递进去的是 2,当我们滑动到 F3 的时候

那么缓存的就是 左边 2 个,右边 2 个,加上当前的 F3 的缓存,一共就是缓存了 5 个;

如果停留在 F2,则左边只能缓存 1 个,右边缓存 2 个,加上当前的 F2 的缓存,一共是 4 个;

如果停留在 F1,则左边没有能缓存的,右边缓存 2 个,加上当前的 F1 的缓存,一共是 3 个;

同理,停留在 F4 和 F5 一样的缓存个数;

如果 setOffscreenPageLimit 设置的是 1,当前停留在 F3 则左右两边各缓存 1 个,加上当前的 F3 的缓存,一共是缓存了 3 个;

其他的都会被销毁;

如果 setOffscreenPageLimit 设置的是 0,则无效,会改成 1;

arduino 复制代码
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

如果传递进去的是 0,则无效,ViewPager 会强制更改成 1;

如何给 ViewPager 设置预加载?

ini 复制代码
mViewPager.setOffscreenPageLimit(2);

也是通过这个方法,进行页面的预加载,但是和缓存有一些区别

如果当前切换到了 F1 则预加载 F2、F3,如果从 F1 切换到了 F3,则预加载 F4、F5;

预加载只会预加载滚动方向的前 x 个,如果是从F1 -> F5,那么预加载的就是右边的 x 个,如果是从 F5 -> F1,就是预加载左边的 x 个;

ViewPager 的预加载会带来什么问题?

因为 setOffscreenPageLimit 设置 0 无效,会强制更改成 1,如果我们从 F2 切换到 F3 的时候,会预加载 F4,但是此时 F4 还不可见,但是系统给我们进行了预加载,如果 F4 上做了大量的网络请求,就会造成资源的浪费,影响性能;

预加载的越多,浪费的性能就越多;如果一个 Fragment 占用 1M,后面预加载的个数比较多的情况下,容易OOM;预加载的 Fragment 在进行网络请求,会浪费流量,还会卡顿;

ViewPager 的懒加载机制

为了规避预加载的问题,我们需要使用懒加载机制来规避这个问题,减少性能消耗;

什么是懒加载,以及如何实现?

页面可见的时候才进行加载;依托这个 setUserVisibleHint(isVisibleToUser: Boolean) 来实现懒加载;

ViewPager渲染原理

onMeasure/setAdapter/setOffscreenPageLimit 等入口最终都会走到 populate 这个方法,我们进入这个方法看下,这个方法比较长,我们只需要找其中的关键点即可,因为 ViewPager 依赖 Adapter 才能渲染 View,所以我们只需要在这个方法中找到 mAdapter 的相关调用:

kotlin 复制代码
mAdapter.startUpdate(this);

mAdapter.destroyItem(this, pos, ii.object);

mAdapter.setPrimaryItem(this, mCurItem, curItem.object);

mAdapter.finishUpdate(this);

mAdapter.instantiateItem(this, position);

这几个方法对应的功能如下:

startUpdate

startUpdate 默认空实现,开放给开发者一些初始化的准备工作,在开发者自己的 Adapter 中复写实现;

less 复制代码
public void startUpdate(@NonNull View container) {}

instantiateItem

创建适配的 item 数据,这里进行的操作就是把我们的 Fragment 加到事务管理器中;

scss 复制代码
public Object instantiateItem(@NonNull ViewGroup container, int position) {    
    if (mCurTransaction == null) { 
        // 获取事务管理器       
        mCurTransaction = mFragmentManager.beginTransaction();    
    }    
    final long itemId = getItemId(position);    
    // Do we already have this fragment?    
    String name = makeFragmentName(container.getId(), itemId);    
    Fragment fragment = mFragmentManager.findFragmentByTag(name);    
    if (fragment != null) {
        // 如果 Fragment 已经存在,直接调用 attach 方法,最终调用到 onAttach 方法                       
        mCurTransaction.attach(fragment);    
    } else {
        // 从我们自定义的 Adapter 中获取实例化的 Fragment;        
        fragment = getItem(position);        
        // 获取到实例化的 Fragment 后并添加到事务管理器中        
        mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));    
    }    
    if (fragment != mCurrentPrimaryItem) {        
        fragment.setMenuVisibility(false);
        // 设置 Fragment 不可见        
        fragment.setUserVisibleHint(false);    
    }    
    return fragment;
}

setPrimaryItem

设置当前显示的 item 的数据,这里就只是进行了 setUserVisibleHint 的操作;

less 复制代码
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {    
    Fragment fragment = (Fragment)object;    
    if (fragment != mCurrentPrimaryItem) {        
        if (mCurrentPrimaryItem != null) { 
            // 非当前 Fragment 设置成 false           
            mCurrentPrimaryItem.setMenuVisibility(false);            
            mCurrentPrimaryItem.setUserVisibleHint(false);        
        }        
        fragment.setMenuVisibility(true);
        // 设置当前 item 可见        
        fragment.setUserVisibleHint(true);        
        mCurrentPrimaryItem = fragment;    
    }
}

finishUpdate

less 复制代码
public void finishUpdate(@NonNull ViewGroup container) {    
    if (mCurTransaction != null) {        
        mCurTransaction.commitNowAllowingStateLoss();        
        mCurTransaction = null;    
    }
}

这里进行了事务管理器的激活,也就是当调用了 comitNowAllowingStateLoss 方法之后,Fragment 的生命周期就开始执行了;

通过 Adapter 的调用顺序(setPrimaryItem - finishUpdate)我们可以看到,setUserVisibleHint 的执行顺序是早于 Fragment 的生命周期的

我们通过下面这张图可以看下 Fragment 的状态

当我们从 tab1 切换到 tab2 的时候,tab3 执行的是 setUserVisibleHint(false),tab1 执行的也是 setUserVisibleHint(false),只有 tab2 执行的是 setUserVisibleHint(true)

Fragment 的生命周期打印如下:

所以,我们就可以根据 setUserVisibleHint 的可见与不可见,来管理 Fragment 是否要懒加载;

为了方便事件的分发和生命周期管理,我们这里实现了一个 BaseLazyFragment

java 复制代码
public abstract class BaseLazyFragment extends Fragment {    
    private View rootView = null;    
    private boolean isViewCreated = false;    
    @Nullable    
    @Override    
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        
        Log.e("","onCreateView: ");        
        if (rootView == null) {            
            rootView = inflater.inflate(getLayoutRes(),container, false);        
        }        
        isViewCreated  = true;        
        initView(rootView);        
        return rootView;    
    }    
    // 让子类完成,初始化布局,初始化控件    
    protected abstract void initView(View rootView);    
    protected abstract int getLayoutRes();    
   
    @Override    
    public void onResume() {        
        super.onResume();        
        Log.e("","onResume");    
    }    
    @Override    
    public void onPause() {        
        super.onPause();        
        Log.e("","onPause");    
    }    
    @Override    
    public void onDestroyView() {        
        super.onDestroyView();        
        Log.e("","onDestroyView");    
    }
}

第一次懒加载优化

根据 setUserVisibleHint 判断 Framgent 是否可见来进行懒加载;

scss 复制代码
public void setUserVisibleHint(boolean isVisibleToUser) {    
    super.setUserVisibleHint(isVisibleToUser);    
    Log.e("","setUserVisibleHint: ");    
    if (isVisibleToUser) {        
        // 加载数据        
        onFragmentLoadStart();    
    } else {        
        // 停掉数据        
        onFragmentLoadStop();    
    }
}

抽取了 BaseLazyFragment,我们需要一个子 Fragment 继承这个 BaseLazyFragment 实现相关方法

java 复制代码
public class MyFragment extends BaseLazyFragment {    
    private static final String TAG = "MyFragment";    
    public static final String POSITION = "Position";    
    private ImageView imageView;    
    private TextView textView;    
    private int tabIndex;    
    public static MyFragment newInstance(int position) {        
        Bundle bundle = new Bundle();        
        bundle.putInt("Position", position);        
        MyFragment fragment = new MyFragment();        
        fragment.setArguments(bundle);        
        return fragment;    
    }    

    @Override    
    protected void initView(View view) {        
        Log.d(TAG, tabIndex + " fragment " + "initView: " );        
        tabIndex = getArguments().getInt(POSITION);        
        imageView = view.findViewById(R.id.iv_content);        
        textView = view.findViewById(R.id.tv_loading);    
    }    
    @Override    
    protected int getLayoutRes() {        
        return R.layout.fragment_vp;    
    }    
    @Override    
    public void onFragmentLoad() {        
        super.onFragmentLoad();        
        textView.setText("startLoad");    
    }    
    @Override    
    public void onFragmentLoadStop() {        
        super.onFragmentLoadStop();        
        textView.setText("stopLoad");    
    }
}

一个简单实现的子类,然后我们编译运行看下效果,结果程序运行,我们直接发生了崩溃:

textView 的空指针异常,我们明明 findViewById 了,但是还是报了空指针异常,我们来看看怎么回事?

根据事件分发我们直到,setUserVisibleHint 早于 Fragment 的生命周期,也就是说分发 onFragmentLoadStop 的 Fragment 还没有创建以及初始化相关 View;所以我们在分发 onFragmentLoadStop 的时候,需要判断下 View 是不是已经创建了,创建了才执行 View 的更新操作;

typescript 复制代码
private boolean isViewCreated = false;

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {    
    super.setUserVisibleHint(isVisibleToUser);    
    Log.e("","setUserVisibleHint: ");    
    if (isViewCreated) {        
        if (isVisibleToUser) {            
            // 加载数据            
            onFragmentLoad();        
        } else {            
            // 停掉数据            
            onFragmentLoadStop();        
        }     
    }
}

我们在运行看下效果:

可以看到,运行不崩溃了,并且每次 Fragment 可见的时候,才会执行 startLoad 逻辑;但是,我们的第一个 Fragment 却一直都是 loading 态

这是为什么呢?还是事件分发的问题,setUserVisibleHint 早于 Fragment 的生命周期,当分发为 true 之后,Fragment 的生命周期才执行,也就是说第一个 Fragment 可见的时候,isViewCreated 还是 false,所以导致 setUserVisibleHint 为 true 的时候无法执行 onFragmentLoad 方法,导致 UI 无法更新,一直显示 loading 中;

第二次懒加载优化

解决第一个 Fragment 状态不更新的问题,所以需要我们在 onCreateView 中根据当前 Fragment 可见的时候,手动分发下状态,修改如下:

less 复制代码
public View onCreateView(@NonNull LayoutInflater inflater,                         
                        @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {    
    Log.e("","onCreateView: ");    
    if (rootView == null) {        
        rootView = inflater.inflate(getLayoutRes(), container, false);    
    }    
    initView(rootView);    
    isViewCreated  = true;    
    if (getUserVisibleHint()) {        
        setUserVisibleHint(true);    
    }    
    return rootView;
}

我们运行看下效果:

可以看到,第一次进来的时候,不再是 loading 了,更新成 startLoad 了;但是如果这个时候我们来看下生命周期的调用会发现,存在重复加载更新和重复暂停更新的问题

也就说 从 不可见 到 可见 才算 可见 (变化的过程)

从 可见 到 不可见 才算 不可见(变化的过程),那么这个问题我们应该如何处理呢?

第三次懒加载优化

我们需要记录上一次状态和本地状态,做对比,如果一样则不处理,不一样才处理;

scss 复制代码
public void setUserVisibleHint(boolean isVisibleToUser) {    
    super.setUserVisibleHint(isVisibleToUser);    
    Log.e(TAG,"setUserVisibleHint: ");    
    if (isViewCreated) {        
        if (isVisibleToUser && !isVisibleStateUp) {            
            // 当前可见,但是上一次不可见            
            dispatchVisibleHint(true);        
        } else if (!isVisibleToUser && isVisibleStateUp) {            
            // 当前不可见,但是上一次可见            
            dispatchVisibleHint(false);        
        }    
    }
}
// 
private void dispatchVisibleHint(boolean isVisibleToUser) {    
    isVisibleStateUp = isVisibleToUser;    
    if (isVisibleToUser) {        
        // 加载数据        
        onFragmentLoad();    
    } else {        
        // 停掉数据        
        onFragmentLoadStop();    
    }
}

我们运行看下效果,可以看到,我们的调用栈都只调用了一次,似乎已经很完美了;

第四次懒加载优化

但是如果我们在 Fragment 上执行二跳的时候呢?假设我们从 Fragment 跳转到一个新的 Activity;

setUserVisibleHint 看起来并没有执行;那么它为什么没有执行呢?因为它的执行是在 ViewPager 中通过 adapter 调用的时候才执行,也就是我们在二跳的时候并没有触发 adapter 的调用;

populate -> mAdapter.setPrimaryItem -> setUserVisibleHint 调用的,跟生命周期没有任何关系;

也就是我们想分发这个状态,就要和生命周期管理起来,那么我们就需要在 Fragment 的 onResume 和 onPasue 增加逻辑处理

scss 复制代码
@Override
public void onResume() {    
    super.onResume();    
    Log.e(TAG,"onResume");    
    if (getUserVisibleHint() && !isVisibleStateUp) {        
        // 当前可见,并且上一次不可见的时候 分发        
        dispatchVisibleHint(true);    
    }
}

@Override
public void onPause() {    
    super.onPause();    
    Log.e(TAG,"onPause");    
    if (!getUserVisibleHint() && isVisibleStateUp) {        
        dispatchVisibleHint(false);    
    }
}

我们运行看下效果:

可以看到,二跳出去的时候,fragment 5 停止了加载,返回的时候

又重新进行了加载,看起来也似乎很完美了;

第五次懒加载优化

如果我们把 ViewPager + Fragment 在嵌套到一个 Fragment 中的时候,我们的状态还能正常吗?就是下面这种效果:

嵌套层架如下:

类似这样的一个层级嵌套,我们参照ViewPager + Fragment 修改我们的其中一个 Fragment 让它内部在套一个 ViewPager + Fragment;我们运行看下效果:

可以看到,Fragment1 正常加载的时候,Fragment2 下的子 Framgent 也执行了加载逻辑,看起来不符合我们期望的结果;

这是因为 ViewPager 的预加载,Fragment1 展示的时候,预加载了 Fragment2,Fragment2 因为是 ViewPager + Fragment 的结构,导致 ViewPager 中的第一个 Framgent 执行了相关声明周期逻辑;

那么我们需要让 Fragment2 的子 Fragment 不执行相关生命周期逻辑;也就是说我们需要判断下 Fragment2 是不是真正的可见,修改如下,增加父 Framgent 是否可见,同时当我们切换到 Fragment2 的时候 要分发相关状态给 Framgent2 的子 Fragment;

scss 复制代码
protected boolean isParentVisible() {    
    Fragment parentFragment = getParentFragment();    
    if (parentFragment instanceof BaseLazyFragment) {        
        BaseLazyFragment baseLazyFragment = (BaseLazyFragment) parentFragment;        
        return !baseLazyFragment.isVisibleStateUp;
    }    
    return false;
}

private void dispatchVisibleHint(boolean isVisibleToUser) {    
    isVisibleStateUp = isVisibleToUser;    
    if (isVisibleStateUp && isParentVisible()) {        
        return;    
    }    
    
    if (isVisibleToUser) {        
        // 加载数据        
        onFragmentLoad();        
        // 手动 分发嵌套执行        
        dispatchChildVisibleState(true);    
    } else {        
        // 停掉数据        
        onFragmentLoadStop();        
        dispatchChildVisibleState(false);    
    }
}

protected void dispatchChildVisibleState(boolean isVisibleToUser) {    
    FragmentManager childFragmentManager = getChildFragmentManager();    
    List<Fragment> fragments = childFragmentManager.getFragments();    
    if (fragments.size() > 0) {        
        for (Fragment fragment : fragments) {            
            if (fragment instanceof BaseLazyFragment                     
                    && !fragment.isHidden()                     
                    && fragment.getUserVisibleHint()) {                
                ((BaseLazyFragment) fragment).dispatchVisibleHint(isVisibleToUser);            
            }         
        }    
    }
}

我们运行看下打印结果:

当我们切换到 Fragment2 的时候,内部的事件才开始分发;好了,完美,我们针对 ViewPager的懒加载优化到这里就完成了,我们的 BaseLazyFragment 的完整实现如下:

java 复制代码
public abstract class BaseLazyFragment extends Fragment {    
    private static final String TAG = "MyFragment";    
    private View rootView = null;    
    private boolean isViewCreated = false;    
    private boolean isVisibleStateUp = false;    
    @Nullable    
    @Override    
    public View onCreateView(@NonNull LayoutInflater inflater,                             
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {        
        Log.e(TAG,"onCreateView: ");        
        if (rootView == null) {            
            rootView = inflater.inflate(getLayoutRes(), container, false);        
        }        
        initView(rootView);        
        isViewCreated  = true;        
        if (getUserVisibleHint()) {            
            setUserVisibleHint(true);        
        }        
        return rootView;    
    }    
    @Override    
    public void setUserVisibleHint(boolean isVisibleToUser) {        
        super.setUserVisibleHint(isVisibleToUser);        
        Log.e(TAG,"setUserVisibleHint: ");        
        if (isViewCreated) {            
            if (isVisibleToUser && !isVisibleStateUp) {                
                // 当前可见,但是上一次不可见                
                dispatchVisibleHint(true);            
            } else if (!isVisibleToUser && isVisibleStateUp) {                
                // 当前不可见,但是上一次可见                
                dispatchVisibleHint(false);            
            }        
        }    
    }    
    private void dispatchVisibleHint(boolean isVisibleToUser) {        
        isVisibleStateUp = isVisibleToUser;        
        if (isVisibleStateUp && isParentVisible()) {            
            return;        
        }        
        if (isVisibleToUser) {            
            // 加载数据            
            onFragmentLoad();            
            // 手动 分发嵌套执行            
            dispatchChildVisibleState(true);        
        } else {            
            // 停掉数据            
            onFragmentLoadStop();            
            dispatchChildVisibleState(false);        
        }    
    }    
    protected boolean isParentVisible() {        
        Fragment parentFragment = getParentFragment();        
        if (parentFragment instanceof BaseLazyFragment) {            
            BaseLazyFragment baseLazyFragment = (BaseLazyFragment) parentFragment;            
            return !baseLazyFragment.isVisibleStateUp;        
        }        
        return false;    
    }    
    protected void dispatchChildVisibleState(boolean isVisibleToUser) {        
        FragmentManager childFragmentManager = getChildFragmentManager();        
        List<Fragment> fragments = childFragmentManager.getFragments();        
        if (fragments.size() > 0) {            
            for (Fragment fragment : fragments) {                
                if (fragment instanceof BaseLazyFragment                        
                        && !fragment.isHidden()                        
                        && fragment.getUserVisibleHint()) {                    
                    ((BaseLazyFragment) fragment).dispatchVisibleHint(isVisibleToUser);                
                }            
            }        
        }    
    }    
    @Override    
    public void onResume() {        
        super.onResume();        
        Log.e(TAG,"onResume");        
        if (getUserVisibleHint() && !isVisibleStateUp) {            
            // 当前可见,并且上一次不可见的时候 分发            
            dispatchVisibleHint(true);        
        }    
    }    
    @Override    
    public void onPause() {        
        super.onPause();        
        Log.e(TAG,"onPause");        
        if (!getUserVisibleHint() && isVisibleStateUp) {            
            dispatchVisibleHint(false);        
        }    
    }    
    // 让子类完成,初始化布局,初始化控件    
    protected abstract void initView(View rootView);    
    protected abstract int getLayoutRes();    
    public void onFragmentLoadStop() {        
        Log.e(TAG, "onFragmentLoadStop");    
    }    
    public void onFragmentLoad() {        
        Log.e(TAG,"onFragmentLoad");    
    }
}

ViewPager 与 ViewPager2 的区别

  1. 在 VP2 中 setUserVisibleHint 已经过时,setMaxLifecycle替代,支持懒加载,并且可以设置 0 预加载和缓存;
  2. 支持垂直滑动;
  3. 支持从右往左滑动;

后面我会单独写一章详细分析 ViewPager2 的文章,如果大家的点赞率比较高的话;

简历润色

简历上可写:深度理解 ViewPager 的 五次 懒加载优化

下一章预告

带你玩转 NestedScrollView 嵌套滑动机制;

欢迎三连

来都来了,点个关注,点个赞吧,你的支持是我最大的动力~~

相关推荐
救救孩子把4 分钟前
深入理解 Java 对象的内存布局
java
落落落sss6 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
万物皆字节12 分钟前
maven指定模块快速打包idea插件Quick Maven Package
java
夜雨翦春韭18 分钟前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
我行我素,向往自由25 分钟前
速成java记录(上)
java·速成
代码敲上天.27 分钟前
数据库语句优化
android·数据库·adb
一直学习永不止步31 分钟前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
邵泽明31 分钟前
面试知识储备-多线程
java·面试·职场和发展
程序员是干活的44 分钟前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
煸橙干儿~~1 小时前
分析JS Crash(进程崩溃)
java·前端·javascript