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.setValue
|
||||||
import androidx.compose.runtime.withFrameNanos
|
import androidx.compose.runtime.withFrameNanos
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.DatePeriod
|
import kotlinx.datetime.DatePeriod
|
||||||
@ -19,6 +20,7 @@ import kotlinx.datetime.minus
|
|||||||
import kotlinx.datetime.number
|
import kotlinx.datetime.number
|
||||||
import kotlinx.datetime.plus
|
import kotlinx.datetime.plus
|
||||||
import kotlinx.datetime.todayIn
|
import kotlinx.datetime.todayIn
|
||||||
|
import plus.rua.project.LunarCache
|
||||||
import plus.rua.project.ui.COLLAPSE_THRESHOLD
|
import plus.rua.project.ui.COLLAPSE_THRESHOLD
|
||||||
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
|
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
@ -72,6 +74,42 @@ class CalendarViewModel(
|
|||||||
) {
|
) {
|
||||||
private val today: LocalDate = clock.todayIn(TimeZone.currentSystemDefault())
|
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)
|
var selectedDate by mutableStateOf(today)
|
||||||
private set
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import com.tyme.solar.SolarDay
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
|
import plus.rua.project.LunarCache
|
||||||
import plus.rua.project.ShiftKind
|
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 {
|
enum class DayCellState {
|
||||||
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
|
NORMAL, OTHER_MONTH, TODAY, SELECTED, SELECTED_TODAY
|
||||||
}
|
}
|
||||||
@ -105,6 +67,9 @@ fun DayCell(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) {
|
||||||
|
LunarCache.getOrCompute(date)
|
||||||
|
}
|
||||||
val currentState = when {
|
val currentState = when {
|
||||||
isSelected && isToday -> DayCellState.SELECTED_TODAY
|
isSelected && isToday -> DayCellState.SELECTED_TODAY
|
||||||
isSelected -> DayCellState.SELECTED
|
isSelected -> DayCellState.SELECTED
|
||||||
@ -162,11 +127,6 @@ fun DayCell(
|
|||||||
|
|
||||||
val selectedOutlineColor = MaterialTheme.colorScheme.primary
|
val selectedOutlineColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
// P0-C: 使用静态缓存避免每次重组时重复创建 SolarDay 对象
|
|
||||||
val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) {
|
|
||||||
dayCellInfoCache.getOrPut(date) { computeDayCellInfo(date) }
|
|
||||||
}
|
|
||||||
|
|
||||||
val lunarColor by transition.animateColor(
|
val lunarColor by transition.animateColor(
|
||||||
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
transitionSpec = { tween(150, easing = FastOutSlowInEasing) },
|
||||||
label = "lunarColor"
|
label = "lunarColor"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user