You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套RecyclerView性能问题:日历多日期加笔记后卡顿崩溃

问题分析与解决方案

核心卡顿原因

  1. 重复创建大量对象:主适配器CalendarAdapter的onBindViewHolder每次触发时,都会为每个日期的RecyclerView重新创建CalendarDayAdapter和LinearLayoutManager,导致内存占用飙升、GC频繁,最终引发卡顿甚至崩溃。
  2. ViewPool使用错误:两个适配器的getItemViewType返回position,导致ViewPool无法跨位置复用ViewHolder;且每个日期RecyclerView未共用统一的复用池,缓存机制失效。

解决方案(最小代码改动)

1. 核心修改思路

  • 复用日期RecyclerView的Adapter和LayoutManager,避免重复创建
  • 统一使用全局RecycledViewPool,修复ViewHolder复用逻辑
  • 仅更新Adapter数据源,而非重建Adapter

2. 具体代码修改

修改CalendarAdapter

class CalendarAdapter(val items: MutableList<CalendarClass>,val dayListener: OnDayClickListener, val notificationlistener: CalendarDayAdapter.OnButtonClickListener) : RecyclerView.Adapter<CalendarAdapter.CalendarPickerViewHolder>() {

    // 全局共用的ViewHolder复用池
    private val sharedViewPool = RecyclerView.RecycledViewPool().apply {
        setMaxRecycledViews(0, 50) // 固定viewType为0,设置足够缓存数量
    }

    inner class CalendarPickerViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView), View.OnClickListener {
        // 缓存所有日期的Adapter,避免重复创建
        private val dayAdapters = mutableMapOf<View, CalendarDayAdapter>()
        private val layoutManager = LinearLayoutManager(currentItemView.context)

        init {
            // 一次性初始化所有日期的RecyclerView配置
            val weeks = listOf(
                currentItemView.findViewById<LinearLayout>(R.id.week1),
                currentItemView.findViewById<LinearLayout>(R.id.week2),
                currentItemView.findViewById<LinearLayout>(R.id.week3),
                currentItemView.findViewById<LinearLayout>(R.id.week4),
                currentItemView.findViewById<LinearLayout>(R.id.week5)
            )
            weeks.forEach { weekLayout ->
                for (i in 0..6) {
                    val dayItem = weekLayout.getChildAt(i)
                    dayItem.setOnClickListener(this)
                    val dayRecyclerView = dayItem.findViewById<RecyclerView>(R.id.notificationView)
                    // 仅设置一次LayoutManager和复用池
                    dayRecyclerView.layoutManager = layoutManager
                    dayRecyclerView.setRecycledViewPool(sharedViewPool)
                    // 初始化空Adapter并缓存
                    val emptyAdapter = CalendarDayAdapter(mutableListOf(), 0, 0, 0, notificationlistener, 0)
                    dayRecyclerView.adapter = emptyAdapter
                    dayAdapters[dayItem] = emptyAdapter
                }
            }
        }

        override fun onClick(p0: View?) {
            val position: Int = bindingAdapterPosition
            val currentTodoItem = items[position]
            if (position != RecyclerView.NO_POSITION) {
                dayListener.onDayClick(p0, currentTodoItem.month, currentTodoItem.year)
            }
        }

        // 更新日期数据的方法,替代重建Adapter
        fun updateDayData(dayItem: View, data: MutableList<NotificationData>, day: Int, month: Int, year: Int, datePosition: Int) {
            val adapter = dayAdapters[dayItem] ?: return
            adapter.items.clear()
            adapter.items.addAll(data)
            adapter.day = day
            adapter.month = month
            adapter.year = year
            adapter.datePosition = datePosition
            adapter.notifyDataSetChanged()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarPickerViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.calender_month, parent, false)
        return CalendarPickerViewHolder(view)
    }

    override fun onBindViewHolder(holder: CalendarPickerViewHolder, position: Int) {
        val currentTodoItem = items[position]
        holder.itemView.apply {
            if(currentTodoItem.name.trim().isNotEmpty()) {
                findViewById<TextView>(R.id.monthYearText).text = currentTodoItem.name
            }
            val week1 = findViewById<LinearLayout>(R.id.week1)
            val week2 = findViewById<LinearLayout>(R.id.week2)
            val week3 = findViewById<LinearLayout>(R.id.week3)
            val week4 = findViewById<LinearLayout>(R.id.week4)
            val week5 = findViewById<LinearLayout>(R.id.week5)

            fun getMonthLength(year:Int,month:Int): Int {
                if(year % 4 == 0 && month == 2){
                    return 29
                }
                return when (month) {
                    1 -> 31
                    2 -> 28
                    3 -> 31
                    4 -> 30
                    5 -> 31
                    6 -> 30
                    7 -> 31
                    8 -> 31
                    9 -> 30
                    10 -> 31
                    11 -> 30
                    12 -> 31
                    else -> 0
                }
            }

            var index = 0
            fun equalDate(year:Int,month:Int,day:Int): MutableList<NotificationData> {
                for(i in 0 until currentTodoItem.notificationdata.size){
                    if (currentTodoItem.notificationdata[i].year == year && currentTodoItem.notificationdata[i].month == month && currentTodoItem.notificationdata[i].day == day)
                    {
                        index = i
                        return currentTodoItem.notificationdata[i].data
                    }
                }
                return mutableListOf(NotificationData("",WHITE,"","",Time_hour_minutes(0,0)))
            }

            var week = 1
            val dayOfLastMonthDate = LocalDate.of(currentTodoItem.year, currentTodoItem.month, 1).dayOfWeek.value
            val monthLength = getMonthLength(currentTodoItem.year,currentTodoItem.month)
            var previousMonthLength: Int = if(currentTodoItem.month>1){
                getMonthLength(currentTodoItem.year,currentTodoItem.month-1)
            } else {
                getMonthLength(currentTodoItem.year-1,12)
            }

            // 修改为更新已有Adapter数据,不再创建新Adapter
            fun updateWeek1(weeks : LinearLayout) {
                for (i in 1 until dayOfLastMonthDate){
                    val dayItem = weeks.getChildAt(i-1)
                    val dayText = dayItem.findViewById<TextView>(R.id.date)
                    val dayValue = previousMonthLength - dayOfLastMonthDate + i + 1
                    dayText.text = dayValue.toString()
                    val data = if(currentTodoItem.month>1){
                        equalDate(currentTodoItem.year,currentTodoItem.month-1, dayValue)
                    } else {
                        equalDate(currentTodoItem.year-1,12, dayValue)
                    }
                    val monthVal = if(currentTodoItem.month>1) currentTodoItem.month-1 else 12
                    val yearVal = if(currentTodoItem.month>1) currentTodoItem.year else currentTodoItem.year-1
                    holder.updateDayData(dayItem, data, dayValue, monthVal, yearVal, index)
                }
                for (i in 0..7-dayOfLastMonthDate) {
                    val dayItem = weeks.getChildAt(i+dayOfLastMonthDate-1)
                    val dayText = dayItem.findViewById<TextView>(R.id.date)
                    if(week >= monthLength){
                        dayText.text = "0"
                        val data = equalDate(currentTodoItem.year,currentTodoItem.month,0)
                        holder.updateDayData(dayItem, data, 0, currentTodoItem.month, currentTodoItem.year, index)
                    } else {
                        dayText.text = week.toString()
                        val data = equalDate(currentTodoItem.year,currentTodoItem.month,week)
                        holder.updateDayData(dayItem, data, week, currentTodoItem.month, currentTodoItem.year, index)
                        week++
                    }
                }
            }

            fun updateWeek(weeks : LinearLayout) {
                for (i in 0..6) {
                    val dayItem = weeks.getChildAt(i)
                    val dayText = dayItem.findViewById<TextView>(R.id.date)
                    if(week >= monthLength){
                        for (j in 1..6-i){
                            val nextDayItem = weeks.getChildAt(i+j)
                            val nextDayText = nextDayItem.findViewById<TextView>(R.id.date)
                            nextDayText.text = j.toString()
                            val data = if(currentTodoItem.month < 12){
                                equalDate(currentTodoItem.year,currentTodoItem.month+1,j)
                            } else {
                                equalDate(currentTodoItem.year+1,1,j)
                            }
                            val monthVal = if(currentTodoItem.month <12) currentTodoItem.month+1 else 1
                            val yearVal = if(currentTodoItem.month <12) currentTodoItem.year else currentTodoItem.year+1
                            holder.updateDayData(nextDayItem, data, j, monthVal, yearVal, index)
                        }
                        break
                    } else {
                        dayText.text = week.toString()
                        val data = equalDate(currentTodoItem.year,currentTodoItem.month,week)
                        holder.updateDayData(dayItem, data, week, currentTodoItem.month, currentTodoItem.year, index)
                        week++
                    }
                }
            }

            updateWeek1(week1)
            updateWeek(week2)
            updateWeek(week3)
            updateWeek(week4)
            updateWeek(week5)
        }
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        return 0 // 所有月份项布局一致,返回固定值
    }

    override fun getItemCount(): Int = items.size

    interface OnDayClickListener{
        fun onDayClick(view: View?, month: Int, year: Int)
    }
}

修改CalendarDayAdapter

class CalendarDayAdapter(
    var items: MutableList<NotificationData>,
    var day: Int,
    var month: Int,
    var year: Int,
    val listener: OnButtonClickListener,
    var datePosition: Int
) : RecyclerView.Adapter<CalendarDayAdapter.CalendarPickerViewHolder>() {

    inner class CalendarPickerViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView), View.OnClickListener{
        private val notificationText = currentItemView.findViewById<TextView>(R.id.notification)

        init {
            notificationText.setOnClickListener(this)
        }

        override fun onClick(p0: View?) {
            val position : Int = bindingAdapterPosition
            if(position != RecyclerView.NO_POSITION){
                listener.onButtonClick(day,month,year,p0, position, datePosition)
            }
        }

        // 封装数据绑定逻辑
        fun bind(data: NotificationData) {
            if (data.title.isNotBlank()) {
                notificationText.text = data.title
                notificationText.background.setTint(data.color)
                notificationText.visibility = View.VISIBLE
            } else {
                notificationText.visibility = View.INVISIBLE
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarPickerViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.notification, parent, false)
        return CalendarPickerViewHolder(view)
    }

    override fun onBindViewHolder(holder: CalendarPickerViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        return 0 // 所有笔记项布局一致,返回固定值
    }

    override fun getItemCount(): Int = items.size

    interface OnButtonClickListener{
        fun onButtonClick(day :Int,month :Int,year :Int, view : View?, position : Int, datePosition : Int)
    }
}

3. 修改说明

  • ViewHolder初始化优化:在CalendarPickerViewHolder的init方法中一次性配置所有日期RecyclerView的LayoutManager和复用池,缓存空Adapter。
  • 数据更新逻辑调整:将原创建Adapter的逻辑改为调用updateDayData更新已有Adapter的数据源,避免重复创建对象。
  • ViewPool修复:两个适配器的getItemViewType均返回固定值0,确保全局复用池可以正常跨位置复用ViewHolder。

内容的提问来源于stack exchange,提问作者lolboer9999999

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 08:42:22