简体   繁体   English

NSFetchedResultsController使加载视图控制器非常慢

[英]NSFetchedResultsController makes loading view controller very slow

(Update: In Edit 4 below, I definitely found the cause of my problem!) (更新:在下面的编辑4中,我肯定找到了我的问题的原因!)

I'm using a tableView with a NSFetchedResultsController . 我使用tableViewNSFetchedResultsController That's how I fetch the data (I call this in viewDidLoad() ) : 这就是我获取数据的方式(我在viewDidLoad()调用它):

let fetchRequest: NSFetchRequest<Entry> = Entry.fetchRequest()
        let sortSections = NSSortDescriptor(key: #keyPath(Entry.section), ascending: false)
        let sortDate = NSSortDescriptor(key: #keyPath(Entry.date), ascending: true)
        fetchRequest.sortDescriptors = [sortSections, sortDate]
        fetchRequest.fetchBatchSize = 15 // this seems to have no impact
        fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObject, sectionNameKeyPath: #keyPath(Entry.section), cacheName: "EntriesCache")

This, somehow, is very slow (I notice this when I segue to the view controller that contains this table view ). 不知怎的,这很慢(当我转向包含此table viewview controller ,我会注意到这一点)。

On my device, I tried it with about 200 Entry objects in my database. 在我的设备上,我在我的数据库中使用大约200个Entry对象进行了尝试。 It takes slightly more than 1 seconds for the view controller to appear. view controller显示的时间略多于1秒。 But I also tried it with about 10 objects, it's not that much faster. 但我也尝试了大约10个物体,它的速度并不快。 (Strangely, on the simulator it's incredibly fast) (奇怪的是,在模拟器上它的速度非常快)

I tried to analyse it by using the Time Profiler . 我尝试使用Time Profiler进行分析。 During this 1 second, the CPU is at 100%. 在这1秒钟内,CPU处于100%。 Is that normal? 这是正常的吗?

Before I noticed this slow performance, I didn't have this line 在我注意到这种缓慢的表现之前,我没有这条线

fetchRequest.fetchBatchSize = 15

I added it but nothing changed. 我添加了它但没有改变。 It's not even a tiny bit faster. 它甚至没有一点点快。 Also I printed the count of the fetched objects after they have loaded: 我还在加载后打印了获取对象的计数:

print(fetchedResultsController.fetchedObjects?.count)

It says that all objects are loaded, not just 15 of them (as you can't see more than that at once in the table view). 它表示所有对象都被加载,而不只是其中的15个(因为在表视图中你不能一次看到它)。 Why is that? 这是为什么?

That's my Entry Entity I use for the table view 这是我用于table view Entry实体 进入实体

I don't know what code/information you need in order to be able to help me (I'm not an expert in terms of performance issues). 我不知道您需要哪些代码/信息才能帮助我(我不是性能问题方面的专家)。 Please tell me, if you need anything more than that. 请告诉我,如果你需要更多的东西。

Thanks you guys! 谢谢你们!

Edit: 编辑:

How I access the managedObjectContext: 我如何访问managedObjectContext:

lazy var managedObject: NSManagedObjectContext = {
        let managedObject = self.appDelegate.persistentContainer.viewContext
        return managedObject
    }()

Edit 2 (maybe I found the cause?): Okay, so I edited my scheme so that it shows me all SQL queries. 编辑2(也许我找到原因?):好的,所以我编辑了我的方案,以便它向我显示所有SQL查询。 First, it loads several times 15 rows (when 15 is the fetchBatchSize ). 首先,它加载了15行(当15是fetchBatchSize )。 But after that it gets interesting: 但之后它变得有趣:

I didn't exactly count it, but I'm pretty sure that it does the following query(/queries) for every object there is in the database . 我并没有完全统计它,但我很确定它会为数据库中的每个对象执行以下查询(/ queries)。 I tried it with 600 objects or so and it took quite a while for these SQL queries to run through: 我尝试了600个左右的对象,这些SQL查询需要花费很长时间才能完成:

CoreData: sql: SELECT t0.Z_ENT, t0.Z_PK, Z_FOK_ENTRY FROM ZENTRYTEXT t0 WHERE  t0.ZENTRY = ? 
CoreData: annotation: sql connection fetch time: 0.0001s
CoreData: annotation: total fetch execution time: 0.0002s for 1 rows.
CoreData: annotation: to-many relationship fault "entryTexts" for objectID 0xd000000006480000 <x-coredata://C53DABDD-5D31-4ADE-B6E7-3ED69454B572/Entry/p402> fulfilled from database.  Got 1 rows
CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTEXT, t0.ZENTRY, t0.Z_FOK_ENTRY FROM ZENTRYTEXT t0 WHERE  t0.Z_PK = ? 
CoreData: annotation: sql connection fetch time: 0.0001s
CoreData: annotation: total fetch execution time: 0.0002s for 1 rows.
CoreData: annotation: fault fulfilled from database for : 0xd000000007940002 <x-coredata://C53DABDD-5D31-4ADE-B6E7-3ED69454B572/EntryText/p485>

I don't know what exactly that is, but I think it's causing the delay. 我不知道究竟是什么,但我认为这会造成延误。 After these queries ran through, the view controller is being displayed. 执行这些查询后,将显示视图控制器。

Edit 3: 编辑3:

Here are my table view datasource methods: 这是我的table view数据源方法:

func numberOfSections(in tableView: UITableView) -> Int {
        guard let sections = fetchedResultsController.sections else {
            return 0
        }
        return sections.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {        
        guard let sectionInfo = fetchedResultsController.sections?[section] else {
            return 0
        }

        return sectionInfo.numberOfObjects
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "bitCell") as! BitCell
        let entry = fetchedResultsController.object(at: indexPath)

        cell.configure(entry: entry)
        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let entry = fetchedResultsController.object(at: indexPath)
        extendBitPopup.fadeIn(withEntry: entry, completion: nil)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.y >= 400 {
            UIView.animate(withDuration: 0.5, animations: { 
                self.arrowUpButton.alpha = 1.0
                self.arrowUpButton.isEnabled = true
                self.arrowUpButton.isUserInteractionEnabled = true
            })
        } else {
            UIView.animate(withDuration: 0.5, animations: {
                self.arrowUpButton.alpha = 0.0
                self.arrowUpButton.isEnabled = false
                self.arrowUpButton.isUserInteractionEnabled = false
            })
        }
    }


    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let entry = fetchedResultsController.object(at: indexPath)
        guard !entry.isFault else {
            return 0
        }
        // this estimates the height the cell needs when the text is inserted
        return BitCell.suggestedHeight(forEntry: entry)
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if let sectionInfo = fetchedResultsController.sections?[section] {
            let dateFormatter = DateFormatter()
            // Entry.section has this format: "yyyyMMdd" I chose this to make a section for each day. 
            dateFormatter.dateFormat = "yyyyMMdd"
            let date = dateFormatter.date(from: sectionInfo.name)!

            dateFormatter.dateStyle = .full
            dateFormatter.timeStyle = .none

            return dateFormatter.string(from: date)
        }

        return ""

    }


    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }


    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 25
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        let view = UIView()
        return view
    }

    func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {

        let moment = UITableViewRowAction(style: .normal, title: "Moment") { (action, indexPath) in
            let entry = self.fetchedResultsController.object(at: indexPath)
            entry.isMoment = !entry.isMoment
            self.appDelegate.saveContext()
            tableView.setEditing(false, animated: true)
        }
        moment.backgroundColor = AppTheme.baseGray

        let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, index) in
            let entry = self.fetchedResultsController.object(at: indexPath)
            self.managedObject.delete(entry)
            self.appDelegate.saveContext()
            tableView.setEditing(false, animated: true)
        }
        delete.backgroundColor = AppTheme.errorColor

        return [delete, moment]
    }

Edit 4 (I found the cause): 编辑4(我找到了原因):

The problem is this function: 问题是这个功能:

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let entry = fetchedResultsController.object(at: indexPath)
        guard !entry.isFault else {
            return 0
        }
        return BitCell.suggestedHeight(forEntry: entry)
    }

I played around with this and now I'm almost certain that this line is the troublemaker: 我玩弄了这个,现在我几乎可以肯定这条线是麻烦制造者:

let entry = fetchedResultsController.object(at: indexPath)

If I return a static CGFloat right before this line, the view controller loads almost instantly (I tested it with 700 objects). 如果我在此行之前返回一个静态CGFloat,视图控制器几乎立即加载(我用700个对象测试它)。 Also, it then fetches only the first 50 items (that's the fetchBatchSize ) and it only loads more if you scroll down. 此外,它然后只获取前50个项目(即fetchBatchSize ),如果向下滚动它只会加载更多项目。

If I return after this line, it fetches all of the data (according to the many SQL queries), it gets extremely slow and this whole delay problem appears. 如果我在这一行之后返回,它会获取所有数据(根据许多SQL查询),它会变得非常慢并且出现整个延迟问题。

So, I think the problem occurs if this line from above tries to get an object that is faulted (maybe it then tries to refetch from the database or something) 所以,我认为如果上面的这一行尝试获取一个faulted的对象(可能它然后尝试从数据库或其他东西重新获取),则会faulted

Now's the question: How to solve this? 现在的问题是:如何解决这个问题? I need the Entry object in order to estimate the cell height, but I only want to call this line if I know that the object isn't faulted (if that's the problem). 我需要Entry对象来估计单元格高度,但是如果我知道对象没有出现故障(如果这是问题),我只想调用此行。 How can I do that? 我怎样才能做到这一点?

Use the estimated height delegate method, and return a fixed size. 使用估计的高度委托方法,并返回固定大小。 The table view should then only query for the actual height of a row when it needs to display that row, so it can properly use the faulting and batching features of the fetch results controller. 然后,表视图只应在需要显示该行时查询行的实际高度,以便它可以正确使用获取结果控制器的错误和批处理功能。

If a table has, say 400 rows, and you've implemented heightForRow, then it will call the delegate method for every single row in the table so that it can calculate the content size of the table view. 如果一个表有400行,并且你已经实现了heightForRow,那么它将为表中的每一行调用委托方法,以便它可以计算表视图的内容大小。 Asking the results controller for the object at a certain index will convert it from a fault automatically, and in any case returning zero size will completely mess up the content size of your table. 在特定索引处询问对象的结果控制器会自动将其从故障转换,并且在任何情况下返回零大小将完全弄乱表的内容大小。

If you supply an estimated size instead, either by using the delegate method or setting it as a property on the table, then the table view will only call the specific height method for rows that are, or are about to be, displayed. 如果您提供估计的大小,或者通过使用委托方法或将其设置为表上的属性,则表视图将仅针对将要或将要显示的行调用特定高度方法。 It will use the estimated height to make a guess at the table view's content size. 它将使用估计的高度来猜测表格视图的内容大小。 This means the content size fluctuates slightly as you scroll, but this is not really noticeable. 这意味着滚动时内容大小会略有波动,但这并不是很明显。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM