perf: 提取农历/节气计算为 LunarCache LRU 缓存,添加启动预计算

- 新增 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) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-21 09:50:48 +08:00
parent f6b5e62284
commit 6aefaf33a6
3 changed files with 148 additions and 44 deletions

View File

@ -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

View File

@ -0,0 +1,106 @@
package plus.rua.project
import com.tyme.solar.SolarDay
import kotlinx.datetime.LocalDate
/**
* 农历/节气/节假日信息缓存
*
* 使用 LinkedHashMapaccessOrder=true实现 LRU 语义读写速度优于 ConcurrentHashMap
* 通过 @Synchronized 保护并发访问冷启动时主线程单线程访问偏向锁使其几乎零开销
*/
object LunarCache {
private const val MAX_SIZE = 800
@Suppress("DEPRECATION")
private val cache = LinkedHashMap<LocalDate, DayCellInfo>(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<LocalDate>) {
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?
)

View File

@ -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<String, Boolean, String?> {
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<LocalDate, Triple<String, Boolean, String?>>()
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"