refactor: performance improvements to ProfilePictureView.kt and recyclers in conversations and home screen

This commit is contained in:
jubb 2021-02-04 16:57:24 +11:00
parent 4e7345cca5
commit c61d54391b
6 changed files with 104 additions and 32 deletions

View file

@ -23,6 +23,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -79,10 +81,11 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder> implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
{ {
private static final int MAX_CACHE_SIZE = 40; private static final int MAX_CACHE_SIZE = 1000;
private static final String TAG = ConversationAdapter.class.getSimpleName(); private static final String TAG = ConversationAdapter.class.getSimpleName();
private final Map<String,SoftReference<MessageRecord>> messageRecordCache = private final Map<String,SoftReference<MessageRecord>> messageRecordCache =
Collections.synchronizedMap(new LRUCache<String, SoftReference<MessageRecord>>(MAX_CACHE_SIZE)); Collections.synchronizedMap(new LRUCache<String, SoftReference<MessageRecord>>(MAX_CACHE_SIZE));
private final SparseArray<String> positionToCacheRef = new SparseArray<>();
private static final int MESSAGE_TYPE_OUTGOING = 0; private static final int MESSAGE_TYPE_OUTGOING = 0;
private static final int MESSAGE_TYPE_INCOMING = 1; private static final int MESSAGE_TYPE_INCOMING = 1;
@ -191,6 +194,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override @Override
public void changeCursor(Cursor cursor) { public void changeCursor(Cursor cursor) {
messageRecordCache.clear(); messageRecordCache.clear();
positionToCacheRef.clear();
super.cleanFastRecords(); super.cleanFastRecords();
super.changeCursor(cursor); super.changeCursor(cursor);
} }
@ -198,8 +202,39 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
@Override @Override
protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) { protected void onBindItemViewHolder(ViewHolder viewHolder, @NonNull MessageRecord messageRecord) {
int adapterPosition = viewHolder.getAdapterPosition(); int adapterPosition = viewHolder.getAdapterPosition();
MessageRecord previousRecord = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getRecordForPositionOrThrow(adapterPosition + 1) : null;
MessageRecord nextRecord = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getRecordForPositionOrThrow(adapterPosition - 1) : null; String prevCachedId = positionToCacheRef.get(adapterPosition + 1,null);
String nextCachedId = positionToCacheRef.get(adapterPosition - 1, null);
MessageRecord previousRecord = null;
if (adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1)) {
if (prevCachedId != null && messageRecordCache.containsKey(prevCachedId)) {
SoftReference<MessageRecord> prevSoftRecord = messageRecordCache.get(prevCachedId);
MessageRecord prevCachedRecord = prevSoftRecord.get();
if (prevCachedRecord != null) {
previousRecord = prevCachedRecord;
} else {
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
}
} else {
previousRecord = getRecordForPositionOrThrow(adapterPosition + 1);
}
}
MessageRecord nextRecord = null;
if (adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1)) {
if (nextCachedId != null && messageRecordCache.containsKey(nextCachedId)) {
SoftReference<MessageRecord> nextSoftRecord = messageRecordCache.get(nextCachedId);
MessageRecord nextCachedRecord = nextSoftRecord.get();
if (nextCachedRecord != null) {
nextRecord = nextCachedRecord;
} else {
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
}
} else {
nextRecord = getRecordForPositionOrThrow(adapterPosition - 1);
}
}
viewHolder.getView().bind(messageRecord, viewHolder.getView().bind(messageRecord,
Optional.fromNullable(previousRecord), Optional.fromNullable(previousRecord),

View file

@ -337,6 +337,9 @@ public class ConversationItem extends LinearLayout
if (recipient != null) { if (recipient != null) {
recipient.removeListener(this); recipient.removeListener(this);
} }
if (profilePictureView != null) {
profilePictureView.recycle();
}
} }
public MessageRecord getMessageRecord() { public MessageRecord getMessageRecord() {

View file

@ -130,6 +130,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
// Set up recycler view // Set up recycler view
val cursor = DatabaseFactory.getThreadDatabase(this).conversationList val cursor = DatabaseFactory.getThreadDatabase(this).conversationList
val homeAdapter = HomeAdapter(this, cursor) val homeAdapter = HomeAdapter(this, cursor)
homeAdapter.setHasStableIds(true)
homeAdapter.glide = glide homeAdapter.glide = glide
homeAdapter.conversationClickListener = this homeAdapter.conversationClickListener = this
recyclerView.adapter = homeAdapter recyclerView.adapter = homeAdapter

View file

@ -35,6 +35,11 @@ class HomeAdapter(context: Context, cursor: Cursor) : CursorRecyclerViewAdapter<
viewHolder.view.bind(thread, isTyping, glide) viewHolder.view.bind(thread, isTyping, glide)
} }
override fun onItemViewRecycled(holder: ViewHolder?) {
super.onItemViewRecycled(holder)
holder?.view?.recycle()
}
private fun getThread(cursor: Cursor): ThreadRecord? { private fun getThread(cursor: Cursor): ThreadRecord? {
return threadDatabase.readerFor(cursor).current return threadDatabase.readerFor(cursor).current
} }

View file

@ -40,9 +40,8 @@ class ConversationView : LinearLayout {
} }
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater LayoutInflater.from(context)
val contentView = inflater.inflate(R.layout.view_conversation, null) .inflate(R.layout.view_conversation, this)
addView(contentView)
} }
// endregion // endregion
@ -84,6 +83,10 @@ class ConversationView : LinearLayout {
} }
} }
fun recycle() {
profilePictureView.recycle()
}
private fun getUserDisplayName(publicKey: String?): String? { private fun getUserDisplayName(publicKey: String?): String? {
if (TextUtils.isEmpty(publicKey)) return null if (TextUtils.isEmpty(publicKey)) return null
return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!) return DatabaseFactory.getLokiUserDatabase(context).getDisplayName(publicKey!!)

View file

@ -29,6 +29,7 @@ class ProfilePictureView : RelativeLayout {
var additionalDisplayName: String? = null var additionalDisplayName: String? = null
var isRSSFeed = false var isRSSFeed = false
var isLarge = false var isLarge = false
private val imagesCached = mutableSetOf<String>()
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
@ -104,54 +105,78 @@ class ProfilePictureView : RelativeLayout {
fun update() { fun update() {
val publicKey = publicKey ?: return val publicKey = publicKey ?: return
val additionalPublicKey = additionalPublicKey val additionalPublicKey = additionalPublicKey
doubleModeImageViewContainer.visibility = if (additionalPublicKey != null && !isRSSFeed) View.VISIBLE else View.INVISIBLE doubleModeImageViewContainer.visibility = if (additionalPublicKey != null && !isRSSFeed) {
singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE setProfilePictureIfNeeded(
largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE doubleModeImageView1,
publicKey,
displayName,
R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(
doubleModeImageView2,
additionalPublicKey,
additionalDisplayName,
R.dimen.small_profile_picture_size)
View.VISIBLE
} else {
glide.clear(doubleModeImageView1)
glide.clear(doubleModeImageView2)
View.INVISIBLE
}
singleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && !isLarge) {
setProfilePictureIfNeeded(
singleModeImageView,
publicKey,
displayName,
R.dimen.medium_profile_picture_size)
View.VISIBLE
} else {
glide.clear(singleModeImageView)
View.INVISIBLE
}
largeSingleModeImageViewContainer.visibility = if (additionalPublicKey == null && !isRSSFeed && isLarge) {
setProfilePictureIfNeeded(
largeSingleModeImageView,
publicKey,
displayName,
R.dimen.large_profile_picture_size)
View.VISIBLE
} else {
glide.clear(largeSingleModeImageView)
View.INVISIBLE
}
rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE
setProfilePictureIfNeeded(
doubleModeImageView1,
publicKey,
displayName,
R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(
doubleModeImageView2,
additionalPublicKey ?: "",
additionalDisplayName,
R.dimen.small_profile_picture_size)
setProfilePictureIfNeeded(
singleModeImageView,
publicKey,
displayName,
R.dimen.medium_profile_picture_size)
setProfilePictureIfNeeded(
largeSingleModeImageView,
publicKey,
displayName,
R.dimen.large_profile_picture_size)
} }
private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) { private fun setProfilePictureIfNeeded(imageView: ImageView, publicKey: String, displayName: String?, @DimenRes sizeResId: Int) {
glide.clear(imageView)
if (publicKey.isNotEmpty()) { if (publicKey.isNotEmpty()) {
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false); if (imagesCached.contains(publicKey)) return
val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false)
val signalProfilePicture = recipient.contactPhoto val signalProfilePicture = recipient.contactPhoto
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0" if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0"
&& (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") { && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "") {
glide.clear(imageView)
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
imagesCached.add(publicKey)
} else { } else {
val sizeInPX = resources.getDimensionPixelSize(sizeResId) val sizeInPX = resources.getDimensionPixelSize(sizeResId)
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val hepk = if (recipient.isLocalNumber && masterPublicKey != null) masterPublicKey else publicKey val hepk = if (recipient.isLocalNumber && masterPublicKey != null) masterPublicKey else publicKey
glide.clear(imageView)
glide.load(AvatarPlaceholderGenerator.generate( glide.load(AvatarPlaceholderGenerator.generate(
context, context,
sizeInPX, sizeInPX,
hepk, hepk,
displayName displayName
)).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) )).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
imagesCached.add(publicKey)
} }
} else { } else {
imageView.setImageDrawable(null) imageView.setImageDrawable(null)
} }
} }
fun recycle() {
imagesCached.clear()
}
// endregion // endregion
} }