Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dev doc] Collections and asynchronous updates #1558

Merged
merged 3 commits into from
Jun 25, 2019
Merged

Conversation

nguyenhuy
Copy link
Member

This document describes the internal workings of ASCollectionNode, ASPagerNode, and ASTableNode, specifically how they handle asynchronous batch updates and then forward them to their backing UICollectionView or UITableView.

Let me know if there is anything else I should mention/explain more, as well as any other feedback. Thanks!

Copy link
Member

@garrettmoon garrettmoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is soooooo helpful Huy! Thank you so much for writing this!

ASRangeController is responsible for determining which collection cell nodes are in a certain [range](/docs/intelligent-preloading.html) and setting the appropriate interface state to each cell node and its subnodes.

ASDataController is the real data source of the backing UICollectionView. It is responsible for processing batch updates and then forwards them to the backing view in a thread-safe manner. Since the main purpose of this document is explaining asynchronous batch updates, ASDataController will be explained in details.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forwards -> forwarding
explained in details. -> explained in detail.

UICollectionView allows clients to submit a single edit operation without wrapping it in a batch update. ASCollectionNode allows this as well. However, under the hood, edit operations are automatically bundled into a batch update, also called a "change set" (i.e `_ASHierarchyChangeSet`). Furthermore, batch updates can be nested and the end result will be a single change set. For details on how this is implemented, look into the implementation of ASCollectionView's `-beginUpdates`, `-endUpdatesAnimated:completion:`, `-performBatchAnimated:updates:completion:`, as well as edit methods such as `-insertItemsAtIndexPaths:` and `-reloadData`.

It's worth noted that at the beginning of its lifecycle, UICollectionView needs to perform an initial data load. It does so by simply calling `-reloadData` which is also wrapped in a change set by ASCollectionView. So the initial data load is just another change set like all others that come after it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth noted -> It's worth noting

# Change set processing pipeline

ASDataController doesn't accept nor process individual edit operations. In fact, the main data-updating method ASDataController exposes is `-updateWithChangeSet:(_ASHierarchyChangeSet *)` and edit operations must be wrapped in change sets before submitted via this method. In this sense, you can consider it as a change set processing pipeline.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must be wrapped in change sets before submitted -> must be wrapped in change sets before being submitted

Since the pipeline operates on multiple threads, every change set needs to go through the pipeline to ensure data consistency, not only with the data source but also with the backing view. Internally, ASDataController uses two queues and two data sets to facilitate the pipeline.

The two queues are called `_editingTransactionQueue` and `_mainSerialQueue`. The former is a serial background `dispatch_queue` while the latter is an ASMainSerialQueue. ASMainSerialQueue which is discussed in detail in the [Threading doc](/development/threading.html).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The former is a serial background `dispatch_queue` while the latter is an ASMainSerialQueue. ASMainSerialQueue which is discussed in detail in the [Threading doc](/development/threading.html).

Instead maybe:

The former is a serial background `dispatch_queue` while the latter is an ASMainSerialQueue; ASMainSerialQueue is discussed in detail in the [Threading doc](/development/threading.html).

The two queues are called `_editingTransactionQueue` and `_mainSerialQueue`. The former is a serial background `dispatch_queue` while the latter is an ASMainSerialQueue. ASMainSerialQueue which is discussed in detail in the [Threading doc](/development/threading.html).

The two data sets of ASDataController are `pendingMap` and `visibleMap`. Each of them is an instance of ASElementMap which is essentially an immutable, main-thread-only collection of ASCollectionElements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

essentially

The two data sets of ASDataController are `pendingMap` and `visibleMap`. Each of them is an instance of ASElementMap which is essentially an immutable, main-thread-only collection of ASCollectionElements.

Each ASCollectionElement represents an item or supplementary view in the collection view. It has enough information for ASDataController to allocate and measure the backing ASCellNode, most importantly a `nodeBlock`. The block is the one returned by the data source at the beginning of the process (more on this later) and retained by the element until it's executed. The block is executed the first time `-[ASCollectionElement node]` is called. Once executed, the result -- an ASCellNode -- is strongly retained by the element and the block is released. That means at any given time, an element either has a node block or a node instance, and never both. In case a caller wants to get the node but only if it's already allocated, the caller should call `-nodeIfAllocated` instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASCellNode, most importantly -> ASCellNode and most importantly

## Data source index space vs UIKit index space

Because each change set is processed asynchronously and it might take the backing UICollectionView multiple main thread run loops to fully consume the change set (especially if the view is busy responding to user events such as scrolling), there is a time window in which the data source has a different, more recent "view" of the data than the backing UICollectionView. This includes, for example, numbers of items and sections in the collection, as well as the index path of any particular item. As a result, it's useful to think in terms of the data source's index space and UIKit index space. There are certain operations that rely on the knowledge of the data source and so they must be operating in the data source index space. On the other hand, any operations related to or originated from UIKit must be operating in the UIKit index space. Failing to do so can cause exceptions and/or crashes, including ones throw by UICollectionView.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw -> thrown

1. The process starts on the main thread. At this point, `pendingMap` and `visibleMap` are the same. A mutable copy of `pendingMap` is made and then updated according to the change set. This includes removing old items and asking the data source for information regarding newly inserted items, such as node block and constrained size. At the end of this step, `pendingMap` is updated to reflect the data source's world view.

2. This is an optional step that is only run if a layout delegate is set to the data controller. By default, the data controller allocates and measures all new items in one pass (step 3). Having a layout delegate allows other classes to customize this behavior. ASCollectionLayout, for example, allocates and measures just enough cells to fill the visible viewport and a bit more. It will allocate more cells on demand as user scrolls. In order to do this, the layout delegate may need to construct a context which must happen on the same main thread run loop and this is the last chance to do so -- the next step will be on a background thread.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as user scrolls -> as a user scrolls

In order to do this, the layout delegate may need to construct a context which must happen on the same main thread run loop and this is the last chance to do so -- the next step will be on a background thread.

maybe:

In order to do this, the layout delegate may need to construct a context which must happen on the same main thread run loop. This is the last chance to do so -- the next step will be on a background thread.

3. On `_editingTransactionQueue`, allocate and measure all elements in the pending map, or call out to the layout delegate and let it decide. At the end of this step, elements are ready to be consumed by the backing view.

4. Dispatch back to main thread using `_mainSerialQueue` by scheduling the next step (step 5) in a block and add it to the queue. If there are any other blocks scheduled to the queue before this point and are not yet consumed, they will be executed before step 5.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatch back to main thread using `_mainSerialQueue` by scheduling the next step (step 5) in a block and add it to the queue. If there are any other blocks scheduled to the queue before this point and are not yet consumed, they will be executed before step 5.

maybe:

Schedule the next step (step 5) in a block and add it to the queue on the main thread via `_mainSerialQueue`. If there are any other blocks scheduled to the queue before this point which are not yet consumed, they will be executed before step 5.

# Animations

Animation flag of the batch update is stored in the change set (or in each change in case of UITableViewRowAnimation) which will then be consumed by the backing view at step 5. This means batch update animations are respected and should work as expected.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Animation flag -> The animation flag

@nguyenhuy
Copy link
Member Author

OK, let's get this in and update it later on if need to. Thanks for reviewing, @garrettmoon!

@nguyenhuy nguyenhuy merged commit a11c084 into master Jun 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants