이재용의 iOS

autoreleasepool in Swift

2023년 1월 17일 • ☕️ 2 min read


autoreleasepool 개념을 Swift에서 알 필요가 있을까?

.

필요하다. 😂 그 이유를 알기 위해선 autoreleasepool 을 사용하게 된 배경을 알아야한다. Obj-C로 거슬러 올라가보자. (신기한 내용이라 이 글을 참고하여 작성했다.)

autorelease? (Objective-C)

ARC가 등장하기 이전, 개발자는 직접 retainrelease를 삽입하여 메모리를 관리했다. 인스턴스를 참조하고 있는 포인터 개수가 0이 되어야 메모리에서 해제된다.

아래는 Label을 생성하는 메소드 예제이다.

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    return label;
}

NSString alloc을 통해 retain count를 1 증가시켰고 메소드를 호출하는 곳에 사용할 수 있도록 리턴했기에 retain count가 1 증가하여 2가 된다.

하지만 위 코드는 문제가 있다. release를 통해 메모리에서 해제해주어야 하지만, 메모리를 해제시킬 수 있는 길이 없다. 아래 코드를 보자.

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    [label release]; // ❌
    return label;
    [label release]; // ❌
}

리턴 전에 release()를 호출한다면, label은 retain count가 0이 되어 메모리 해제되어 사용 시점에 앱이 크래시난다. 리턴 후에 release()를 호출한다면, 실행되지 않는 코드이다.

이를 위한 해결책으로 autorelease라는 개념이 등장한다.

-(NSString *)getCoolLabel {
    NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
    return [label autorelease];
}

객체의 retain count를 바로 감소시키는 대신, autorelease를 통해 pool에 저장하고, 미래에 release를 실행한다. 기본적으로 객체가 저장되는 pool은 메모리 릭없이 label을 사용하고 난 후, 해당 쓰레드의 런루프가 끝나는 시점에 해제된다. 그러므로, 위 문제점들은 해결된다.

@autoreleasepool

하지만, autorelease를 사용하면 앱에 백만개 이상의 인스턴스를 한번에 저장하고 있는 경우가 생긴다. 만약 파일의 콘텐츠 크기가 너무 크다면 앱이 크래시가 날 수 있다. 이 때, @autoreleasepool 블록을 통해 방지할 수 있다. autoreleasepool은 해당 블록이 끝나면 블록 안에 있는 인스턴스들은 할당 해제되는 것을 보장한다. 아래처럼 for문 루프 한번이 끝나는 시점에 할당된 인스턴스들이 할당해제된다.

-(void)emojifyAllFiles {
    int numberOfFiles = 1000000;
    for(i=0;i<numberOfFiles;i++) {
        @autoreleasepool {
            NSString *contents = [self getFileContents:files[i]];
            NSString *emojified = [contents emojified];
            [self writeContents:contents toFile:files[i]];
        }
    }
}

autoreleasepool을 사용하지 않는다면 1000000개의 파일들을 담을 메모리 공간이 필요하지만, 이제는 메모리 사용량을 유지할 수 있게 된다.

Swift에서 @autoreleasepool 을 알아야할까?

이론적으로 알 필요 없다. Swift에서는 ARC 최적화로 인하여 위와 같이 autorelease를 통해 담아두어야할 상황이 생기지 않는다. 그리고, 순수한 Swift에서는 autorelease라는 개념이 존재하지 않는다.

그러나, 알아야한다. Objective-C 레가시 코드가 Foundation 프레임워크 내에 존재하기 때문이다.

아래 Data initializer는 Obj-C [NSData dataWithContentsOfURL] 메소드와 브릿지되어있기 때문에 내부적으로 autorelease가 불린다.

func run() {
    guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
        return
    }
    for i in 0..<1000000 {
        let url = URL(fileURLWithPath: file)
        let imageData = try! Data(contentsOf: url)
    }
}

메모리 사용량 문제가 발생하므로 autoreleasepool로 해결할 수 있다.

autoreleasepool {
    let url = URL(fileURLWithPath: file)
    let imageData = try! Data(contentsOf: url)
}

이런 레가시 코드들을 어떻게 확인할 수 있을까?

없다 😭
Allocation instrument 디버깅툴로 직접 확인하는 방법밖에 아직 찾지 못했다.

.

.