背景 本文是在工作中对App启动耗时中页面展现耗时的一个优化,特意记录优化方案和遇到的问题。 主要是针对首页Recyclerview itemview的一个优化,减少itemview inflate耗时,从而减少onCreateViewHolder耗时,最终减少页面展现的耗时
思路 关于view的异步inflate,官方给出了一个方案:AsyncLayoutInflater 。对其原理感兴趣的朋友可以看下这篇文章:https://juejin.cn/post/6844904170508681224
关于如何减少ViewHolder的inflate时间,基于官方的方法,我做了两套方案:
1.异步初始化viewholder和 2.提前初始化viewholder private var sUseAsyncInflate = false private var sUseAheadInflate = false fun init (application: Application ?) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { aheadInflateView(application, sFirstScreenTypes) sUseAheadInflate = true } else { sUseAsyncInflate = true } }
一、提前初始化viewholder 提前初始化比较简单,只需要在首页展示之前初始化好对应的viewholder,我的方案细节如下:
在Application里仅异步初始化首屏viewholder
AsyncLayoutInflater异步初始化需要指定一个ViewParent,这种场景是Recyclerview
Recyclerview必须指定一个LayoutManager,这里和首页一样,使用的LinearLayoutManager
异步初始化完成后,需要移除itemview的parent
private var sFirstScreenTypes: IntArray = intArrayOf( CommonConstants.card_show_subtype_13, CommonConstants.card_show_subtype_551, CommonConstants.card_show_subtype_14 ) private fun aheadInflateView (application: Context ?, viewTypes: IntArray ) { sFirstScreenTypes = viewTypes sAsyncLayoutInflater = MyAsyncLayoutInflater(application!!) sFakeParent = RecyclerView(application) sFakeParent!!.layoutManager = LinearLayoutManager(application) for (viewType in viewTypes) { val model = ViewHolderTypeManager.transformViewHolder(viewType) var resid = model.layoutId if (resid < 0 ) { resid = CommonGlobalContext.getAppContext().resources.getIdentifier( model.layout, "layout" , CommonGlobalContext.getAppContext().packageName ) } sAsyncLayoutInflater!!.inflate( resid, sFakeParent, viewType, null ) { view, resid, parent, viewType, fakeViewHolder -> if (null != view.parent) { (view.parent as ViewGroup).removeView(view) } DebugLog.d(TAG, "aheadInflateView 完成 $viewType " ) saveAsyncInflateView(application, viewType, view) } } } private fun saveAsyncInflateView (context: Context , type: Int , view: View ?) : Int { if (!typeToContainer.containsKey(type)) { val container = ViewHolderContainer(context, type, mReferenceQueue) container.position = -1 container.isFake = false container.view = view typeToContainer[type] = container } else { } return -1 }
ViewHolderContainer 是一个链表类,内部有一个next指针,所以typeToContainer其实是一个type对应一个链表private val typeToContainer: MutableMap<Int , ViewHolderContainer?> = HashMap()
使用的时候直接在onCreateViewHolder中判断即可
二、异步初始化viewholder 异步初始化稍微复杂一点,思路是需要先绑定一个fake viewholder
,等 real viewholder
infalte完成在替换为real viewholder
基本原理如下:
1.区分占位viewholer和原viewholder 因为是异步的,在real view
没有inflate完成时,需要展示一个占位的view。占位view的viewtype = 原viewtype * (-1)
2.原viewholder inflate完成后,替换掉占位viewholer 占位view在原本view inflate完成后,需要被替换,需要保存占位view的位置,然后更新为 real view
实现方案:
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(type, 0 );
2.因为不缓存,所以在调用 notifyItemChanged(int position) 时会重新走到 onCreateViewHolder
简单说下 Recyclerview 分为四级缓存,1.mChangedScrap 2.mAttachedScrap、mCachedViews 3.mViewCacheExtension 4.mRecyclerPool
更多Recyclerview缓存相关的细节分析,请看我的这篇文章:RecyclerView 缓存机制
下面贴一下大概的实现方案:
@Override public int getItemViewType (int position) { int type = ViewHolderTypeManager.transformViewHolder((Card) mDataList.get(position)).getType(); if (mUseAsyncInflate && AsyncInflateHelper.INSTANCE.isFake(mContext, type, originPos, false )) { type *=-1 ; mRecyclerView.getRecycledViewPool().setMaxRecycledViews(type, 0 ); } } public RecyclerView.ViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { boolean useFake = mUseAsyncInflate; if (useFake) { View view = AsyncInflateHelper.INSTANCE.getContainerForType(viewType); if (!isFake) { if (null != view) { DebugLog.d("ConorLee" , "use async inflate viewtype is " + viewType); tmpViewHolder = (BaseNewViewHolder) getViewHolder(view, model.getClassName()); } else { useFake = false ; } } else { View viewGroup = LayoutInflater.from(mContext).inflate(R.layout.common_album_item_default, parent, false ); tmpViewHolder = new EmptyViewHolder(mContext, viewGroup); DebugLog.d("ConorLee" , "fake onCreate is " + viewType); int layoutId = model.getLayoutId(); if (layoutId < 0 ) { layoutId = CommonGlobalContext.getAppContext().getResources().getIdentifier(model.getLayout(), "layout" , CommonGlobalContext.getAppContext().getPackageName()); } MyAsyncLayoutInflater asyncLayoutInflater = new MyAsyncLayoutInflater(mContext); asyncLayoutInflater.inflate(layoutId, parent, viewType, tmpViewHolder, new MyAsyncLayoutInflater.OnInflateFinishedListener() { @Override public void onInflateFinished (@NonNull View view, int resid, @Nullable ViewGroup parent, int viewType, BaseNewViewHolder fakeViewHolder) { DebugLog.d("ConorLee" , "async inflate complete " + viewType); int pos = AsyncInflateHelper.INSTANCE.updatePosAndView(viewType, view); notifyItemChanged(pos); if (pos == -1 ) { DebugLog.d("ConorLee" , "async inflate error " + viewType); } } }); } } }
在onBindViewHolder()方法中如果发现holder是fake,那么可以直接return,显著减少onBind时间public void onBindViewHolder (@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) { int viewHolderType = holder.getItemViewType(); if (mUseAsyncInflate && viewHolderType < 0 ) { AsyncInflateHelper.INSTANCE.isFake(mContext, viewHolderType, position, true ); return ; } }
这里再说明一下:
fakeviewholder是在onCreateViewHolder中创建,并且在onBindViewHolder和position绑定,绑定之后由前面的typeToContainer类保存。
异步初始化完成后,根据type在typeToContainer中找到第一个fakeviewholder,拿到位置并更新
跨页面缓存相同type的ViewHolder,如何回收? 可以参考LeakCanary检测Activity是否泄漏的方法,检测viewholder的View或Context是否被回收。 基本原理如下:
用弱引用(WeakReference)包裹实体类ViewHolderContainer,然后给这个弱引用绑定一个引用队列。
如果弱引用的内容被回收,该弱引用会被加入到引用队列。然后某个时间点遍历引用队列,回收ViewHolderContainer
因为Activity生命周期的原因,如果在onDestroy里立刻去遍历引用队列,可能还未回收,所以onDestroy方法不是一个完美的时间点
建议可以在上一个页面的onResume方法中延迟去做
清除的方法大概如下:
fun clearViewContainer () { var needLoop = true while (needLoop) { val ref = mReferenceQueue.poll() as KeyedWeakReference<*>? ref?.let { var head = typeToContainer[ref.viewHolderType] var prev: ViewHolderContainer? = null while (head != null ) { if (head.mKeyedWeakReference.get () == null ) { DebugLog.d(TAG, "页面退出,回收container type is " + ref.viewHolderType + " pos is " + ref.viewHolderPos) if (null == prev) { head = head.next typeToContainer[ref.viewHolderType] = head } else { prev.next = head.next } break } prev = head head = head.next } } ?: let{ needLoop = false } } }
结论 采用这两种方式,可以显著减少viewholder的onCreateViewHolder和onBindViewHolder这两个方法的耗时,从而页面整体展现可以较以前提高100~200ms
不足:
1.异步初始化完成后,其实应该根据当前列表的firstvisiblepos和lastvisiblepos找到对应的fakeviewholder,这样效率更高。目前为了实现简单,每次异步完成都是遍历列表,拿第一个fakeviewholder,儿没有判断是否可见
2.不支持在viewholder中new Handler(), 因为底层用的官方AsyncLayoutInflater,初始化view在子线程,此时new Handler()会抛出异常
Ref View 的异步 Inflate+ 全局缓存:加速你的页面
本文链接:http://agehua.github.io/2022/07/21/async-inflate-strategy/
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!