如何应对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 嵌套滑动机制;

欢迎三连

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

相关推荐
wm104313 分钟前
java web springboot
java·spring boot·后端
smile-yan14 分钟前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风15 分钟前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~18 分钟前
Maven极简安装&配置-241223
java·maven
皮蛋很白21 分钟前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea
青年有志23 分钟前
JavaWeb(一) | 基本概念(web服务器、Tomcat、HTTP、Maven)、Servlet 简介
java·web
上海研博数据27 分钟前
flink+kafka实现流数据处理学习
java
KpLn_HJL29 分钟前
leetcode - 2139. Minimum Moves to Reach Target Score
java·数据结构·leetcode
小扳2 小时前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构
龙少95432 小时前
【深入理解@EnableCaching】
java·后端·spring