<commit_before>---
layout:     post
title:      ViewPager2
subtitle:   ViewPager禁用预加载
date:       2021-04-12
author:     Tristan
header-img: img/viewpager2_bg.png
catalog: true
tags:
    - 预加载
    - 懒加载
    - 离屏加载
    
---

## 背景
在开发首页、城市选择页和我的优惠券三个页面中，同时都使用了ViewPager用于横向页面切换作分页展示。ViewPager满足基本的功能需要是OK的，但要说完胜却有以下不足。比如，1)无法将离屏缓存页参数设置小于1，即不能关闭预加载；2)无法通过Adapter动态更新数据；3)禁止通过手势滑动翻页时，依然会出现页面滚动的迹象。

## 预加载
ViewPager默认情况下切换到当前页面时，会默认预加载左右两侧的布局到ViewPager中(尽管两侧的View并不可见)，我们称这种情况叫预加载；由于ViewPager对offscreenPageLimit设置了限制，页面的预加载是不可避免。
~~~
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {//不允许小于1
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
} 
~~~

## 懒加载
既然不能禁用预加载，那么就出现了懒加载，这是没有办法下的办法。懒加载只会在页面可见时加载数据。

#### ViewPager如何实现懒加载
1. PagerAdapter常用方法如下：
* **instantiateItem(ViewGroup container, int position)** //初始化ItemView，返回需要添加ItemView<br/>
* destroyItem(iewGroup container, int position, Object object) //销毁ItemView，移除指定的ItemView<br/>
* isViewFromObject(View view, Object object) //View和Object是否对应<br/>
* **setPrimaryItem(ViewGroup container, int position, Object object)** //设置当前页面的主Item<br/>
* getCount() //获取Item个数<br/>

2. ViewPager实现懒加载要借助Fragment的setUserVisibleHint方法，缺此方法不可行。
ViewPager为了进行布局预加载，首先要通过PagerAdapter调用instantiateItem(ViewGroup container, int position)方法创建ItemView。PagerAdapter搭载Fragment的两个实现类FragmentPagerAdapter和FragmentStatePagerAdapter都是通过此方法方法创建Fragment对象的。很不幸，FragmentPagerAdapter和FragmentStatePagerAdapter一股脑的在instantiateItem()中进行创建且进行add或attach操作，并没有在setPrimaryItem()方法中对Fragment进行操作。
~~~
void populate(int newCurrentItem) {
    // Locate the currently focused item or add it if needed.
    int curIndex = -1;//=================================================================================1_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);//======================================================1_2
    }

    // 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;//==================================================================2_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 {
                ii = addNewItem(pos, itemIndex + 1);//====================================================2_2
                extraWidthLeft += ii.widthFactor;
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        float extraWidthRight = curItem.widthFactor;
        itemIndex = curIndex + 1;//=======================================================================3_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 {
                    ii = addNewItem(pos, itemIndex);//=====================================================3_2
                    itemIndex++;
                    extraWidthRight += ii.widthFactor;
                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                }
            }
        }

        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);//=========================================4
    }
    
}
~~~
~~~
ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);//================================================5
    ii.widthFactor = mAdapter.getPageWidth(position);
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}
~~~
~~~
@Override
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);//========================================6_1
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);//================================================================6_2
    } else {
        fragment = getItem(position);//====================================================================6_3
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}
~~~
setPrimaryItem(ViewGroup container, int position, Object object)，该方法表示当前页面正在显示主要Item，何为主要Item？如果预加载的ItemView已经划入屏幕，当前的PrimaryItem依然不会改变，除非新的ItemView完全划入屏幕，且滑动已经停止才会判断。
~~~
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);//================================================隐藏
        }
        fragment.setMenuVisibility(true);
        fragment.setUserVisibleHint(true);//================================================================可见
        mCurrentPrimaryItem = fragment;
    }
}
~~~
因此，预加载会导致不可见的Fragment一股脑的调用onCreate、onCreateView、onResume等方法，用户唯有通过Fragment.setUserVisibleHint()方法识别当前可见的Fragment，这样基于Fragment的懒加载方案就实现了。懒加载可以控制数据延迟加载，但无法阻止布局提前创建。

> Fragment中的setUserVisibleHint方法在v1.1.0中已经标记为已过时(@Deprecated)。

## ViewPager2

#### ViewPager2介绍
ViewPager2内部通过挂载一个RecyclerView实现分屏显示，ViewPager2的预加载和缓存基本交由RecyclerView来处理，它设置Adapter其实是给RecyclerView设置Adapter。
~~~
public final class ViewPager2 extends ViewGroup {

    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

    private LinearLayoutManager mLayoutManager;
    RecyclerView mRecyclerView;
    
    private boolean mUserInputEnabled = true;
    
    private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
    
    private void initialize(Context context, AttributeSet attrs) {
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());
        mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);

        mLayoutManager = new LinearLayoutManagerImpl(context);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
        setOrientation(context, attrs);

        mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());

        ...
        
        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
    }
    
    public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
        final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
        mAccessibilityProvider.onDetachAdapter(currentAdapter);
        unregisterCurrentItemDataSetTracker(currentAdapter);
        mRecyclerView.setAdapter(adapter);
        mCurrentItem = 0;
        restorePendingState();
        mAccessibilityProvider.onAttachAdapter(adapter);
        registerCurrentItemDataSetTracker(adapter);
    }
}
~~~

ViewPager2作为独立的库被发布，引入时只需要在build.gradle添加如下依赖即可：
> implementation 'androidx.viewpager2:viewpager2:1.1.0'

ViewPager2和ViewPager在使用方式上差不多，主要有以下一些API：
* **setOffscreenPageLimit()** 设置屏幕外加载页面数量
* **setAdapter()** 设置适配器
* **setUserInputEnabled()** 设置是否允许用户输入/触摸
* registerOnPageChangeCallback() 注册页面改变回调
* setOrientation() 设置布局方向
* setPageTransformer() 设置页面滑动时的变换效果

#### ViewPager2离屏加载

~~~
/**
 * Value to indicate that the default caching mechanism of RecyclerView should be used instead
 * of explicitly prefetch and retain pages to either side of the current page.
 * @see #setOffscreenPageLimit(int)
 */
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        throw new IllegalArgumentException(
                "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
    }
    mOffscreenPageLimit = limit;
    // Trigger layout so prefetch happens through getExtraLayoutSize()
    mRecyclerView.requestLayout();
}
~~~
注释直译：该值指示应使用RecyclerView的默认缓存机制，而不是显式预取并将页面保留在当前页面的任何一侧。
白话文说：我这个值是给RecyclerView的默认缓存用的，而不是直接用来预加载左右侧的页面的（“显示”代表这里面有隐含）。
~~~
private class LinearLayoutManagerImpl extends LinearLayoutManager {
    @Override
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int pageLimit = getOffscreenPageLimit();
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            // Only do custom prefetching of offscreen pages if requested
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
            return;
        }
        final int offscreenSpace = getPageSize() * pageLimit;
        extraLayoutSpace[0] = offscreenSpace;
        extraLayoutSpace[1] = offscreenSpace;
    }
}
~~~
可以看见，ViewPager2和ViewPager在预加载的实现上有所不同：ViewPager默认预加载值就是1，如果外部设置小于1的话会直接抛出异常；ViewPager2提供了一个负数作为默认值（早期版本这个值是0，当前的版本是-1），然后将该值提供给RecyclerView的LinearLayoutManager处理默认的预加载方案。
OffscreenPageLimit本质上是重写LinearLayoutManager的calculateExtraLayoutSpace方法，该方法是最新的recyclerView包加入的功能。
calculateExtraLayoutSpace方法定义了布局额外的空间，何为布局额外的空间？默认空间等于RecyclerView的宽高空间，定义这个意在可以放大可布局的空间，该方法参数extraLayoutSpace是一个长度为2的int数组，第一条数据接受左边/上边的额外空间，第二条数据接受右边/下边的额外空间，故上诉代码是表明左右/上下各扩大offscreenSpace。

综上代码，OffscreenPageLimit其实就是放大了LinearLayoutManager的布局空间。

**布局对比**
为了对比两者加载布局的效果，这里准备了LinearLayout同时展示ViewPager和ViewPager2，设置相同的Item布局和数据源，然后用Android布局分析工具抓取两者的布局结构，效果如下：
![image](https://user-images.githubusercontent.com/4709890/114721708-c5c4c280-9d6b-11eb-94c0-fef04333903b.png)
从分析结果来看，ViewPager会默认会预布局两侧各一个布局，ViewPager2默认不进行预布局，主要由各自的默认offscreenPageLimit参数决定，ViewPager默认为1且不允许小于1，ViewPager2默认为0。
![image](https://user-images.githubusercontent.com/4709890/114721916-f147ad00-9d6b-11eb-898d-611627296248.png)
分析运行结果，在设置相同的offscreenPageLimit时，两者都会预布局左右(上下)两者的offscreenPageLimit个ItemView。
从对比结果上来看，ViewPager2的offscreenPageLimit和ViewPager运行结果一样，但是ViewPager2最小offscreenPageLimit可以设置为0。

**预加载**
上面分析我们已经知道，pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT时，ViewPager2的预加载其实是交给了RecyclerView的LinearLayoutManager来管理的，ViewPager2的预加载即RecyclerView的预加载。
RecyclerView是默认开启预加载的，如果要关闭预加载，可以使用下面的代码：
~~~
private boolean mItemPrefetchEnabled = true;

((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
~~~
预加载的开关在LayoutManager上，只需要获取LayoutManager并调用setItemPrefetchEnabled()即可控制开关。

**缓存**
ViewPager2默认会缓存2条ItemView，而且在最新的RecyclerView中可以自定义缓存Item的个数，方法如下：
~~~
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {

    final Recycler mRecycler = new Recycler();

    public void setItemViewCacheSize(int size) {
        mRecycler.setViewCacheSize(size);
    }

    public final class Recycler {

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        static final int DEFAULT_CACHE_SIZE = 2;

        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }
    }

}
~~~
~~~
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemViewCacheSize(0);
~~~
预加载和缓存在View层面没有本质的区别，都是已经准备了布局，但是没有加载到parent视图上；预加载和离屏加载在View层面有本质的区别，离屏加载的View已经添加到parent上。

## ViewPager2对Fragment的支持
#### Fragment生命周期测试
目前，ViewPager2对Fragment的支持只能使用FragmentStateAdapte。默认情况下，ViewPager2开启了预加载，离屏加载是关闭的。
1. 离屏加载关闭时，经过验证，是否开启预加载，对Fragment的生命周期没有影响，结果如下图：
![image](https://user-images.githubusercontent.com/4709890/114726946-61583200-9d70-11eb-959e-2f22dcde8df9.png)
2. 打开离线加载，设置offscreenPageLimit=1时：
![image](https://user-images.githubusercontent.com/4709890/114727138-88166880-9d70-11eb-8a0d-f4bbc5d53fad.png)

打印结果解读：
备注：log日志下标是从2开始的，标注的页码是从1开始，请自行矫正；
* 默认情况下，ViewPager2会缓存两条数据，所以滑动到第4页，第1页的Fragment才开始移除，这可以理解；
* 设置offscreenPageLimit=1时，ViewPager2在第1页会加载两条数据，这可以理解，会把下一页View提前加载进来；以后每滑一页，会加载下一页数组，直到第5页，会移除第1页的Fragment；第6页会移除第2页的Fragment

如何理解offscreenPageLimit对Fragment的影响，假设offscreenPageLimit=1，这样ViewPager2最多可以承托3个ItemView，再加上2个缓存的ItemView，就是5个，由于offscreenPageLimit会在ViewPager2两边放置一个，所以向前最多承载4个，向后最多能承载1个(预加载对Fragment没有影响，所以不计算)，这样很自然就是第5个时候，回收第1个。

#### FragmentStateAdapter源码简单解读
onCreateViewHolder()方法
~~~
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}
static FragmentViewHolder create(ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
}
~~~
onCreateViewHolder()创建一个宽高都MATCH_PARENT的FrameLayout，注意这里并不像PagerAdapter是Fragment的rootView。

onBindViewHolder()
~~~

public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    final long itemId = holder.getItemId();
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null && boundItemId != itemId) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
    mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
    //保证目标Fragment不为空，意思是可以提前创建
    ensureFragment(position);
    /** Special case when {@link RecyclerView} decides to keep the {@link container}
     * attached to the window, but not to the view hierarchy (i.e. parent is null) */
    final FrameLayout container = holder.getContainer();
    //如果ItemView已经在添加到Window中，且parent不等于null，会触发绑定viewHoder操作；
    if (ViewCompat.isAttachedToWindow(container)) {
        if (container.getParent() != null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if (container.getParent() != null) {
                    container.removeOnLayoutChangeListener(this);
                    //将Fragment和ViewHolder绑定
                    placeFragmentInViewHolder(holder);
                }
            }
        });
    }
    //回收垃圾Fragments
    gcFragments();
} 
~~~
* onBindViewHolder()首先会获取当前position对应的Fragment，这意味着预加载的Fragment对象会提前创建；
* 如果当前的holder.itemView已经添加到屏幕且已经布局且parent不等于空，就会将Fragment绑定到ViewHodler;
* 每次调用都会gc一次，主要的避免用户修改数据源造成垃圾对象；

onViewAttachedToWindow()
~~~
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
    placeFragmentInViewHolder(holder);
    gcFragments();
}
~~~
onViewAttachedToWindow()方法调用onViewAttachedToWindow将Fragment和hodler绑定。

onViewRecycled()
~~~
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}
~~~
当onViewRecycled()时才会触发Fragment移除。

核心添加操作：
~~~
//将Fragment.rootView添加到FrameLayout;
scheduleViewAttach(fragment, container);//将rootI
mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();

//主要是监听onFragmentViewCreated方法，获取rootView然后添加到container
private void scheduleViewAttach(final Fragment fragment, final FrameLayout container) {
    // After a config change, Fragments that were in FragmentManager will be recreated. Since
    // ViewHolder container ids are dynamically generated, we opted to manually handle
    // attaching Fragment views to containers. For consistency, we use the same mechanism for
    // all Fragment views.
    mFragmentManager.registerFragmentLifecycleCallbacks(
            new FragmentManager.FragmentLifecycleCallbacks() {
                @Override
                public void onFragmentViewCreated(@NonNull FragmentManager fm,
                        @NonNull Fragment f, @NonNull View v,
                        @Nullable Bundle savedInstanceState) {
                    if (f == fragment) {
                        fm.unregisterFragmentLifecycleCallbacks(this);
                        addViewToContainer(v, container);
                    }
                }
            }, false);
}
~~~<commit_msg>Update 2021-04-12-ViewPager2.md<commit_after>---
layout:     post
title:      ViewPager2
subtitle:   ViewPager禁用预加载
date:       2021-04-12
author:     Tristan
header-img: img/viewpager2_bg.png
catalog: true
tags:
    - 预加载
    - 懒加载
    - 离屏加载
    
---

## 背景
在开发首页、城市选择页和我的优惠券三个页面中，同时都使用了ViewPager用于横向页面切换作分页展示。ViewPager满足基本的功能需要是OK的，但要说完胜却有以下不足。比如，1)无法将离屏缓存页参数设置小于1，即不能关闭预加载；2)无法通过Adapter动态更新数据；3)禁止通过手势滑动翻页时，依然会出现页面滚动的迹象。

## 预加载
ViewPager默认情况下切换到当前页面时，会默认预加载左右两侧的布局到ViewPager中(尽管两侧的View并不可见)，我们称这种情况叫预加载；由于ViewPager对offscreenPageLimit设置了限制，页面的预加载是不可避免。
~~~
private static final int DEFAULT_OFFSCREEN_PAGES = 1;

public void setOffscreenPageLimit(int limit) {
    if (limit < DEFAULT_OFFSCREEN_PAGES) {//不允许小于1
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
} 
~~~

## 懒加载
既然不能禁用预加载，那么就出现了懒加载，这是没有办法下的办法。懒加载只会在页面可见时加载数据。

#### ViewPager如何实现懒加载
1. PagerAdapter常用方法如下：
* **instantiateItem(ViewGroup container, int position)** //初始化ItemView，返回需要添加ItemView<br/>
* destroyItem(iewGroup container, int position, Object object) //销毁ItemView，移除指定的ItemView<br/>
* isViewFromObject(View view, Object object) //View和Object是否对应<br/>
* **setPrimaryItem(ViewGroup container, int position, Object object)** //设置当前页面的主Item<br/>
* getCount() //获取Item个数<br/>

2. ViewPager实现懒加载要借助Fragment的setUserVisibleHint方法，缺此方法不可行。
ViewPager为了进行布局预加载，首先要通过PagerAdapter调用instantiateItem(ViewGroup container, int position)方法创建ItemView。PagerAdapter搭载Fragment的两个实现类FragmentPagerAdapter和FragmentStatePagerAdapter都是通过此方法方法创建Fragment对象的。很不幸，FragmentPagerAdapter和FragmentStatePagerAdapter一股脑的在instantiateItem()中进行创建且进行add或attach操作，并没有在setPrimaryItem()方法中对Fragment进行操作。
~~~
void populate(int newCurrentItem) {
    // Locate the currently focused item or add it if needed.
    int curIndex = -1;//=================================================================================1_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);//======================================================1_2
    }

    // 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;//==================================================================2_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 {
                ii = addNewItem(pos, itemIndex + 1);//====================================================2_2
                extraWidthLeft += ii.widthFactor;
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        float extraWidthRight = curItem.widthFactor;
        itemIndex = curIndex + 1;//=======================================================================3_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 {
                    ii = addNewItem(pos, itemIndex);//=====================================================3_2
                    itemIndex++;
                    extraWidthRight += ii.widthFactor;
                    ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null;
                }
            }
        }

        mAdapter.setPrimaryItem(this, mCurItem, curItem.object);//=========================================4
    }
    
}
~~~
~~~
ItemInfo addNewItem(int position, int index) {
    ItemInfo ii = new ItemInfo();
    ii.position = position;
    ii.object = mAdapter.instantiateItem(this, position);//================================================5
    ii.widthFactor = mAdapter.getPageWidth(position);
    if (index < 0 || index >= mItems.size()) {
        mItems.add(ii);
    } else {
        mItems.add(index, ii);
    }
    return ii;
}
~~~
~~~
@Override
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);//========================================6_1
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);//================================================================6_2
    } else {
        fragment = getItem(position);//====================================================================6_3
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}
~~~
setPrimaryItem(ViewGroup container, int position, Object object)，该方法表示当前页面正在显示主要Item，何为主要Item？如果预加载的ItemView已经划入屏幕，当前的PrimaryItem依然不会改变，除非新的ItemView完全划入屏幕，且滑动已经停止才会判断。
~~~
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            mCurrentPrimaryItem.setMenuVisibility(false);
            mCurrentPrimaryItem.setUserVisibleHint(false);//================================================隐藏
        }
        fragment.setMenuVisibility(true);
        fragment.setUserVisibleHint(true);//================================================================可见
        mCurrentPrimaryItem = fragment;
    }
}
~~~
因此，预加载会导致不可见的Fragment一股脑的调用onCreate、onCreateView、onResume等方法，用户唯有通过Fragment.setUserVisibleHint()方法识别当前可见的Fragment，这样基于Fragment的懒加载方案就实现了。懒加载可以控制数据延迟加载，但无法阻止布局提前创建。

> Fragment中的setUserVisibleHint方法在v1.1.0中已经标记为已过时(@Deprecated)。

## ViewPager2

#### ViewPager2介绍
ViewPager2内部通过挂载一个RecyclerView实现分屏显示，ViewPager2的预加载和缓存基本交由RecyclerView来处理，它设置Adapter其实是给RecyclerView设置Adapter。
~~~
public final class ViewPager2 extends ViewGroup {

    public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

    private LinearLayoutManager mLayoutManager;
    RecyclerView mRecyclerView;
    
    private boolean mUserInputEnabled = true;
    
    private @OffscreenPageLimit int mOffscreenPageLimit = OFFSCREEN_PAGE_LIMIT_DEFAULT;
    
    private void initialize(Context context, AttributeSet attrs) {
        mRecyclerView = new RecyclerViewImpl(context);
        mRecyclerView.setId(ViewCompat.generateViewId());
        mRecyclerView.setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);

        mLayoutManager = new LinearLayoutManagerImpl(context);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);
        setOrientation(context, attrs);

        mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        mRecyclerView.addOnChildAttachStateChangeListener(enforceChildFillListener());

        ...
        
        attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
    }
    
    public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
        final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
        mAccessibilityProvider.onDetachAdapter(currentAdapter);
        unregisterCurrentItemDataSetTracker(currentAdapter);
        mRecyclerView.setAdapter(adapter);
        mCurrentItem = 0;
        restorePendingState();
        mAccessibilityProvider.onAttachAdapter(adapter);
        registerCurrentItemDataSetTracker(adapter);
    }
}
~~~

ViewPager2作为独立的库被发布，引入时只需要在build.gradle添加如下依赖即可：
> implementation 'androidx.viewpager2:viewpager2:1.1.0'

ViewPager2和ViewPager在使用方式上差不多，主要有以下一些API：
* **setOffscreenPageLimit()** 设置屏幕外加载页面数量
* **setAdapter()** 设置适配器
* **setUserInputEnabled()** 设置是否允许用户输入/触摸
* registerOnPageChangeCallback() 注册页面改变回调
* setOrientation() 设置布局方向
* setPageTransformer() 设置页面滑动时的变换效果

#### ViewPager2离屏加载

~~~
/**
 * Value to indicate that the default caching mechanism of RecyclerView should be used instead
 * of explicitly prefetch and retain pages to either side of the current page.
 * @see #setOffscreenPageLimit(int)
 */
public static final int OFFSCREEN_PAGE_LIMIT_DEFAULT = -1;

public void setOffscreenPageLimit(@OffscreenPageLimit int limit) {
    if (limit < 1 && limit != OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        throw new IllegalArgumentException(
                "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0");
    }
    mOffscreenPageLimit = limit;
    // Trigger layout so prefetch happens through getExtraLayoutSize()
    mRecyclerView.requestLayout();
}
~~~
注释直译：该值指示应使用RecyclerView的默认缓存机制，而不是显式预取并将页面保留在当前页面的任何一侧。
白话文说：我这个值是给RecyclerView的默认缓存用的，而不是直接用来预加载左右侧的页面的（“显示”代表这里面有隐含）。
~~~
private class LinearLayoutManagerImpl extends LinearLayoutManager {
    @Override
    protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
            @NonNull int[] extraLayoutSpace) {
        int pageLimit = getOffscreenPageLimit();
        if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
            // Only do custom prefetching of offscreen pages if requested
            super.calculateExtraLayoutSpace(state, extraLayoutSpace);
            return;
        }
        final int offscreenSpace = getPageSize() * pageLimit;
        extraLayoutSpace[0] = offscreenSpace;
        extraLayoutSpace[1] = offscreenSpace;
    }
}
~~~
可以看见，ViewPager2和ViewPager在预加载的实现上有所不同：ViewPager默认预加载值就是1，如果外部设置小于1的话会直接抛出异常；ViewPager2提供了一个负数作为默认值（早期版本这个值是0，当前的版本是-1），然后将该值提供给RecyclerView的LinearLayoutManager处理默认的预加载方案。
OffscreenPageLimit本质上是重写LinearLayoutManager的calculateExtraLayoutSpace方法，该方法是最新的recyclerView包加入的功能。
calculateExtraLayoutSpace方法定义了布局额外的空间，何为布局额外的空间？默认空间等于RecyclerView的宽高空间，定义这个意在可以放大可布局的空间，该方法参数extraLayoutSpace是一个长度为2的int数组，第一条数据接受左边/上边的额外空间，第二条数据接受右边/下边的额外空间，故上诉代码是表明左右/上下各扩大offscreenSpace。

综上代码，OffscreenPageLimit其实就是放大了LinearLayoutManager的布局空间。

**布局对比**
为了对比两者加载布局的效果，这里准备了LinearLayout同时展示ViewPager和ViewPager2，设置相同的Item布局和数据源，然后用Android布局分析工具抓取两者的布局结构，效果如下：
![image](https://user-images.githubusercontent.com/4709890/114721708-c5c4c280-9d6b-11eb-94c0-fef04333903b.png)
从分析结果来看，ViewPager会默认会预布局两侧各一个布局，ViewPager2默认不进行预布局，主要由各自的默认offscreenPageLimit参数决定，ViewPager默认为1且不允许小于1，ViewPager2默认为0。
![image](https://user-images.githubusercontent.com/4709890/114721916-f147ad00-9d6b-11eb-898d-611627296248.png)
分析运行结果，在设置相同的offscreenPageLimit时，两者都会预布局左右(上下)两者的offscreenPageLimit个ItemView。
从对比结果上来看，ViewPager2的offscreenPageLimit和ViewPager运行结果一样，但是ViewPager2最小offscreenPageLimit可以设置为0。

**预加载**
上面分析我们已经知道，pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT时，ViewPager2的预加载其实是交给了RecyclerView的LinearLayoutManager来管理的，ViewPager2的预加载即RecyclerView的预加载。
RecyclerView是默认开启预加载的，如果要关闭预加载，可以使用下面的代码：
~~~
private boolean mItemPrefetchEnabled = true;

((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
~~~
预加载的开关在LayoutManager上，只需要获取LayoutManager并调用setItemPrefetchEnabled()即可控制开关。

**缓存**
ViewPager2默认会缓存2条ItemView，而且在最新的RecyclerView中可以自定义缓存Item的个数，方法如下：
~~~
public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {

    final Recycler mRecycler = new Recycler();

    public void setItemViewCacheSize(int size) {
        mRecycler.setViewCacheSize(size);
    }

    public final class Recycler {

        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
        int mViewCacheMax = DEFAULT_CACHE_SIZE;

        static final int DEFAULT_CACHE_SIZE = 2;

        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }
    }

}
~~~
~~~
((RecyclerView)viewPager.getChildAt(0)).getLayoutManager().setItemViewCacheSize(0);
~~~
预加载和缓存在View层面没有本质的区别，都是已经准备了布局，但是没有加载到parent视图上；预加载和离屏加载在View层面有本质的区别，离屏加载的View已经添加到parent上。

## ViewPager2对Fragment的支持
#### Fragment生命周期测试
目前，ViewPager2对Fragment的支持只能使用FragmentStateAdapte。默认情况下，ViewPager2开启了预加载，离屏加载是关闭的。
1. 离屏加载关闭时，经过验证，是否开启预加载，对Fragment的生命周期没有影响，结果如下图：
![image](https://user-images.githubusercontent.com/4709890/114726946-61583200-9d70-11eb-959e-2f22dcde8df9.png)
2. 打开离线加载，设置offscreenPageLimit=1时：
![image](https://user-images.githubusercontent.com/4709890/114727138-88166880-9d70-11eb-8a0d-f4bbc5d53fad.png)

打印结果解读：
备注：log日志下标是从2开始的，标注的页码是从1开始，请自行矫正；
* 默认情况下，ViewPager2会缓存两条数据，所以滑动到第4页，第1页的Fragment才开始移除，这可以理解；
* 设置offscreenPageLimit=1时，ViewPager2在第1页会加载两条数据，这可以理解，会把下一页View提前加载进来；以后每滑一页，会加载下一页数组，直到第5页，会移除第1页的Fragment；第6页会移除第2页的Fragment

如何理解offscreenPageLimit对Fragment的影响，假设offscreenPageLimit=1，这样ViewPager2最多可以承托3个ItemView，再加上2个缓存的ItemView，就是5个，由于offscreenPageLimit会在ViewPager2两边放置一个，所以向前最多承载4个，向后最多能承载1个(预加载对Fragment没有影响，所以不计算)，这样很自然就是第5个时候，回收第1个。

小结：使用ViewPager2+Fragment实现懒加载，其实ViewPager2默认的离屏加载就支持了，不需要将offscreenPageLimit设置大于0。新版本Fragment中再也不会监听得到setUserVisibleHint方法的调用，该方法也已被标记为过时。

#### FragmentStateAdapter源码简单解读
onCreateViewHolder()方法
~~~
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}
static FragmentViewHolder create(ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
}
~~~
onCreateViewHolder()创建一个宽高都MATCH_PARENT的FrameLayout，注意这里并不像PagerAdapter是Fragment的rootView。

onBindViewHolder()
~~~

public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
    final long itemId = holder.getItemId();
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null && boundItemId != itemId) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
    mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
    //保证目标Fragment不为空，意思是可以提前创建
    ensureFragment(position);
    /** Special case when {@link RecyclerView} decides to keep the {@link container}
     * attached to the window, but not to the view hierarchy (i.e. parent is null) */
    final FrameLayout container = holder.getContainer();
    //如果ItemView已经在添加到Window中，且parent不等于null，会触发绑定viewHoder操作；
    if (ViewCompat.isAttachedToWindow(container)) {
        if (container.getParent() != null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if (container.getParent() != null) {
                    container.removeOnLayoutChangeListener(this);
                    //将Fragment和ViewHolder绑定
                    placeFragmentInViewHolder(holder);
                }
            }
        });
    }
    //回收垃圾Fragments
    gcFragments();
} 
~~~
* onBindViewHolder()首先会获取当前position对应的Fragment，这意味着预加载的Fragment对象会提前创建；
* 如果当前的holder.itemView已经添加到屏幕且已经布局且parent不等于空，就会将Fragment绑定到ViewHodler;
* 每次调用都会gc一次，主要的避免用户修改数据源造成垃圾对象；

onViewAttachedToWindow()
~~~
public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
    placeFragmentInViewHolder(holder);
    gcFragments();
}
~~~
onViewAttachedToWindow()方法调用onViewAttachedToWindow将Fragment和hodler绑定。

onViewRecycled()
~~~
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}
~~~
当onViewRecycled()时才会触发Fragment移除。

核心添加操作：
~~~
//将Fragment.rootView添加到FrameLayout;
scheduleViewAttach(fragment, container);//将rootI
mFragmentManager.beginTransaction().add(fragment, "f" + holder.getItemId()).commitNow();

//主要是监听onFragmentViewCreated方法，获取rootView然后添加到container
private void scheduleViewAttach(final Fragment fragment, final FrameLayout container) {
    // After a config change, Fragments that were in FragmentManager will be recreated. Since
    // ViewHolder container ids are dynamically generated, we opted to manually handle
    // attaching Fragment views to containers. For consistency, we use the same mechanism for
    // all Fragment views.
    mFragmentManager.registerFragmentLifecycleCallbacks(
            new FragmentManager.FragmentLifecycleCallbacks() {
                @Override
                public void onFragmentViewCreated(@NonNull FragmentManager fm,
                        @NonNull Fragment f, @NonNull View v,
                        @Nullable Bundle savedInstanceState) {
                    if (f == fragment) {
                        fm.unregisterFragmentLifecycleCallbacks(this);
                        addViewToContainer(v, container);
                    }
                }
            }, false);
}
~~~
Adapter小结:
* 目前ViewPager2对Fragment支持只能用FragmentStateAdapter，FragmentStateAdapter在遇到预加载时，只会创建Fragment对象，不会把Fragment真正的加入到布局中，所以自带懒加载效果；
* FragmentStateAdapter不会一直保留Fragment实例，回收的ItemView也会移除Fragment，所以得做好Fragment重建后恢复数据的准备；
* FragmentStateAdapter在遇到offscreenPageLimit>0时，处理离屏Fragment和可见Fragment没有什么区别，所以无法通过setUserVisibleHint判断显示与否，这一点知得注意；

## ViewPager2总结
1. 高效的复用，利用RecyclerView预加载和缓存机制轻松实现了复用性上的借用。
2. 自带懒加载，采用更贴合Fragment生命周期的页面管理方式便捷实现了懒加载。
3. 更新有效的Adapter，解决了notifyDataSetChanged方法失效的诸多问题。
{'fraction_non_alphanumeric': 0.12792107452751694, 'fraction_numerical': 0.014216094140021396, 'mean_word_length': 3.5227394903773788, 'pattern_counts': {'":': 4, '<': 41, '<?xml version=': 0, '>': 48, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1858400', 'n_tokens_mistral': 15143, 'n_tokens_neox': 15613, 'n_words': 2157}