ฉันจะหลีกเลี่ยงการจับภาพตนเองเป็นบล็อกเมื่อใช้งาน API ได้อย่างไร


222

ฉันมีแอพที่ใช้งานได้และกำลังแปลงเป็น ARC ใน Xcode 4.2 หนึ่งในคำเตือนก่อนการตรวจสอบที่เกี่ยวข้องกับการจับselfอย่างยิ่งในบล็อกที่นำไปสู่วงจรการเก็บรักษา ฉันได้สร้างตัวอย่างโค้ดง่ายๆเพื่ออธิบายปัญหา ฉันเชื่อว่าฉันเข้าใจความหมายของสิ่งนี้ แต่ฉันไม่แน่ใจว่า "ถูกต้อง" หรือวิธีที่แนะนำให้ใช้สถานการณ์ประเภทนี้

  • self เป็นตัวอย่างของคลาส MyAPI
  • รหัสด้านล่างนั้นง่ายขึ้นเพื่อแสดงเฉพาะการโต้ตอบกับวัตถุและบล็อกที่เกี่ยวข้องกับคำถามของฉัน
  • สมมติว่า MyAPI รับข้อมูลจากแหล่งข้อมูลระยะไกลและ MyDataProcessor ทำงานกับข้อมูลนั้นและสร้างเอาต์พุต
  • โปรเซสเซอร์ได้รับการกำหนดค่าด้วยบล็อกเพื่อสื่อสารความคืบหน้าและสถานะ

ตัวอย่างโค้ด:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

คำถาม: ฉันกำลังทำอะไร "ผิด" และ / หรือควรปรับให้สอดคล้องกับอนุสัญญา ARC อย่างไร?

คำตอบ:


509

คำตอบสั้น ๆ

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

__block MyDataProcessor *dp = self;
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

__blockตัวแปรเครื่องหมายคำหลักที่สามารถปรับเปลี่ยนภายในบล็อก (เราไม่ได้ทำอย่างนั้น) แต่ยังพวกเขาจะไม่ถูกเก็บไว้โดยอัตโนมัติเมื่อบล็อกจะถูกเก็บไว้ (ยกเว้นกรณีที่คุณกำลังใช้ ARC) หากคุณทำเช่นนี้คุณต้องแน่ใจว่าไม่มีสิ่งใดที่จะพยายามดำเนินการบล็อกหลังจากที่อินสแตนซ์ MyDataProcessor ถูกนำออกใช้ ( ป.ร. ให้โครงสร้างของรหัสของคุณที่ไม่ควรจะมีปัญหา.) อ่านข้อมูลเพิ่มเติมเกี่ยวกับ__block

หากคุณใช้ ARCความหมายของ__blockการเปลี่ยนแปลงและการอ้างอิงจะถูกเก็บไว้ซึ่งในกรณีนี้คุณควรประกาศ__weakแทน

คำตอบที่ยาว

สมมติว่าคุณมีรหัสดังนี้:

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

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

กรณีนี้สามารถแก้ไขได้ง่ายโดยการทำเช่นนี้แทน:

id progressDelegate = self.delegate;
self.progressBlock = ^(CGFloat percentComplete) {
    [progressDelegate processingWithProgress:percentComplete];
}

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

แม้ว่าคุณจะเจ๋งกับพฤติกรรมแบบนั้นคุณก็ยังไม่สามารถใช้เคล็ดลับนั้นได้ในกรณีของคุณ:

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

ที่นี่คุณกำลังส่งselfต่อไปยังผู้รับมอบสิทธิ์โดยตรงในวิธีการโทรดังนั้นคุณต้องเข้าไปที่นั่นสักแห่ง หากคุณมีอำนาจควบคุมคำจำกัดความของประเภทบล็อกสิ่งที่ดีที่สุดคือการส่งผู้แทนเข้าสู่บล็อกเป็นพารามิเตอร์:

self.dataProcessor.progress = ^(MyDataProcessor *dp, CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
};

วิธีนี้จะหลีกเลี่ยงรอบการเก็บข้อมูลและจะเรียกผู้รับมอบสิทธิ์ปัจจุบันเสมอ

หากคุณไม่สามารถเปลี่ยนบล็อกได้คุณสามารถจัดการกับมันได้ เหตุผลที่วัฏจักรการเก็บรักษาเป็นคำเตือนไม่ใช่ข้อผิดพลาดคือพวกเขาไม่จำเป็นต้องสะกดคำลงโทษสำหรับแอปพลิเคชันของคุณ หากMyDataProcessorสามารถปล่อยบล็อกได้เมื่อการดำเนินการเสร็จสิ้นก่อนที่ผู้ปกครองจะพยายามปล่อยมันวงจรจะพังและทุกอย่างจะถูกล้างอย่างถูกต้อง หากคุณมั่นใจในสิ่งนี้สิ่งที่ถูกต้องที่ควรทำคือใช้#pragmaเพื่อยับยั้งคำเตือนสำหรับบล็อคโค้ดนั้น (หรือใช้แฟล็กคอมไพเลอร์ต่อไฟล์ แต่อย่าปิดใช้งานคำเตือนสำหรับโครงการทั้งหมด)

คุณสามารถใช้เคล็ดลับที่คล้ายกันข้างต้นประกาศการอ้างอิงที่อ่อนแอหรือไม่ได้รับและการใช้สิ่งนั้นในบล็อก ตัวอย่างเช่น:

__weak MyDataProcessor *dp = self; // OK for iOS 5 only
__unsafe_unretained MyDataProcessor *dp = self; // OK for iOS 4.x and up
__block MyDataProcessor *dp = self; // OK if you aren't using ARC
self.progressBlock = ^(CGFloat percentComplete) {
    [dp.delegate myAPI:dp isProcessingWithProgress:percentComplete];
}

ทั้งสามข้อข้างต้นจะให้การอ้างอิงแก่คุณโดยไม่เก็บผลลัพธ์แม้ว่าพวกเขาทั้งหมดจะทำงานแตกต่างกันเล็กน้อย: __weakจะพยายามอ้างอิงที่ศูนย์เมื่อปล่อยวัตถุ __unsafe_unretainedจะทำให้คุณมีตัวชี้ที่ไม่ถูกต้อง __blockจะเพิ่มอีกระดับหนึ่งทางอ้อมและอนุญาตให้คุณเปลี่ยนค่าการอ้างอิงจากภายในบล็อก (ไม่เกี่ยวข้องในกรณีนี้เนื่องจากdpไม่ได้ใช้งานที่อื่น)

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


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

18
O_O ฉันเพิ่งผ่านไปด้วยปัญหาที่แตกต่างกันเล็กน้อยอ่านติดและตอนนี้ออกจากหน้านี้รู้สึกมีความรู้และเท่ห์ทั้งหมด ขอบคุณ!
Orc JMR

ถูกต้องว่าถ้าด้วยเหตุผลบางอย่างในช่วงเวลาของการดำเนินการบล็อกdpจะได้รับการปล่อยตัว (ตัวอย่างเช่นถ้ามันเป็นตัวควบคุมมุมมองและมันก็ poped) แล้วสาย[dp.delegate ...จะทำให้ EXC_BADACCESS?
peetonn

คุณสมบัติที่ควรบล็อก (เช่น dataProcess.progress) ควรเป็นstrongหรือweak?
djskinner

1
คุณอาจดูlibextobjcซึ่งมีมาโครสองตัวที่เรียกว่ามาโคร@weakify(..)และ@strongify(...)ที่ให้คุณสามารถใช้งานแบบselfบล็อกในแบบที่ไม่มีการยึดติด

25

นอกจากนี้ยังมีตัวเลือกในการระงับคำเตือนเมื่อคุณมั่นใจว่าวงจรจะพังในอนาคต:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"

self.progressBlock = ^(CGFloat percentComplete) {
    [self.delegate processingWithProgress:percentComplete];
}

#pragma clang diagnostic pop

วิธีการที่คุณจะได้ไม่ต้องลิงรอบด้วย__weak, selfaliasing และ Prefixing Ivar อย่างชัดเจน


8
ฟังดูเหมือนเป็นการปฏิบัติที่แย่มากที่ใช้โค้ดมากกว่า 3 บรรทัดซึ่งสามารถแทนที่ด้วย __weak id weakSelf = self;
เบ็นซินแคลร์

3
มักจะมีบล็อกรหัสขนาดใหญ่กว่าซึ่งสามารถได้รับประโยชน์จากคำเตือนที่ถูกระงับ
โซล

2
ยกเว้นว่า__weak id weakSelf = self;มีพฤติกรรมที่แตกต่างกันมากกว่าการระงับคำเตือน คำถามเริ่มต้นด้วย "... ถ้าคุณมั่นใจว่าวงจรการรักษาจะพัง"
ทิม

บ่อยครั้งที่คนตาบอดทำให้ตัวแปรอ่อนแอโดยไม่เข้าใจการแตกแยก ตัวอย่างเช่นฉันเคยเห็นคนอ่อนแอวัตถุและจากนั้นในบล็อกที่พวกเขาทำ: [array addObject:weakObject];ถ้าอ่อนแอวัตถุได้รับการปล่อยตัวสิ่งนี้ทำให้เกิดความผิดพลาด เห็นได้ชัดว่าไม่เป็นที่ต้องการในรอบการเก็บรักษา คุณต้องเข้าใจว่าบล็อกของคุณมีชีวิตอยู่นานพอที่จะรับประกันการอ่อนตัวหรือไม่และคุณต้องการให้การดำเนินการในบล็อกนั้นขึ้นอยู่กับว่าวัตถุที่อ่อนแอนั้นยังใช้งานได้หรือไม่
mahboudz

14

สำหรับวิธีการทั่วไปฉันมีคำจำกัดความเหล่านี้ในส่วนหัวของคอมไพล์ หลีกเลี่ยงการจับและยังช่วยให้คอมไพเลอร์ช่วยหลีกเลี่ยงการใช้id

#define BlockWeakObject(o) __typeof(o) __weak
#define BlockWeakSelf BlockWeakObject(self)

จากนั้นในรหัสคุณสามารถทำได้:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};

เห็นด้วยซึ่งอาจทำให้เกิดปัญหาภายในบล็อก ReactiveCocoa มีทางออกที่น่าสนใจอีกประการสำหรับปัญหานี้ซึ่งช่วยให้คุณสามารถใช้งานต่อไปselfภายในบล็อก @weakify (ตัวเอง); id block = ^ {@ เสริมสร้าง (ตัวเอง); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex

@dmpontifex มันเป็นมาโครจาก libextobjc github.com/jspahrsummers/libextobjc
Elechtron

11

ฉันเชื่อว่าโซลูชันที่ไม่มี ARC สามารถใช้งานร่วมกับ ARC ได้โดยใช้__blockคำหลัก:

แก้ไข: ตามการเปลี่ยนเป็นบันทึกประจำรุ่น ARCวัตถุที่ประกาศพร้อมที่__blockเก็บข้อมูลจะยังคงอยู่ ใช้__weak(ที่ต้องการ) หรือ__unsafe_unretained(เพื่อความเข้ากันได้ย้อนหลัง)

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

// Use this inside blocks
__block id myself = self;

self.dataProcessor.progress = ^(CGFloat percentComplete) {
    [myself.delegate myAPI:myself isProcessingWithProgress:percentComplete];
};

self.dataProcessor.completion = ^{
    [myself.delegate myAPIDidFinish:myself];
    myself.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

ไม่ทราบว่า__blockคำหลักนั้นหลีกเลี่ยงการเก็บรักษาไว้เป็นการอ้างอิง ขอบคุณ! ฉันอัพเดตคำตอบเสาหินของฉัน :-)
benzado

3
ตามเอกสารของ Apple "ในโหมดการนับการอ้างอิงด้วยตนเอง __block id x; มีผลกระทบจากการไม่เก็บ x ในโหมด ARC, __block id x; เริ่มต้นที่จะรักษา x (เช่นเดียวกับค่าอื่น ๆ ทั้งหมด)"
XJones

11

เมื่อรวมคำตอบอื่น ๆ เข้าด้วยกันนี่คือสิ่งที่ฉันใช้ตอนนี้เพื่อให้ผู้อ่อนแอที่พิมพ์ลงไปใช้ในบล็อก:

__typeof(self) __weak welf = self;

ฉันตั้งค่าเป็นส่วนย่อยของรหัส XCodeพร้อมคำนำหน้า "welf" ในวิธีการ / ฟังก์ชั่นที่นิยมหลังจากพิมพ์เฉพาะ "เรา"


คุณแน่ใจไหม? ลิงค์นี้และเอกสารเสียงดังกราวดูเหมือนว่าทั้งสองสามารถและควรใช้เพื่ออ้างอิงถึงวัตถุ แต่ไม่ใช่ลิงก์ที่จะทำให้เกิดรอบการเก็บ: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner

จากเอกสาร clang: clang.llvm.org/docs/BlockLanguageSpec.html "ในภาษา Objective-C และ Objective-C ++ เราอนุญาตให้ตัวระบุ __weak สำหรับตัวแปร __block ของประเภทวัตถุหากไม่ได้เปิดใช้งานการรวบรวมขยะ ตัวแปรเหล่านี้จะถูกเก็บไว้โดยไม่เก็บข้อความที่ถูกส่ง "
Kendall Helmstetter Gelner

ขอให้เรายังคงอภิปรายนี้ในการแชท
Rob

6

warning => "การจับภาพตัวเองภายในบล็อกนั้นมีแนวโน้มที่จะนำไปสู่วงจรการเก็บรักษา"

เมื่อคุณอ้างถึงตนเองหรือทรัพย์สินภายในบล็อกซึ่งมีการยึดมั่นในตนเองมากกว่าที่แสดงไว้ข้างต้นคำเตือน

ดังนั้นเพื่อหลีกเลี่ยงมันเราต้องทำให้มันเป็นสัปดาห์อ้างอิง

__weak typeof(self) weakSelf = self;

ดังนั้นแทนที่จะใช้

blockname=^{
    self.PROPERTY =something;
}

เราควรใช้

blockname=^{
    weakSelf.PROPERTY =something;
}

หมายเหตุ: รอบการเก็บรักษามักเกิดขึ้นเมื่อวัตถุสองชิ้นที่อ้างถึงซึ่งกันและกันซึ่งทั้งคู่มีจำนวนการอ้างอิง = 1 และวิธีการ delloc จะไม่ถูกเรียกใช้



-1

หากคุณแน่ใจว่ารหัสของคุณจะไม่สร้างวงจรการเก็บรักษาหรือว่าวงจรจะเสียในภายหลังวิธีที่ง่ายที่สุดในการปิดเสียงเตือนคือ:

// code sample
self.delegate = aDelegate;

self.dataProcessor = [[MyDataProcessor alloc] init];

[self dataProcessor].progress = ^(CGFloat percentComplete) {
    [self.delegate myAPI:self isProcessingWithProgress:percentComplete];
};

[self dataProcessor].completion = ^{
    [self.delegate myAPIDidFinish:self];
    self.dataProcessor = nil;
};

// start the processor - processing happens asynchronously and the processor is released in the completion block
[self.dataProcessor startProcessing];

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

x.y.z = ^{ block that retains x}

ถูกมองว่ามีการเก็บรักษาโดย x ของ y (ทางด้านซ้ายของการมอบหมาย) และโดย y ของ x (ทางด้านขวา) การเรียกใช้เมธอดจะไม่ถูกวิเคราะห์เช่นเดียวกันแม้ว่าจะเป็นวิธีการเข้าถึงคุณสมบัติ ที่เทียบเท่ากับ dot-access แม้ว่าเมธอดการเข้าถึงคุณสมบัติเหล่านั้นจะถูกสร้างขึ้นคอมไพเลอร์ดังนั้น

[x y].z = ^{ block that retains x}

เฉพาะด้านขวาเท่านั้นที่เห็นว่าเป็นการสร้างการเก็บรักษา (โดย y ของ x) และไม่มีการสร้างคำเตือนการเก็บรอบ

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