mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Improve conversation scroll performance.
Fixes a performance bottleneck in `ConversationFragment` caused by expensive calculations in `ConversationItemDecorations.hasHeader()`. This method is invoked in `RecyclerView.ItemDecoration.getItemOffsets()`, which runs on every layout pass and happens frequently during scrolling. The most expensive calculation in `hasHeader()` is `toEpochDay()`. That method calls `Long.toLocalDate()`, which clones a `TimeZone` object on each call. Upon opening one conversation (without scrolling), I observed that `toEpochDay()` was called over 1000 times in less than a second, rapidly allocating memory and causing garbage collection pressure that potentially leads to ANRs. We only need to calculate `hasHeader()` once for each conversation element, so caching the result of that method will eliminate the unnecessary calculations and improve the memory usage of `ConversationFragment`.
This commit is contained in:
@@ -34,7 +34,7 @@ private typealias ConversationElement = MappingModel<*>
|
||||
* This is a converted and trimmed down version of [org.thoughtcrime.securesms.util.StickyHeaderDecoration].
|
||||
*/
|
||||
class ConversationItemDecorations(hasWallpaper: Boolean = false, private val scheduleMessageMode: Boolean = false) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private val hasHeaderByPosition: MutableMap<Int, Boolean> = mutableMapOf()
|
||||
private val headerCache: MutableMap<Long, DateHeaderViewHolder> = hashMapOf()
|
||||
private var unreadViewHolder: UnreadViewHolder? = null
|
||||
|
||||
@@ -48,6 +48,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
set(value) {
|
||||
field = value
|
||||
updateUnreadState(value)
|
||||
hasHeaderByPosition.clear()
|
||||
}
|
||||
|
||||
var hasWallpaper: Boolean = hasWallpaper
|
||||
@@ -212,7 +213,21 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
(currentItems[bindingAdapterPosition] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the item at [bindingAdapterPosition] should have a header.
|
||||
*/
|
||||
private fun hasHeader(bindingAdapterPosition: Int): Boolean {
|
||||
return hasHeaderByPosition.getOrPut(
|
||||
key = bindingAdapterPosition,
|
||||
defaultValue = { calculateHasHeader(bindingAdapterPosition) }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the item at [bindingAdapterPosition] should have a header. Avoid using this method in performance critical areas, because it
|
||||
* bypasses the caching mechanism used in [hasHeader].
|
||||
*/
|
||||
private fun calculateHasHeader(bindingAdapterPosition: Int): Boolean {
|
||||
val model = if (bindingAdapterPosition in currentItems.indices) {
|
||||
currentItems[bindingAdapterPosition]
|
||||
} else {
|
||||
@@ -236,9 +251,13 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
|
||||
return false
|
||||
}
|
||||
|
||||
return model.toEpochDay() != previousDay
|
||||
val result = model.toEpochDay() != previousDay
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a header view for the provided [ConversationMessageElement] and caches it for future use.
|
||||
*/
|
||||
private fun getHeader(parent: RecyclerView, model: ConversationMessageElement): DateHeaderViewHolder {
|
||||
val headerHolder: DateHeaderViewHolder = headerCache.getOrPut(model.toEpochDay()) {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.conversation_item_header, parent, false)
|
||||
|
||||
Reference in New Issue
Block a user