If you work on an Android app (or any touch screen based app actually), there's a very good chance you have at least one activity based on a ListView. In many cases, it may be the screen of your app where users interact with the app the most. In the case of Facebook for Android, this ListView is your News Feed. Keeping News Feed working well in Facebook's Android app presents a variety of engineering challenges starting with performance considerations for touchscreens.
ListView presents an interesting performance problem that wasn't as dominant on non-touch interfaces - the smoothness of the scrolling experience. Users expect that the content will seamlessly keep pace with their fingers as they scroll on a touchscreen. This need wasn't as important when using a keyboard and mouse.
To get your list to appear as if it scrolls smoothly, you need to render about 60 frames of it per second. Basic arithmetic translates this challenge into rendering one frame in under 16.7 milliseconds. This is pretty easily done for most frames with the exception of a notable subset. Whenever a new view goes into the viewport in your ListView, the ListView will call its adapter's
getView method. That method, in turn, has to get a view, and bind all its content into it in less than 16.7ms. (In fact, since other actions have to happen, such as measuring and drawing, there's even less time). This is easily achievable for simple content, such as the views that contain text only. It becomes challenging when trying to render rich content which contains complicated views — a lot of text and images, for example. Android's simple tool to solve this is view recycling: The adapter's
getView method will get an existing
convertView to use if one was made before. This trick works very well, but only when your views are similar to each other.
Facebook's News Feed, a popular screen in the Facebook for Android app, is an extreme example of a complicated ListView. This makes delivering a smooth scrolling experience especially hard.
First, each story you see in the feed is pretty tall, and contains several pieces of text and photos. This makes the challenge of binding all of the story's data without skipping frames significantly harder.
Secondly, there are multiple variations of stories in your feed. Aside from a simple story containing some text and a photo, you can see multiple variations of attachments: a preview for a link, an album, a video, etc. The story might be shared, in which case the story contains another story inside it. Hardest of all, we need to render aggregated stories which means one story can actually be composed of several stories that are related. A good example of this is when many of your friends wish you a happy birthday. These aggregated stories are the most challenging to render, as one of them can easily be twice as tall as the screen of the device.
As an extra challenge, the typical Android phone is not a high-end device. So the amount of computations you can fit in under 16.7ms is likely less than the amount on the majority of phones you develop on.
Around 2012, we started working on converting News Feed from a web-view based solution to native rendering. We knew this was going to be a complicated task due to the sheer number of features news feed supported already at that time. Our design was based on the following concept: For every piece of a story in the feed, we created a custom view class. That custom view class would have a method called
bindModel which would take the object this view was supposed to display and update the view to display it.
So, this meant to display a story we had a
StoryView class, which among other things, had a
HeaderView inside it to display the header of the story.
This approach was simple and intuitive for finding the right piece of code or knowing where to add it, but had various drawbacks:
StoryView, but once bound to a story, two
StoryViewswould be radically different and recycling one into the other wasn't effective.
bindModelmethod inside views meant coupling the business logic with the layout logic. Once something as complicated as a view for displaying a story existed, it was natural to subclass it when you needed a variation of that logic. This resulted in a complicated and hard to navigate hierarchy of classes, which is something to avoid.
bindModelis called. Since
bindModelis called while the user is scrolling, it's a pretty bad time to do heavy work. Since this is the place where you unpack a complicated story object into the various parts, that would not always be the case, which made
bindModela computationally intense method to run for some of the views.
All of these problems were solvable, but each required its own custom solution, which took time, and made maintenance more complicated. For example, to solve the recycling problem we had to implement our own custom recycler to work on top of the list view. With that in mind, we decided to rewrite our rendering code to avoid these pitfalls.
About a year ago we felt it was the time to invest in a new architecture for our rendering code for News Feed. We knew we wanted to avoid the pitfalls of the previous design, and try to get rid of our more fragile code. For that purpose we considered a new idea: Splitting each story in the News Feed to several items in the ListView. Using this idea, the header part of a story will be its own item in the ListView. This nice trick results in various advantages right away. First, this makes Android's recycling effective again. If before two custom views for a story were two different to have one recycled into the other, now, recycling is happening on a sub story level.
So a story's header view is being recycled with a different header, and these have the same layout. The second big advantage is that splitting the views lets us store less views in memory, and bind a story over several frames. If before a huge aggregated story presented a difficult task of binding it without skipping a frame, now this story is split into many parts, and only one of those needs to be bound during frame. This effectively amortizes the binding time for a story over several frames, resulting in less skipped frames.
The other idea we incorporated to the design is decoupling the binding logic from the custom Views themselves. For this purpose we basically split our previous custom view into two classes: A simpler “dumb” custom view, and a Binder class. The custom view is now limited to a basic set of setters (such as “setTitle”, “setSubtitle” and “setProfilePic” for the header) and is unaware of a story object. The binder class is the replacement for the
bindModel method. It has three methods:
bind method takes a view and sets the right info from the story on it, the
unbind reverses this and does cleanup which might be necessary (usually it's not needed) and lastly, the
prepare method is meant to solve the issue of performing hard work during binding time. It is called before the first time a binder is bound, and intelligently scheduled when there's free CPU time on the UI thread. This makes it ideal for allocating click listeners, formatting strings, building spannables and so forth.
To make our adapter work, we just need to generate the right list of binders. This is not a simple task; there are many types of binders, and each story has a different number of them according to the parts it should render. In the previous design, this was solved by the hierarchy of views, but now that there's no “one view” per story we needed to create it in a different way. We decided to do this by creating a
PartDefinition class to define each possible part of a story. There are two types of part definitions: A single part definition which defines what views it needs, whether it is needed for a specific story, and how to create the correct binder for it. The second type is a group part definition that lets us logically group several parts into one.
Using these classes we rebuilt the hierarchy of parts of a story. Now to generate the list of binders for a story, we simply start from the root part and recursively expand it to the list of single parts, which we then use to generate binders.
This rewrite effort has resulted in many benefits:
This has been a great undertaking to replace such a core piece of code seamlessly, but the hard work was worth it as it paid of both in improving the performance, and the reliability and maintainability of the code.