This article is originally shared by rongyun technical team, the original title “Swastika dry goods: IM” message “list Katon optimization practice”, the content has been revised for better understanding of the article.
1, the introduction
With the popularity of mobile Internet, BOTH IM developers and ordinary users, IM instant messaging applications are essential in daily use, such as: a certain letter of acquaintance social contact, a certain Q of IM living fossils, a certain nail of enterprise scene, almost everyone must install.
Here are a few of the major IM apps (check the front page to see which ones they are, so I won’t waste any time) :
As you can see above, the front page of these IMs (the “messages” list interface) is mandatory for users every time they open the app. Over time, this home page “news” list will become more and more diverse.
Regardless of IM, as the amount and type of data in the “messages” list increases, it will definitely affect the scrolling experience of the list. As the “first page” of the entire IM, the experience of this list directly determines the user’s first impression, which is very important!
With this in mind, the major IMs on the market focus on and optimize the sliding experience of the “message” list (mainly the lag problem).
This article is going to share rongyun IM technology team based on the analysis and practice of their own products “message” list stuck problem (this article takes Android terminal as an example), to show you an IM in solving similar problems analysis ideas and solutions, I hope to bring you inspiration.
2. Related articles
IM client optimization
- IM Development: How I Solved the problem of client Lag caused by a Large number of offline messages
- IM Development Dry Product Sharing: Practice of Full-text Chat Message Retrieval Technology of netease Yunxin IM Client
- “Sharing of Rongyun Technology: Practice of Network Link Preservation Technology of Rongyun Android IM Products”
- Ali Technology Sharing: Xianyu IM Mobile Terminal Cross-transformation Practice based on Flutter
3. Technical background
For an IM software, the “message” list is the first interface that users contact, and whether the “message” list slides smoothly has a great impact on the user experience.
As functionality increases and data accumulates, more information is displayed on the Messages list.
We found that every time after using the product for a period of time, such as when we swipe back to the “Message” list interface after finishing a Call, there would be a serious lag phenomenon.
So we started a detailed analysis of the “message” list stalling, hoping to find the root of the problem and optimize it with appropriate solutions.
PS: The source code for the products discussed in this article is available from public sources. Interested readers can download it from Appendix 1: Source Code Download.
4. What exactly is Caton?
When it comes to APP lag, many people will say that it is caused by the inability to complete rendering in THE UI 16ms.
So why do you need to do it in 16ms? And what needs to be done in less than 16ms?
With these two questions in mind, we’ll take a closer look in this section.
4.1 RefreshRate and FrameRate
Refresh rate: The number of times the screen refreshes per second, in terms of hardware. Most current phones refresh at 60 hz (the screen refreshes 60 times per second), while some high-end phones refresh at 120Hz (like the iPad Pro).
Frame rate: The number of frames drawn per second, for software. As long as the frame rate is consistent with the refresh rate, we usually see smooth graphics. So at 60FPS we don’t feel stuck.
So what’s the relationship between refresh rate and frame rate?
Just to give you an intuitive example:
If the frame rate is 60 frames per second and the screen has a refresh rate of 30Hz, the top half of the screen stays in the previous frame, while the bottom half of the screen renders the next frame — a situation known as a rip. On the other hand, if the frame rate is 30 frames per second and the screen refresh rate is 60Hz, then two connected frames will show the same picture, and this will result in a “lag”.
So it doesn’t make sense to increase the framerate or refresh rate unilaterally, you need to increase both at the same time.
Since most Android screens have a refresh rate of 60Hz, in order to achieve a Frame rate of 60FPS, a Frame must be drawn in 16.67ms (i.e. 1000ms/60Frame = 16.666ms/Frame).
4.2 VSYNC technology
Because the display starts at the top row of pixels and refreshes row by row, there is a time lag between the refreshes from the top to the bottom.
There are two common problems:
1) If the frame rate (FPS) is greater than the refresh rate, there will be the aforementioned rip; **Copy the code
2) If the frame rate is higher, the data of the next frame will be overwritten before the next frame can be displayed, and the middle frame will be skipped. This situation is called frame skipping. **Copy the code
In order to solve the problem that the frame rate is higher than the refresh rate, VSYNC technology is introduced. In simple terms, the display sends a VSYNC signal every 16ms. The system will wait for the VSYNC signal to arrive before rendering a frame and updating the buffer, thus locking the frame rate and refresh rate.
4.3 How does the system generate a frame
Before Android4.0: processing user input events, drawing, and rasterization are all performed by the CPU application main thread, which can easily cause stuttering. The main reason is that the main thread is too heavy to handle many events, and the CPU has only a few ALU units (arithmetic logic units) and is not good at doing graphical calculations.
On Android4.0, hardware acceleration is enabled by default.
When hardware acceleration is enabled: the image computation that the CPU is not good at is handed over to the GPU. The GPU contains a large number of ALU units, which are designed for the realization of a large number of mathematical operations (so mining generally uses the GPU). When hardware acceleration is enabled, rendering in the main thread is handed over to a separate RenderThread, so that when the main thread synchronizes content to the RenderThread, the main thread is freed to do other work, and the RenderThread does the rest of the work.
Then the complete frame flow is as follows:
As shown above:
-
1) In the first 16ms, the display displays the content of frame 0, and CPU/GPU finishes processing the first frame;
-
2) After the vSYNC signal arrives, the CPU will process the second frame immediately and then hand it to the GPU (the monitor will display the image of the first frame).
The whole process seems fine, but as soon as the frame rate (FPS) falls below the refresh rate, the picture freezes.
A and B represent two buffers. Because the CPU/GPU processing time exceeded 16ms, in the second 16ms, the display should have shown the contents of buffer B, but now has to repeat the contents of buffer A, which is dropped frames (stacken).
Since buffer A is occupied by the display and buffer B is occupied by the GPU, the CPU cannot start processing the content of the next frame when the VSync signal comes, so the CPU does not trigger the drawing for the second 16ms.
4.4 Triple Buffer
In order to solve the problem of dropping frames when the frame rate (FPS) is less than the screen refresh rate, Android4.1 introduces a three-level buffer.
In the case of dual buffers, the Display and GPU occupy one buffer each, so the CPU cannot draw when the vSYNC signal arrives. Now add a buffer and the CPU can draw when the vSYNC signal arrives.
In the second 16ms, although A frame is repeatedly displayed, the CPU can still use the C buffer to complete the drawing work when the Display occupies buffer A and the GPU occupies buffer B, so that the CPU is fully utilized. The subsequent display is also relatively smooth, effectively avoiding further aggravation of Jank.
Through the drawing process, we know that the lag occurs because the frame is dropped, and the frame is dropped because the data is not ready for display when the vSYNC signal arrives. Therefore, to deal with the lag, we need to shorten the CPU/GPU drawing time as much as possible, so that we can ensure the completion of a frame rendering in 16ms.
5. Analysis of Caton problem
5.1 Stuck effect in middle and low-end mobile phones
With the above theoretical foundation, we began to analyze the problem of “message” list stalling. Since the Pixel5 used by Boss is a high-end machine, the lag is not obvious, we specially borrowed a low-end machine from the test students.
The configuration of this low-end machine is as follows:
Let’s take a look at the effect before optimization:
Look at the refresh rate of the phone:
60Hz is fine.
Check out the SDM450 architecture on Qualcomm’s website:
The CPU of this phone is 8-core A53 Processor:
A53 Processor is generally used as a small core in large and small core architectures. Its main function is to save power, and it is generally responsible for scenarios with low performance requirements, such as standby state and background execution, etc. Indeed, A53 achieves extreme power consumption.
On Samsung Galaxy A20s phones, all of them use this Processor and there is no large core, so the processing speed is naturally not very fast, which requires our APP to be better optimized.
With a general understanding of the phone, we use the tool to look at the caton point.
5.2 Analyze the Caton point
Start the GPU rendering mode analysis tool provided by the system and view the Message list.
You can see the histogram is already high in the sky. There is a green horizontal line (representing 16ms) at the bottom of the figure, beyond which frames may drop.
Based on Google’s color map, let’s take a look at the approximate location of time spent.
First of all, let’s be clear that although the tool is called the GPU rendering pattern Analysis tool, most of the operations shown take place in the CPU.
Secondly, according to the color comparison table, we may also find that the color given by Google does not correspond to the color on the real phone. So we can only estimate the approximate location of the time.
As you can see from our screenshots, the green parts are heavily weighted, one part is Vsync delay, the other part is input processing + animation + measurement/layout.
The Vsync delay icon is interpreted as the time taken to operate between two consecutive frames.
The SurfaceFlinger will insert a Vsync message into the UI thread’s MessageQueue next time it distributes Vsync. The message will not be executed immediately, but will be executed after the previous message has been executed. So the Vsync delay refers to the time between Vsync being put into MessageQueue and being executed. The longer this part takes, the more processing is going on in the UI thread, and some of the tasks need to be offloaded to other threads.
The input processing, animation, and measurement/layout parts are all callbacks when the vSYNC signal arrives and starts executing the doFrame method.
void doFrame(long frameTimeNanos, int frame) {
/ /… Omit irrelevant code
try{
Trace.traceBegin(Trace.TRACE_TAG_VIEW, “Choreographer#doFrame”);
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
// Input processing
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
/ / animation
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
// Measure/layout
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally{
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
If this part is time consuming, you need to check if you are doing time-consuming operations in the input event callbacks, or if there are a lot of custom animations, or if the layout level is too deep and it takes too long to measure the View and layout.
6. Specific optimization plan and practice summary
6.1 Asynchronous Execution
With a general direction in mind, we started to optimize the “Messages” list.
In our problem analysis, we found that Vsync latency was significant, so our first thought was to strip the time-consuming tasks from the main thread and put them into the worker thread. In order to locate the main thread method time-consuming more quickly, didi Dokit or Tencent Matrix can be used for slow function positioning.
We found that in the ViewModel of the “messages” list, LiveData was used to subscribe to changes in the user information table, group information table, and group membership table in the database. Whenever these three tables change, the Messages list is retraversed to update the data, and then the page is notified to refresh.
This part of the logic is executed in the main thread, which takes about 80ms. If the “message” list is large, the database table data changes greatly, this part of the time will increase.
mConversationListLiveData.addSource(getAllUsers(), new Observer<List>() {
@Override
public void onChanged(List users) {
if(users ! = null&& users.size() > 0) {
// Iterate over the Message list
Iterator iterable = mUiConversationList.iterator();
while(iterable.hasNext()) {
BaseUiConversation uiConversation = iterable.next();
// Update user information for each item
uiConversation.onUserInfoUpdate(users);
}
mConversationListLiveData.postValue(mUiConversationList);
}
}
});
Since this part is time-consuming, we can put the operation of traversing the updated data into a child thread and then call the postValue method to notify the page to refresh.
We also found that the “message” list data needed to be fetched from the database each time we entered the “message” list, and the session data was read from the database as we loaded more.
After the session data is read, the obtained sessions are filtered out. For example, sessions in different organizations are filtered out.
After the filtration is complete, the weight will be removed:
- 1) If the session already exists, update the current session;
- 2) If not, create a new session and add it to the “Messages” list.
You then need to sort the “Messages” list according to certain rules, and finally inform the UI to refresh.
This part takes 500ms to 600ms, and the time will increase as the amount of data increases. Therefore, this part must be executed in the child thread.
However, attention must be paid to thread safety, otherwise the data will be added more than once, and the “Messages” list will have multiple duplicate data.
6.2 Adding a Cache
When checking the code, we found that there are many places to get the information of the current user, and the current user information is saved in the local SP (later changed to MMKV) and stored in Json format. The user information will be read from SP (IO operation), and then deserialized into objects (reflection).
/ * *
* Get the current user information
* /
public UserCacheInfo getUserCache() {
try{
String userJson = sp.getString(Const.USER_INFO, “”);
if(TextUtils.isEmpty(userJson)) {
return null;
}
Gson gson = newGson();
UserCacheInfo userCacheInfo = gson.fromJson(userJson, UserCacheInfo.class);
returnuserCacheInfo;
} catch(Exception e) {
e.printStackTrace();
}
return null;
}
Getting information about the current user each time can be time-consuming.
To solve this problem, we cache the user information obtained for the first time, return the current user information if it exists in memory, and update the objects in memory each time the current user information is modified.
/ * *
* Get the current user information
* /
public UserCacheInfo getUserCacheInfo(){
// If the current user information already exists, it is returned directly
if(mUserCacheInfo ! = null){
return mUserCacheInfo;
}
// it does not exist to read from SP
mUserCacheInfo = getUserInfoFromSp();
if(mUserCacheInfo == null) {
mUserCacheInfo = newUserCacheInfo();
}
return mUserCacheInfo;
}
/ * *
* Save user information
* /
public void saveUserCache(UserCacheInfo userCacheInfo) {
// Update the cache object
mUserCacheInfo = userCacheInfo;
// Save user information to SP
saveUserInfo(userCacheInfo);
}
6.3 Reducing the Refresh times
In this scenario, unreasonable flushes are reduced on the one hand, and partial global flushes are changed to local flushes on the other.
In the ViewModel of the “Messages” list, LiveData subscribes to changes in the user information table, group information table, and group membership table in the database. Whenever these three tables change, the Messages list is retraversed to update the data, and then the page is notified to refresh.
The logic seems fine, but the code is written in a loop to notify the page refresh once every session data is updated, or 100 times if there are 100 sessions.
mConversationListLiveData.addSource(getAllUsers(), new Observer<List>() {
@Override
public void onChanged(List users) {
if(users ! = null&& users.size() > 0) {
// Iterate over the Message list
Iterator iterable = mUiConversationList.iterator();
while(iterable.hasNext()) {
BaseUiConversation uiConversation = iterable.next();
// Update user information for each item
uiConversation.onUserInfoUpdate(users);
// The pre-optimized code frequently notifies the page refresh
//mConversationListLiveData.postValue(mUiConversationList);
}
mConversationListLiveData.postValue(mUiConversationList);
}
}
});
The optimization is to extract the page refresh notification code out of the loop and refresh it once the data is updated.
There is a Draft function in our APP. Every time you come out of the session, you need to judge whether there is any undeleted text (Draft) in the input box of the session. If there is, you will save it and display [Draft] + content in the “message” list. Because of the draft, you need to refresh the page every time you return from the session to the “Messages” list. Before optimization, the global refresh is used here, and we actually only need to refresh the item corresponding to the session that just exited.
Alerting users to unread messages is a common feature for an IM application. The user profile picture in the “message” list will show the unread message of the current session. When we enter the session, the unread message needs to be cleared and the “message” list updated. Before optimization, the global refresh is also used here. In fact, this part can be changed to refresh a single item.
Typing is a new feature in our APP. Whenever a user is typing in a session, typing is displayed in the “message” list. Copywriting. Global refresh of the list is also used here before optimization, and if typing is present in several sessions at the same time, basically the entire list of “messages” is always refreshed. So this is also a partial refresh, flushing only currently typed session items.
6.4 onCreateViewHolder optimization
While analyzing the Systrace report, we found that a slide was accompanied by a large number of CreateView operations.
Why does this happen?
If the layout of the new item is the same as that of the old item, the CreateView will not be used, but the old item will be reused, and the bindView will be used to set the data. This reduces IO and reflection time when creating a view.
So why is this different than expected?
Let’s look at the cache mechanism of RecyclerView.
RecyclerView has 4 levels of cache, we only analyze the commonly used 2 levels here:
- 1) mCachedViews;
- 2) mRecyclerPool.
The default size of mCachedViews is 2, and the item is placed in mCachedViews as soon as it is removed from the screen view range, since the user is likely to move the item back to the screen view range. Therefore, the createView and bindView operations do not need to be re-executed for the item that is put into mCachedViews.
The FIFO principle is adopted in mCachedViews. If the number of caches reaches the maximum, the first entry item will be removed and put into the next level cache.
MRecyclerPool is the RecycledViewPool type, which creates the corresponding cache pool according to the item type. The default size of each cache pool is 5. The item removed from mCachedViews will be deleted. And put into the corresponding cache pool according to the corresponding itemType.
There are two things to note here:
- 1) The first is that the item is cleared, which means that the next time the item is used, the bindView method needs to be re-executed to reset the data.
- The default size of each buffer pool is 5. That is to say, items of different types will be put into different buffer pools. Every time a new item is displayed, the corresponding buffer pool will be searched first to see if there is any item that can be reused. If yes, execute bindView. If no, perform createView and bindView operations to recreate the view.
The large number of CreateViews in the Systrace report indicates that there is a problem with reusing items, resulting in the need to recreate each new item displayed.
Let’s consider an extreme scenario where our “messages” list is divided into three types of items:
- 1) Group chat item;
- 2) Item;
- 3) Secret talk about item.
We can display 10 items on one screen. The top 10 items are all group chats. Starting from 11 to 20 are single chat items, and from 21 to 30 are secret chat items.
We can see from the figure that group chat 1 and group chat 2 have been removed from the screen and are now put into the mCachedViews cache. For chat 1 and chat 2, CreateView and BindView operations need to be performed because there are no reusable items in the chat cache pool of mRecyclerPool.
Since the previous group chat items were removed from the screen, there was no way to get reusable items from the single chat cache pool, so we always need CreateView and BindView.
Until chat 1 enters the cache pool, as shown in the figure above, if the item that is about to enter the screen is either a single chat item or a group chat item, it can be reused, but it is a secret chat. Since there are no reusable items in the secret chat cache pool, So the next secret chat item to enter the screen also needs to execute CreateView and BindView. The whole RecyclerView cache mechanism in this case, the basic failure.
As an extra note, why is the group chat cache pool group 1 to group 5, but not group 6 to group 10? So it’s not a mistake, it’s RecyclerView, so if the cache pool is full, it’s not going to add any new items.
/ * *
* Add a scrap ViewHolder to the pool.
*
* If the pool is already full for that ViewHolder’s type, it will be immediately discarded.
*
* @param scrap ViewHolder to be added to the pool.
* /
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// If the cache pool is greater than or equal to the maximum number of caches, this function is returned
if(mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if(DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException(“this scrap item already exists”);
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
This explains why we found so many CreateViews in the Systrace report. Knowing the problem, we need to find a way to solve it. The main reason for the failure is that we have three different item types at the same time. If we can change the three different items into one, then when we enter the screen in single chat 4, Retrieves reusable items from the cache pool, bypassing CreateView and resetting data directly from BindView.
When we checked the code, we found that no matter group chat, single chat or secret chat, we all used the same layout and could have used the same itemType. The reason for the separation was the use of some design patterns, so that group chat, single chat and secret chat can be implemented in their own classes, and it will be easier and clearer if there are new extensions in the future.
There is a trade-off between performance and mode, but if you think about it, the layout of the different types of chats on the “messages” list is basically the same, and the only difference between the different chat types is the UI presentation, which can be reconfigured in bindView.
We only register BaseConversationProvider when we register, so there is only one itemType. GroupConversationProvider, PrivateConversationProvider, SecretConversationProvider are inherited in the BaseConversationProvider class, The onCreateViewHolder method is only implemented in the BaseConversationProvider class.
Include a List in the BaseConversationProvider class, Used to hold GroupConversationProvider, PrivateConversationProvider, SecretConversationProvider these three objects, bindViewHolder methods are executed, We first execute the parent class’s methods, which process some logic common to all three types of chat, such as the profile picture and the time when the last message was sent. After that, we use isItemViewType to determine which type of chat is currently being sent, and call the corresponding subclass bindViewHolder. Perform subclass-specific data processing. Here, attention should be paid to page display errors caused by reuse. For example, the color of the session title is changed in the secret chat, but the color of the session title in the group chat is also changed due to item reuse.
After modification, we can save a lot of CreateView operation (IO+ reflection), RecyclerView cache mechanism can run well.
6.5 Preloading + global caching
Although we reduced the number of times we created CreateView, we still needed it on the first screen when we first entered it, and we found that it took a long time to create.
Can this time be optimized?
The first idea was to load the layout asynchronously in onCreateViewHolder, placing IO and reflection on child threads, but this was later removed (for more on this later). If asynchronously loading is not possible, then consider making the creation of the View ahead of time and caching it.
We first created a ConversationItemPool class that is used to preload items in child threads and cache them. The cached item is retrieved directly from the class when onCreateViewHolder is executed, thus reducing onCreateViewHolder execution time.
/ * *
* Add a scrap ViewHolder to the pool.
*
* If the pool is already full for that ViewHolder’s type, it will be immediately discarded.
*
* @param scrap ViewHolder to be added to the pool.
* /
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
// If the cache pool is greater than or equal to the maximum number of caches, this function is returned
if(mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if(DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException(“this scrap item already exists”);
}
scrap.resetInternal();
scrapHeap.add(scrap);
}
The ConversationItemPool uses a thread-safe queue to cache created items. Since this is a global cache, there are memory leaks to watch out for.
So how many items do we need to preload?
After comparing the test machines with different resolutions, the number of items displayed on the first screen is generally 10-12. Since the first three items cannot be retrieved from the cache during the first slide, the CreateView method needs to be executed, so we need to include these three items. So we set the number of preloads to 16. We then recycle the View into the cache pool again in the onViewDetachedFromWindow method.
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// Fetch item from the cache pool
View view = ConversationListItemPool.getInstance().getItemFromPool();
// If not, create Item normally
if(view == null) {
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.rc_conversationlist_item,parent,false);
}
return ViewHolder.createViewHolder(parent.getContext(), view);
}
Note: The onCreateViewHolder method is used to degrade the View. In case the cache View is not retrieved, create one and use it normally. We managed to reduce the onCreateViewHolder time to 2 milliseconds or less, which was zero when the RecyclerView cache was in effect.
A solution to the time-consuming creation of views from XML is to use open source libraries such as the X2C framework, in addition to preloading in asynchronous threads. The main principle is to convert XML files into Java code during compilation to create views, saving IO and reflection time. Or use jetpack Compose’s declarative UI to build the layout.
6.6 onBindViewHolder optimization
When we looked at the Systrace report, we found that in addition to CreateView, BindView also took a lot of time, and even more time than CreateView. In this way, if 10 new items are displayed in a sliding process, it will take more than 100 milliseconds.
This was absolutely unacceptable, so we started cleaning up the onBindViewHolder time-consuming operation.
The onBindViewHolder method is only used for UI Settings and should not be used for time-consuming operations or business logic processing. We need to store time-consuming operations and business logic processing in the data source in advance.
When we checked the onBindViewHolder method, we found that if the user’s avatar does not exist, it will be regenerated as a default avatar, which will be generated using the initial of the user’s name. In this method, MD5 encryption is first performed, then a Bitmap is created, then compressed, and then stored locally (IO). This sequence of operations was very time consuming, so we decided to pull the operation out of the onBindViewHolder, put the generated data into the data source ahead of time, and get it directly from the data source when we used it.
In our “message” list, each session needs to display the time when the last message was sent. The time display format is very complicated. Each time in onBindViewHolder, the number of milliseconds of the last message is formatted into the corresponding String. This part is also very time-consuming, we will also extract this part of the code processing, onBindViewHolder just need to extract the formatted string from the data source to display.
The current number of unread messages is displayed above our profile picture, but the number of unread messages can vary.
Such as:
- 1) If the number of unread messages is a digit, the background graph is round;
- 2) The number of unread messages is two digits, and the background image is an ellipse;
- 3) If the number of unread messages is greater than 99, 99+ will be displayed, and the background image will be longer;
- 4) The message is masked, showing only a small dot, not the quantity.
The diagram below:
Because of these cases, the code here directly sets different PNG background images based on the number of unread messages. In fact, this part of the background can be implemented using Shape.
If PNG images are used, PNG images need to be decoded and rendered by GPU, which consumes CPU resources. The Shape information will be directly transferred to the bottom layer by GPU rendering, which is faster. So we replaced the PNG image with the Shape implementation.
In addition to image Settings, TextView is most commonly used in onBindViewHolder. TextView takes a large proportion of the time spent measuring text, which can actually be executed in child threads. Android officials have also realized this. So a new class has been introduced in Android P: PrecomputedText, which allows the most time-consuming text measurements to be executed in child threads. Since this class is only available in Android P, we can use AppCompatTextView instead of TextView, and do version compatibility processing in AppCompatTextView.
AppCompatTextView tv = (AppCompatTextView) view;
// Use this method instead of setText
tv.setTextFuture(PrecomputedTextCompat.getTextFuture(text,tv.getTextMetricsParamsCompat(),ThreadManager.getInstance().ge tTextExecutor()));
It is very simple to use, the principle is not described here, you can Google. StaticLayout is also used for Rendering in the lower version to speed things up, see the Instagram post Improving Comment Rendering on Android.
4.7 Layout Optimization
In addition to reducing BindView time, the layout hierarchy also affects onMeasure and onLayout time. When using the GPU rendering pattern analysis tool, we found that measurement and layout took a lot of time, so we planned to reduce the layout level of item.
Before optimization, the maximum level of our Item layout was 5. In fact, some of them just add an extra layer of layout to control the ease of hidden, we finally use the constraint layout, the maximum level is reduced to 2.
We also checked to see if there was any repetition of the background color, which would cause overdrawing. Overdrawing is when a pixel is drawn multiple times in the same frame. If the invisible UI is also being drawn, pixels in some areas will be drawn multiple times, wasting a lot of CPU and GPU resources.
In addition to removing duplicate backgrounds, we can also minimize the use of transparency. Android will draw the same area twice, once with the original content and again with the new transparency effect. Basically, transparency animations on Android cause overdrawing, so try to minimize the use of transparency animations, and try not to use alpha on views. For details, please refer to Google’s official video.
After using the constraint layout to reduce the hierarchy and remove the repetitive background, we found that there were still spots. Looking up relevant information online, I found that some netizens also gave feedback that the constraint layout used in RecyclerView item would be stuck, which should be caused by the Bug of constraint layout. We also checked the version number of constraint layout we used.
// App dependencies
AppCompatVersion = ‘1.1.0’
ConstraintLayoutVersion = ‘2.0.0 -beta3’
With the beta version, we moved to the latest stable version 2.1.0. I found things much better. So try not to use beta versions for commercial applications.
6.8 Other Optimizations
In addition to the optimizations mentioned above, there are several smaller optimizations, such as the following.
** For example, to use a higher version of RecyclerView, the prefetch function will be enabled by default:
As can be seen from the above figure, the UI thread has been idle since it finished processing data and handed it to the Render thread. It needs to wait for the arrival of a Vsync signal before processing data, and the idle time is wasted. After prefetching is enabled, the idle time can be used reasonably.
**
2) Set the setHasFixedSize of RecyclerView to true. When our item is fixed in width and height, Update the UI without recalculating using the Adapter’s onItemRangeChanged(), onItemRangeInserted(), onItemRangeRemoved(), and onItemRangeMoved() methods.
**
** If you don’t use RecyclerView animation, By ((SimpleItemAnimator) the rv. GetItemAnimator ()). SetSupportsChangeAnimations (false) to close the default animation to promote efficiency.
7. Optimization scheme of discarding
In the process of doing the “message” list optimization, we adopted some optimization schemes, but ultimately did not adopt them, which is also listed here to illustrate.
7.1 Asynchronous Loading Layout
As mentioned earlier, in order to reduce CreateView time, we initially planned to use asynchronous loading layout to execute IO and reflection in child threads.
We use Google’s official AsyncLayoutInflater to asynchronously load layouts, which notifies us of callbacks when the layout is loaded. But it’s typically used in the onCreate method. The onCreateViewHolder method needs to return the ViewHolder, so there’s no way to use it directly.
To solve this problem, we define a class AsyncFrameLayout that inherits from FrameLayout, We add AsyncFrameLayout as the root layout of the ViewHolder in the onCreateViewHolder method and call the custom inflate method to load the layout asynchronously, Add the layout to AsyncFrameLayout as a child View of AsyncFrameLayout.
public void inflate(int layoutId, OnInflateCompleted listener) {
new AsyncLayoutInflater(getContext()).inflate(layoutId, this, newAsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NotNull View view, int resid, @Nullable @org.jetbrains.annotations.Nullable ViewGroup parent) {
// The tag is inflate completed
isInflated = true;
// After loading the layout, add it to AsyncFrameLayout
parent.addView(view);
if(listener ! = null) {
// After loading data, request BindView binding data again
listener.onCompleted(mBindRequest);
}
mBindRequest = null;
}
});
}
Note here: Because onCreateViewHolder is executed asynchronously, onBinderViewHolder is executed. Your vehicle may not be loaded at that time. Therefore, you need to identify whether your vehicle is successfully loaded with a sign called Isvehicle. If the load is not complete, the data is not bound. Also log the BindView request, and when the layout is loaded, make an active call to refresh the data.
The main reason for not using this approach is that it would increase the hierarchy of the layout, and after using preloading, you don’t need to use this option.
7.2 DiffUtil
DiffUtil is an official data comparison tool provided by Google. It can compare two groups of old and new data, find out the difference, and then notify RecyclerView to refresh.
DiffUtil uses Eugene W. Myers’ difference algorithm to calculate the minimum number of updates needed to convert one list to another. However, comparing data can also be time-consuming, so you can also use the AsyncListDiffer class to perform comparison operations in asynchronous threads.
In using DiffUtil, we find that there are too many data items to compare. To solve this problem, we wrap the data source, add a field to the data source indicating whether to update, change all variables to private type, and provide a set method. Set the update or not field to true in the set method. In this way, when comparing two sets of data, we only need to judge whether the field is true to know whether there is an update.
The idea is nice, but when you actually encapsulate the data source, it turns out that there are classes in a class (that is, there are objects in a class, not primitive data types), and that the outside world can skip the set method by getting an object and modifying its fields by changing its references. To solve this problem, we need to provide all the set methods of the class attributes in the encapsulated class, and no get methods of the class within the class, which is a big change.
If this were the only problem, we could have solved it, but we found that the Message list has a feature that whenever one of the sessions receives a new message, it moves to the top of the message list. Because of the position change, the entire list needs to be refreshed once, which defeats the purpose of using DiffUtil for local refreshes. For example, if the fifth session of the “messages” list receives a new message, the fifth session needs to move to the first session. If the entire list is not refreshed, the problem of duplicate sessions will occur.
Because of this problem, we abandoned DiffUtil because even if we solved the repeated session problem, the benefit would still not be great.
7.3 Refresh when sliding stops
In order to avoid a large number of refresh operations of the “message” list, we recorded the data update of the “message” list when sliding, and waited for the refresh after the sliding stopped.
However, in the actual test process, the refresh after stopping will cause the interface to freeze once, which is more obvious on mid – and low-end computers, so this strategy is abandoned.
7.4 Paging load in advance
Because the list of “messages” can be large, we load the data in pages.
To ensure that the user doesn’t perceive the load wait time, we want to get more data before the user is about to slide to the end of the list, allowing the user to slide down without a trace.
The idea is ideal, but in the practice process, it is also found that there will be a moment of lag in the middle and low end of the machine, so this method is temporarily abandoned.
In addition to the above scheme being abandoned, we found in the optimization process that the sliding speed of “message” list of similar products of other brands is not particularly fast. If the sliding speed is slow, the number of items to be displayed in a sliding process will be small, so that too much data does not need to be rendered in a sliding process. This is also an optimization point, and later we may consider the practice of sliding slower.
8. Summary of this paper
In the development process, with the continuous addition of business, the complexity of our methods and logic will continue to increase, at this time must pay attention to method time-consuming, time-consuming as far as possible to extract the sub-thread execution.
Use Recyclerview do not have brain refresh, can local brush is not global brush, can delay brush is not immediately brush.
The analysis of Caton can be carried out with tools, which will improve the efficiency a lot. After finding the general problem and troubleshooting direction through Systrace, you can use Android Studio’s own Profiler to locate the specific code. (This article has been simultaneously published at: www.52im.net/thread-3732…)