写点什么

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

用户头像
Android架构
关注
发布于: 21 小时前

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 数据列表 mFragme


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


ntList 的下标)*/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(inflateKey);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."};


BasicInflater(Context context) {super(context);}


public LayoutInflater cloneInContext(Context newContext) {return new BasicInflater(newContext);}


protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {for (String prefix : sClassPrefixList) {try {View view = this.createView(name, prefix, attrs);if (view != null) {return view;}} catch (ClassNotFoundException ignored) {}}return super.onCreateView(name, attrs);}}}


这里我用一个 AsyncInflateItem 来管理一次要 inflate 的一个单位,


/**


  • @author zoutao*/public class AsyncInflateItem {String inflateKey;int layoutResId;ViewGroup parent;OnInflateFinishedCallback callback;View inflatedView;


private boolean cancelled;private boolean inflating;


//还有一些 set get 方法}


以及最后 inflate 的回调 callback:


public interface OnInflateFinishedCallback {void onInflateFinished(AsyncInflateItem result);}


经过这样的封装,外面可以直接在 Application 的 onCreate 中,开始异步的 inflate view 的任务。调用如下:


AsyncInflateUtil.startTask();


/**


  • @author zoutao*/public class AsyncInflateUtil {public static void startTask() {Context context = new MutableContextWrapper(CommonContext.getApplication());AsyncInflateManager.getInstance().asyncInflateViews(context,new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));


}


public class InflateKey {public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";public static final String SUB_TAB_1_FRAGMENT = "sub1";public static final String SUB_TAB_2_FRAGMENT = "sub2";public static final String SUB_TAB_3_FRAGMENT = "sub3";public static final String SUB_TAB_4_FRAGMENT = "sub4";}}


注意:这里会有一个坑。就是在 Application 的 onCreate 中,能拿到的 Context 只有 Application,这样 inflate 的 View,View 持有的 Context 就是 Application,这会导致一个问题。


如果用 View.getContext()这个 context 去进行 Activity 的跳转就会。。抛异常


Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?


而如果想要传入 Activity 来创建 LayoutInflater,时机又太晚。众所周知,Context 是一个抽象类,实现它的包装类就是 ContextWrapper,而 Activity、Appcation 等都是 ContextWrapper 的子类,然而,ContextWrapper 还有一个神奇的子类,


package android.content;


/**


  • Special version of {@link ContextWrapper} that allows the base context to

  • be modified after it is initially set.*/public class MutableContextWrapper extends ContextWrapper {public MutableContextWrapper(Context base) {super(base);}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

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