写点什么

Fragment 极度懒加载 -+-Layout 子线程预加载,奇妙的 APP 启动速度优化思路

用户头像
Android架构
关注
发布于: 35 分钟前

当我们要使用 ViewPager 来加载 Fragment 时,官方为我们提供了这两种 Adapter,都是继承自 PagerAdapter。


区别,上官方描述:


FragmentPagerAdapter


This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider `[FragmentStatePagerAdapter](


)`.


FragmentStatePagerAdapter


This version of the pager is more useful when there are a large number of pages, working more like a list view. When pages are not visible to the user, their entire fragment may be destroyed, only keeping the saved state of that fragment. This allows the pager to hold on to much less memory associated with each visited page as compared to`[FragmentPagerAdapter](


)` at the cost of potentially more overhead when switching between pages


总结:


  • 使用 FragmentStatePagerAdapter 时,如果 tab 对于用户不可见了,Fragment 就会被销毁,FragmentPagerAdapter 则不会,使用 FragmentPagerAdapter 时,所有的 tab 上的 Fragment 都会 hold 在内存里

  • 当 tab 非常多时,推荐使用 FragmentStatePagerAdapter

  • 当 tab 不多,且固定时,推荐用 FragmentPagerAdapter


我们项目中就是使用的 ViewPager+FragmentPagerAdapter。

3.4 FragmentPagerAdapter 的刷新问题

正常情况,我们使用 adapter 时,想要刷新数据只需要:


  1. 更新 dataSet

  2. 调用 notifyDataSetChanged()


但是,这个在这个 Adapter 中是不适用的。因为(这一步没耐心的可以直接看后面的总结):


  1. 默认的 PagerAdapter 的 destoryItem 只会把 Fragment detach 掉,而不会 remove

  2. 当再次调用 instantiateItem 的时候,之前 detach 掉的 Fragment,又会从 mFragmentManager 中取出,又可以 attach 了



3,ViewPager 的 dataSetChanged 代码如下:



4,且 adapter 的默认实现



简单总结一下:


1,ViewPager 的 dataSetChanged()中会去用 adapter.getItemPosition 来判断是否要移除当前 Item(position = POSITION_NONE 时 remove)


2,PagerAdapter 的 getItemPosition 默认实现为 POSITION_UNCHANGED


上述两点导致 ViewPager 构建完成 Adapter 之后,不会有机会调用到 Adapter 的 instantiateItem 了。


再者,即使重写了 getItemPosition 方法,每次返回 POSITION_NONE,还是不会替换掉 Fragment,这是因为 instantiateItem 方法中,会根据 getItemId()去从 FragmetnManager 中找到已经创建好的 Fragment 返回回去,而 getItemId()的默认实现是 return position。

3.5?FragmentPagerAdapter 刷新的正确姿势

重写 getItemId()和 getItemPosition()


class TabsAdapter extends FragmentPagerAdapter {


private ArrayList<Fragment> mFragmentList;private ArrayList<String> mPageTitleList;private int mCount;


TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> pageTitleList) {super(fm);mFragmentList = fragmentList;mCount = fragmentList.size();mPageTitleList = pageTitleList;}


@Overridepublic Fragment getItem(int position) {return mFragmentList.get(position);}


@Overridepublic CharSequence getPageTitle(int position) {return mPageTitleList.get(position);}


@Overridepublic int getCount() {return mCount;}


@Overridepublic long getItemId(int position) {//这个地方的重写非常关键,super 中是返回 position,//如果不重写,还是会继续找到 FragmentManager 中缓存的 Fragmentreturn mFragmentList.get(position).hashCode();}


@Overridepublic int getItemPosition(@NonNull Object object) {//不在数据集合里面的话,return POSITION_NONE,进行 item 的重建 int index = mFragmentList.indexOf(object);if (index == -1) {return POSITION_NONE;} else {return mFragmentList.indexOf(object);}}


void refreshFragments(ArrayList<Fragment> fragmentList) {mFragmentList = fragmentList;notifyDataSetChanged();}}


其他的相关代码:


(1)实现 ViewPager.OnPageChangeListener,来监控 ViewPager 的滑动状态,才可以在滑动到下一个 tab 的时候进行 Fragment 替换的操作,其中 mDefaultTab 是我们通过接口返回的当前启动展示的 tab 序号


@Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}


@Overridepublic void onPageSelected(int position) {mCurrentSelectedTab = position;}


@Overridepublic void onPageScrollStateChanged(int state) {if (!hasReplacedAllEmptyFragments && mCurrentSelectedTab != mDefaultTab && state == 0) {//当满足: 1. 没有全部替换完 2. 当前 tab 不是初始化的默认 tab(默认 tab 不会用空的 Fragment 去替换) 3. 滑动结束了,即 state = 0replaceEmptyFragmentsIfNeed(mCurrentSelectedTab);}}


备注:


onPageScrollStateChanged 接滑动的状态值。一共有三个取值:

0:什么都没做 1:开始滑动 2:滑动结束

一次引起页面切换的滑动,state 的顺序分别是: 1? ->? 2? ->? 0


2)进行 Fragment 的替换,这里因为我们的 tab 数量是可能根据全局 config 信息而改变的,所以这个地方写的稍微纠结了一些。


/**


  • 如果全部替换完了,直接 return

  • 替换过程:

  • 1. 找到当前空的 tab 在 mEmptyFragmentList 中的实际下标

  • @param tabId 要替换的 tab 的 tabId - (当前空的 Fragment 在 adapter 数据列表 mFragmentList 的下标)*/private void replaceEmptyFragmentsIfNeed(int tabId) {if (hasReplacedAllEmptyFragments) {return;}int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); //找到当前的空 Fragment 在 mEmptyFragmentList 是第几个 if (tabRealIndex > -1) {if (Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) {mTabsAdapter.refreshFragments(mFragmentList); //将 mFragmentList 中的相应 empty fragment 替换完成之后刷新数据 boolean hasAllReplaced = true;for (Fragment fragment : mFragmentList) {if (fragment instanceof EmptyPlaceHolderFragment) {hasAllReplaced = false;break;}}if (hasAllReplaced) {mEmptyFragmentList.clear(); //全部替换完成的话,释放引用}hasReplacedAllEmptyFragments = hasAllReplaced;}}}

四、神奇的的预加载(预加载 View,而不是 data)

Android 在启动过程中可能涉及到的一些 View 的预加载方案:


  1. WebView 提前创建好,因为 webview 创建的耗时较长,如果首屏有 h5 的页面,可以提前创建好。

  2. Application 的 onCreate 时,就可以开始在子线程中进行后面要用到的 Layout 的 inflate 工作了,最先想到的应该是官方提供的 AsyncLayoutInflater

  3. 填充 View 的数据的预加载,今天的内容不涉及这一项

4.1 需要预加载什么

直接看图,这个是首页四个子 Tab Fragment 的基类的 layout,因为某些东西设计的不合理,导致层级是非常的深,直接导致了首页上的三个 tab 加上 FeedMainFragment 自身,光将这个 View inflate 出来的时间就非常长。因此我们考虑在子线程中提前 inflate layout


4.2?修改 AsyncLayoutInflater

官方提供了一个类,可以来进行异步的 inflate,但是有两个缺点:


  1. 每次都要现场 new 一个出来

  2. 异步加载的 view 只能通过 callback 回调才能获得(死穴)


因此决定自己封装一个 AsyncInflateManager,内部使用线程池,且对于 inflate 完成的 View 有一套缓存机制。而其中最核心的 LayoutInflater 则直接 copy 出来就好。


先看 AsyncInflateManager 的实现,这里我直接将代码 copy 进来,而不是截图了,这样你们如果想用其中部分东西,可以直接 copy:


/**


  • @author zoutao

  • <p>

  • 用来提供子线程 inflate view 的功能,避免某个 view 层级太深太复杂,主线程 inflate 会耗时很长,

  • 实就是对 AsyncLayoutInflater 进行了抽取和封装*/public class AsyncInflateManager {private static AsyncInflateManager sInstance;private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; //保存 inflateKey 以及 InflateItem,里面包含所有要进行 inflate 的任务 private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap;private ExecutorService mThreadPool; //用来进行 inflate 工作的线程池


private AsyncInflateManager() {mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());mInflateMap = new ConcurrentHashMap<>();mInflateLatchMap = new ConcurrentHashMap<>();}


public static AsyncInflateManager getInstance() {单例}


/**


  • 用来获得异步 inflate 出来的 view

  • @param context

  • @param layoutResId 需要拿的 layoutId

  • @param parent container

  • @param inflateKey 每一个 View 会对应一个 inflateKey,因为可能许多地方用的同一个 layout,但是需要 inflate 多个,用 InflateKey 进行区分

  • @param inflater 外部传进来的 inflater,外面如果有 inflater,传进来,用来进行可能的 SyncInflate,

  • @return 最后 inflate 出来的 view*/@UiThread@NonNullpublic View getInflatedView(Context context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {AsyncInflateItem item = mInflateMap.get(inflateKey);CountDownLatch latch = mInflateLatchMap.get(inflateKey);if (item != null) {View resultView = item.inflatedView;if (resultView != null) {//拿到了 view 直接返回 removeInflateKey(inflateKe


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


y);replaceContextForView(resultView, context);return resultView;}


if (item.isInflating() && latch != null) {//没拿到 view,但是在 inflate 中,等待返回 try {latch.wait();} catch (InterruptedException e) {Log.e(TAG, e.getMessage(), e);}removeInflateKey(inflateKey);if (resultView != null) {replaceContextForView(resultView, context);return resultView;}


}//如果还没开始 inflate,则设置为 false,UI 线程进行 inflateitem.setCancelled(true);}}//拿异步 inflate 的 View 失败,UI 线程 inflatereturn inflater.inflate(layoutResId, parent, false);}


/**


  • inflater 初始化时是传进来的 application,inflate 出来的 view 的 context 没法用来 startActivity,

  • 因此用 MutableContextWrapper 进行包装,后续进行替换*/private void replaceContextForView(View inflatedView, Context context) {if (inflatedView == null || context == null) {return;}Context cxt = inflatedView.getContext();if (cxt instanceof MutableContextWrapper) {((MutableContextWrapper) cxt).setBaseContext(context);}}


@UiThreadprivate void asyncInflate(Context context, AsyncInflateItem item) {if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {return;}onAsyncInflateReady(item);inflateWithThreadPool(context, item);}


private void onAsyncInflateReady(AsyncInflateItem item) {...}


private void onAsyncInflateStart(AsyncInflateItem item) {...}


private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {item.setInflating(false);CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);if (latch != null) {//释放锁 latch.countDown();}...}


private void removeInflateKey(String inflateKey) {...}


private void inflateWithThreadPool(Context context, AsyncInflateItem item) {mThreadPool.execute(new Runnable() {@Overridepublic void run() {if (!item.isInflating() && !item.isCancelled()) {try {onAsyncInflateStart(item);item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent, false);onAsyncInflateEnd(item, true);} catch (RuntimeException e) {Log.e(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", e);onAsyncInflateEnd(item, false);}}}});}


/**


  • copy from AsyncLayoutInflater - actual inflater*/private static class BasicInflater extends LayoutInflater {private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.webkit.", "android.app."};

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Fragment极度懒加载-+-Layout子线程预加载,奇妙的APP启动速度优化思路