รักษาวงจรใน "ตัวเอง" กับบล็อก


167

ฉันเกรงว่าคำถามนี้ค่อนข้างง่าย แต่ฉันคิดว่ามันเกี่ยวข้องกับโปรแกรมเมอร์ Objective-C จำนวนมากที่เข้าบล็อก

สิ่งที่ฉันได้ยินคือตั้งแต่บล็อกจับตัวแปรภายในที่อ้างอิงภายในพวกเขาเป็นconstสำเนาการใช้selfภายในบล็อกอาจส่งผลให้เกิดการวนรอบในกรณีที่ควรคัดลอกบล็อกนั้น ดังนั้นเราควรจะใช้__blockเพื่อบังคับให้บล็อกจัดการโดยตรงselfแทนที่จะคัดลอกบล็อก

__block typeof(self) bself = self;
[someObject messageWithBlock:^{ [bself doSomething]; }];

แทนที่จะเป็นเพียงแค่

[someObject messageWithBlock:^{ [self doSomething]; }];

สิ่งที่ฉันอยากรู้มีดังต่อไปนี้ถ้าเป็นจริงมีวิธีที่ฉันสามารถหลีกเลี่ยงความอัปลักษณ์ (นอกเหนือจากการใช้ GC) หรือไม่


3
ฉันชอบเรียกselfพร็อกซี่ของฉันthisเพียงเพื่อพลิกสิ่งต่าง ๆ ใน JavaScript ฉันเรียกการthisปิดของฉันselfดังนั้นจึงรู้สึกดีและสมดุล :)
devios1

ฉันสงสัยว่าจะต้องมีการดำเนินการใด ๆ ที่เทียบเท่าหากฉันใช้บล็อก Swift
Ben Lu

@BenLu อย่างแน่นอน! ในการปิดอย่างรวดเร็ว (และฟังก์ชั่นที่ผ่านรอบตัวเองที่กล่าวถึงตัวเองโดยปริยายหรือโดยชัดแจ้ง) จะรักษาตัวเองไว้ บางครั้งสิ่งนี้เป็นที่ต้องการและบางครั้งมันก็สร้างวงจร (เพราะการปิดตัวเองกลายเป็นของตัวเอง (หรือเป็นเจ้าของโดยบางสิ่งบางอย่างที่เป็นเจ้าของ)) เหตุผลหลักที่เกิดขึ้นคือ ARC
Ethan

1
เพื่อหลีกเลี่ยงปัญหาวิธีที่เหมาะสมในการกำหนด 'ตนเอง' ที่จะใช้ในบล็อกคือ '__typeof (self) __ อ่อนแออ่อนแอSelf = self;' เพื่อให้มีการอ้างอิงที่อ่อนแอ
XLE_22

คำตอบ:


169

พูดอย่างเคร่งครัดความจริงที่ว่ามันเป็นสำเนา const ไม่มีอะไรเกี่ยวข้องกับปัญหานี้ บล็อกจะเก็บค่า obj-c ใด ๆ ที่ถูกจับเมื่อสร้างขึ้น มันเพิ่งเกิดขึ้นที่วิธีแก้ปัญหาสำหรับปัญหา const-copy เหมือนกับวิธีแก้ปัญหาสำหรับปัญหาการเก็บรักษา คือการใช้__blockคลาสหน่วยเก็บข้อมูลสำหรับตัวแปร

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

โปรดทราบว่าการอ้างอิง ivar มีปัญหาเดียวกันแน่นอน หากคุณจำเป็นต้องอ้างอิง Ivar bself->ivarในบล็อกของคุณทั้งใช้ทรัพย์สินแทนหรือการใช้งาน


ภาคผนวก: เมื่อรวบรวมเป็น ARC จะ__blockไม่มีการหยุดพักไว้อีกต่อไป หากคุณกำลังรวบรวม ARC คุณจำเป็นต้องใช้__weakหรือ__unsafe_unretainedแทน


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

4
ไม่มีปัญหาเควิน ดังนั้นหน่วงเวลาจากการเลือกคำตอบของคำถามในทันทีดังนั้นฉันต้องกลับมาอีกครั้งในภายหลัง ไชโย
Jonathan Sterling

__unsafe_unretained id ตัวเอง = ตนเอง;
คาเลบ

4
@JKLaiho: แน่นอนว่า__weakก็ดีเช่นกัน หากคุณทราบว่าวัตถุไม่สามารถอยู่นอกขอบเขตเมื่อมีการเรียกใช้บล็อกจะ__unsafe_unretainedเร็วขึ้นเล็กน้อย แต่โดยทั่วไปแล้วจะไม่สร้างความแตกต่าง หากคุณใช้__weakตรวจสอบให้แน่ใจว่าได้โยนมันลง__strongในตัวแปรท้องถิ่นแล้วทดสอบว่าไม่ใช่nilก่อนทำอะไรกับมัน
Lily Ballard

2
@Rpranata: ใช่ __blockผลข้างเคียงของการไม่รักษาและการปล่อยนั้นเป็นไปอย่างหมดจดเนื่องจากการไม่สามารถให้เหตุผลที่เหมาะสมเกี่ยวกับสิ่งนั้นได้ ด้วย ARC คอมไพเลอร์ได้รับความสามารถนั้นและ__blockตอนนี้ยังคงรักษาและเผยแพร่ หากคุณต้องการหลีกเลี่ยงสิ่งนั้นคุณต้องใช้__unsafe_unretainedซึ่งแนะนำให้คอมไพเลอร์ไม่ทำการเก็บหรือปล่อยค่าในตัวแปร
Lily Ballard

66

เพียงใช้:

__weak id weakSelf = self;

[someObject someMethodWithBlock:^{
    [weakSelf someOtherMethod];
}];

สำหรับข้อมูลเพิ่มเติม: WWDC 2011 - บล็อกและ Grand Central Dispatch ในการปฏิบัติ

https://developer.apple.com/videos/wwdc/2011/?id=308

หมายเหตุ: หากไม่ได้ผลคุณสามารถลอง

__weak typeof(self)weakSelf = self;

2
และคุณบังเอิญพบมัน :)
Tieme

2
คุณสามารถตรวจสอบวิดีโอได้ที่นี่ - developer.apple.com/videos/wwdc/2011/…
nanjunda

คุณสามารถอ้างอิงตนเองภายใน "someOtherMethod" ได้หรือไม่? ตัวเองจะอ้างอิงจุดอ่อนตัวเอง ณ จุดนั้นหรือว่าจะสร้างวงจรการรักษาด้วยหรือไม่
Oren

สวัสดี @Oren ถ้าคุณพยายามอ้างอิงตนเองภายใน "someOtherMethod" คุณจะได้รับคำเตือน Xcode วิธีการของฉันทำให้การอ้างอิงตัวเองอ่อนแอ
3lvis

1
ฉันได้รับคำเตือนเมื่ออ้างอิงตนเองภายในบล็อกโดยตรง การใส่ตัวเองเข้าไปในบางส่วนวิธีอื่นไม่ได้ทำให้เกิดการเตือนใด ๆ นั่นเป็นเพราะ xcode ไม่ฉลาดพอหรือไม่ใช่ปัญหา? จะอ้างอิงตนเองภายใน someOtherMethod อ้างถึงอ่อนแอตัวเองอยู่แล้วเพราะนั่นคือสิ่งที่คุณกำลังเรียกวิธีการใน?
Oren

22

สิ่งนี้อาจชัดเจน แต่คุณต้องทำselfนามแฝงที่น่าเกลียดเมื่อคุณรู้ว่าคุณจะได้รับรอบการรักษา selfหากบล็อกเป็นเพียงสิ่งหนึ่งที่ยิงแล้วฉันคิดว่าคุณสามารถละเว้นรักษาใน กรณีที่ไม่ดีคือเมื่อคุณมีบล็อกเป็นอินเทอร์เฟซการเรียกกลับ ชอบที่นี่:

typedef void (^BufferCallback)(FullBuffer* buffer);

@interface AudioProcessor : NSObject {…}
@property(copy) BufferCallback bufferHandler;
@end

@implementation AudioProcessor

- (id) init {
    
    [self setBufferCallback:^(FullBuffer* buffer) {
        [self whatever];
    }];
    
}

นี่ API ไม่สมเหตุสมผลมาก แต่มันจะสมเหตุสมผลเมื่อสื่อสารกับซูเปอร์คลาส เราคงตัวจัดการบัฟเฟอร์ตัวจัดการบัฟเฟอร์เก็บเราไว้ เปรียบเทียบกับสิ่งนี้:

typedef void (^Callback)(void);

@interface VideoEncoder : NSObject {…}
- (void) encodeVideoAndCall: (Callback) block;
@end

@interface Foo : NSObject {…}
@property(retain) VideoEncoder *encoder;
@end

@implementation Foo
- (void) somewhere {
    [encoder encodeVideoAndCall:^{
        [self doSomething];
    }];
}

ในสถานการณ์เหล่านี้ฉันไม่ได้ทำselfนามแฝง คุณจะได้รับรอบการเก็บรักษา แต่การดำเนินการจะสั้นและบล็อกจะออกจากหน่วยความจำในที่สุดทำลายวงจร แต่ประสบการณ์ของฉันกับบล็อกมีขนาดเล็กมากและอาจเป็นไปได้ว่าselfนามแฝงออกมาเป็นแนวปฏิบัติที่ดีที่สุดในระยะยาว


6
จุดดี. มันเป็นเพียงวงจรการเก็บรักษาถ้าตัวเองทำให้บล็อกมีชีวิตอยู่ ในกรณีของบล็อกที่ไม่เคยคัดลอกหรือบล็อกที่มีระยะเวลา จำกัด รับประกัน (เช่นบล็อกที่สมบูรณ์สำหรับภาพเคลื่อนไหว UIView) คุณไม่ต้องกังวลกับเรื่องนี้
Lily Ballard

6
โดยหลักการแล้วคุณถูกต้อง อย่างไรก็ตามหากคุณต้องใช้รหัสในตัวอย่างคุณจะผิดพลาด บล็อกคุณสมบัติควรเสมอได้รับการประกาศให้เป็นไม่ได้copy retainหากพวกเขาเป็นเพียงแค่retainไม่มีการรับประกันว่าพวกเขาจะถูกย้ายออกจากกองซึ่งหมายความว่าเมื่อคุณไปดำเนินการมันจะไม่อยู่ที่นั่นอีกต่อไป (และการคัดลอกและบล็อกที่คัดลอกแล้วได้รับการปรับให้เหมาะกับการเก็บ)
Dave DeLong

อ่าแน่นอนว่าพิมพ์ผิด ฉันผ่านretainช่วงเวลาหนึ่งไปแล้วและตระหนักถึงสิ่งที่คุณกำลังพูดอย่างรวดเร็ว :) ขอบคุณ!
zoul

ผมค่อนข้างมั่นใจว่าretainจะถูกละเว้นอย่างสมบูรณ์สำหรับบล็อก (ยกเว้นกรณีที่พวกเขาได้ย้ายไปอยู่ออกมาจาก stack ด้วยcopy)
สตีเวนฟิชเชอ

@Dave DeLong, ไม่, มันจะไม่ผิดพลาดเนื่องจาก @property (keep) ใช้สำหรับการอ้างอิงวัตถุเท่านั้นไม่ใช่ block .. ไม่จำเป็นต้องใช้สำเนาที่นี่เลย ..
DeniziOS

20

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

กรณีแรกแสดงให้เห็นเมื่อวงจรการเก็บรักษาจะเกิดขึ้นเนื่องจากมีบล็อกที่อ้างอิงในบล็อก:

#import <Foundation/Foundation.h>

typedef void (^MyBlock)(void);

@interface ContainsBlock : NSObject 

@property (nonatomic, copy) MyBlock block;

- (void)callblock;

@end 

@implementation ContainsBlock
@synthesize block = _block;

- (id)init {
    if ((self = [super init])) {

        //__block ContainsBlock *blockSelf = self; // to fix use this.
        self.block = ^{
                NSLog(@"object is %@", self); // self retain cycle
            };
    }
    return self;
}

- (void)dealloc {
    self.block = nil;
    NSLog (@"ContainsBlock"); // never called.
    [super dealloc];
} 

- (void)callblock {
    self.block();
} 

@end 

 int main() {
    ContainsBlock *leaks = [[ContainsBlock alloc] init];
    [leaks callblock];
    [leaks release];
}

คุณไม่จำเป็นต้อง blockSelf ในกรณีที่สองเพราะวัตถุที่เรียกไม่มีบล็อกอยู่ในนั้นที่จะทำให้วงจรการเก็บรักษาเมื่อคุณอ้างอิงตนเอง:

#import <Foundation/Foundation.h>

typedef void (^MyBlock)(void);

@interface BlockCallingObject : NSObject 
@property (copy, nonatomic) MyBlock block;
@end

@implementation BlockCallingObject 
@synthesize block = _block;

- (void)dealloc {
    self.block = nil;
    NSLog(@"BlockCallingObject dealloc");
    [super dealloc];
} 

- (void)callblock {
    self.block();
} 
@end

@interface ObjectCallingBlockCallingObject : NSObject 
@end

@implementation ObjectCallingBlockCallingObject 

- (void)doneblock {
    NSLog(@"block call complete");
}

- (void)dealloc {
    NSLog(@"ObjectCallingBlockCallingObject dealloc");
    [super dealloc];
} 

- (id)init {
    if ((self = [super init])) {

        BlockCallingObject *myobj = [[BlockCallingObject alloc] init];
        myobj.block = ^() {
            [self doneblock]; // block in different object than this object, no retain cycle
        };
        [myobj callblock];
        [myobj release];
    }
    return self;
}
@end

int main() {

    ObjectCallingBlockCallingObject *myObj = [[ObjectCallingBlockCallingObject alloc] init];
    [myObj release];

    return 0;
} 

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

9

โปรดจำไว้ว่าการรักษาวัฏจักรอาจเกิดขึ้นได้หากบล็อกของคุณอ้างถึงวัตถุอื่นซึ่งยังคงอยู่selfอยู่

ฉันไม่แน่ใจว่าการรวบรวมขยะสามารถช่วยในการรักษารอบนี้ หากวัตถุรักษาบล็อก (ซึ่งฉันจะเรียกวัตถุเซิร์ฟเวอร์) outlives self(วัตถุลูกค้า) อ้างอิงถึงselfภายในบล็อกจะไม่ได้รับการพิจารณาวงจรทั้งหมดจนกว่าวัตถุรักษาตัวเองจะถูกปล่อยออกมา หากวัตถุเซิร์ฟเวอร์อยู่ไกลกว่าไคลเอนต์ของคุณคุณอาจมีหน่วยความจำรั่วไหลที่สำคัญ

เนื่องจากไม่มีวิธีแก้ปัญหาที่สะอาดฉันขอแนะนำวิธีแก้ปัญหาต่อไปนี้ โปรดเลือกอย่างน้อยหนึ่งรายการเพื่อแก้ไขปัญหาของคุณ

  • ใช้การบล็อกเท่านั้นสำหรับการทำให้เสร็จสมบูรณ์และไม่ใช่สำหรับเหตุการณ์ปลายเปิด ตัวอย่างเช่นใช้บล็อกสำหรับวิธีการเช่นdoSomethingAndWhenDoneExecuteThisBlock:และไม่ใช่วิธีการที่ชอบsetNotificationHandlerBlock:และไม่ชอบวิธีการบล็อกที่ใช้เพื่อทำให้สำเร็จนั้นมีจุดสิ้นสุดที่แน่นอนและควรได้รับการปล่อยตัวโดยออบเจ็กต์เซิร์ฟเวอร์หลังจากได้รับการประเมิน สิ่งนี้จะช่วยป้องกันไม่ให้วงจรชีวิตนานเกินไปแม้ว่ามันจะเกิดขึ้น
  • เต้นรำที่อ้างอิงอ่อน ๆ ที่คุณอธิบายไว้
  • จัดเตรียมวิธีการล้างวัตถุของคุณก่อนที่จะวางจำหน่ายซึ่ง "ยกเลิกการเชื่อมต่อ" วัตถุจากวัตถุเซิร์ฟเวอร์ที่อาจเก็บการอ้างอิงไว้ และเรียกวิธีนี้ก่อนที่จะเรียกการปล่อยบนวัตถุ ในขณะที่วิธีนี้ใช้ได้อย่างสมบูรณ์ถ้าวัตถุของคุณมีลูกค้าเพียงคนเดียว (หรือเป็นคนเดียวในบริบท) แต่จะพังถ้ามันมีลูกค้าหลายราย คุณจะเอาชนะกลไกการนับที่นี่ได้ นี้จะคล้ายกับการเรียกร้องแทนdeallocrelease

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

และสุดท้ายรูปแบบนี้ควรส่งสัญญาณเตือน:

- (เป็นโมฆะ) dealloc {
    [myServerObject releaseCallbackBlocksForObject: self];
    ...
}

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


GC ช่วยเหลือหากคุณใช้__weakอย่างเหมาะสม
tc

การติดตามการรวบรวมขยะสามารถแน่นอนจัดการกับรอบการเก็บรักษา การรักษารอบเป็นเพียงปัญหาสำหรับสภาพแวดล้อมการนับอ้างอิง
newacct

เพียงเพื่อให้ทุกคนรู้ว่าการรวบรวมขยะถูกเลิกใช้ใน OS X v10.8 เพื่อสนับสนุนการนับการอ้างอิงอัตโนมัติ (ARC) และมีกำหนดจะถูกลบออกในเวอร์ชันอนาคตของ OS X ( developer.apple.com/library/mac/#releasenotes / ObjectiveC / … )
Ricardo Sanchez-Saez

1

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

__block SomeType* this = self;
[someObject messageWithBlock:^{
  [this doSomething]; // here would be BAD_ACCESS in case of __unsafe_unretained with
                      //  multithreading and self was already released
  this = nil;
}];

จะปลอดภัยกว่าหรือไม่ที่จะใช้ __ อ่อนแอแทน __block เพื่อหลีกเลี่ยงความจำเป็นในการใช้ตัวแปรหลังจากใช้งาน ฉันหมายความว่าวิธีนี้เป็นวิธีที่ดีถ้าคุณต้องการทำลายวงจรประเภทอื่น ๆ แต่แน่นอนว่าฉันไม่เห็นข้อได้เปรียบใด ๆ สำหรับ "ตัวตน" ในการรักษาวงจร
ale0xB

คุณไม่สามารถใช้ __ อ่อนแอได้หากเป้าหมายแพลตฟอร์มของคุณคือ iOS 4.x นอกจากนี้บางครั้งคุณจำเป็นต้องมีการเรียกใช้รหัสในบล็อกสำหรับวัตถุที่ถูกต้องไม่ใช่สำหรับศูนย์
b1gbr0

1

คุณสามารถใช้ไลบรารี libextobjc มันค่อนข้างเป็นที่นิยมใช้ใน ReactiveCocoa เป็นต้น https://github.com/jspahrsummers/libextobjc

มี 2 ​​แมโคร @ weakify และ @strongify ดังนั้นคุณสามารถมี:

@weakify(self)
[someObject messageWithBlock:^{
   @strongify(self)
   [self doSomething]; 
}];

สิ่งนี้ป้องกันการอ้างอิงโดยตรงโดยตรงดังนั้นเราจึงไม่เข้าสู่วงจรการรักษาตัวเอง และยังช่วยป้องกันตนเองจากการกลายเป็นศูนย์ครึ่งทาง แต่ยังคงลดจำนวนการเก็บรักษาไว้อย่างเหมาะสม เพิ่มเติมในลิงค์นี้: http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html


1
ก่อนที่จะแสดงรหัสที่ง่ายมันจะเป็นการดีกว่าที่จะรู้ว่าอะไรอยู่ข้างหลังมันทุกคนควรรู้โค้ดสองบรรทัดที่แท้จริง
Alex Cio

0

แล้วเรื่องนี้ล่ะ

- (void) foo {
     __weak __block me = self;

     myBlock = ^ {
        [[me someProp] someMessage];
     }
     ...
 }

ฉันไม่ได้รับคำเตือนของคอมไพเลอร์อีกต่อไป


-1

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

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