Bagaimana cara saya menghindari menangkap diri dalam blok ketika menerapkan API?

222

Saya memiliki aplikasi yang berfungsi dan saya sedang berusaha mengubahnya menjadi ARC di Xcode 4.2. Salah satu peringatan pra-cek melibatkan penangkapan dengan selfkuat di blok yang mengarah ke siklus penyimpanan. Saya telah membuat contoh kode sederhana untuk menggambarkan masalah ini. Saya percaya saya mengerti apa artinya ini tetapi saya tidak yakin cara yang "benar" atau direkomendasikan untuk menerapkan skenario jenis ini.

  • diri adalah turunan dari kelas MyAPI
  • kode di bawah ini disederhanakan untuk hanya menampilkan interaksi dengan objek dan blok yang relevan dengan pertanyaan saya
  • berasumsi bahwa MyAPI mendapatkan data dari sumber jarak jauh dan MyDataProcessor bekerja pada data itu dan menghasilkan output
  • prosesor dikonfigurasikan dengan blok untuk mengkomunikasikan kemajuan & status

contoh kode:

// 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];

Pertanyaan: apa yang saya lakukan "salah" dan / atau bagaimana ini harus dimodifikasi agar sesuai dengan konvensi ARC?

XJones
sumber

Jawaban:

509

Jawaban singkat

Alih-alih mengakses selfsecara langsung, Anda harus mengaksesnya secara tidak langsung, dari referensi yang tidak akan disimpan. Jika Anda tidak menggunakan Penghitungan Referensi Otomatis (ARC) , Anda dapat melakukan ini:

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

Kata __blockkunci menandai variabel yang dapat dimodifikasi di dalam blok (kami tidak melakukan itu) tetapi juga mereka tidak secara otomatis dipertahankan ketika blok tersebut dipertahankan (kecuali Anda menggunakan ARC). Jika Anda melakukan ini, Anda harus yakin bahwa tidak ada lagi yang akan mencoba untuk menjalankan blok setelah instance MyDataProcessor dirilis. (Mengingat struktur kode Anda, itu seharusnya tidak menjadi masalah.) Baca lebih lanjut tentang__block .

Jika Anda menggunakan ARC , semantik __blockperubahan dan referensi akan dipertahankan, dalam hal ini Anda harus mendeklarasikannya __weak.

Jawaban panjang

Katakanlah Anda memiliki kode seperti ini:

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

Masalahnya di sini adalah diri mempertahankan referensi ke blok; sementara itu blok harus mempertahankan referensi ke diri sendiri untuk mengambil properti delegasi dan mengirim metode delegasi. Jika semua hal lain di aplikasi Anda melepaskan rujukannya ke objek ini, jumlah tetapnya tidak akan nol (karena blok mengarah ke sana) dan blok tidak melakukan kesalahan (karena objek menunjuk ke sana) dan seterusnya sepasang objek akan bocor ke tumpukan, menempati memori tetapi selamanya tidak dapat dijangkau tanpa debugger. Tragis, sungguh.

Kasing itu bisa dengan mudah diperbaiki dengan melakukan ini sebagai gantinya:

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

Dalam kode ini, self mempertahankan blok, blok mempertahankan delegasi, dan tidak ada siklus (terlihat dari sini; delegasi dapat mempertahankan objek kami tapi itu di luar kendali kami saat ini). Kode ini tidak akan mengambil risiko kebocoran dengan cara yang sama, karena nilai properti delegasi ditangkap saat blok dibuat, alih-alih mendongak ketika dijalankan. Efek sampingnya adalah, jika Anda mengubah delegasi setelah blok ini dibuat, blok tersebut masih akan mengirim pesan pembaruan ke delegasi lama. Apakah itu mungkin terjadi atau tidak tergantung pada aplikasi Anda.

Meskipun Anda keren dengan perilaku itu, Anda masih tidak bisa menggunakan trik itu dalam kasus Anda:

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

Di sini Anda mengirimkan selflangsung ke delegasi dalam pemanggilan metode, jadi Anda harus mendapatkannya di suatu tempat. Jika Anda memiliki kendali atas definisi tipe blok, hal terbaik adalah meneruskan delegasi ke blok sebagai parameter:

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

Solusi ini menghindari siklus mempertahankan dan selalu memanggil delegasi saat ini.

Jika Anda tidak dapat mengubah blok, Anda bisa mengatasinya . Alasan mempertahankan siklus adalah peringatan, bukan kesalahan, adalah bahwa mereka tidak selalu mengeja malapetaka untuk aplikasi Anda. Jika MyDataProcessormampu melepaskan blok ketika operasi selesai, sebelum induknya akan mencoba melepaskannya, siklus akan rusak dan semuanya akan dibersihkan dengan benar. Jika Anda bisa yakin akan hal ini, maka hal yang benar untuk dilakukan adalah menggunakan a #pragmauntuk menekan peringatan untuk blok kode itu. (Atau gunakan flag kompiler per file. Tetapi jangan menonaktifkan peringatan untuk seluruh proyek.)

Anda juga bisa melihat menggunakan trik serupa di atas, menyatakan referensi lemah atau tidak dibatasi dan menggunakannya di blok. Sebagai contoh:

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

Ketiga hal di atas akan memberi Anda referensi tanpa mempertahankan hasilnya, meskipun mereka semua berperilaku sedikit berbeda: __weakakan mencoba untuk nol referensi ketika objek dilepaskan; __unsafe_unretainedakan meninggalkan Anda dengan pointer yang tidak valid; __blocksebenarnya akan menambah tingkat tipuan lain dan memungkinkan Anda untuk mengubah nilai referensi dari dalam blok (tidak relevan dalam kasus ini, karena dptidak digunakan di tempat lain).

Apa yang terbaik akan tergantung pada kode apa yang dapat Anda ubah dan apa yang tidak bisa Anda ubah. Tapi semoga ini memberi Anda beberapa ide tentang bagaimana untuk melanjutkan.

benzado
sumber
1
Jawaban yang luar biasa! Terima kasih, saya memiliki pemahaman yang jauh lebih baik tentang apa yang sedang terjadi dan bagaimana semua ini bekerja. Dalam hal ini, saya memiliki kendali atas semuanya jadi saya akan merancang ulang beberapa objek sesuai kebutuhan.
XJones
18
O_O Saya baru saja lewat dengan masalah yang sedikit berbeda, macet membaca, dan sekarang meninggalkan halaman ini merasa semua berpengetahuan dan keren. Terima kasih!
Orc JMR
apakah benar, bahwa jika karena alasan tertentu pada saat eksekusi blok dpakan dirilis (misalnya jika itu adalah pengontrol tampilan dan dilepaskan), maka baris [dp.delegate ...akan menyebabkan EXC_BADACCESS?
peetonn
Haruskah properti yang memegang blok (misalnya dataProcess.progress) menjadi strongatau weak?
djskinner
1
Anda mungkin melihat libextobjc yang menyediakan dua makro berguna yang dipanggil @weakify(..)dan @strongify(...)yang memungkinkan Anda untuk digunakan selfdalam blok dengan cara yang tidak mempertahankan.
25

Ada juga opsi untuk menekan peringatan ketika Anda yakin bahwa siklus akan rusak di masa depan:

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

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

#pragma clang diagnostic pop

Dengan cara itu Anda tidak perlu bermain-main dengan __weak, selfaliasing dan awalan ivar eksplisit.

zoul
sumber
8
Kedengarannya seperti praktik yang sangat buruk yang membutuhkan lebih dari 3 baris kode yang dapat diganti dengan __weak id weakSelf = self;
Ben Sinclair
3
Seringkali ada blok kode yang lebih besar yang dapat mengambil manfaat dari peringatan yang ditekan.
zoul
2
Kecuali itu __weak id weakSelf = self;memiliki perilaku yang secara fundamental berbeda dari menekan peringatan. Pertanyaannya dimulai dengan "... jika Anda yakin siklus penahannya akan rusak"
Tim
Terlalu sering orang membabi buta membuat variabel lemah, tanpa benar-benar memahami konsekuensi. Sebagai contoh, saya telah melihat orang melemahkan suatu objek dan kemudian, pada blok yang mereka lakukan: [array addObject:weakObject];Jika OjectOutject telah dirilis, ini menyebabkan crash. Jelas itu tidak disukai daripada mempertahankan siklus. Anda harus memahami apakah blok Anda benar-benar hidup cukup lama untuk menjamin pelemahan, dan juga apakah Anda ingin tindakan di blok bergantung pada apakah objek yang lemah masih valid.
mahboudz
14

Untuk solusi umum, saya mendefinisikan ini di header precompile. Menghindari pengambilan dan masih memungkinkan bantuan kompiler dengan menghindari penggunaanid

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

Kemudian dalam kode dapat Anda lakukan:

BlockWeakSelf weakSelf = self;
self.dataProcessor.completion = ^{
    [weakSelf.delegate myAPIDidFinish:weakSelf];
    weakSelf.dataProcessor = nil;
};
Damien Pontifex
sumber
Setuju, ini bisa menyebabkan masalah di dalam blok. ReactiveCocoa memiliki solusi menarik lain untuk masalah ini yang memungkinkan Anda untuk terus menggunakan selfdi dalam blok Anda @weakify (mandiri); id block = ^ {@strongify (self); [self.delegate myAPIDidFinish: self]; };
Damien Pontifex
@dmpontifex itu adalah makro dari libextobjc github.com/jspahrsummers/libextobjc
Elechtron
11

Saya percaya solusi tanpa ARC juga bekerja dengan ARC, menggunakan __blockkata kunci:

EDIT: Per Transisi ke Catatan Rilis ARC , objek yang dideklarasikan dengan __blockpenyimpanan masih dipertahankan. Gunakan __weak(lebih disukai) atau __unsafe_unretained(untuk kompatibilitas mundur).

// 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];
Tony
sumber
Tidak menyadari bahwa __blockkata kunci menghindari mempertahankan referensi itu. Terima kasih! Saya memperbarui jawaban monolitik saya. :-)
benzado
3
Menurut Apple docs "Dalam mode penghitungan referensi manual, __block id x; memiliki efek tidak mempertahankan x. Dalam mode ARC, __block id x; default untuk mempertahankan x (seperti semua nilai lainnya)."
XJones
11

Menggabungkan beberapa jawaban lain, inilah yang saya gunakan sekarang untuk diri yang diketik lemah untuk digunakan dalam blok:

__typeof(self) __weak welf = self;

Saya menetapkan itu sebagai Cuplikan Kode XCode dengan awalan penyelesaian "welf" dalam metode / fungsi, yang mengenai setelah mengetik hanya "kami".

Kendall Helmstetter Gelner
sumber
Apakah kamu yakin Tautan ini dan dokumen dentang tampaknya berpikir keduanya dapat dan harus digunakan untuk menyimpan referensi ke objek tetapi bukan tautan yang akan menyebabkan siklus retain: stackoverflow.com/questions/19227982/using-block-and-weak
Kendall Helmstetter Gelner
Dari dokumen dentang: clang.llvm.org/docs/BlockLanguageSpec.html "Dalam bahasa Objective-C dan Objective-C ++, kami mengizinkan specifier __ lemah untuk __block variabel tipe objek. Jika pengumpulan sampah tidak diaktifkan, kualifikasi ini menyebabkan variabel-variabel ini harus disimpan tanpa menyimpan pesan yang sedang dikirim. "
Kendall Helmstetter Gelner
6

peringatan => "menangkap diri di dalam blok cenderung memimpin siklus tetap"

ketika Anda merujuk diri atau propertinya di dalam blok yang sangat dipertahankan sendiri daripada yang ditunjukkan peringatan di atas.

jadi untuk menghindarinya kita harus membuatnya seminggu ref

__weak typeof(self) weakSelf = self;

jadi alih-alih menggunakan

blockname=^{
    self.PROPERTY =something;
}

kita harus gunakan

blockname=^{
    weakSelf.PROPERTY =something;
}

Catatan: mempertahankan siklus biasanya terjadi ketika beberapa bagaimana dua objek merujuk satu sama lain dimana keduanya memiliki jumlah referensi = 1 dan metode delloc mereka tidak pernah dipanggil.

Anurag Bhakuni
sumber
-1

Jika Anda yakin bahwa kode Anda tidak akan membuat siklus penyimpanan, atau bahwa siklus tersebut akan rusak kemudian, maka cara paling sederhana untuk membungkam peringatan itu adalah:

// 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];

Alasan bahwa ini berfungsi adalah bahwa sementara akses titik properti diperhitungkan oleh analisis Xcode, dan karenanya

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

terlihat memiliki mempertahankan oleh xy (di sisi kiri penugasan) dan oleh xy (di sisi kanan), panggilan metode tidak tunduk pada analisis yang sama, bahkan ketika mereka panggilan properti-metode akses yang setara dengan akses-titik, bahkan ketika metode-metode akses properti dihasilkan oleh kompiler, begitu juga

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

hanya sisi kanan yang dilihat sebagai membuat retain (dengan y dari x), dan tidak ada peringatan siklus retain yang dihasilkan.

Ben Artin
sumber