From 6aefaf33a63cc129ada6437c4c66371cc107216d Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 21 May 2026 09:50:48 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=8F=90=E5=8F=96=E5=86=9C=E5=8E=86/?= =?UTF-8?q?=E8=8A=82=E6=B0=94=E8=AE=A1=E7=AE=97=E4=B8=BA=20LunarCache=20LR?= =?UTF-8?q?U=20=E7=BC=93=E5=AD=98=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E9=A2=84=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 LunarCache 单例,LinkedHashMap(accessOrder=true) 实现 LRU 语义, @Synchronized 保护并发,容量 800 条,超限时淘汰 20% - CalendarViewModel init 中在 Default 线程预计算当前月前后各 1 个月 - DayCell 移除内联 computeDayCellInfo 和 mutableMap 缓存,统一使用 LunarCache Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plus/rua/project/CalendarViewModel.kt | 38 +++++++ .../kotlin/plus/rua/project/LunarCache.kt | 106 ++++++++++++++++++ .../kotlin/plus/rua/project/ui/DayCell.kt | 48 +------- 3 files changed, 148 insertions(+), 44 deletions(-) create mode 100644 core/src/main/kotlin/plus/rua/project/LunarCache.kt diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index f6175a6..3f57ab7 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.datetime.DatePeriod @@ -19,6 +20,7 @@ import kotlinx.datetime.minus import kotlinx.datetime.number import kotlinx.datetime.plus import kotlinx.datetime.todayIn +import plus.rua.project.LunarCache import plus.rua.project.ui.COLLAPSE_THRESHOLD import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP import kotlin.time.Clock @@ -72,6 +74,42 @@ class CalendarViewModel( ) { private val today: LocalDate = clock.todayIn(TimeZone.currentSystemDefault()) + init { + coroutineScope.launch(Dispatchers.Default) { + // 预计算当前月前后各 1 个月 + val currentYear = today.year + val currentMonth = today.month.number + + @Suppress("DEPRECATION") // monthNumber 无替代 API + val monthsToPrecompute = listOf( + currentMonth - 1 to currentYear, + currentMonth to currentYear, + currentMonth + 1 to currentYear + ).map { (month, year) -> + when { + month < 1 -> 12 to year - 1 + month > 12 -> 1 to year + 1 + else -> month to year + } + } + + monthsToPrecompute.forEach { (month, year) -> + val firstOfMonth = LocalDate(year, month, 1) + val offset = firstOfMonth.dayOfWeek.ordinal + val startDate = firstOfMonth.minus(DatePeriod(days = offset)) + val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) + val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day + val rows = ((offset + daysInMonth - 1) / 7) + 1 + val totalDays = rows * 7 + + val dates = (0 until totalDays).map { i -> + startDate.plus(DatePeriod(days = i)) + } + LunarCache.precompute(dates) + } + } + } + var selectedDate by mutableStateOf(today) private set diff --git a/core/src/main/kotlin/plus/rua/project/LunarCache.kt b/core/src/main/kotlin/plus/rua/project/LunarCache.kt new file mode 100644 index 0000000..914e73c --- /dev/null +++ b/core/src/main/kotlin/plus/rua/project/LunarCache.kt @@ -0,0 +1,106 @@ +package plus.rua.project + +import com.tyme.solar.SolarDay +import kotlinx.datetime.LocalDate + +/** + * 农历/节气/节假日信息缓存。 + * + * 使用 LinkedHashMap(accessOrder=true)实现 LRU 语义,读写速度优于 ConcurrentHashMap。 + * 通过 @Synchronized 保护并发访问;冷启动时主线程单线程访问,偏向锁使其几乎零开销。 + */ +object LunarCache { + private const val MAX_SIZE = 800 + + @Suppress("DEPRECATION") + private val cache = LinkedHashMap(256, 0.75f, true) + + /** + * 获取指定日期的信息,缓存 miss 时同步计算。 + */ + @Synchronized + @Suppress("DEPRECATION") // monthNumber 无替代 API + fun getOrCompute(date: LocalDate): DayCellInfo { + cache[date]?.let { return it } + val computed = compute(date) + cache[date] = computed + trimIfNeeded() + return computed + } + + /** + * 批量预计算并填充缓存。 + * + * @param dates 日期列表 + */ + @Synchronized + fun precompute(dates: List) { + dates.forEach { date -> + if (!cache.containsKey(date)) { + cache[date] = compute(date) + } + } + trimIfNeeded() + } + + private fun trimIfNeeded() { + if (cache.size > MAX_SIZE) { + val toRemove = (cache.size * 0.2).toInt().coerceAtLeast(1) + val iterator = cache.keys.iterator() + var removed = 0 + while (iterator.hasNext() && removed < toRemove) { + iterator.next() + iterator.remove() + removed++ + } + } + } + + @Suppress("DEPRECATION") // monthNumber 无替代 API + private fun compute(date: LocalDate): DayCellInfo { + val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) + val holidayBadge = solarDay.getLegalHoliday()?.let { if (it.isWork()) "班" else "休" } + val lunarDay = solarDay.getLunarDay() + + // 农历传统节日(仅当天) + val lunarFestival = lunarDay.getFestival() + if (lunarFestival != null) { + return DayCellInfo(lunarFestival.getName(), true, holidayBadge) + } + + // 节气(当天才显示) + val termDay = solarDay.getTermDay() + if (termDay.getDayIndex() == 0) { + return DayCellInfo(termDay.getSolarTerm().getName(), true, holidayBadge) + } + + // 公历节日(仅当天) + val solarFestival = solarDay.getFestival() + if (solarFestival != null) { + return DayCellInfo(solarFestival.getName(), true, holidayBadge) + } + + // 默认:农历日期 + val name = lunarDay.getName() + val text = if (name == "初一") { + val lunarMonth = lunarDay.getLunarMonth() + "${lunarMonth.getName()}月" + } else { + name + } + return DayCellInfo(text, false, holidayBadge) + } +} + +/** + * 日期单元格显示信息。 + * + * @param annotationText 底部标注文字(农历/节气/节日) + * @param isAnnotationHighlight 是否为高亮标注(节日/节气) + * @param holidayBadge 法定调休角标("班"/"休"/null) + */ +data class DayCellInfo( + val annotationText: String, + val isAnnotationHighlight: Boolean, + val holidayBadge: String? +) diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index 02f2b0d..c1b262b 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -34,48 +34,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import com.tyme.solar.SolarDay import kotlinx.datetime.LocalDate +import plus.rua.project.LunarCache import plus.rua.project.ShiftKind -// P0-C: 静态缓存 SolarDay 计算结果,避免 Pager 滑动/切换时重复创建对象触发 GC -@Suppress("DEPRECATION") // monthNumber 无替代 API -private fun computeDayCellInfo(date: LocalDate): Triple { - val solarDay = SolarDay.fromYmd(date.year, date.monthNumber, date.day) - val holidayBadge = solarDay.getLegalHoliday()?.let { if (it.isWork()) "班" else "休" } - val lunarDay = solarDay.getLunarDay() - - // 农历传统节日(仅当天) - val lunarFestival = lunarDay.getFestival() - if (lunarFestival != null) { - return Triple(lunarFestival.getName(), true, holidayBadge) - } - - // 节气(当天才显示) - val termDay = solarDay.getTermDay() - if (termDay.getDayIndex() == 0) { - return Triple(termDay.getSolarTerm().getName(), true, holidayBadge) - } - - // 公历节日(仅当天) - val solarFestival = solarDay.getFestival() - if (solarFestival != null) { - return Triple(solarFestival.getName(), true, holidayBadge) - } - - // 默认:农历日期 - val name = lunarDay.getName() - val text = if (name == "初一") { - val lunarMonth = lunarDay.getLunarMonth() - "${lunarMonth.getName()}月" - } else { - name - } - return Triple(text, false, holidayBadge) -} - -private val dayCellInfoCache = mutableMapOf>() - enum class DayCellState { NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY } @@ -105,6 +67,9 @@ fun DayCell( onClick: () -> Unit, modifier: Modifier = Modifier ) { + val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) { + LunarCache.getOrCompute(date) + } val currentState = when { isSelected && isToday -> DayCellState.SELECTED_TODAY isSelected -> DayCellState.SELECTED @@ -162,11 +127,6 @@ fun DayCell( val selectedOutlineColor = MaterialTheme.colorScheme.primary - // P0-C: 使用静态缓存避免每次重组时重复创建 SolarDay 对象 - val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) { - dayCellInfoCache.getOrPut(date) { computeDayCellInfo(date) } - } - val lunarColor by transition.animateColor( transitionSpec = { tween(150, easing = FastOutSlowInEasing) }, label = "lunarColor"