Triển khai Nhập dữ liệu cốt lõi nhanh chóng và hiệu quả trên iOS 5


101

Câu hỏi : Làm cách nào để đưa ngữ cảnh con của tôi thấy các thay đổi vẫn tồn tại trên ngữ cảnh mẹ để chúng kích hoạt NSFetchedResultsController của tôi cập nhật giao diện người dùng?

Đây là thiết lập:

Bạn có một ứng dụng tải xuống và thêm nhiều dữ liệu XML (khoảng 2 triệu bản ghi, mỗi bản ghi có kích thước gần bằng một đoạn văn bản thông thường) Tệp .sqlite có kích thước khoảng 500 MB. Việc thêm nội dung này vào Dữ liệu cốt lõi mất thời gian, nhưng bạn muốn người dùng có thể sử dụng ứng dụng trong khi dữ liệu tải vào kho dữ liệu tăng dần. Nó phải vô hình và không thể nhận thấy đối với người dùng rằng một lượng lớn dữ liệu đang được di chuyển xung quanh, do đó, không bị treo, không bị giật: cuộn như bơ. Tuy nhiên, ứng dụng hữu ích hơn, càng nhiều dữ liệu được thêm vào, vì vậy chúng tôi không thể đợi mãi cho dữ liệu được thêm vào kho dữ liệu Core. Trong mã, điều này có nghĩa là tôi thực sự muốn tránh mã như thế này trong mã nhập:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

Ứng dụng chỉ dành cho iOS 5 nên thiết bị chậm nhất cần hỗ trợ là iPhone 3GS.

Dưới đây là các tài nguyên tôi đã sử dụng cho đến nay để phát triển giải pháp hiện tại của mình:

Hướng dẫn lập trình dữ liệu cốt lõi của Apple: Nhập dữ liệu hiệu quả

  • Sử dụng Autorelease Pools để giảm bộ nhớ
  • Các mối quan hệ Chi phí. Nhập phẳng, sau đó vá các mối quan hệ ở cuối
  • Đừng truy vấn nếu bạn có thể giúp nó, nó làm chậm mọi thứ theo cách O (n ^ 2)
  • Nhập hàng loạt: lưu, đặt lại, thoát và lặp lại
  • Tắt Trình quản lý hoàn tác khi nhập

iDeveloper TV - Hiệu suất dữ liệu cốt lõi

  • Sử dụng 3 bối cảnh: Loại ngữ cảnh chính, chính và giam giữ

iDeveloper TV - Cập nhật dữ liệu cốt lõi cho Mac, iPhone và iPad

  • Chạy lưu trên các hàng đợi khác với performanceBlock giúp mọi thứ trở nên nhanh chóng.
  • Mã hóa làm chậm mọi thứ, hãy tắt nó đi nếu bạn có thể.

Nhập và hiển thị các tập dữ liệu lớn trong dữ liệu cốt lõi của Marcus Zarra

  • Bạn có thể làm chậm quá trình nhập bằng cách dành thời gian cho vòng chạy hiện tại để mọi thứ trở nên suôn sẻ cho người dùng.
  • Mã mẫu chứng minh rằng có thể thực hiện việc nhập lớn và giữ cho giao diện người dùng đáp ứng nhanh, nhưng không nhanh như với 3 ngữ cảnh và lưu không đồng bộ vào đĩa.

Giải pháp hiện tại của tôi

Tôi đã có 3 phiên bản NSManagedObjectContext:

masterManagedObjectContext - Đây là ngữ cảnh có NSPersistingStoreCoordinator và chịu trách nhiệm lưu vào đĩa. Tôi làm điều này để các lưu của tôi có thể không đồng bộ và do đó rất nhanh. Tôi tạo nó khi khởi chạy như thế này:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - Đây là ngữ cảnh mà giao diện người dùng sử dụng ở mọi nơi. Nó là con của masterManagedObjectContext. Tôi tạo nó như thế này:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - Ngữ cảnh này được tạo trong lớp con NSOperation của tôi, lớp này chịu trách nhiệm nhập dữ liệu XML vào Dữ liệu cốt lõi. Tôi tạo nó trong phương thức chính của hoạt động và liên kết nó với ngữ cảnh chính ở đó.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

Điều này thực sự hoạt động rất, RẤT nhanh. Chỉ bằng cách thực hiện 3 thiết lập ngữ cảnh này, tôi đã có thể cải thiện tốc độ nhập của mình hơn 10 lần! Thành thật mà nói, điều này thật khó tin. (Thiết kế cơ bản này phải là một phần của mẫu Dữ liệu cốt lõi tiêu chuẩn ...)

Trong quá trình nhập tôi lưu 2 cách khác nhau. Mỗi 1000 mục tôi lưu trên bối cảnh nền:

BOOL saveSuccess = [backgroundContext save:&error];

Sau đó, vào cuối quá trình nhập, tôi lưu trên ngữ cảnh chính / cha, bề ngoài, đẩy các sửa đổi ra các ngữ cảnh con khác bao gồm ngữ cảnh chính:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

Vấn đề : Vấn đề là giao diện người dùng của tôi sẽ không cập nhật cho đến khi tôi tải lại chế độ xem.

Tôi có một UIViewController đơn giản với một UITableView đang được cung cấp dữ liệu bằng NSFetchedResultsController. Khi quá trình Nhập hoàn tất, NSFetchedResultsController không thấy có thay đổi nào từ ngữ cảnh chính / chính và do đó, giao diện người dùng không tự động cập nhật như tôi vẫn thấy. Nếu tôi bật UIViewController ra khỏi ngăn xếp và tải lại thì tất cả dữ liệu ở đó.

Câu hỏi : Làm cách nào để đưa ngữ cảnh con của tôi thấy các thay đổi vẫn tồn tại trên ngữ cảnh mẹ để chúng kích hoạt NSFetchedResultsController của tôi cập nhật giao diện người dùng?

Tôi đã thử những cách sau chỉ làm treo ứng dụng:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

26
+1000000 cho câu hỏi được hình thành tốt nhất, được chuẩn bị kỹ càng nhất từ ​​trước đến nay. Tôi cũng có câu trả lời ... Tuy nhiên, sẽ mất vài phút để gõ nó lên ...
Jody Hagins

1
Khi bạn nói ứng dụng bị treo, nó ở đâu? Nó đang làm gì?
Jody Hagins

Xin lỗi để đưa ra điều này sau một thời gian dài. Bạn có thể vui lòng làm rõ "Nhập khẩu phẳng, sau đó vá các mối quan hệ ở cuối" nghĩa là gì? Bạn không cần phải có các đối tượng đó trong bộ nhớ để thiết lập các mối quan hệ? Tôi đang cố gắng triển khai một giải pháp rất giống với giải pháp của bạn và tôi thực sự có thể sử dụng một số trợ giúp để giảm dung lượng bộ nhớ.
Andrea Sprega

Xem Tài liệu Apple được liên kết với tài liệu đầu tiên của bài viết này. Nó giải thích điều này. Chúc may mắn!
David Weiss

1
Thực sự câu hỏi hay và tôi nhặt một vài thủ thuật gọn gàng từ mô tả mà bạn cung cấp các thiết lập của bạn
djskinner

Câu trả lời:


47

Có lẽ bạn cũng nên lưu MOC chính trong các bước tiến. Không có ý nghĩa gì nếu MOC đó đợi cho đến khi kết thúc để lưu. Nó có một chuỗi riêng và nó cũng sẽ giúp giảm thiểu bộ nhớ.

Bạn đã viết:

Sau đó, vào cuối quá trình nhập, tôi lưu trên ngữ cảnh chính / cha, bề ngoài, đẩy các sửa đổi ra các ngữ cảnh con khác bao gồm ngữ cảnh chính:

Trong cấu hình của bạn, bạn có hai con (MOC chính và MOC nền), cả hai đều được gán cho "cái".

Khi bạn tiết kiệm cho một đứa trẻ, nó sẽ đẩy những thay đổi lên cho phụ huynh. Các con khác của MOC đó sẽ thấy dữ liệu vào lần tiếp theo khi họ thực hiện tìm nạp ... chúng không được thông báo rõ ràng.

Vì vậy, khi BG lưu, dữ liệu của nó được đẩy lên MASTER. Tuy nhiên, lưu ý rằng không có dữ liệu nào trong số này nằm trên đĩa cho đến khi MASTER lưu. Hơn nữa, bất kỳ mục mới nào sẽ không nhận được ID vĩnh viễn cho đến khi MASTER lưu vào đĩa.

Trong trường hợp của bạn, bạn đang kéo dữ liệu vào MAIN MOC bằng cách hợp nhất từ ​​lưu MASTER trong thông báo DidSave.

Điều đó sẽ hoạt động, vì vậy tôi tò mò về nơi nó được "treo". Tôi sẽ lưu ý rằng bạn đang không chạy trên luồng MOC chính theo cách chuẩn (ít nhất là không dành cho iOS 5).

Ngoài ra, bạn có thể chỉ quan tâm đến việc hợp nhất các thay đổi từ MOC chính (mặc dù đăng ký của bạn có vẻ như chỉ dành cho điều đó). Nếu tôi sử dụng thông báo update-on-did-save-information, tôi sẽ làm điều này ...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

Bây giờ, đối với những gì có thể là vấn đề thực sự của bạn liên quan đến việc treo ... bạn hiển thị hai lệnh gọi khác nhau để lưu trên tổng thể. cái đầu tiên được bảo vệ tốt trong performanceBlock của chính nó, nhưng cái thứ hai thì không (mặc dù bạn có thể đang gọi saveMasterContext trong một performanceBlock ...

Tuy nhiên, tôi cũng sẽ thay đổi mã này ...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

Tuy nhiên, lưu ý rằng MAIN là con của MASTER. Vì vậy, nó không cần phải hợp nhất các thay đổi. Thay vào đó, chỉ cần xem DidSave trên chính và chỉ cần tìm nạp lại! Dữ liệu đã có sẵn trong cha mẹ bạn, chỉ chờ bạn yêu cầu nó. Đó là một trong những lợi ích của việc có dữ liệu ở vị trí gốc ngay từ đầu.

Một giải pháp thay thế khác cần xem xét (và tôi muốn nghe về kết quả của bạn - đó là rất nhiều dữ liệu) ...

Thay vì đặt MOC nền trở thành con của CHÍNH, hãy biến nó thành con của CHÍNH.

Có được điều này. Mỗi khi BG lưu, nó sẽ tự động được đẩy vào MAIN. Bây giờ, MAIN phải gọi save, và sau đó master phải gọi save, nhưng tất cả những gì đang làm là di chuyển con trỏ ... cho đến khi master lưu vào đĩa.

Cái hay của phương pháp đó là dữ liệu đi từ MOC nền vào thẳng MOC ứng dụng của bạn (sau đó đi qua để được lưu).

một số hình phạt cho đường chuyền, nhưng tất cả các công việc nặng nhọc được thực hiện trong MASTER khi nó chạm vào đĩa. Và nếu bạn khởi động các lưu đó trên master với performanceBlock, thì main thread chỉ gửi yêu cầu và trả về ngay lập tức.

Xin vui lòng cho tôi biết làm thế nào nó đi!


Câu trả lời xuất sắc. Tôi sẽ thử những ý tưởng này hôm nay và xem những gì tôi khám phá được. Cảm ơn bạn!
David Weiss

Tuyệt vời! Điều đó hoạt động hoàn hảo! Tuy nhiên, tôi sẽ thử gợi ý của bạn về MASTER -> MAIN -> BG và xem hiệu suất đó hoạt động như thế nào, đó có vẻ là một ý tưởng rất thú vị. Cảm ơn bạn vì những ý tưởng tuyệt vời!
David Weiss

4
Đã cập nhật để thay đổi performanceBlockAndWait thành performanceBlock. Không rõ tại sao cái này lại xuất hiện trong hàng đợi của tôi, nhưng khi tôi đọc nó lần này, rõ ràng là ... không hiểu tại sao tôi lại để nó đi trước đó. Có, performanceBlockAndWait đang tham gia lại. Tuy nhiên, trong môi trường lồng nhau như thế này, bạn không thể gọi phiên bản đồng bộ trên ngữ cảnh con từ trong ngữ cảnh mẹ. Thông báo có thể (trong trường hợp này là) được gửi từ ngữ cảnh gốc, điều này có thể gây ra bế tắc. Tôi hy vọng điều đó sẽ rõ ràng cho bất kỳ ai đến đọc sau. Cảm ơn, David.
Jody Hagins

1
@DavidWeiss Bạn đã thử MASTER -> MAIN -> BG chưa? Tôi quan tâm đến mẫu thiết kế này và hy vọng biết nó có phù hợp với bạn không. Cảm ơn bạn.
nonamelive

2
Vấn đề với MASTER -> TRANG CHỦ -> BG mô hình là khi bạn nạp từ BG bối cảnh, nó cũng sẽ lấy từ CHỦ và điều đó sẽ chặn UI và làm cho bạn ứng dụng không đáp ứng
Rostyslav
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.