Skip to content

Commit

Permalink
Add CachedLayoutInflater to improve conversation render performance.
Browse files Browse the repository at this point in the history
  • Loading branch information
greyson-signal authored and alex-signal committed May 14, 2020
1 parent 7fd3bfa commit ed33e04
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.Conversions;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
Expand Down Expand Up @@ -171,8 +172,7 @@ public long getItemId(int position) {
case MESSAGE_TYPE_UPDATE:
long start = System.currentTimeMillis();

LayoutInflater inflater = LayoutInflater.from(parent.getContext());
V itemView = ViewUtil.inflate(inflater, parent, getLayoutForViewType(viewType));
V itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false);

itemView.setOnClickListener(view -> {
if (clickListener != null) {
Expand All @@ -197,7 +197,7 @@ public long getItemId(int position) {
return new PlaceholderViewHolder(v);
case MESSAGE_TYPE_HEADER:
case MESSAGE_TYPE_FOOTER:
return new HeaderFooterViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
return new HeaderFooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false));
default:
throw new IllegalStateException("Cannot create viewholder for type: " + viewType);
}
Expand Down Expand Up @@ -417,7 +417,19 @@ void toggleSelection(MessageRecord record) {
}
}

/**
* Provided a pool, this will initialize it with view counts that make sense.
*/
@MainThread
static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) {
pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING, 15);
pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING, 15);
pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15);
pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1);
pool.setMaxRecycledViews(MESSAGE_TYPE_FOOTER, 1);
pool.setMaxRecycledViews(MESSAGE_TYPE_UPDATE, 5);
}

private void cleanFastRecords() {
synchronized (releasedFastRecords) {
Iterator<MessageRecord> recordIterator = fastRecords.iterator();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewSwitcher;
Expand Down Expand Up @@ -105,6 +106,7 @@
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.HtmlUtil;
Expand Down Expand Up @@ -161,6 +163,16 @@ public class ConversationFragment extends Fragment {
private MessageRequestViewModel messageRequestViewModel;
private ConversationViewModel conversationViewModel;

public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));

CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent, parent, 10);
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2);
}

@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Expand Down Expand Up @@ -413,6 +425,7 @@ private void initializeListAdapter() {
ConversationAdapter adapter = new ConversationAdapter(GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
list.setAdapter(adapter);
list.addItemDecoration(new StickyHeaderDecoration(adapter, false, false));
ConversationAdapter.initializePool(list.getRecycledViewPool());

setLastSeen(conversationViewModel.getLastSeen());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,6 @@ protected void onFinishInflate() {

bodyText.setOnLongClickListener(passthroughClickListener);
bodyText.setOnClickListener(passthroughClickListener);

bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));
}

@Override
Expand Down Expand Up @@ -522,6 +520,7 @@ private void setBodyText(MessageRecord messageRecord, @Nullable String searchQue
bodyText.setClickable(false);
bodyText.setFocusable(false);
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
bodyText.setMovementMethod(LongClickMovementMethod.getInstance(getContext()));

if (messageRecord.isRemoteDelete()) {
String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
Expand Down Expand Up @@ -118,6 +119,7 @@
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
Expand Down Expand Up @@ -264,6 +266,12 @@ public void onResume() {
}
}

@Override
public void onStart() {
super.onStart();
ConversationFragment.prepare(requireContext());
}

@Override
public void onPause() {
super.onPause();
Expand Down
133 changes: 133 additions & 0 deletions app/src/main/java/org/thoughtcrime/securesms/util/CachedInflater.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.util;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.asynclayoutinflater.view.AsyncLayoutInflater;

import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
* A class that can be used to pre-cache layouts. Usage flow:
*
* - At some point before you want to use the views, call {@link #cacheUntilLimit(int, ViewGroup, int)}.
* - Later, use {@link #inflate(int, ViewGroup, boolean)}, which will prefer using cached views
* before inflating new ones.
*/
public class CachedInflater {

private static final String TAG = Log.tag(CachedInflater.class);

private final Context context;

/**
* Does *not* work with the application context.
*/
public static CachedInflater from(@NonNull Context context) {
return new CachedInflater(context);
}

private CachedInflater(@NonNull Context context) {
this.context = context;
}

/**
* Identical to {@link LayoutInflater#inflate(int, ViewGroup, boolean)}, but will prioritize
* pulling a cached view first.
*/
@MainThread
@SuppressWarnings("unchecked")
public <V extends View> V inflate(@LayoutRes int layoutRes, @Nullable ViewGroup parent, boolean attachToRoot) {
View cached = ViewCache.getInstance().pull(layoutRes);
if (cached != null) {
if (parent != null && attachToRoot) {
parent.addView(cached);
}
return (V) cached;
} else {
return (V) LayoutInflater.from(context).inflate(layoutRes, parent, attachToRoot);
}
}

/**
* Will inflate as many views as necessary until the cache holds the amount you specify.
*/
@MainThread
public void cacheUntilLimit(@LayoutRes int layoutRes, @Nullable ViewGroup parent, int limit) {
ViewCache.getInstance().cacheUntilLimit(context, layoutRes, parent, limit);
}

/**
* Clears all cached views. This should be done if, for instance, the theme changes.
*/
@MainThread
public void clear() {
Log.d(TAG, "Clearing view cache.");
ViewCache.getInstance().clear();
}

private static class ViewCache {

private static final ViewCache INSTANCE = new ViewCache();

private final Map<Integer, List<View>> cache = new HashMap<>();

private long lastClearTime;

static ViewCache getInstance() {
return INSTANCE;
}

@MainThread
void cacheUntilLimit(Context context, @LayoutRes int layoutRes, @Nullable ViewGroup parent, int limit) {
AsyncLayoutInflater inflater = new AsyncLayoutInflater(context);

int existingCount = Util.getOrDefault(cache, layoutRes, Collections.emptyList()).size();
int inflateCount = Math.max(limit - existingCount, 0);

for (int i = 0; i < inflateCount; i++) {
final long enqueueTime = System.currentTimeMillis();
inflater.inflate(layoutRes, parent, (view, resId, p) -> {
Util.assertMainThread();
if (enqueueTime < lastClearTime) {
Log.d(TAG, "Prefetch is no longer valid. Ignoring.");
return;
}

List<View> views = cache.get(resId);

views = views == null ? new LinkedList<>() : views;
views.add(view);

cache.put(resId, views);
});
}
}

@MainThread
@Nullable View pull(@LayoutRes int layoutRes) {
List<View> views = cache.get(layoutRes);
return views != null && !views.isEmpty() ? views.remove(0)
: null;
}

@MainThread
void clear() {
lastClearTime = System.currentTimeMillis();
cache.clear();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public void onResume(Activity activity) {
OverridePendingTransition.invoke(activity);
activity.startActivity(intent);
OverridePendingTransition.invoke(activity);
CachedInflater.from(activity).clear();
}
}

Expand Down

0 comments on commit ed33e04

Please sign in to comment.