Bagian Data IntiNameKeyPath dengan Masalah Kinerja Atribut Hubungan

Saya memiliki Model Data Inti dengan tiga entitas:
Person, Group, Photo dengan hubungan di antara keduanya sebagai berikut:

  • Orang ‹‹-----------> Grup (hubungan satu ke banyak)
  • Orang ‹-------------> Foto (satu lawan satu)

Saat saya melakukan pengambilan menggunakan NSFetchedResultsController di UITableView, saya ingin mengelompokkan objek Person ke dalam beberapa bagian menggunakan atribut name entitas Group.

Untuk itu, saya menggunakan sectionNameKeyPath:@"group.name".

Masalahnya adalah ketika saya menggunakan atribut dari hubungan Group, NSFetchedResultsController mengambil semuanya di muka dalam kumpulan kecil yang terdiri dari 20 (saya punya setFetchBatchSize: 20) alih-alih mengambil kumpulan saat saya menggulir tableView.

Jika saya menggunakan atribut dari entitas Person (seperti sectionNameKeyPath:@"name") untuk membuat bagian, semuanya berfungsi dengan baik: NSFetchResultsController memuat sejumlah kecil 20 objek saat saya menggulir.

Kode yang saya gunakan untuk membuat instance NSFetchedResultsController:

- (NSFetchedResultsController *)fetchedResultsController {

    if (_fetchedResultsController) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:[Person description]
                                              inManagedObjectContext:self.managedObjectContext];

    [fetchRequest setEntity:entity];

    // Specify how the fetched objects should be sorted
    NSSortDescriptor *groupSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"group.name"
                                                                        ascending:YES];

    NSSortDescriptor *personSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthName"
                                                                         ascending:YES
                                                                          selector:@selector(localizedStandardCompare:)];


    [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:groupSortDescriptor, personSortDescriptor, nil]];

    [fetchRequest setRelationshipKeyPathsForPrefetching:@[@"group", @"photo"]];
    [fetchRequest setFetchBatchSize:20];

    NSError *error = nil;
    NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];

    if (fetchedObjects == nil) {
        NSLog(@"Error Fetching: %@", error);
    }

    _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                                    managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"group.name" cacheName:@"masterCache"];

    _fetchedResultsController.delegate = self;

    return _fetchedResultsController;
}

Inilah yang saya dapatkan di Instrumen jika saya membuat bagian berdasarkan "group.name" tanpa interaksi apa pun dengan UI Aplikasi: Pengambilan Data Inti dengan  Bagian berdasarkan Hubungan

Dan inilah yang saya dapatkan (dengan sedikit menggulir di UITableView) jika sectionNameKeyPath nihil: Pengambilan Data Inti tanpa Bagian apa pun

Tolong, adakah yang bisa membantu saya mengatasi masalah ini?

EDIT 1:

Tampaknya saya mendapatkan hasil yang tidak konsisten dari simulator dan Instrumen: ketika saya menanyakan pertanyaan ini, aplikasi dimulai di simulator dalam waktu sekitar 10 detik (oleh Time Profiler) menggunakan kode di atas.

Namun saat ini, dengan menggunakan kode yang sama seperti di atas, aplikasi dimulai di simulator dalam waktu 900 md meskipun aplikasi melakukan pengambilan di muka sementara untuk semua objek dan tidak memblokir UI.

Saya telah melampirkan beberapa tangkapan layar baru: Time Profiler with SimulatorPengambilan di Muka di Simulator tanpa menggulir Pengambilan di Muka di Simulator dengan menggulir  dan pengambilan dalam jumlah kecil

EDIT 2: Saya menyetel ulang simulator dan hasilnya menarik: setelah melakukan operasi impor dan keluar dari aplikasi, proses pertama terlihat seperti ini: Pertama dijalankan setelah reset simulator dan impor baru Setelah sedikit menggulir:   Jalankan pertama setelah penyetelan ulang simulator, impor baru dan beberapa pengguliran Sekarang inilah yang terjadi pada putaran kedua: Jalankan kedua setelahnya  reset simulator dan impor baru Setelah putaran kelima: Jalanan kelima

EDIT 3: Menjalankan aplikasi untuk ketujuh kalinya dan delapan kali, saya mendapatkan ini: Jalankan ketujuhLari kedelapan


person Razvan    schedule 27.08.2014    source sumber
comment
Saya yakin Rick telah menyarankan tautan ini tetapi jawabannya dimoderasi. Bagaimanapun... Silakan mencobanya. Dia pikir itu bisa menjawab pertanyaanmu.   -  person staticVoidMan    schedule 31.08.2014
comment
Pertanyaan yang ditulis dengan baik.   -  person Lorenzo B    schedule 14.09.2014
comment
@codeFi, apakah salah satu kekhawatiran utama Anda di sini adalah pengambilan memblokir interaksi pengguna?   -  person quellish    schedule 15.09.2014
comment
@quellish Ya, ini memblokir interaksi pengguna saat aplikasi dimulai karena memerlukan waktu lama untuk menampilkan UI tetapi masalah ini hanya terjadi di simulator. Anehnya, saat menjalankan aplikasi di iPhone 4S meskipun saya mengambil entitas Grup dan Foto terlebih dahulu dan menggunakan atribut nama entitas Grup sebagai sectionNameKeyPath, aplikasi dimuat dalam ~900 md.   -  person Razvan    schedule 15.09.2014
comment
Jadi saya tidak tahu harus berbuat apa... di simulator saya mendapatkan satu hal, di perangkat yang lain...   -  person Razvan    schedule 15.09.2014
comment
Jika Anda membuat profil waktu, apakah sebagian besar waktu itu diambil, atau yang lainnya? Bukan hal yang aneh jika menambahkan toko ke koordinator toko persisten memerlukan waktu lama.   -  person quellish    schedule 15.09.2014
comment
@quellish Saya telah mengedit posting saya dengan informasi baru.   -  person Razvan    schedule 15.09.2014
comment
Di profil waktu, balikkan pohon panggilan, jangan pisahkan berdasarkan thread, dan tampilkan panggilan teratas, akan memperjelas di mana waktu dihabiskan   -  person quellish    schedule 15.09.2014
comment
@quellish tidak ada gunanya melakukan itu sekarang karena sepertinya semuanya dimuat dengan sangat cepat (690ms). Namun jika Anda ingin tahu apa yang paling memakan waktu komputasi saat ini adalah __pread dari libsystem_kernel.dylib (75ms).   -  person Razvan    schedule 15.09.2014
comment
@codeFi, dalam komentar Anda mengatakan sepertinya Core Data lebih suka menyimpan jempol sebagai biner dalam tabel database, apakah itu di simulator, atau di perangkat? Perilaku dan kinerja penyimpanan catatan eksternal dapat berbeda secara signifikan antara simulator dan perangkat.   -  person quellish    schedule 16.09.2014
comment
@quellish ini terjadi di simulator. Saya belum melihat apa yang terjadi pada perangkat dari perspektif ini.   -  person Razvan    schedule 16.09.2014
comment
Saat Anda menentukan dalam model bahwa Data Inti dapat menggunakan penyimpanan eksternal untuk atribut model biner, Data Inti memutuskan pada waktu proses apakah akan menyimpan data biner tersebut di penyimpanan SQLite atau di file eksternal. Alasan Core Data tentang hal itu bisa berbeda saat dijalankan di perangkat vs. simulator. Selain itu, memigrasikan penyimpanan yang telah menulis file catatan eksternal bisa sangat lambat. Apakah ada alasan untuk berpikir bahwa Anda sedang melakukan migrasi pada proses Instrumen di mana Anda melihat kelambatan?   -  person quellish    schedule 16.09.2014
comment
Tidak, saya tidak mengimpor apa pun. Tes yang saya lakukan adalah dengan database yang sudah terisi.   -  person Razvan    schedule 16.09.2014


Jawaban (3)


Ini adalah tujuan yang Anda nyatakan: Saya memerlukan objek Person untuk dikelompokkan dalam beberapa bagian berdasarkan grup entitas hubungan, atribut nama, dan NSFetchResultsController untuk melakukan pengambilan dalam kelompok kecil saat saya menggulir dan bukan di muka seperti yang dilakukan sekarang.

Jawabannya sedikit rumit, terutama karena cara NSFetchedResultsController membuat bagian, dan pengaruhnya terhadap perilaku pengambilan.

TL;DR; Untuk mengubah perilaku ini, Anda perlu mengubah cara NSFetchedResultsController membuat bagian.

Apa yang terjadi?

Ketika NSFetchedResultsController diberikan permintaan pengambilan dengan pagination (fetchLimit dan/atau FetchBatchSize), beberapa hal terjadi.

Jika tidak ada sectionNameKeyPath yang ditentukan, itu akan melakukan apa yang Anda harapkan. Pengambilan mengembalikan array hasil proxy, dengan objek nyata untuk jumlah item FetchBathSize pertama. Jadi misalnya jika Anda memiliki setFetchBatchSize hingga 2, dan predikat Anda cocok dengan 10 item di toko, hasilnya berisi dua objek pertama. Objek lainnya akan diambil secara terpisah saat diakses. Ini memberikan pengalaman respons paginasi yang lancar.

Namun, ketika sectionNameKeyPath ditentukan, pengontrol hasil yang diambil harus melakukan lebih banyak hal. Untuk menghitung bagian, ia perlu mengakses jalur kunci tersebut pada semua objek di hasil. Ini menyebutkan 10 item dalam hasil dalam contoh kita. Dua yang pertama telah diambil. 8 lainnya akan diambil selama enumerasi untuk mendapatkan nilai jalur kunci yang diperlukan untuk membangun informasi bagian. Jika Anda memiliki banyak hasil untuk permintaan pengambilan, ini bisa sangat tidak efisien. Ada sejumlah bug umum mengenai fungsi ini:

NSFetchedResultsController awalnya membutuhkan waktu terlalu lama untuk menyiapkan bagian

NSFetchedResultsController mengabaikan properti FetchLimit

NSFetchedResultsController, Indeks Tabel, dan Masalah Performa Pengambilan Batch

... Dan beberapa lainnya. Jika dipikir-pikir, ini masuk akal. Untuk membangun objek NSFetchedResultsSectionInfo memerlukan pengontrol hasil yang diambil untuk melihat setiap nilai dalam hasil untuk sectionNameKeyPath, mengagregasinya ke gabungan nilai yang unik, dan menggunakan informasi tersebut untuk membuat jumlah objek NSFetchedResultsSectionInfo yang benar, menetapkan nama dan judul indeks, mengetahui berapa banyak objek dalam hasil yang terdapat pada suatu bagian, dll. Untuk menangani kasus penggunaan umum, tidak ada jalan lain untuk mengatasi hal ini. Dengan mengingat hal tersebut, jejak Instrumen Anda mungkin lebih masuk akal.

Bagaimana Anda bisa mengubahnya?

Anda dapat mencoba membuat NSFetchedResultsController Anda sendiri yang memberikan strategi alternatif untuk membuat objek NSFetchedResultsSectionInfo, namun Anda mungkin mengalami beberapa masalah yang sama. Misalnya, jika Anda menggunakan fungsionalitas FetchedObjects yang ada untuk mengakses anggota hasil pengambilan, Anda akan mengalami perilaku yang sama saat mengakses objek yang merupakan kesalahan. Implementasi Anda memerlukan strategi untuk mengatasi hal ini (ini bisa dilakukan, tetapi sangat bergantung pada kebutuhan dan persyaratan Anda).

Ya Tuhan tidak. Bagaimana dengan peretasan sementara yang hanya membuat kinerjanya sedikit lebih baik tetapi tidak menyelesaikan masalah?

Mengubah model data Anda tidak akan mengubah perilaku di atas, namun dapat sedikit mengubah dampak kinerja. Pembaruan batch tidak akan berdampak signifikan pada perilaku ini, dan pada kenyataannya tidak akan berfungsi dengan baik dengan pengontrol hasil yang diambil. Namun, mungkin akan lebih berguna bagi Anda untuk mengatur relationshipKeyPathsForPrefetching agar menyertakan hubungan grup Anda, yang dapat meningkatkan perilaku pengambilan dan kesalahan secara signifikan. Strategi lain mungkin adalah melakukan pengambilan lain untuk melakukan kesalahan batch pada objek-objek ini sebelum Anda mencoba menggunakan pengontrol hasil yang diambil, yang akan mengisi berbagai tingkat cache Data Inti dalam memori dengan cara yang lebih efisien.

Cache NSFetchedResultsController terutama untuk informasi bagian. Hal ini mencegah bagian tersebut harus dihitung ulang sepenuhnya pada setiap perubahan (dalam kasus terbaik), namun sebenarnya dapat membuat pengambilan awal untuk membuat bagian tersebut memakan waktu lebih lama. Anda harus bereksperimen untuk melihat apakah cache bermanfaat untuk kasus penggunaan Anda.

Jika kekhawatiran utama Anda adalah operasi Data Inti ini memblokir interaksi pengguna, Anda dapat memindahkannya dari thread utama. NSFetchedResultsController dapat digunakan pada konteks antrean pribadi (latar belakang) , yang akan mencegah operasi Data Inti memblokir UI.

person quellish    schedule 15.09.2014
comment
Anda melewatkan inti jawaban saya. Pembaruan batch digunakan untuk memperbarui atribut baru (misalnya groupName) pada entitas Person ketika name dari Group berubah. Dan ini tidak ada hubungannya dengan NSFethedResultsController dengan cara apa pun. Tentang apa yang Anda katakan di sini: Mengubah model data Anda tidak akan mengubah perilaku di atas, namun dapat sedikit mengubah dampak kinerja. Saya kira itu tidak benar tetapi untuk memastikan saya akan memberikan contoh dan kembali dengan beberapa tes. - person Lorenzo B; 15.09.2014
comment
Pertanyaan penulis adalah tentang NSFetchedResultsController kinerja saat menghitung bagian untuk pertama kalinya. Pembaruan batch tidak relevan dengan hal ini, dan dalam skenario umum akan berbahaya - mengoordinasikan perubahan batch dengan NSFetchedResultsController bukanlah hal yang sepele. Perilaku yang penulis jelaskan dan cari panduannya adalah endemik bagaimana NSFetchedResultsController harus melakukan tugasnya. Menormalkan model data tidak mengubah hal ini, ini hanya berarti diperlukan lebih banyak data untuk melihat dampak kinerja yang sama - yang terbaik. - person quellish; 15.09.2014
comment
@quellish sebenarnya flexaddicted benar: beberapa hari yang lalu, saya mencoba menggunakan atribut groupName pada entitas Person dan membuat bagian darinya. Ini berfungsi seperti yang diharapkan: tidak ada lagi pengambilan di muka, objek Person dikelompokkan berdasarkan groupName dan FetchController mengambil batch kecil saat saya menggulir. Namun, dari sudut pandang saya, pendekatan ini menggagalkan tujuan menjalin hubungan. - person Razvan; 15.09.2014

Berdasarkan pengalaman saya, cara untuk mencapai tujuan Anda adalah dengan mendenormalisasi model Anda. Secara khusus, Anda dapat menambahkan atribut group di entitas Person Anda dan menggunakan atribut tersebut sebagai sectionNameKeyPath. Jadi, saat Anda membuat Person Anda juga harus meneruskan grup tempatnya.

Proses denormalisasi ini benar karena memungkinkan Anda menghindari pengambilan objek Group terkait karena tidak diperlukan. Kerugiannya adalah jika Anda mengubah nama grup, semua orang yang terkait dengan nama tersebut harus berubah, sebaliknya Anda mungkin memiliki nilai yang salah.

Aspek kuncinya di sini adalah sebagai berikut. Perlu Anda ingat bahwa Data Inti bukanlah database relasional. Model tidak boleh dirancang sebagai skema database, tempat normalisasi dapat dilakukan, namun harus dirancang dari perspektif bagaimana data disajikan dan digunakan dalam antarmuka pengguna.

Edit 1

Saya tidak dapat memahami komentar Anda, dapatkah Anda menjelaskannya dengan lebih baik?

Apa yang menurut saya sangat menarik adalah bahwa meskipun aplikasi melakukan pengambilan dimuka penuh di simulator, aplikasi dimuat dalam 900 md (dengan 5000 objek) di perangkat meskipun simulator memuat jauh lebih lambat.

Bagaimanapun, saya tertarik mengetahui detail tentang entitas Photo Anda. Jika Anda mengambil foto terlebih dahulu, keseluruhan eksekusi dapat terpengaruh.

Apakah Anda perlu mengambil Photo terlebih dahulu dalam tampilan tabel Anda? Apakah itu jempol (foto kecil)? Atau gambar biasa? Apakah Anda memanfaatkan Bendera Penyimpanan Eksternal?

Menambahkan atribut tambahan (misalnya group) ke entitas Person mungkin tidak menjadi masalah. Memperbarui nilai atribut tersebut ketika name dari objek Group berubah tidak menjadi masalah jika Anda melakukannya di latar belakang. Selain itu, mulai iOS 8 Anda telah menyediakan pembaruan batch seperti yang dijelaskan dalam Pembaruan Batch Data Inti.

person Lorenzo B    schedule 14.09.2014
comment
Terima kasih atas jawaban Anda yang bermanfaat. Saya sudah memikirkan hal itu tetapi seperti yang Anda katakan, masalahnya muncul ketika saya ingin mengubah nama grup untuk ribuan objek yang terkait dengan grup tertentu. Apa yang menurut saya sangat menarik adalah bahwa meskipun aplikasi melakukan pengambilan dimuka penuh di simulator, aplikasi dimuat dalam 900 md (dengan 5000 objek) di perangkat meskipun simulator memuat jauh lebih lambat. Saya rasa ada hal lain yang terjadi pada perangkat... Perangkat tersebut adalah iPhone 4S dengan versi iOS 7 terbaru. - person Razvan; 14.09.2014
comment
Mengenai kalimat terakhir Anda, Anda memang benar! Saya tidak akan menyajikan kepada pengguna ribuan objek yang dikelompokkan berdasarkan bagian dalam satu tampilan tabel. Sebagai gantinya saya akan memiliki tampilan tabel dengan beberapa sel yang mewakili grup dan memisahkannya dari masing-masing sel ke konten setiap grup. Namun ini adalah latihan pribadi bagi saya untuk memahami Data Inti dan masalah ini sangat membuat saya frustasi. - person Razvan; 15.09.2014
comment
Menambahkan beberapa petunjuk lainnya. - person Lorenzo B; 15.09.2014
comment
Kepada orang yang tidak memberikan suara. Komentar pada suara negatif juga harus disisipkan. - person Lorenzo B; 15.09.2014
comment
Tujuan yang dinyatakan penulis adalah: Saya memerlukan objek Person untuk dikelompokkan menjadi beberapa bagian berdasarkan grup entitas hubungan, atribut nama, dan NSFetchResultsController untuk melakukan pengambilan dalam kelompok kecil saat saya menggulir dan bukan di muka seperti yang dilakukan sekarang. Jawaban Anda tidak menjawab hal ini. - person quellish; 15.09.2014
comment
@quellish melakukan dernomalisasi model dapat meningkatkan kinerja. Jika Anda memasukkan atribut baru, misalnya groupName, ke dalam entitas Person, atribut tersebut dapat digunakan untuk mengelompokkan dan menghindari pengambilan di awal. - person Lorenzo B; 15.09.2014
comment
@flexaddicted mendenormalisasi model tidak akan mengubah perilaku pengambilan saat membuat bagian awal, yang merupakan panduan yang diminta oleh penulis. Mendenormalisasi model akan mempersingkat waktu yang dibutuhkan untuk melakukan hal yang tidak ingin dia lakukan. Jika Anda cukup mendeMORALISASI pengontrol hasil yang diambil, mungkin itu akan mengubah perilakunya. Tapi mungkin tidak. - person quellish; 15.09.2014
comment
@flexaddicted di perangkat, aplikasi dimuat dengan sangat cepat (dalam 900 md) sementara di simulator dimuat dalam beberapa detik. - person Razvan; 15.09.2014
comment
@flexaddicted mengenai foto yang saya gunakan untuk ditampilkan dalam tampilan tabel: itu adalah thumbnail kecil (masing-masing 5.000 berukuran 48KB) dan atributnya telah mengaktifkan opsi Izinkan Penyimpanan Eksternal. Namun, tampaknya Core Data lebih suka menyimpan jempol sebagai biner dalam tabel database karena ukuran file sqlite mencapai ~130MB. - person Razvan; 15.09.2014

Setelah hampir setahun sejak saya memposting pertanyaan ini, saya akhirnya menemukan penyebab yang mengaktifkan perilaku ini (yang sedikit berubah di Xcode 6):

  1. Mengenai waktu pengambilan yang tidak konsisten: Saya menggunakan cache dan pada saat itu saya bolak-balik membuka, menutup, dan mengatur ulang simulator.

  2. Mengenai fakta bahwa semuanya diambil di muka dalam kelompok kecil tanpa menggulir (di Instrumen Data Inti Xcode 6 hal itu tidak terjadi lagi - sekarang ini adalah pengambilan besar yang membutuhkan waktu beberapa detik):

Tampaknya setFetchBatchSize tidak berfungsi dengan benar dengan parent/child contexts. Masalah ini dilaporkan pada tahun 2012 dan tampaknya masih ada http://openradar.appspot.com/11235622.

Untuk mengatasi masalah ini, saya membuat independent context lain dengan NSMainQueueConcurrencyType dan mengatur persistence coordinator agar sama dengan yang digunakan contexts saya yang lain.

Lebih lanjut tentang masalah #2 di sini: https://stackoverflow.com/a/11470560/1641848

person Razvan    schedule 28.07.2015