肇鑫的技术博客

业精于勤,荒于嬉

深入研究RealmSwift的通知

RealmSwift官方文档在介绍通知时,简单易懂。在一般情况下,我们只需要要使用官方文档的代码就足够使用。但是,如果我们有更高的追求,就需要深入研究了。官方的代码:

class ViewController: UITableViewController {
    var notificationToken: NotificationToken? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        let realm = try! Realm()
        let results = realm.objects(Person.self).filter("age > 5")

        // Observe Results Notifications
        notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
            guard let tableView = self?.tableView else { return }
            switch changes {
            case .initial:
                // Results are now populated and can be accessed without blocking the UI
                tableView.reloadData()
            case .update(_, let deletions, let insertions, let modifications):
                // Query results have changed, so apply them to the UITableView
                tableView.beginUpdates()
                tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                     with: .automatic)
                tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                     with: .automatic)
                tableView.endUpdates()
            case .error(let error):
                // An error occurred while opening the Realm file on the background worker thread
                fatalError("\(error)")
            }
        }
    }

    deinit {
        notificationToken?.invalidate()
    }
}

其中.update(_, let deletions, let insertions, let modifications):中的三个变量,其实是相互关联的。根据苹果的文档performBatchUpdates(_:completion:)

Deletes are processed before inserts in batch operations. This means the indexes for the deletions are processed relative to the indexes of the table view’s state before the batch operation, and the indexes for the insertions are processed relative to the indexes of the state after all the deletions in the batch operation.

在批量操作中,删除优先于插入。这意味着删除的索引是基于批量操作之前的表格状态,而插入的索引则基于所有删除操作完成之后的状态。

问题来了。苹果告诉我们,在批量操作时,删除优先于插入,先删除,后插入。但是实际上还有更新操作,它的顺序又在哪里呢?

测试代码:

import UIKit
import RealmSwift

class TableViewController: UITableViewController {
    var items:Results<Item>!
    var token:NotificationToken? = nil

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            let realm = try Realm()
            items = realm.objects(Item.self).sorted(byKeyPath: "id")
            
            token = items.observe{ [weak self] (changes: RealmCollectionChange) in
                guard let tableView = self?.tableView else { return }
                switch changes {
                case .initial:
                    // Results are now populated and can be accessed without blocking the UI
                    tableView.reloadData()
                case .update(_, let deletions, let insertions, let modifications):
                    print("dels: \(deletions), ins: \(insertions), modifies: \(modifications)")
                    
                    // Query results have changed, so apply them to the UITableView
                    tableView.beginUpdates()
                    tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
                                         with: .automatic)
                    tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
                                         with: .automatic)
                    tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
                                         with: .automatic)
                    tableView.endUpdates()
                case .error(let error):
                    // An error occurred while opening the Realm file on the background worker thread
                    fatalError("\(error)")
                }
            }
        } catch let error {
            print(error)
        }
        
        DispatchQueue.main.async {
            self.addItems()
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
            self.addDeleteUpdate()
        }
    }
    
    deinit {
        token?.invalidate()
        token = nil
    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let item = items[indexPath.row]
        cell.textLabel?.text = String(item.id)
        cell.detailTextLabel?.text = item.title

        return cell
    }
}

extension TableViewController {
    func addItems() {
        do {
            let realm = try Realm()
            try realm.write {
                (0..<100).forEach {
                    let item = Item()
                    item.id = $0
                    item.title = String($0)
                    realm.add(item)
                }
            }
        } catch let error {
            print(error)
        }
    }
    
    func addDeleteUpdate() {
        do {
            let realm = try Realm()
            try realm.write {
                // add
                
                (100..<200).forEach {
                    let item = Item()
                    item.id = $0
                    item.title = String($0)
                    realm.add(item)
                }
                
                // delete
                stride(from: 20, to: 60, by: 2).forEach {
                    let item = realm.object(ofType: Item.self, forPrimaryKey: $0)
                    realm.delete(item!)
                }
                
                // update
                (80..<120).forEach {
                    let item = realm.object(ofType: Item.self, forPrimaryKey: $0)!
                    item.title = String(item.id + 1000)
                }
            }
        } catch let error {
            print(error)
        }
    }
}

应用启动之后,我们先插入100个数据,然后执行插入、删除和更新的操作。然后我们打印.update(_, let deletions, let insertions, let modifications):的数据:

dels: [20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58], ins: [80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], modifies: [80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

我们可以看到,删除的索引是和原始顺序一致的。插入的索引则是基于删除,进行了移动。最后更新的部分,也是和原始顺序一致的,并且我们可以看到,它的后面的部分,和插入的索引进行了合并,所以只有一半。

结论

当进行批量操作时,执行的顺序是,先执行修改、之后是删除、最后是插入。