RecyclerView - memory increasing when swiping - android

I have a problem with the RecyclerView. I need to set the recycler flag viewHolder.setIsRecyclable(false) to false for some important reasons (ViewModel, LiveData,..). When I add a lot of items to the list, the app works fine, the memory does not raise dramatically, but when i swipe up and down the memory increase dramatically. My Questions: Why does memory consumption increase when swiping? The views are already added, so why ist the memory increasing?

From Documentation
fun setIsRecyclable(recyclable: Boolean): Unit
Informs the recycler whether this item can be recycled. Views which
are not recyclable will not be reused for other items until
setIsRecyclable() is later set to true. Calls to setIsRecyclable()
should always be paired (one call to setIsRecyclabe(false) should
always be matched with a later call to setIsRecyclable(true)). Pairs
of calls may be nested, as the state is internally reference-counted.
The recycler view basically destroys the views and recreates them when it is necessary (within the user-screen-visible range). Since you set viewHolder.setIsRecyclable(false) the views won't be recycled leads to increase in the memory.
Q: The views are already added, so why is the memory increasing?
The views already added are not destroyed and again new views are created when you swipe up which leads to increase in the memory.
viewHolder.setIsRecyclable(false) is not the solution for your expected result.

Related

RecyclerView ANR during fast fling

I have a RecyclerView (1.2.1), with a List adapter, and ViewHolder, backed by a Room PagingSource. There's about 700 items on the list. The paging seems to work fine, and I've flattened my View hierarchy as much as I can.
Upon initially loading the recycler view, everything seems fine. Paging works, everything seems snappy. onCreateViewHolder in my adapter is called 14 times, and initially 5 are visible on the screen.
Slower scrolling is fine (it does call onCreateViewHolder more often than I expected, but there's no jank).
The problem comes when rapidly flinging through the list. After 3-5 fast flings, it appears to decide that it needs to have more cached view holders, and makes many, many calls to onCreateViewHolder - this method is clocking in at ~5ms, but there's just too many of them, and the scrolling stops. It appears to call onCreateViewHolder ~700 times - the same as the number of items on the list, like it's not recycling the views at all.
At that point, sometimes the app will recover, and at that point everything is smooth and it doesn't appear to need to create more ViewHolders. Sometimes however I will get the ANR dialog.
I've tried tweaking the recyclerView.recycledViewPool.setMaxRecycledViews(), but this doesn't appear to to increase the recycledViewCount until after the mass onCreateViewHolder calls.
Is there anything I can do to resolve this? Make the fling speed slower? Tune the view holder recycling somehow so that it doesn't go nuts and try and create so many at once?
I don't think I can get the layout inflation any better, given my design and data constraints. And even if I could, it's still creating waaay too many to be able to get them done in under 16ms!
In my situation, I needed to do 2 things. First and foremost, I needed to adjust my paging configuration. My prefetchDistance was too small in relation to my page size! This got rid of the ANR - no more mass creation of ViewHolders!
The recyclerView would still pause scrolling when loading a page (especially towards the end). I added a loadStateFlow collector that handles showing a loading indicator so that the user knows there's more data coming.

Does Android batch UI operations or re-draw every single command?

This question came to my mind when re-binding data to views in RecycleView. Before selectively applying UI changes to the views, I usually reset them all to their default states.
Such as
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
/* Reseting to normal/default state */
holder.title.visibility = View.VISIBLE
holder.poster.visibility = View.VISIBLE
/* Applying data */
if (data.poster.url == null) {
holder.poster.visibility = View.GONE
}
}
Here the poster View has potentially its visibility changed to VISIBLE and GONE again within a very short time interval.
Does Android actually invalidate and request drawing for both visibility changes? For example, if before binding, holder.poster view was GONE, then Android would draw it visible and then gone again?
Or does it batch them and only execute the latest state? For example, if we want the app to run at 60fps, it might batch UI operations in 16ms intervals. I can change Visibility hundreds of times, but it will actually draw the very last state within that 16ms batch.
The 2nd point holds true, but for a much more simple reason in your use-case.
The onBindViewHolder method runs on the main thread. Thus no other operation is possible on that thread until it returns.
Every change to a view property might invalidate the view. Layouting and drawing happens once, after onBindViewHolder has returned.
Yes, ViewHolders are recycled. That's the reason for their existence: so that expensive-to-build ViewHolders can be recycled to display different items. Yes, you need to zero out any changes to the contents of the ViewHolder that may have been made while an item was bound to it.
Rendering is also batched. Any change you make to a View property invalidates rendering or layout (as appropriate), and layout and rendering passes occur later. Property changes on views ultimately make calls to RequestLayout(), and/or RequestRender() to schedule layout and/or rendering passes at a later time. There is virtually zero cost for the second and subsequent changes.
I'm not quite sure when "a later time" is, but there's no delay involved. Right away, for all practical purposes, but batched. Something along the lines of process all queued messages, and when there are none, start a layout pass if it's been requested.
Rendering then takes place in a separate pass, after the layout pass has taken place.
Recycler view actual does a LOT of work figuring out where items were displayed, where items will be displayed and animating between the old and new location. This occurs in the layout pass, after you have notified the RecylerView that items in your adapter have changed. But it's done in a batch. You can toggle away on visibility to your hearts content. Only one render pass, and one layout pass takes place. (That's a sight simplification; atypically for an Android view, internally, RecyclerView, actually executes two layout passes, but that's not something you need to know or are supposed to know. It's a hack. I THINK you're being called during the first layout pass).
With respect to toggling bits and pieces in an onBindVieHolder call... rest assured that layout and rendering for the contents of the ViewHolder have been thoroughly and completely invalidated by the time you get the call. You are about to display something else, somewhere else. So go to it!

What's the difference between RecyclerView.setItemViewCacheSize and RecycledViewPool.setMaxRecycledViews?

The documentation says that setItemViewCacheSize
sets the number of offscreen views to retain before adding them to the
potentially shared recycled view pool.
and setMaxRecycledViews
sets the maximum number of ViewHolders to hold in the pool before
discarding.
But don't they both function as a cache where views are taken from (i.e., the first sets the number of views cached by the RV, while the second sets that of the RVP)?
Also, when a view is needed, where is it taken first, from the RVP or from the RV's cache?
And what's the optimal (scrolling-wise, ignoring memory) configuration for the two for a simple unnested recyclerview?
Here's the full documentation for setItemViewCacheSize():
Set the number of offscreen views to retain before adding them to the potentially shared recycled view pool.
The offscreen view cache stays aware of changes in the attached adapter, allowing a LayoutManager to reuse those views unmodified without needing to return to the adapter to rebind them.
In other words, when you scroll the RecyclerView such that there's a view that is just barely completely off-screen, the RecyclerView will keep it around so that you can scroll it back into view without having to re-execute onBindViewHolder().
This is different from the recycled view pool, which is a pool of views that the RecyclerView has said it doesn't need anymore, but which is kept around to avoid the expensive task of inflating new views.
In short, the "item view cache" holds elements in such a way that the RecyclerView can avoid calling both onCreateViewHolder() and onBindViewHolder(), whereas the recycled view pool holds elements such that the RecyclerView can avoid calling onCreateViewHolder() but will still have to call onBindViewHolder().
Also, when a view is needed, where is it taken first, from the RVP or from the RV's cache?
I don't think it really matters, and I don't know of a precise definition, but generally I think you can imagine that views that have just exited the device's viewport and then return to the viewport will be taken from the "item view cache", while views that come on-screen but were not previously on-screen will come from the recycled view pool.
And what's the optimal (scrolling-wise, ignoring memory) configuration for the two for a simple unnested recyclerview?
Just use the defaults. I would never consider changing these unless I had profiled my app and determined beyond a doubt that the defaults weren't working for me. But, if I just take you at your word, ignoring memory, the larger the cache size the better. But really, just use the defaults.
I've read this article by Pavel Shmakov and it explains the difference between Pool and Cache
Pool and Cache in Action
If a ViewHolder was found nowhere, it is created and bound.
If a ViewHolder was found in pool, it is bound.
If a ViewHolder was found in cache, there is nothing to be done.
So, as long as the cache isn’t full, ViewHolders go there. If it’s
full, a new ViewHolder pushes a ViewHolder from the “other end” of the
cache into the pool. If a pool is already full, that ViewHolder is
pushed into oblivion, to the garbage collector that is
Now let’s look at the way pool and cache behave in a couple of actual
RecyclerView usage scenarios.
Consider scrolling:
As we scroll downwards, there is a “tail” behind the currently seen
items consisting of cached items and then a pooled item. When the item
8 appears on screen, no suitable ViewHolder is found in cache: no
ViewHolder associated with position 8 there. So we use a pooled
ViewHolder, which was previously at position 3. When item 6 disappears
on top, it goes to the cache, pushing 4 into the pool.
The picture is different when we start scrolling in the opposite
direction:
Here we find a ViewHolder for position 5 in view cache and reuse it
right away, without rebinding. And that seems to be the main use-case
of the cache — to make scrolling in opposite direction, to the items
we’ve just seen, more efficient. So if you have a news feed, the cache
might be useless, since users won’t go back too often. But if it’s a
list of things to choose from, say a gallery of wallpapers, you might
want to extend the capacity of the cache.
Read more here https://android.jlelse.eu/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714

How to dispose of views that are removed by MvxListView adapter

I have a list of lists of lists, implemented as a MvxListView.
It looks like
-Parent-
MvxListView containing items of Fruit
-HumanCell-
Some forms and an MvxListView containing items of ExistingFruitColors
-ExistingFruitColors-
Some forms and an MvxListView containing items of ExistingShapes
-ExistingShapes
Some forms
I noticed that when I'm navigating in my app (this closing and reopening the ViewModel) as well as when I'm scrolling in the listview, I'm seeing some garbage collecting GC_BRIDGE appearing in the debug screen. In 2 minutes, memory consumption can grow from 6MB to 12MB for a simple 30 cells list.
The fact that a lot of object are created is not a problem, but I'm curious if it's possible to override a method either in the adapter or in the list to garbage or dispose of some views and listeners.
There is no changes happening in the list, but there's a lot of memory variations.
I'm thinking about listening to ViewModel changes and only update the listview canvas when a real change happens, but that's an ugly solution.
Is there something I should be aware when nesting binded lists into binded lists?
Can I force the MvxListView to just draw my items and not attempt to do anything else with it?
Is there something I should be aware when nesting binded lists into binded lists?
IMO, generally, this is normally better done as a single list with sections/groups. It's easy to use header/footer cells for each section and to use a custom adapter to decide which cell to draw.
Having lists within lists can also give a strange touch experience for the user - since the lists can scroll independently - but if this is the UX you desire, then this should be OK.
Can I force the MvxListView to just draw my items and not attempt to do anything else with it?
Sorry, but I'm not entirely sure what this question means.
In general, MvxListView (and ListView) only draw the cells/items currently on screen - recycling is used on cells to reduce the memory used.
When a Activity is Finished, then the MvxListView's ItemSource is set to null - which should result in the ListView memory being cleared up when the Activity is complete.
One thing to be aware of is that in order to estimate the total list scroll size, then the list view does initially have to consider all the list items - it does have to estimate how high each cell is.

Showing animation of Views created from a custom adapter

I'm trying to show an animation with all Views that I've created from an adapter. When I scroll down, it shows the animation correctly, but when I scroll up, I see these Views recreate themselves and show the animation again. Then, when I scroll down, it happens again.
My assumption is that the mechanism of creating a View from an adapter is to load the View into memory; just the group of Views which are on screen right now (but above and below views are not loaded into memory). These will be loaded again when I scroll to these views, right?
Is there any way to fix this problem?
PS: Sorry for my English, I hope you understand my problem.
My assumption is that the mechanism of creating a View from an adapter
is to load the View into memory; just the group of Views which are on
screen right now (but above and below views are not loaded into
memory)
That's somewhat correct: a ListView will not try to visualize any data that isn't (at least partially) visible. It also 'recycles' views, meaning that any view that isn't currently used to present data to the user and is of the same 'type' as the next data item, may get reused.
Hence you shouldn't rely on persisting data with or make any assumptions about the existence of particular views. In stead, use something that's separate from the views; e.g. the dataset you're visualizing.
Quite often, you'll supply a list of POJOs to a BaseAdapter or ArrayAdapter. You could simply add a boolean to the POJO indicating whether it should animate or not, and change that whenever the animation for that particular item finishes. Alternatively, you could keep track of these values in a separate collection (which is probably the more straightforward approach if you're dealing with a Cursor as data source rather than POJOs).

Categories

Resources