การใช้การนำเข้าข้อมูลหลักที่รวดเร็วและมีประสิทธิภาพบน iOS 5


101

คำถาม : ฉันจะทำให้บริบทลูกของฉันเห็นการเปลี่ยนแปลงที่ยังคงอยู่ในบริบทหลักได้อย่างไรเพื่อที่จะเรียก NSFetchedResultsController ของฉันเพื่ออัปเดต UI

นี่คือการตั้งค่า:

คุณมีแอปที่ดาวน์โหลดและเพิ่มข้อมูล XML จำนวนมาก (ประมาณ 2 ล้านระเบียนแต่ละรายการมีขนาดเท่ากับย่อหน้าปกติของข้อความ) ไฟล์. sqlite จะมีขนาดประมาณ 500 MB การเพิ่มเนื้อหานี้ลงในข้อมูลหลักต้องใช้เวลา แต่คุณต้องการให้ผู้ใช้สามารถใช้แอปได้ในขณะที่ข้อมูลโหลดลงในที่เก็บข้อมูลทีละน้อย ผู้ใช้จะต้องมองไม่เห็นและมองไม่เห็นว่ามีการเคลื่อนย้ายข้อมูลจำนวนมากไปรอบ ๆ จึงไม่แฮงค์ไม่กระวนกระวายใจ: เลื่อนเหมือนเนย ถึงกระนั้นแอปก็มีประโยชน์มากขึ้นยิ่งมีการเพิ่มข้อมูลมากขึ้นดังนั้นเราจึงไม่สามารถรอได้ตลอดไปเพื่อเพิ่มข้อมูลไปยังที่เก็บข้อมูลหลัก ในรหัสหมายความว่าฉันต้องการหลีกเลี่ยงรหัสเช่นนี้ในรหัสนำเข้า:

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

แอปนี้เป็น iOS 5 เท่านั้นดังนั้นอุปกรณ์ที่ช้าที่สุดที่ต้องรองรับคือ iPhone 3GS

นี่คือแหล่งข้อมูลที่ฉันใช้ในการพัฒนาโซลูชันปัจจุบันของฉัน:

คู่มือการเขียนโปรแกรมข้อมูลหลักของ Apple: การนำเข้าข้อมูลอย่างมีประสิทธิภาพ

  • ใช้ Autorelease Pools เพื่อให้หน่วยความจำไม่ทำงาน
  • ต้นทุนความสัมพันธ์ นำเข้าแบบแบนจากนั้นแก้ไขความสัมพันธ์ในตอนท้าย
  • อย่าถามว่าคุณสามารถช่วยได้หรือไม่มันจะทำให้สิ่งต่างๆช้าลงในลักษณะ O (n ^ 2)
  • นำเข้าเป็นชุด: บันทึกรีเซ็ตระบายและทำซ้ำ
  • ปิดตัวจัดการการเลิกทำเมื่อนำเข้า

iDeveloper TV - ประสิทธิภาพข้อมูลหลัก

  • ใช้บริบท 3 ประเภท ได้แก่ ประเภทบริบทหลักหลักและประเภทการกักขัง

iDeveloper TV - ข้อมูลหลักสำหรับการอัปเดต Mac, iPhone และ iPad

  • การรันจะช่วยประหยัดคิวอื่น ๆ ด้วย performBlock ทำให้สิ่งต่างๆรวดเร็ว
  • การเข้ารหัสจะทำให้สิ่งต่างๆช้าลงให้ปิดหากทำได้

การนำเข้าและการแสดงชุดข้อมูลขนาดใหญ่ในข้อมูลหลักโดย Marcus Zarra

  • คุณสามารถชะลอการนำเข้าได้โดยให้เวลากับการวนรอบการทำงานปัจจุบันเพื่อให้ผู้ใช้รู้สึกราบรื่น
  • โค้ดตัวอย่างพิสูจน์ได้ว่าเป็นไปได้ที่จะทำการอิมพอร์ตจำนวนมากและรักษาการตอบสนองของ UI ไว้ แต่ไม่เร็วเท่ากับ 3 บริบทและการบันทึกแบบไม่ซิงค์ลงในดิสก์

โซลูชันปัจจุบันของฉัน

ฉันมี NSManagedObjectContext 3 อินสแตนซ์:

masterManagedObjectContext - นี่คือบริบทที่มี NSPersistentStoreCoordinator และรับผิดชอบในการบันทึกลงในดิสก์ ฉันทำเช่นนี้เพื่อให้การบันทึกของฉันเป็นแบบอะซิงโครนัสและเร็วมาก ฉันสร้างมันเมื่อเปิดตัวเช่นนี้:

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

mainManagedObjectContext - นี่คือบริบทที่ UI ใช้ทุกที่ เป็นลูกของ masterManagedObjectContext ฉันสร้างมันแบบนี้:

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

backgroundContext - บริบทนี้ถูกสร้างขึ้นในคลาสย่อย NSOperation ของฉันที่รับผิดชอบการนำเข้าข้อมูล XML ไปยัง Core Data ฉันสร้างมันในวิธีการหลักของการดำเนินการและเชื่อมโยงกับบริบทหลักที่นั่น

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

สิ่งนี้ใช้งานได้จริงรวดเร็วมาก เพียงแค่ตั้งค่าบริบท 3 รายการนี้ฉันก็สามารถปรับปรุงความเร็วในการนำเข้าได้มากกว่า 10 เท่า! สุจริตนี้ยากที่จะเชื่อ (การออกแบบพื้นฐานนี้ควรเป็นส่วนหนึ่งของเทมเพลตข้อมูลหลักมาตรฐาน ... )

ในระหว่างกระบวนการนำเข้าฉันบันทึก 2 วิธีที่แตกต่างกัน ทุก 1,000 รายการที่ฉันบันทึกในบริบทพื้นหลัง:

BOOL saveSuccess = [backgroundContext save:&error];

จากนั้นในตอนท้ายของกระบวนการอิมพอร์ตฉันบันทึกบริบทหลัก / พาเรนต์ซึ่งเห็นได้ชัดว่าจะผลักดันการปรับเปลี่ยนไปยังบริบทย่อยอื่น ๆ รวมถึงบริบทหลัก:

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

ปัญหา : ปัญหาคือ UI ของฉันจะไม่อัปเดตจนกว่าฉันจะโหลดมุมมองใหม่

ฉันมี UIViewController ที่เรียบง่ายพร้อม UITableView ที่ถูกป้อนข้อมูลโดยใช้ NSFetchedResultsController เมื่อกระบวนการนำเข้าเสร็จสมบูรณ์ NSFetchedResultsController จะเห็นว่าไม่มีการเปลี่ยนแปลงใด ๆ จากบริบทหลัก / หลักดังนั้น UI จึงไม่อัปเดตโดยอัตโนมัติอย่างที่ฉันเคยเห็น ถ้าฉันเปิด UIViewController ออกจากสแต็กและโหลดอีกครั้งข้อมูลทั้งหมดจะอยู่ที่นั่น

คำถาม : ฉันจะทำให้บริบทลูกของฉันเห็นการเปลี่ยนแปลงที่ยังคงอยู่ในบริบทหลักได้อย่างไรเพื่อที่จะเรียก NSFetchedResultsController ของฉันเพื่ออัปเดต UI

ฉันได้ลองสิ่งต่อไปนี้ซึ่งทำให้แอปแฮงค์:

- (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 สำหรับคำถามที่มีรูปแบบดีที่สุดและเตรียมพร้อมมากที่สุด ฉันมีคำตอบเช่นกัน ... จะใช้เวลาสักครู่ในการพิมพ์แม้ว่า ...
Jody Hagins

1
เมื่อคุณบอกว่าแอปค้างอยู่ที่ไหน? มันทำอะไร?
Jody Hagins

ขออภัยที่ต้องพูดถึงเรื่องนี้หลังจากผ่านไปนาน คุณช่วยชี้แจงได้ไหมว่า "นำเข้าแบนแล้วแก้ไขความสัมพันธ์ในตอนท้าย" หมายความว่าอย่างไร คุณยังไม่จำเป็นต้องมีวัตถุนั้นในหน่วยความจำเพื่อสร้างความสัมพันธ์อีกหรือ? ฉันกำลังพยายามใช้โซลูชันที่คล้ายกับของคุณและฉันสามารถใช้ความช่วยเหลือบางอย่างเพื่อลดรอยเท้าของหน่วยความจำได้
Andrea Sprega

ดู Apple Docs ที่เชื่อมโยงกับส่วนแรกของบทความนี้ มันอธิบายเรื่องนี้ โชคดี!
David Weiss

1
คำถามที่ดีจริงๆและฉันได้เลือกเทคนิคที่เป็นระเบียบจากคำอธิบายที่คุณให้ไว้ในการตั้งค่าของคุณ
djskinner

คำตอบ:


47

คุณควรจะบันทึก MOC หลักอย่างก้าวกระโดดเช่นกัน ไม่มีความรู้สึกว่า MOC รอจนกว่าจะสิ้นสุดการบันทึก มีด้ายเป็นของตัวเองและจะช่วยให้ความจำลดลงด้วย

คุณเขียน:

จากนั้นในตอนท้ายของกระบวนการอิมพอร์ตฉันบันทึกบริบทหลัก / พาเรนต์ซึ่งเห็นได้ชัดว่าจะผลักดันการปรับเปลี่ยนไปยังบริบทย่อยอื่น ๆ รวมถึงบริบทหลัก:

ในการกำหนดค่าของคุณคุณมีลูกสองคน (MOC หลักและ MOC พื้นหลัง) ทั้งคู่ได้รับการเลี้ยงดูจาก "ต้นแบบ"

เมื่อคุณบันทึกในเด็กการเปลี่ยนแปลงจะผลักดันการเปลี่ยนแปลงไปยังพาเรนต์ เด็กคนอื่น ๆ ของ MOC นั้นจะเห็นข้อมูลในครั้งถัดไปที่พวกเขาทำการดึงข้อมูล ... พวกเขาไม่ได้รับแจ้งอย่างชัดเจน

ดังนั้นเมื่อ BG บันทึกข้อมูลจะถูกผลักไปที่ MASTER อย่างไรก็ตามโปรดทราบว่าไม่มีข้อมูลนี้อยู่ในดิสก์จนกว่า MASTER จะบันทึก นอกจากนี้รายการใหม่ใด ๆ จะไม่ได้รับ ID ถาวรจนกว่า MASTER จะบันทึกลงในดิสก์

ในสถานการณ์ของคุณคุณกำลังดึงข้อมูลเข้าสู่ MOC หลักโดยการรวมจากการบันทึก MASTER ระหว่างการแจ้งเตือน DidSave

มันน่าจะใช้ได้ผมเลยสงสัยว่ามัน "แขวน" ตรงไหน ฉันจะทราบว่าคุณไม่ได้ทำงานบนเธรด MOC หลักในรูปแบบบัญญัติ (อย่างน้อยก็ไม่ใช่สำหรับ iOS 5)

นอกจากนี้คุณอาจสนใจที่จะรวมการเปลี่ยนแปลงจาก MOC หลักเท่านั้น (แม้ว่าการลงทะเบียนของคุณดูเหมือนจะเป็นเพียงการเปลี่ยนแปลงเท่านั้น) ถ้าจะใช้ update-on-did-save-notification ฉันจะทำเช่นนี้ ...

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

ตอนนี้สำหรับสิ่งที่อาจเป็นปัญหาที่แท้จริงของคุณเกี่ยวกับการแฮงค์ ... คุณจะแสดงการโทรที่แตกต่างกันสองสายเพื่อประหยัดค่าใช้จ่ายในต้นแบบ อันแรกได้รับการปกป้องอย่างดีใน performBlock ของตัวเอง แต่อันที่สองไม่ใช่ (แม้ว่าคุณอาจเรียกใช้ saveMasterContext ใน performBlock ...

อย่างไรก็ตามฉันจะเปลี่ยนรหัสนี้ด้วย ...

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

อย่างไรก็ตามโปรดทราบว่า MAIN เป็นลูกของ MASTER ดังนั้นจึงไม่ควรรวมการเปลี่ยนแปลง แต่เพียงแค่ดู DidSave บนต้นแบบและเพียงแค่เรียกใหม่! ข้อมูลอยู่ในผู้ปกครองของคุณแล้วรอให้คุณถาม นั่นเป็นประโยชน์อย่างหนึ่งของการมีข้อมูลในพาเรนต์ตั้งแต่แรก

อีกทางเลือกหนึ่งที่ควรพิจารณา (และฉันสนใจที่จะได้ยินเกี่ยวกับผลลัพธ์ของคุณนั่นคือข้อมูลจำนวนมาก) ...

แทนที่จะทำให้ MOC เบื้องหลังเป็นลูกของ MASTER ให้ทำให้เป็นลูกของ MAIN

รับสิ่งนี้ ทุกครั้งที่บันทึก BG มันจะถูกผลักเข้าสู่ MAIN โดยอัตโนมัติ ตอนนี้ MAIN ต้องเรียกบันทึกจากนั้นต้นแบบจะต้องเรียกบันทึก แต่สิ่งที่ทำคือย้ายตัวชี้ ... จนกว่าต้นแบบจะบันทึกลงในดิสก์

ความสวยงามของวิธีนั้นคือข้อมูลจะมาจาก MOC พื้นหลังตรงไปยัง MOC ของแอปพลิเคชันของคุณ (จากนั้นส่งผ่านเพื่อบันทึก)

นอกจากนี้บางคนโทษสำหรับการส่งผ่าน แต่ทั้งหมดยกของหนักได้รับทำในระดับปริญญาโทเมื่อมันฮิตดิสก์ และถ้าคุณเตะการบันทึกเหล่านั้นบนต้นแบบด้วย performBlock เธรดหลักก็จะส่งคำขอและส่งคืนทันที

โปรดแจ้งให้เราทราบว่ามันเป็นอย่างไร!


คำตอบที่ยอดเยี่ยม ฉันจะลองใช้แนวคิดเหล่านี้วันนี้และดูสิ่งที่ฉันค้นพบ ขอบคุณ!
David Weiss

สุดยอด! ที่ทำงานได้อย่างสมบูรณ์แบบ! ถึงกระนั้นฉันจะลองใช้คำแนะนำของคุณเกี่ยวกับ MASTER -> MAIN -> BG และดูว่าประสิทธิภาพนั้นเป็นอย่างไรซึ่งดูเหมือนจะเป็นแนวคิดที่น่าสนใจมาก ขอบคุณสำหรับไอเดียดีๆ!
David Weiss

4
อัปเดตเพื่อเปลี่ยน performBlockAndWait เป็น performBlock ไม่แน่ใจว่าทำไมสิ่งนี้ถึงโผล่ขึ้นมาอีกครั้งในคิวของฉัน แต่เมื่อฉันอ่านมันครั้งนี้มันชัดเจน ... ไม่แน่ใจว่าทำไมฉันถึงปล่อยมันไปก่อน ใช่ performBlockAndWait เป็นผู้เข้าใหม่ อย่างไรก็ตามในสภาพแวดล้อมที่ซ้อนกันเช่นนี้คุณไม่สามารถเรียกเวอร์ชันซิงโครนัสบนบริบทลูกจากภายในบริบทหลักได้ การแจ้งเตือนสามารถ (ในกรณีนี้คือ) ส่งจากบริบทหลักซึ่งอาจทำให้เกิดการชะงักงัน ฉันหวังว่าจะชัดเจนสำหรับทุกคนที่เข้ามาและอ่านสิ่งนี้ในภายหลัง ขอบคุณเดวิด
Jody Hagins

1
@DavidWeiss คุณเคยลอง MASTER -> MAIN -> BG หรือยัง? ฉันสนใจรูปแบบการออกแบบนี้และหวังว่าจะได้ทราบว่าเหมาะกับคุณหรือไม่ ขอบคุณ.
nonamelive

2
ปัญหาเกี่ยวกับ MASTER -> MAIN -> รูปแบบ BG คือเมื่อคุณดึงข้อมูลจากบริบท BG มันจะดึงมาจาก MAIN ด้วยและจะบล็อก UI และทำให้แอปของคุณไม่ตอบสนอง
Rostyslav
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.