Menerapkan Impor Data Inti yang Cepat dan Efisien di iOS 5

101

Pertanyaan : Bagaimana cara mendapatkan konteks anak saya untuk melihat perubahan bertahan pada konteks induk sehingga memicu NSFetchedResultsController saya untuk memperbarui UI?

Berikut pengaturannya:

Anda memiliki aplikasi yang mengunduh dan menambahkan banyak data XML (sekitar 2 juta catatan, masing-masing kira-kira seukuran paragraf teks normal). File .sqlite berukuran sekitar 500 MB. Menambahkan konten ini ke Data Inti membutuhkan waktu, tetapi Anda ingin pengguna dapat menggunakan aplikasi saat data dimuat ke penyimpanan data secara bertahap. Harus tidak terlihat dan tidak terlihat oleh pengguna bahwa sejumlah besar data sedang dipindahkan, jadi tidak ada hang, tidak ada kegugupan: menggulir seperti mentega. Namun, aplikasi ini lebih berguna, semakin banyak data ditambahkan ke dalamnya, jadi kami tidak bisa menunggu selamanya untuk data ditambahkan ke penyimpanan Data Inti. Dalam kode, ini berarti saya sangat ingin menghindari kode seperti ini di kode impor:

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

Aplikasinya hanya untuk iOS 5, jadi perangkat paling lambat yang perlu didukung adalah iPhone 3GS.

Berikut adalah sumber daya yang telah saya gunakan sejauh ini untuk mengembangkan solusi saya saat ini:

Panduan Pemrograman Data Inti Apple: Mengimpor Data Secara Efisien

  • Gunakan Kumpulan Autorelease untuk menjaga memori tetap rendah
  • Biaya Hubungan. Impor rata, lalu perbaiki hubungan di bagian akhir
  • Jangan bertanya jika Anda dapat membantunya, ini memperlambat segalanya dengan cara O (n ^ 2)
  • Impor dalam Batch: simpan, setel ulang, tiriskan, dan ulangi
  • Matikan Undo Manager saat mengimpor

iDeveloper TV - Performa Data Inti

  • Gunakan 3 Konteks: Tipe konteks Master, Main dan Confinement

iDeveloper TV - Core Data untuk Pembaruan Mac, iPhone & iPad

  • Menjalankan menghemat antrean lain dengan performBlock membuat segalanya menjadi cepat.
  • Enkripsi memperlambat segalanya, matikan jika Anda bisa.

Mengimpor dan Menampilkan Kumpulan Data Besar dalam Data Inti oleh Marcus Zarra

  • Anda dapat memperlambat pengimporan dengan memberikan waktu untuk putaran proses saat ini, sehingga semuanya terasa lancar bagi pengguna.
  • Kode Sampel membuktikan bahwa dimungkinkan untuk melakukan impor besar dan menjaga UI tetap responsif, tetapi tidak secepat dengan 3 konteks dan penyimpanan asinkron ke disk.

Solusi Saya Saat Ini

Saya punya 3 contoh NSManagedObjectContext:

masterManagedObjectContext - Ini adalah konteks yang memiliki NSPersistentStoreCoordinator dan bertanggung jawab untuk menyimpan ke disk. Saya melakukan ini agar penyimpanan saya tidak sinkron dan karenanya sangat cepat. Saya membuatnya saat peluncuran seperti ini:

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

mainManagedObjectContext - Ini adalah konteks yang digunakan UI di mana-mana. Ini adalah turunan dari masterManagedObjectContext. Saya membuatnya seperti ini:

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

backgroundContext - Konteks ini dibuat di subclass NSOperation saya yang bertanggung jawab untuk mengimpor data XML ke Core Data. Saya membuatnya dalam metode utama operasi dan menautkannya ke konteks utama di sana.

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

Ini sebenarnya bekerja sangat, SANGAT cepat. Hanya dengan melakukan pengaturan 3 konteks ini, saya dapat meningkatkan kecepatan impor saya lebih dari 10x! Sejujurnya, ini sulit dipercaya. (Desain dasar ini harus menjadi bagian dari templat Data Inti standar ...)

Selama proses impor saya menyimpan 2 cara berbeda. Setiap 1000 item yang saya simpan pada konteks latar belakang:

BOOL saveSuccess = [backgroundContext save:&error];

Kemudian di akhir proses impor, saya menyimpan konteks master / induk yang, seolah-olah, mendorong modifikasi ke konteks anak lainnya termasuk konteks utama:

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

Masalah : Masalahnya adalah UI saya tidak akan diperbarui hingga saya memuat ulang tampilan.

Saya memiliki UIViewController sederhana dengan UITableView yang sedang diberi makan data menggunakan NSFetchedResultsController. Ketika proses Impor selesai, NSFetchedResultsController tidak melihat perubahan dari konteks induk / master sehingga UI tidak otomatis diperbarui seperti yang biasa saya lihat. Jika saya mengeluarkan UIViewController dari tumpukan dan memuatnya lagi semua data ada di sana.

Pertanyaan : Bagaimana cara mendapatkan konteks anak saya untuk melihat perubahan bertahan pada konteks induk sehingga memicu NSFetchedResultsController saya untuk memperbarui UI?

Saya telah mencoba yang berikut ini yang hanya membuat aplikasi hang:

- (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];
}
David Weiss
sumber
26
+1000000 untuk pertanyaan dengan bentuk terbaik dan paling siap yang pernah ada. Saya punya jawaban juga ... Akan butuh beberapa menit untuk mengetiknya ...
Jody Hagins
1
Saat Anda mengatakan aplikasi digantung, di mana tempatnya? Apa yang dilakukannya?
Jody Hagins
Maaf untuk membicarakan ini setelah waktu yang lama. Bisakah Anda menjelaskan apa yang dimaksud dengan "Impor datar, lalu perbaiki hubungan di akhir"? Bukankah Anda masih harus memiliki objek itu dalam memori untuk membangun hubungan? Saya mencoba menerapkan solusi yang sangat mirip dengan Anda dan saya benar-benar dapat menggunakan bantuan untuk menurunkan jejak memori.
Andrea Sprega
Lihat Apple Docs yang ditautkan ke bagian pertama artikel ini. Ini menjelaskan ini. Semoga berhasil!
David Weiss
1
Pertanyaan yang sangat bagus dan saya mengambil beberapa trik rapi dari deskripsi yang Anda berikan tentang pengaturan Anda
djskinner

Jawaban:

47

Anda mungkin harus menyimpan master MOC dengan langkah-langkah juga. Tidak masuk akal jika MOC menunggu sampai akhir untuk menyelamatkan. Ini memiliki utasnya sendiri, dan itu akan membantu menjaga memori tetap rendah juga.

Kau menulis:

Kemudian di akhir proses impor, saya menyimpan konteks master / induk yang, seolah-olah, mendorong modifikasi ke konteks anak lainnya termasuk konteks utama:

Dalam konfigurasi Anda, Anda memiliki dua turunan (MOC utama dan MOC latar belakang), keduanya dipasangkan ke "master".

Ketika Anda menghemat seorang anak, itu mendorong perubahan ke orang tua. Turunan lain dari MOC tersebut akan melihat data tersebut saat mereka melakukan pengambilan ... mereka tidak diberi tahu secara eksplisit.

Jadi, saat BG menyimpan, datanya didorong ke MASTER. Namun, perhatikan bahwa tidak ada data ini di disk hingga MASTER menyimpan. Selain itu, item baru apa pun tidak akan mendapatkan ID permanen hingga MASTER disimpan ke disk.

Dalam skenario Anda, Anda menarik data ke MAIN MOC dengan menggabungkan dari penyimpanan MASTER selama pemberitahuan DidSave.

Seharusnya itu berhasil, jadi saya penasaran di mana "digantung". Saya akan mencatat, bahwa Anda tidak menjalankan utas MOC utama dengan cara kanonik (setidaknya tidak untuk iOS 5).

Selain itu, Anda mungkin hanya tertarik untuk menggabungkan perubahan dari MOC master (meskipun pendaftaran Anda sepertinya hanya untuk itu). Jika saya menggunakan update-on-do-save-notification, saya akan melakukan ini ...

- (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
    }];
}

Sekarang, untuk masalah apa sebenarnya Anda tentang hang ... Anda menunjukkan dua panggilan berbeda untuk menghemat master. yang pertama terlindungi dengan baik di performBlock-nya sendiri, tetapi yang kedua tidak (meskipun Anda mungkin memanggil saveMasterContext di performBlock ...

Namun, saya juga akan mengubah kode ini ...

- (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];
    }];
}

Namun, perhatikan bahwa MAIN adalah anak dari MASTER. Jadi, itu tidak harus menggabungkan perubahan. Sebaliknya, perhatikan saja DidSave pada master, dan ambil kembali! Datanya sudah ada di orang tuamu, hanya menunggu kamu memintanya. Itulah salah satu manfaat memiliki data di induk sejak awal.

Alternatif lain untuk dipertimbangkan (dan saya tertarik untuk mendengar tentang hasil Anda - itu banyak data) ...

Alih-alih menjadikan latar belakang MOC sebagai anak dari MASTER, jadikanlah sebagai anak dari MAIN.

Dapatkan ini. Setiap kali BG menyimpan, secara otomatis akan didorong ke MAIN. Sekarang, MAIN harus memanggil save, dan kemudian master harus memanggil save, tapi yang mereka lakukan hanyalah memindahkan pointer ... sampai master menyimpan ke disk.

Keindahan dari metode itu adalah bahwa data pergi dari latar belakang MOC langsung ke aplikasi Anda MOC (kemudian melewati untuk disimpan).

Ada beberapa penalti untuk operan tersebut, tapi semua pengangkatan berat dilakukan di MASTER saat mengenai disk. Dan jika Anda menendang simpanan tersebut pada master dengan performBlock, maka utas utama hanya mengirimkan permintaan, dan segera kembali.

Tolong beritahu saya bagaimana kelanjutannya!

Jody Hagins
sumber
Jawaban yang sangat bagus. Saya akan mencoba ide-ide ini hari ini dan melihat apa yang saya temukan. Terima kasih!
David Weiss
Hebat! Itu bekerja dengan sempurna! Namun, saya akan mencoba saran Anda tentang MASTER -> MAIN -> BG dan melihat bagaimana kinerja itu bekerja, sepertinya ide yang sangat menarik. Terima kasih atas ide-ide hebatnya!
David Weiss
4
Diperbarui untuk mengubah performBlockAndWait menjadi performBlock. Tidak yakin mengapa ini muncul lagi dalam antrian saya, tetapi ketika saya membacanya kali ini, sudah jelas ... tidak yakin mengapa saya membiarkannya pergi sebelumnya. Ya, performBlockAndWait adalah peserta kembali. Namun, dalam lingkungan bertingkat seperti ini, Anda tidak dapat memanggil versi sinkron pada konteks anak dari dalam konteks induk. Notifikasi dapat (dalam hal ini) dikirim dari konteks induk, yang dapat menyebabkan kebuntuan. Saya harap ini jelas bagi siapa saja yang datang dan membaca ini nanti. Terima kasih, David.
Jody Hagins
1
@DavidWeiss Sudahkah Anda mencoba MASTER -> MAIN -> BG? Saya tertarik dengan pola desain ini dan berharap mengetahui apakah itu bekerja dengan baik untuk Anda. Terima kasih.
nonamelive
2
Masalah dengan pola MASTER -> MAIN -> BG adalah ketika Anda mengambil dari konteks BG, itu juga akan mengambil dari MAIN dan itu akan memblokir UI dan membuat aplikasi Anda tidak responsif
Rostyslav