Gần đây mình khá tò mò về chủ đề này, nên mình quyết định tìm hiểu kỹ về nó. Sau khi đã thoả mãn sự tò mò thì nay mình viết bài chia sẻ về nó.
1. Bối cảnh
Bạn nào lập trình từ hồi non-arc thì sẽ rõ về autorelease pool hơn các bạn lập trình thời arc. Cùng xét ví dụ sau khi dùng non-arc, thời mà vẫn dùng retain, release bằng tay thay vì ARC(Automatic Reference Counting) tự động thực hiện điều đó.
-(void) run {
NSURL *baseURL = [self getBaseURL];
[baseURL retain];
// Other code...
[baseURL release];
}
-(NSURL *)getBaseURL {
NSURL *url = [NSURL URLWithString:@"https://thanhvu.dev"];
return url;
}
Khi NSURL khởi tạo thì sẽ tự động gọi retain dẫn tới retain count tăng lên bằng 1. Sau khi return thì biến đó lại được bên caller retain tiếp (retain count 2). Điều khó khăn hiện tại là giờ làm sao để release biến url trong hàm getBaseURL. Vì khi ở hàm run không dùng nữa thì retain count còn 2 – 1 = 1. Nếu gọi thêm release lần nữa tức là 2 lần thì rất khó kiểm soát.
-(NSURL *)getBaseURL {
NSURL *url = [NSURL URLWithString:@"https://thanhvu.dev"];
[url Release]; // Nếu làm như này thì không ổn, do url bị huỷ trước khi return mất rồi.
return url;
[url release]; // Như này cũng không được, vì return rồi thì đâu có chạy tiếp.
}
Vì lý do như vậy nên autorelease và autorelease pool ra đời.
2. Khái niệm
Autorelease Pool là một thành phần chứa các object mà chúng sẽ được nhận message release khi mà pool bị huỷ (drain).
Vậy autorelease pool có liên quan gì tới ví dụ bên trên?. Code bên nên được viết như sau.
-(void) run {
NSURL *baseURL = [self getBaseURL];
[baseURL retain];
// Other code...
[baseURL release];
}
-(NSURL *)getBaseURL {
NSURL *url = [NSURL URLWithString:@"https://thanhvu.dev"];
return [url autorelease];
}
Khi gọi autorelease thì objc runtime tự động lưu object đó vào trong pool. Và khi pool được huỷ (drain) thì nó sẽ giải phóng các object có trong đó.
Vậy thì pool đang ở đâu, tôi đâu có thấy cái pool nào trong đoạn code này.???
Mình thì hay tò mò, tải thử opensource framework CF thì không thấy đả động. Sau khi tìm hiểu thì hoá ra nó nằm ở trong Foundation.framework. Thử dùng công cụ disassembler hàm runMode:beforeDate: của NSRunLoop thì được kết quả như sau:
bool -[NSRunLoop runMode:beforeDate:](void * self, void * _cmd, void * arg2, void * arg3) { rsi = _cmd; r14 = arg3; r15 = arg2; r13 = self; if (arg2 == 0x0) { r12 = rsi; if (_CFExecutableLinkedOnOrAfter(0x6) != 0x0) { var_-48 = @class(NSException); rbx = r14; r14 = *_NSInvalidArgumentException; __NSMethodExceptionProem(r13, r12); rdx = r14; r14 = rbx; [var_-48 raise:rdx format:@"%@: mode argument cannot be nil"]; } } if ((*(r13 + *_OBJC_IVAR_$_NSRunLoop._rl) != 0x0) && (_CFRunLoopIsCurrent() != 0x0)) { rdx = @"NSDefaultRunLoopMode"; if ([r15 isEqual:rdx] != 0x0) { r15 = *_kCFRunLoopDefaultMode; } if (_CFRunLoopFinished(*(r13 + *_OBJC_IVAR_$_NSRunLoop._rl), r15) != 0x0) { rax = 0x0; } else { rbx = _NSPushAutoreleasePool(0x0); // Khởi tạo pool [r14 timeIntervalSinceReferenceDate]; var_-48 = intrinsic_movsd(var_-48, xmm0); CFAbsoluteTimeGetCurrent(); intrinsic_movapd(xmm0, intrinsic_subsd(intrinsic_movsd(xmm1, var_-48), xmm0)); CFRunLoopRunInMode(r15, 0x1, rdx); _NSPopAutoreleasePool(rbx); // Drain pool rax = 0x1; } } else { rax = 0x0; } return rax; }
Hoá ra autorelease pool luôn ở quanh ta, cho dù ARC hay Non-ARC 😀
Ơ nhưng nghe nói ARC không cho gọi autorelease, retain, release nữa cơ mà. Thực ra bản chất của ARC vẫn là Non-ARC, điểm khác là ARC nó chỉ giúp cho lập trình viên không phải gọi bằng tay các phương thức như autorelease, retain, release nữa mà nó sẽ tự động gọi cho ta khi compile.
Các bạn có thể nghĩ thằng này chém gió, lấy đâu ra thông tin vậy. Thì mình xin thử run code ARC như sau.
#import <Foundation/Foundation.h>
@interface DemoClass: NSObject
-(NSURL *) getURL;
@end
@implementation DemoClass
-(NSURL *) getURL {
NSURL *url = [NSURL URLWithString:@"https://thanhvu.dev"];
return url;
}
@end
int main(int argc, const char * argv[]) {
DemoClass *obj = [[DemoClass alloc] init];
NSURL *baseURL = [obj getURL];
return 0;
}
Ta tiến hành đặt debug vào trong method -[DemoClass getURL] và run. Tại cửa sổ debug lldb thì gõ lệnh “dis” và có kết quả lệnh assembly như sau.
ARC`-[DemoClass getURL]: 0x100000e20 <+0>: push rbp 0x100000e21 <+1>: mov rbp, rsp 0x100000e24 <+4>: sub rsp, 0x20 0x100000e28 <+8>: mov qword ptr [rbp - 0x8], rdi 0x100000e2c <+12>: mov qword ptr [rbp - 0x10], rsi 0x100000e30 <+16>: mov rdi, qword ptr [rip + 0x301] ; (void *)0x00007fff8f4640f0: NSURL 0x100000e37 <+23>: mov rsi, qword ptr [rip + 0x2da] ; "URLWithString:" 0x100000e3e <+30>: lea rdx, [rip + 0x1f3] ; @"https://thanhvu.dev" 0x100000e45 <+37>: mov rax, qword ptr [rip + 0x1c4] ; (void *)0x00007fff5e822680: objc_msgSend 0x100000e4c <+44>: call rax 0x100000e4e <+46>: mov rdi, rax 0x100000e51 <+49>: call 0x100000f2a ; symbol stub for: objc_retainAutoreleasedReturnValue 0x100000e56 <+54>: mov qword ptr [rbp - 0x18], rax -> 0x100000e5a <+58>: mov rdi, qword ptr [rbp - 0x18] 0x100000e5e <+62>: mov rax, qword ptr [rip + 0x1b3] ; (void *)0x00007fff5e822550: objc_retain 0x100000e65 <+69>: call rax 0x100000e67 <+71>: xor ecx, ecx 0x100000e69 <+73>: mov esi, ecx 0x100000e6b <+75>: lea rdi, [rbp - 0x18] 0x100000e6f <+79>: mov qword ptr [rbp - 0x20], rax 0x100000e73 <+83>: call 0x100000f30 ; symbol stub for: objc_storeStrong 0x100000e78 <+88>: mov rax, qword ptr [rbp - 0x20] 0x100000e7c <+92>: mov rdi, rax 0x100000e7f <+95>: add rsp, 0x20 0x100000e83 <+99>: pop rbp 0x100000e84 <+100>: jmp 0x100000f24 ; symbol stub for: objc_autoreleaseReturnValue 0x100000e89 <+105>: nop dword ptr [rax]
WTF, Ở dòng có địa chỉ “0x100000e84 <+100>” có gọi tới hàm objc_autoreleaseReturnValue kìa. :D. Chỗ đó là chỗ ARC nó gọi autorelease đó. Cũng giống như bên Non-ARC mà.
3. Khi nào thì nên dùng Autorelease Pool
Như 2 phần trên mình có giải thích, dù ra Non-ARC hay ARC thì vẫn dùng autorelease. Và mặc định pool được drain khi kết thúc runloop.
Tuy nhiên có những trường hợp compiler không optimize được và ở đó thì mình gọi hàm rất nhiều lần, gây ra tình trạng bộ nhớ tăng lên mức quá cao và sau đó lại giảm xuống sau khi kết thúc runloop (drain pool). Mình có 1 ví dụ như sau.
https://github.com/ThanhDev2703/AutoreleasePool
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self runWithAutorelease];
}
-(void) run {
NSURL *url = [[NSBundle mainBundle] URLForResource:@"largeFile" withExtension:@"jpg"];
for (int i = 0 ; i < 100000; i++) {
NSData *data = [[NSData alloc] initWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
NSLog(@"image size %f %f", image.size.width, image.size.height);
}
}
-(void) runWithAutorelease {
NSURL *url = [[NSBundle mainBundle] URLForResource:@"largeFile" withExtension:@"jpg"];
for (int i = 0 ; i < 100000; i++) {
@autoreleasepool {
NSData *data = [[NSData alloc] initWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
NSLog(@"image size %f %f", image.size.width, image.size.height);
}
}
}
@end
Khi gọi hàm, các giá trị return (data và image) được lưu vào autorelease pool và sẽ giải phóng sau khi kết thúc runloop. Tuy nhiên số lượng lặp quá nhiều và bộ nhớ lớn lên nên gây ra bộ nhớ tăng mãi mà không release.
Giải pháp chúng ta là đặt autorelease pool ở trong vòng for. Nó sẽ release biến data và image sau khi kết thúc pool thay vì phải đợi tới cuối runloop.
4. Tổng kết
Autorelease pool là một thành phần rất quan trọng trong iOS.
Chúng ta nên sử dụng autorelease trong những trường hợp sau:
- Khi bạn phát triển ứng dụng mà không dùng UIKit(Command line App), thì lúc này không có Autorelease Pool ở mỗi vòng lặp runloop nên chạy thời gian dài sẽ tăng memory cao dẫn tới dễ crash.
- Khi bạn chạy vòng lặp nhiều chu kỳ và mỗi chu kỳ lại sử dụng nhiều biến, gọi nhiều hàm. Vấn đề này mình đã giải thích ở bên trên.
- Khi bạn run nhiều hơn 1 thread, trường hợp nếu thread đó không chạy NSRunLoop và thread đó xử lý dữ liệu nhiều, chạy lâu, gọi nhiều hàm thì ta nên dùng AutoRelease Pool ở đây để tránh tình trạng memory tăng quá cao chưa kịp giảm xuống đã tèo cả App.
Cảm ơn mọi người đã đọc bài viết.