Autorelease Pool

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 autoreleaseautorelease 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.???

Apple có viết như này

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools.

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 không sử dụng autorelease pool, memory tăng vèo vèo
Khi sử dụng autorelease pool, memory ổn định.

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.

Leave a Reply