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:
parent
f6b5e62284
commit
6aefaf33a6
@ -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
|
||||
|
||||
|
||||
106
core/src/main/kotlin/plus/rua/project/LunarCache.kt
Normal file
106
core/src/main/kotlin/plus/rua/project/LunarCache.kt
Normal file
@ -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<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?
|
||||
)
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user