Hiểu cách đếm tham chiếu với Cacao và Objective-C


122

Tôi chỉ mới bắt đầu xem xét Objective-C và Cocoa với mục đích chơi với iPhone SDK. Tôi khá thoải mái với C mallocfreekhái niệm, nhưng sơ đồ đếm tham chiếu của Cocoa khiến tôi khá bối rối. Tôi đã nói rằng nó rất thanh lịch một khi bạn hiểu nó, nhưng tôi vẫn chưa vượt qua được cái bướu.

Làm thế nào để làm release, retainautoreleasehoạt động và các quy ước về việc sử dụng chúng là gì?

(Hoặc thất bại, bạn đã đọc những gì giúp bạn có được nó?)

Câu trả lời:


148

Hãy bắt đầu với retainrelease; autoreleasethực sự chỉ là một trường hợp đặc biệt khi bạn hiểu các khái niệm cơ bản.

Trong Cocoa, mỗi đối tượng theo dõi số lần nó đang được tham chiếu (cụ thể là NSObjectlớp cơ sở thực hiện điều này). Bằng cách gọi retainmột đối tượng, bạn đang nói với nó rằng bạn muốn tăng số lượng tham chiếu của nó lên từng đối tượng. Bằng cách gọi release, bạn cho đối tượng biết bạn đang buông nó ra và số lượng tham chiếu của nó sẽ giảm. Nếu sau khi gọi release, số tham chiếu bây giờ là 0, thì bộ nhớ của đối tượng đó được giải phóng bởi hệ thống.

Cách cơ bản khác này từ mallocfreelà bất kỳ đối tượng nhất định không cần phải lo lắng về các bộ phận khác của hệ thống đâm vì bạn đã giải phóng bộ nhớ họ đang sử dụng. Giả sử mọi người cùng chơi và giữ lại / giải phóng theo quy tắc, khi một đoạn mã giữ lại và sau đó giải phóng đối tượng, bất kỳ đoạn mã nào khác cũng tham chiếu đến đối tượng sẽ không bị ảnh hưởng.

Điều đôi khi có thể gây nhầm lẫn là biết các trường hợp mà bạn nên gọi retainrelease. Nguyên tắc chung của tôi là nếu tôi muốn bám vào một đối tượng trong một khoảng thời gian nào đó (ví dụ: nếu đó là một biến thành viên trong một lớp), thì tôi cần đảm bảo rằng số lượng tham chiếu của đối tượng biết về tôi. Như đã mô tả ở trên, số lượng tham chiếu của một đối tượng được tăng lên bằng cách gọi retain. Theo quy ước, nó cũng được tăng lên (thực sự là 1) khi đối tượng được tạo bằng phương thức "init". Trong một trong hai trường hợp này, tôi có trách nhiệm gọi releaseđối tượng khi tôi hoàn thành việc đó. Nếu tôi không, sẽ có một bộ nhớ bị rò rỉ.

Ví dụ về tạo đối tượng:

NSString* s = [[NSString alloc] init];  // Ref count is 1
[s retain];                             // Ref count is 2 - silly
                                        //   to do this after init
[s release];                            // Ref count is back to 1
[s release];                            // Ref count is 0, object is freed

Bây giờ cho autorelease. Autorelease được sử dụng như một cách thuận tiện (và đôi khi cần thiết) để yêu cầu hệ thống giải phóng đối tượng này sau một thời gian ngắn. Từ góc độ hệ thống ống nước, khi autoreleaseđược gọi, luồng hiện tại sẽ NSAutoreleasePoolđược cảnh báo về cuộc gọi. Các NSAutoreleasePoolbây giờ biết rằng một khi nó nhận được một cơ hội (sau khi lặp hiện tại của vòng lặp sự kiện), nó có thể gọi releasetrên đối tượng. Từ quan điểm của chúng tôi với tư cách là lập trình viên, nó đảm nhận việc gọi releasecho chúng tôi, vì vậy chúng tôi không cần phải làm như vậy (và trên thực tế, chúng tôi không nên).

Điều quan trọng cần lưu ý là (một lần nữa, theo quy ước) tất cả các phương thức của lớp tạo đối tượng đều trả về một đối tượng đã được autoreleased. Ví dụ, trong ví dụ sau, biến "s" có số tham chiếu là 1, nhưng sau khi vòng lặp sự kiện hoàn thành, nó sẽ bị hủy.

NSString* s = [NSString stringWithString:@"Hello World"];

Nếu bạn muốn bám vào chuỗi đó, bạn cần phải gọi retainmột cách rõ ràng, và sau đó gọi releasenó một cách rõ ràng khi bạn hoàn tất.

Hãy xem xét đoạn mã (rất phức tạp) sau và bạn sẽ thấy một tình huống autoreleasebắt buộc:

- (NSString*)createHelloWorldString
{
    NSString* s = [[NSString alloc] initWithString:@"Hello World"];

    // Now what?  We want to return s, but we've upped its reference count.
    // The caller shouldn't be responsible for releasing it, since we're the
    // ones that created it.  If we call release, however, the reference 
    // count will hit zero and bad memory will be returned to the caller.  
    // The answer is to call autorelease before returning the string.  By 
    // explicitly calling autorelease, we pass the responsibility for
    // releasing the string on to the thread's NSAutoreleasePool, which will
    // happen at some later time.  The consequence is that the returned string 
    // will still be valid for the caller of this function.
    return [s autorelease];
}

Tôi nhận ra rằng tất cả những điều này hơi khó hiểu - mặc dù vậy, tại một số thời điểm, nó sẽ nhấp chuột. Dưới đây là một số tài liệu tham khảo để giúp bạn tiếp tục:

  • Giới thiệu của Apple về quản lý bộ nhớ.
  • Lập trình ca cao cho Mac OS X (Phiên bản thứ 4) , của Aaron Hillegas - một cuốn sách được viết rất hay với rất nhiều ví dụ tuyệt vời. Nó đọc như một hướng dẫn.
  • Nếu bạn thực sự muốn tìm hiểu, bạn có thể đến Nông trại Big Nerd . Đây là cơ sở đào tạo do Aaron Hillegas - tác giả của cuốn sách nói trên điều hành. Tôi đã tham dự khóa học Giới thiệu về ca cao ở đó vài năm trước, và đó là một cách tuyệt vời để học.

8
Bạn đã viết: "Bằng cách gọi autorelease, chúng tôi tạm thời tăng số lượng tham chiếu". Tôi nghĩ rằng điều này là sai; autorelease chỉ đánh dấu đối tượng sẽ được phát hành trong tương lai, nó không làm tăng số lượng giới thiệu: cocoadev.com/index.pl?AutoRelease
LKM

2
"Bây giờ dành cho tự động phát hành. Tự động phát hành được sử dụng như một cách thuận tiện (và đôi khi cần thiết) để yêu cầu hệ thống giải phóng đối tượng này sau một thời gian ngắn." Là một câu dẫn trước, điều này là sai. Nó không yêu cầu hệ thống "giải phóng [nó] lên", nó yêu cầu hệ thống giảm số lượng giữ lại.
mmalc

3
Cảm ơn rất nhiều vì lời giải thích tốt. Chỉ một điều vẫn chưa rõ ràng. Nếu NSString* s = [[NSString alloc] initWithString:@"Hello World"];trả về một đối tượng đã tự động phát hành (như bạn viết) tại sao tôi phải thực hiện return [s autorelease];và đặt nó "autorelease" một lần nữa chứ không phải chỉ return s?
znq

3
@Stefan: [[NSString alloc] initWithString:@"Hello World"]sẽ KHÔNG trả về đối tượng đã phát hành tự động. Bất cứ khi nào allocđược gọi, số lượng tham chiếu được đặt thành 1 và mã đó có trách nhiệm đảm bảo nó được phát hành. Các [NSString stringWithString:]cuộc gọi, mặt khác, không trả về một đối tượng autoreleased.
Matt Dillard

6
Câu đố vui: Vì câu trả lời sử dụng @ "" và NSString, các chuỗi là không đổi trong suốt và do đó, số lượng giữ lại tuyệt đối sẽ không đổi và hoàn toàn không liên quan .... không làm cho câu trả lời sai, bằng bất kỳ cách nào, chỉ củng cố thực tế rằng số lượng giữ lại tuyệt đối không bao giờ thực sự là điều bạn phải lo lắng.
bbum

10

Nếu bạn hiểu quy trình giữ lại / phát hành thì có hai quy tắc vàng rõ ràng là "duh" đối với các lập trình viên Cocoa thành lập, nhưng tiếc là hiếm khi được giải thích rõ ràng về điều này cho người mới.

  1. Nếu một hàm trả về một đối tượng có alloc, createhoặc copytrong tên của nó sau đó các đối tượng là của bạn. Bạn phải gọi [object release]khi bạn hoàn thành nó. Hoặc CFRelease(object), nếu đó là một đối tượng Core-Foundation.

  2. Nếu nó KHÔNG có một trong những từ này trong tên của nó thì đối tượng đó thuộc về người khác. Bạn phải gọi [object retain]nếu bạn muốn giữ đối tượng sau khi kết thúc hàm của mình.

Bạn sẽ được phục vụ tốt nếu cũng tuân theo quy ước này trong các hàm do chính bạn tạo ra.

(Nitpickers: Vâng, không may là có một vài lệnh gọi API là ngoại lệ đối với các quy tắc này nhưng chúng rất hiếm).


11
Điều này là không đầy đủ và không chính xác. Tôi tiếp tục thất bại trong việc hiểu tại sao mọi người cố gắng lặp lại các quy tắc thay vì chỉ trỏ đến các tài liệu liên quan: developer.apple.com/documentation/Cocoa/Conceptual/MemoryMgmt/...
mmalc

4
Đặc biệt, các quy tắc của Tổ chức cốt lõi khác với quy tắc của Cacao; xem developer.apple.com/documentation/CoreFoundation/Conceptual/…
mmalc

1
Tôi cũng không đồng ý. Nếu một hàm trả về thứ gì đó mà nó không muốn sở hữu, nó sẽ tự động khôi phục nó. Nó là người gọi công việc chức năng để giữ lại nó (nếu muốn). Nó không nên làm gì với tên của bất kỳ phương thức nào đang được gọi. Đó là nhiều hơn C Style mã hóa trong đó quyền sở hữu của các đối tượng không rõ ràng.
Sam

1
Lấy làm tiếc! Tôi nghĩ rằng tôi đã vội vàng trong việc bỏ phiếu từ chối. Quy tắc quản lý bộ nhớ Câu trả lời của bạn gần như trích dẫn tài liệu apple.
Sam

8

Nếu bạn đang viết mã cho máy tính để bàn và bạn có thể nhắm mục tiêu Mac OS X 10.5, thì ít nhất bạn nên xem xét sử dụng tính năng thu gom rác Objective-C. Nó thực sự sẽ đơn giản hóa hầu hết quá trình phát triển của bạn - đó là lý do tại sao Apple đã nỗ lực hết sức để tạo ra nó ngay từ đầu và làm cho nó hoạt động tốt.

Đối với các quy tắc quản lý bộ nhớ khi không sử dụng GC:

  • Nếu bạn tạo một đối tượng mới sử dụng +alloc/+allocWithZone:, +new, -copyhoặc -mutableCopyhoặc nếu bạn -retainmột đối tượng, bạn đang nắm quyền sở hữu của nó và phải đảm bảo nó được gửi -release.
  • Nếu bạn nhận một đối tượng theo bất kỳ cách nào khác, bạn không phải là chủ sở hữu của nó và không nên đảm bảo nó được gửi đi -release.
  • Nếu bạn muốn chắc chắn một đối tượng được gửi -releasebạn có thể gửi cho mình, hoặc bạn có thể gửi các đối tượng -autoreleasevà hiện tại hồ bơi autorelease sẽ gửi nó -release(một lần cho mỗi nhận -autorelease) khi hồ bơi được để ráo nước.

Thông thường -autoreleaseđược sử dụng như một cách để đảm bảo rằng các đối tượng tồn tại trong suốt thời gian của sự kiện hiện tại, nhưng sẽ được làm sạch sau đó, vì có một nhóm tự động phát hành xung quanh quá trình xử lý sự kiện của Cocoa. Trong Cocoa, việc trả lại các đối tượng cho một người gọi được tự động trả về phổ biến hơn nhiều so với việc trả lại các đối tượng mà chính trình gọi đó cần giải phóng.


6

Objective-C sử dụng Đếm tham chiếu , có nghĩa là mỗi Đối tượng có một số tham chiếu. Khi một đối tượng được tạo, nó có số tham chiếu là "1". Nói một cách đơn giản, khi một đối tượng được tham chiếu (tức là được lưu trữ ở đâu đó), nó sẽ được "giữ lại", có nghĩa là số lượng tham chiếu của nó tăng lên một. Khi một đối tượng không còn cần thiết nữa, nó được "giải phóng" có nghĩa là số lượng tham chiếu của nó giảm đi một.

Khi số tham chiếu của đối tượng là 0, đối tượng được giải phóng. Đây là cách đếm tham chiếu cơ bản.

Đối với một số ngôn ngữ, tham chiếu được tự động tăng và giảm, nhưng mục tiêu-c không phải là một trong những ngôn ngữ đó. Do đó lập trình viên có trách nhiệm giữ lại và phát hành.

Một cách điển hình để viết một phương thức là:

id myVar = [someObject someMessage];
.... do something ....;
[myVar release];
return someValue;

Vấn đề cần nhớ để giải phóng bất kỳ tài nguyên nào có được bên trong mã vừa tẻ nhạt vừa dễ xảy ra lỗi. Objective-C giới thiệu một khái niệm khác nhằm mục đích làm cho việc này dễ dàng hơn nhiều: Autorelease Pools. Hồ bơi tự động vui lòng là các đối tượng đặc biệt được cài đặt trên mỗi luồng. Chúng là một lớp khá đơn giản, nếu bạn tra cứu NSAutoreleasePool.

Khi một đối tượng nhận được thông báo "autorelease" được gửi tới nó, đối tượng sẽ tìm kiếm bất kỳ nhóm tự động nào nằm trên ngăn xếp cho luồng hiện tại này. Nó sẽ thêm đối tượng vào danh sách như một đối tượng để gửi thông báo "phát hành" vào một thời điểm nào đó trong tương lai, thường là khi bản thân nhóm được phát hành.

Lấy đoạn mã trên, bạn có thể viết lại đoạn mã ngắn hơn và dễ đọc hơn bằng cách nói:

id myVar = [[someObject someMessage] autorelease];
... do something ...;
return someValue;

Bởi vì đối tượng được tự động phát hành, chúng ta không cần phải gọi rõ ràng là "phát hành" trên nó. Điều này là do chúng tôi biết một số nhóm autorelease sẽ làm điều đó cho chúng tôi sau này.

Hy vọng rằng điều này sẽ giúp. Bài viết trên Wikipedia khá tốt về việc đếm tham chiếu. Có thể tìm thấy thêm thông tin về bể tự động phát hành tại đây . Cũng lưu ý rằng nếu bạn đang xây dựng cho Mac OS X 10.5 trở lên, bạn có thể yêu cầu Xcode xây dựng với tính năng thu thập rác được bật, cho phép bạn hoàn toàn bỏ qua giữ lại / phát hành / tự động vui lòng.


2
Điều này chỉ là sai. Không cần gửi bản phát hành someObject hoặc autorlease trong một trong các ví dụ được hiển thị.
mmalc

6

Joshua (# 6591) - Công cụ thu thập rác trong Mac OS X 10.5 có vẻ khá tuyệt, nhưng không khả dụng cho iPhone (hoặc nếu bạn muốn ứng dụng của mình chạy trên các phiên bản Mac OS X trước 10.5).

Ngoài ra, nếu bạn đang viết thư viện hoặc thứ gì đó có thể được sử dụng lại, việc sử dụng chế độ GC sẽ khóa bất kỳ ai sử dụng mã cũng sử dụng chế độ GC, vì vậy theo tôi hiểu, bất kỳ ai đang cố gắng viết mã có thể tái sử dụng rộng rãi đều có xu hướng quản lý bộ nhớ thủ công.


2
Hoàn toàn có thể viết một khung kết hợp hỗ trợ cả GC và đếm tham chiếu.
mmalc

6

Từ trước đến nay, khi mọi người bắt đầu cố gắng diễn đạt lại tài liệu tham khảo, họ hầu như luôn nhận được sai sót hoặc cung cấp một mô tả không đầy đủ.

Apple cung cấp mô tả đầy đủ về hệ thống quản lý bộ nhớ của Cocoa trong Hướng dẫn lập trình quản lý bộ nhớ cho Cocoa , ở phần cuối của phần này có một bản tóm tắt ngắn gọn nhưng chính xác về Quy tắc quản lý bộ nhớ .




2
Trên thực tế, đây là bản tóm tắt một trang tốt hơn nhiều: developer.apple.com/mac/library/documentation/Cocoa/Conceptual/…
Brian Moeskau

6

Tôi sẽ không thêm vào chi tiết cụ thể của việc giữ lại / phát hành ngoài việc bạn có thể muốn nghĩ đến việc giảm $ 50 và nhận cuốn sách Hillegass, nhưng tôi thực sự khuyên bạn nên tham gia sử dụng các công cụ Instruments rất sớm trong quá trình phát triển ứng dụng của bạn (ngay cả đầu tiên!). Để làm như vậy, Chạy-> Bắt đầu với các công cụ hiệu suất. Tôi sẽ bắt đầu với Leaks chỉ là một trong nhiều công cụ có sẵn nhưng sẽ giúp bạn chỉ ra khi bạn quên phát hành. Nó không làm bạn nản lòng về lượng thông tin bạn sẽ được trình bày. Nhưng hãy xem hướng dẫn này để bắt đầu và nhanh chóng:
HƯỚNG DẪN CỦA COCOA: CỐ ĐỊNH LÁ BỘ NHỚ BẰNG CÔNG CỤ

Trên thực tế, cố gắng buộc các lỗ rò rỉ có thể là một cách tốt hơn để học cách ngăn chặn chúng! Chúc may mắn ;)


5

Matt Dillard đã viết :

return [[s autorelease] phát hành];

Autorelease không giữ lại đối tượng. Autorelease chỉ cần đặt nó vào hàng đợi để phát hành sau. Bạn không muốn có một tuyên bố phát hành ở đó.




4

Câu trả lời của NilObject là một khởi đầu tốt. Dưới đây là một số thông tin bổ sung liên quan đến quản lý bộ nhớ thủ công ( bắt buộc trên iPhone ).

Nếu cá nhân bạn alloc/initlà một đối tượng, nó đi kèm với số tham chiếu là 1. Bạn có trách nhiệm dọn dẹp sau khi nó không còn cần thiết nữa, bằng cách gọi [foo release]hoặc[foo autorelease] . phát hành làm sạch nó ngay lập tức, trong khi autorelease thêm đối tượng vào nhóm autorelease, nó sẽ tự động giải phóng nó sau đó.

autorelease chủ yếu dành cho khi bạn có một phương thức cần trả về đối tượng được đề cập ( vì vậy bạn không thể giải phóng nó theo cách thủ công, nếu không bạn sẽ trả về một đối tượng nil ) nhưng bạn cũng không muốn giữ nó. .

Nếu bạn có được một đối tượng mà bạn không gọi bổ sung / init để lấy nó - ví dụ:

foo = [NSString stringWithString:@"hello"];

nhưng bạn muốn bám vào đối tượng này, bạn cần gọi [foo keep]. Nếu không, có thể nó sẽ nhận được autoreleasedvà bạn sẽ giữ một tham chiếu nil (như trong stringWithStringví dụ trên ). Khi bạn không cần nữa, hãy gọi [foo release].


2

Các câu trả lời ở trên cung cấp các trình bày lại rõ ràng về những gì tài liệu nói; vấn đề mà hầu hết những người mới gặp phải là các trường hợp không có giấy tờ. Ví dụ:

  • Autorelease : tài liệu nói rằng nó sẽ kích hoạt bản phát hành "vào một thời điểm nào đó trong tương lai." KHI NÀO?! Về cơ bản, bạn có thể tin tưởng vào đối tượng đang tồn tại cho đến khi bạn thoát mã trở lại vòng lặp sự kiện hệ thống. Hệ thống CÓ THỂ giải phóng đối tượng bất kỳ lúc nào sau chu kỳ sự kiện hiện tại. (Tôi nghĩ Matt đã nói điều đó, sớm hơn.)

  • Chuỗi tĩnh : NSString *foo = @"bar";- bạn phải giữ lại hoặc giải phóng nó? Không. Làm thế nào về

    -(void)getBar {
        return @"bar";
    }

    ...

    NSString *foo = [self getBar]; // still no need to retain or release
  • Quy tắc Sáng tạo : Nếu bạn đã tạo ra nó, bạn sở hữu nó và dự kiến ​​sẽ phát hành nó.

Nói chung, cách mà các lập trình viên Cocoa mới gặp rắc rối là không hiểu quy trình nào trả về một đối tượng có retainCount > 0 .

Đây là một đoạn trích từ Quy tắc rất đơn giản để quản lý bộ nhớ trong ca cao :

Quy tắc về số lượng giữ chân

  • Trong một khối nhất định, việc sử dụng -copy, -alloc và -retain phải ngang bằng với việc sử dụng -release và -autorelease.
  • Các đối tượng được tạo bằng cách sử dụng các hàm tạo tiện lợi (ví dụ stringWithString của NSString) được coi là tự động phát hành.
  • Triển khai phương thức -dealloc để giải phóng các biến phiên bản mà bạn sở hữu

Dấu đầu dòng thứ nhất cho biết: nếu bạn đã gọi alloc(hoặc new fooCopy), bạn cần gọi nhả đối tượng đó.

Dấu đầu dòng thứ 2 cho biết: nếu bạn sử dụng một hàm tạo tiện lợi và bạn cần đối tượng để treo xung quanh (như với một hình ảnh sẽ được vẽ sau này), bạn cần giữ lại (và sau đó thả nó ra sau đó).

Thứ 3 nên tự giải thích.


"Autorelease: docs nói rằng nó sẽ kích hoạt một bản phát hành" vào một thời điểm nào đó trong tương lai. "KHI NÀO ?!" Tài liệu nói rõ về điểm đó: "autorelease chỉ có nghĩa là" gửi thông báo phát hành sau "(để biết một số định nghĩa về sau — xem" Autorelease Pools ")." Chính xác thời điểm phụ thuộc vào ngăn xếp nhóm tự động phát hành ...
mmalc

... "Hệ thống CÓ THỂ giải phóng đối tượng bất kỳ lúc nào sau chu kỳ sự kiện hiện tại." Điều này làm cho hệ thống âm thanh khá ít xác định hơn là ...
mmalc

... NSString foo = [self getBar]; // vẫn không cần giữ lại hoặc giải phóng Điều này là sai. Bất kỳ ai gọi getBar đều không biết chi tiết triển khai, vì vậy * nên giữ lại / phát hành (thường thông qua trình truy cập) nếu họ muốn sử dụng nó bên ngoài phạm vi hiện tại.
mmalc

Bài viết "Các quy tắc rất đơn giản để quản lý bộ nhớ trong ca cao" ở một số khía cạnh đã lỗi thời - cụ thể là "Các đối tượng được tạo bằng cách sử dụng các hàm tạo tiện lợi (ví dụ: stringWithString của NSString) được coi là tự động phát hành." là không đúng - nó chỉ đơn giản là "không thuộc sở hữu của người nhận".
mmalc


0

Như một số người đã đề cập, Giới thiệu về Quản lý Bộ nhớ của Apple đến nay là nơi tốt nhất để bắt đầu.

Một liên kết hữu ích mà tôi chưa thấy đề cập là Quản lý bộ nhớ thực tế . Bạn sẽ tìm thấy nó ở giữa các tài liệu của Apple nếu bạn đọc qua chúng, nhưng nó đáng để liên kết trực tiếp. Đó là một bản tóm tắt điều hành tuyệt vời về các quy tắc quản lý bộ nhớ với các ví dụ và những sai lầm phổ biến (về cơ bản những câu trả lời khác ở đây đang cố gắng giải thích, nhưng cũng không phải).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.