Android 朋友圈列表Feed流的最優(yōu)化方案,讓你的RecyclerView從49幀 -> 57幀

Github鏈接,給個(gè)Star鼓勵(lì)我寫更多好庫

ezgif-1-4516d51ebf.gif

事先說明:我在demo中一進(jìn)入Activity就立刻觸發(fā)下拉刷新,所以你看到幀率可能掉到了40,是因?yàn)橄到y(tǒng)的startActivity本身就掉幀非常厲害。想真實(shí)測出幀率,需要進(jìn)入Activity后等幀率穩(wěn)定在60了,再手動下拉刷新

包含功能:

  • 9張圖。如果只有一張圖,那么單張圖的寬高根據(jù)圖片原始寬高等比例縮放
  • 只有一張圖的時(shí)候,這個(gè)圖可能是視頻,圖中間有播放按鈕
  • 內(nèi)容支持表情。[微笑]要顯示為圖片??
  • 內(nèi)容有@人功能,@人有點(diǎn)擊事件
  • 每個(gè)Item帶有評論,XXX回復(fù)XXX:你好[微笑]

傳統(tǒng)做法的效果:

  • 首次進(jìn)入Activity后觸發(fā)下拉刷新,請求成功后setAdpater,這時(shí)候幀率會掉到49幀左右。丟失11幀
  • 手指往下滾動,滾動過程中,幀率在57幀 - 60幀徘徊
  • 退出Activity再次進(jìn)入,由于java底層的代碼優(yōu)化,執(zhí)行效率會上升。首次setAdpater幀率為53幀左右


    1659104674017.jpg

我優(yōu)化后的效果:

  • 首次進(jìn)入Activity后觸發(fā)下拉刷新,請求成功后setAdpater,這時(shí)候幀率會掉到57幀左右。丟失3幀左右
  • 手指往下滾動,滾動過程中,全程60幀


    1659104746498.jpg

我的基礎(chǔ)優(yōu)化方案(別人帖子也會講的):

  • ? 1、每個(gè)item中眾多元素的點(diǎn)擊事件不要每次都new。應(yīng)該是onCreateViewHolder中imageView.setOnClickListener(this)。然后在onBindViewHolder中imageView.setTag。避免滾動過程中頻繁new ClickListener()
  • ? 2、SpanText做好緩存,避免每次滾動都解析??傊瑱z查Adpater中所有new Object()的代碼,能不new就不new
  • ? 3、手寫DrawableCenterTextView,解決系統(tǒng)的Button的DrawableLeft會貼邊問題。否則要多一層LinearLayout包裹
  • ? 4、需要重復(fù)計(jì)算的size要設(shè)置為全局變量static,避免每次計(jì)算。比如每張圖片的寬高,表情圖片的15sp大小
  • ? 5、底部加載更多:使用notifyItemRangeInserted代替notifyDataSetChanged。后者會觸發(fā)2、3次onCreateViewHolder

我的進(jìn)階優(yōu)化方案(你在別的帖子看不到的):

  • ? 1、glide首次加載圖片會創(chuàng)建線程池,耗時(shí)約50ms,可以移到App打開時(shí)的歡迎界面就創(chuàng)建好。節(jié)省50ms
  • ? 2、首次setAdpater前先不著急結(jié)束下拉刷新狀態(tài)。先開啟Thread,在Thread中解析文字的表情和@人解析,組成SpanText并緩存到model中,節(jié)省約12ms
  • ? 3、采用LruCache緩存最新的32個(gè)表情的drawable,這樣可以加快常見表情的解析速度
  • ? 4、在Thread中按List<Model>.count() 解析item的xml布局,并存放在LinkedList<View> 中(為了節(jié)省內(nèi)存,我最多限制8條)。在onCreateViewHolder中進(jìn)行 .poll,節(jié)省150ms左右
  • ? 5、在Thread中按List<Model>.count() 預(yù)創(chuàng)建圖片和評論的緩存池:LinkedList<ImageView> 和 LinkedList<評論TextView>。在item顯示的時(shí)候。從緩沖池中取,而不是new。節(jié)省100ms左右
  • ? 6、在Adpater的 onViewRecycled 中把圖片和評論remove后存入緩存池。這一步主要為了滾動流暢
  • ? 7、九張圖采用GridLayout而不是 UnScrollView或者GridManager。后者會太重量級且會帶來更多的內(nèi)存消耗。這里其實(shí)最好自己寫一個(gè)ViewGroup

本方案用到的基礎(chǔ)常識:

  • 1、ListView或RecyclerView如果當(dāng)前是不可見狀態(tài),你去setAdpater不會起到任何效果,代碼不會走onCreateViewHolder等回調(diào)。當(dāng)你設(shè)置為VISIBLE的時(shí)候,才會觸發(fā)Adapter里的回調(diào)。
  • 2、RecyclerView的第3級緩存ViewCacheExtension,用起來還要去反射ViewHolder的Layout.Params,挺不方便的,所以我沒用他。
  • 3、RecyclerViewPool 緩存池每種type的最大值默認(rèn)為5。如果item是固定高度,那么緩存池的size總是在0和1之間徘徊,因?yàn)榈谝粋€(gè)item剛被回收,底部item就進(jìn)來了。
  • 4、所以RecyclerViewPool 緩存池只有在非常極限情況下才會size == 5 (即:頂部的幾個(gè)item的height非常小,但是底部的item高度非常大)。
  • 5、onViewRecycled(holder: RecyclerView.ViewHolder) 是Item進(jìn)入RecyclerViewPool的回調(diào),進(jìn)入之后,vh的data就等于無用了
  • 6、LinkedList 要比ArrayList在增刪的時(shí)候更快,尤其是增刪第0個(gè)和最后一個(gè)object。且由于ArrayList底層用的是int[10], 所以存在內(nèi)存浪費(fèi)。所以用LinkedList做緩存池優(yōu)于ArrayList

本方案部分技術(shù)細(xì)節(jié):

Activity:

      //在子線程中進(jìn)行預(yù)加載
                    preloadCacheViewsInThread(beanList) {
                        //預(yù)加載完畢,回到主線程處理UI
                        runOnUiThread {
                            handleRefreshSuccess(beanList)
                        }
                    }

private fun preloadCacheViewsInThread(topicBeanList: MutableList<TopicBean>, success: () -> Unit) {
        Thread {
            var pictureCountInFirstPage = 0 //第一頁有多少個(gè)圖片
            var commentCountInFirstPage = 0 //第一頁有多少條小評論
            var nowTime = System.currentTimeMillis()
            for (topicBean in topicBeanList){
                topicBean.findTotalSpanText(this@FriendActivity2, this@FriendActivity2)
                topicBean.pictures?.let { pictureCountInFirstPage += it.count() }
            }
            Log.e(
                "dq",
                "預(yù)處理Model耗時(shí)為:" + (System.currentTimeMillis() - nowTime) + "毫秒"
            ) //方法運(yùn)行時(shí)間為:12毫秒

            val layoutInflater = LayoutInflater.from(this@FriendActivity2)

            //開始預(yù)加載每個(gè)Item的xml布局
            nowTime = System.currentTimeMillis()
            var i = 0
            while (i < 8 && i < topicBeanList.count()) {
                //這個(gè)8是預(yù)估的數(shù)字,也就是屏幕中的 + mCacheView(size == 2)+ pool里的(max是5,一般就是1)
                val itemView: View = layoutInflater.inflate(R.layout.listitem_topic, null)
               //緩存起來,放到onCreateHolder中使用
                cachedItemViewList!!.add(itemView)
                i++
            }
            Log.e(
                "dq",
                "預(yù)加載耗時(shí)為:" + (System.currentTimeMillis() - nowTime) + "毫秒"
            ) //方法運(yùn)行時(shí)間為:150毫秒


            //開始預(yù)加載每個(gè)Item中的圖片的imageview
            i = 0
            while (i < 10 && i < pictureCountInFirstPage) {
                val pictureImageView = ImageView(this@FriendActivity2)
                cachedImageViewList.add(pictureImageView)
                i++
            }
            Log.e(
                "dq",
                "預(yù)Image和Text耗時(shí)為:" + (System.currentTimeMillis() - nowTime) + "毫秒"
            ) //方法運(yùn)行時(shí)間為:120毫秒

            success()

        }.start()
    }

Adpater:

//創(chuàng)建ViewHolder并綁定上itemview
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
            var view: View? = null
            cachedItemViewList?.let {
                view = it.poll()
                Log.e("dq", "onCreateViewHolder = 用上緩存")
            }
            if (view == null){
                view = mInflater.inflate(R.layout.listitem_topic, parent, false)
                Log.e("dq", "onCreateViewHolder = 沒用上緩存")
            }
            val viewHolder = TopicViewHolder(view!!)

            if (this::cachedImageViewList.isInitialized) {
                //是用了預(yù)加載
                viewHolder.pictureGridLayout.cachedImageViewList = cachedImageViewList
                viewHolder.linearLayoutForListView.cachedTextViewList = cachedTextViewList
            }

            return viewHolder
    }


    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
        if (holder is TopicViewHolder) {
            for (view in holder.pictureGridLayout.children) {
                if (view is ImageView){
                    //必須要移除,不然會:You must call removeView()
                    cachedImageViewList.add(view);
                    view.setImageDrawable(null)
                    Log.e("dq","緩沖池回收圖片 " + cachedImageViewList.size);
                }
            }
            holder.pictureGridLayout.removeAllViews()

            for (view in holder.linearLayoutForListView.children) {
                if (view is QMUILinkTextView){
                    //必須要移除,不然會:The specified child already has a parent. You must call removeView()
                    cachedTextViewList.add(view);
                    Log.e("dq","緩沖池回收評論 " + cachedTextViewList.size);
                }
            }
            //必須要移除,不然會: You must call removeView()
            holder.linearLayoutForListView.removeAllViews()
        }
    }

Author:DQ

我的其他開源庫,給個(gè)Star鼓勵(lì)我寫更多好庫:

Android 朋友圈列表Feed流的最優(yōu)化方案,讓你的RecyclerView從49幀 -> 57幀

Android 仿大眾點(diǎn)評、仿小紅書 下拉拖拽關(guān)閉Activity

Android 仿快手直播間手畫禮物,手繪禮物

Android 直播間聊天消息列表RecyclerView。一秒內(nèi)收到幾百條消息依然不卡頓

Android 仿快手直播界面加載中,頂部的滾動條狀LoadingView

Android Kotlin MVVM框架,全世界最優(yōu)化的分頁加載接口、最接地氣的封裝

Android 基于個(gè)推+華為push的一整套完善的android IM聊天系統(tǒng)

IOS1:1完美仿微信聊天表情鍵盤

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容