본문 바로가기

Software/iOS & Objective-C

메모리 관리 <Objective-C?>

이글은 Naver "맥부기 Cafe의  문씨님의 강좌에서 퍼온글 입니다."

Objective-C는 C++과 마찬가지로 C에서 파생된 언어이다.
다만 구조적 특징을 보면 C++보다 Objective-C가 좀더 C에 가깝다.

C++은 C에서 메모리 관리를 직접하는 포인터 개념에 충실한 반면 Objective-C는 메모리 관리를
어느정도 대신 해주는 개념을 가지고 있다.

언어를 대체적으로 C나, C++, Objective-C로 세분류로 나누고 보면
일단 C나 C++은 개발하기가 어렵다. 하지만 메모리 관리를 직접할수 있기 때문에 고수가 개발한다면 정말 강력한 앱을 만들수도 있다.
Java는 메모리 관리를 신경 안쓰는 언어이다. 덕분에 개발은 편하다. 하지만 메모리 관리하는 가비지컬렉션 대문에 모바일 환경에서는 상당히 느리다.

직접 메모리 관리는 너무 개발하기 힘들고 Java는 느려서 원하는 성능을 내기가 힘들다.
후에 모바일 기기의 선능이 PC급으로 올라가면 얘기가 달라지겠지만...

그렇다면 개발하기는 편하면서 가비지컬렉션을 사용하지 않는 방법은 없을까? 하는 질문에 등장하는것이 Objective-C이다.

물론 Objective-C도 가비지컬렉션을 지원한다. 하지만 모바일 기기인 아이폰이나 터치, 아이패드 등에서는 지원하지 않는다. 즉 OSX에서는 지원하지만 iOS에서는 지원하지 않는다.

애플도 가비지컬렉션의 무거움을 알기때문에 일부러 지원을 안했다고 한다.

Objective-C에서는 리테인 카운트(retain count)기법을 사용한다. 해당 객체를 몇군데어서 사용하고 있는지 그것을 수치로 가지고 있는것이다. 그리고 아무곳에서도 사용하지 않을때 지워지는 개념이다.

개념만 본다면 더이상 사용하지 않는 객체를 찾아서 지우는 가비지컬렉션과 유사하기도하다.
다른점이라면 가비지컬렉션은 시스템에 기생해서 메모리를 체크하는 방식이지만 Objective-C는 로직으로 제거하는 방식이다. 즉 Java에서는 객체를 쓰고 뒤처리를 안하지만 Objective-C에서는 반드시 뒤처리를 한다. 객체를 사용후 해당 객체에게 이곳에서는 사용을 중지했다고 알려주는 것 이다.

Objective-C에서 모든 객체들의 순환(Life Cycle)을 이해 하려면 우선 NSObject를 알아야 한다.
모든 객체는 NSObject를 상속 받았기 때문이다. 그래서 NSObject를 알면 메모리관리를 알게 된다.
NSObject의 NS는 Next Step의 약자이고 Object는 말 그대로 객체를 뜻한다.

먼저 NSObject의 헤더를 보자
그림

클래스 선언 부분인 @interface NSObject를 보면 최상위의 클래스 이기때문에 상속받는 부모클래스가 없다. 보통 @interface test:NSObject 식으로 부모클래스 선언이 반드시 있다. 물론 없으면 에러 경고를 띄우기도 한다.
그리고 선언된 함수들을 보면 +와 -가 있다. +의 경우 클래스 함수(Class Method)라 하고 
                                                           -의 경우 인스턴트 함수(Instance Method)라 한다.
클래스를 도장이라고 본다면 인스턴트는 도장으로 찍어서 만든 것이라 할 수 있다. 클래스도 주소가 존재하기 때문에 객체로 취급 받기도 한다. 물론 클래스는 단일 존재라 단 하나만 존재한다. 그리고 인스턴트는 이 클래스로 원하는 만큼 만들어 낼 수 있다.

그럼 예를 들어 보겠다.

NSObject *object = [NSObject alloc];

object라는 변수에 NSObject라는 클래스에 alloc이라는 클래스 함수를 호출해서 새로운 인스턴트 객체를 만든후 그 객체의 주소를 object변수에 저장했다.

그럼 다른 예를 들어 보겠다.

NSArray *array = [NSArray alloc];

array라는 객체변수에 NSArray라는 클래스에 alloc이라는 클래스 함수를 호출해서 새로운 NSArray인스턴트 객체를 만들고 그 주소를 array에 지정한다.

객체 생성은 반드시 alloc라는 클래스 함수를 호출해서 원하는 객체를 찍어내는 것이다.
모든 클래스는 NSObject를 상속받고 있기 대문에 alloc함수는 어디는 존재하게 되는것이다.

객체를 생성했으면 이번에는 초기화를 해주자.

NSArray *array = [NSArray alloc];
[array init];
위에 헤더에도 있는 init은 기본 초기화 함수이다. init은 인스턴트 함수이기 때문에 객체 생성후 최초로 객체에 호출되는 함수이다.
단! 위의 코드는 문제가 있다. 애플의 가이드 문서도 위와 같은 사용은 금지하고 있다.

왜 그런것일까?

NSArray *array = [NSArray alloc]; 에서 객체를 생성한것 까진 좋았다.
문제는 [array init]; 이다.

위 코드는 array에 init이라는 메세지만 날려줄뿐이다.

init을 날렸는데 객체가 판단하기에 뭔가 잘못된 것이 있으면 객체가 스스로 해제 되기도 한다.


위의 코드는 예문이라 그럴일은 절대 없지만 다른 코드에서는 그럴일도 있을 수 있다. init함수는 초기화가 제대로 되면 자기자신의 주소를 돌려주고 잘못되면 스스로 해제를 한 후 nil(0값)을 돌려준다.

즉 위의 코드는 init만 날리고 돌려주는 값에 대한 대처가 안되어 있기 대문에 array변수가 가지고 있는 객체의 주소는 쓰레기가 되어 버린다.



NSArray *array = [NSArray alloc];
array = [array init];
아니면
NSArray *array = [[NSArray alloc] init];
을 사용한다.

결국 코드의 간결화를 위해 후자를 사용함.



이제 초기화를 했으면 해제를 해야한다.

NSObject헤더에 인스턴트 함수중에 release와 autorelease가 있다.
autorelease는 잠시 후에 다루고 일단 release부터 보자.

만든 객체에 release를 날려주면 객체는 해제가 된다.

[array release];

단 여기서 많은 사람들이 해매는 개념이 있다.

release는 객체를 완전히 제거하는 명령이 아닌 단순히 이곳에서는 이 객체를 더이상 쓰지 않는다고 알려주는 함수이다. NSObject에는 객체를 생성하는 함수는 있지만 완전 제거하는 함수는 존재하지 않는다. C에서 본다면 free문이 없다는 것이다.

대신 NSObject에는 retainCount라는 개념이 있다.

alloc으로 처음 생성될때 1이라는 수치로 시작된다.
그리고 release라는 함수가 호출되면 내부에 retainCount를 하나 줄인다.
위와 같은 경우는 1에서 하나 줄어서 0이 된다.
그러면 객체는 자동으로 메모리 상에서 제거가 된다.

Objective-C에서 메모리 누수란 이 retain count가 0이 안되고 남아있어서 벌어지는 문제이다.

그러면 release가 있으니 반대도 있다.
retain이라는 함수가 그 반대 역활을 한다.

NSArray *array = [[NSArray alloc] init]; // 1
[array retain];  //2
[array release]; //1
[array release]; //0

리테인 카운터의 개념 및 규칙은 "올린만큼 내려라" 이다.

autorelease의 존재는 정말 코딩을 편하게 해주는 기능이다.
지금 이대로 autorlease없이 코딩한다면 위와 같이 전부 객체 생성을 한후 사용이 끝난 부분에서 일일이 전부 release를 날려줘야 한다.

코딩 하다보면 사용하는 변수가 점점 많아지는데 이것들을 언제 일일이 release하겠는가?

오토릴리즈(autorelease)를 사용안한다면
NSArray *array = [[NSArray alloc] init];
... 한참 사용후
[array release];
이런식으로 사용하겠지만...

오토릴리즈를 사용한다면
NSArray *array = [[NSArray alloc] init] autorelease];
....사용

그리고 사용후에는 release를 신경 쓸 필요가 없다.
왠지 일일이 뒤처리 할필요가 없어진듯하다.

마치 Java같다.

오토릴리즈라 한다면 언젠간 자동으로 해제된다는 것인데
어떤 원리 일까?

오토릴리즈는 편한 존재이지만 그 원리는 이해 못하기 때문에 오히려 처음 배우는 사람에게는 혼란을 일으킨다.

언제 해제될까? 라는 궁금증이 이 함수에 대한 신뢰를 잃게 되는것이다.
그래서 불편하더라도 확실히 생성하고 확살히 릴리즈를 날리는 코더를 많이 볼 수 있었다.

오토릴리즈는 오토릴리즈 풀(NSAutoreleasePool)과 연결되어 있다.

생성된 객체에 autorelease를 날려주면 해당 객체는 자동으로 오토릴리즈 풀에 등록이 된다.

해제를 시켜주는 것이 아닌 등록이다.
(다른 말로 수영장(Pool)에 빠트려 놓다는 것!)

그리고 특정 시간이 지나면 그 수영장은 물을 빼고 청소를 한다.

그때 전에 빠진것들은 다 릴리즈 처리가 되면서 해제되는 것이다.

그럼 NSAutoreleasePool이 모아서 한거번에 release처리해준다는 것은 알았다.
그러면 언제 그런일(drain)이 일어날까?

저자에 따르면 일단 프로세스가 이벤트 루프로 돌아갈때 일어난다고 보고있다.

앱이 실행중일때 아무것도 안하고 있다면
메인 스레드는 이벤트 루프에서 무한 루프를 돌고 있다.

그러다 어떠한 이벤트가 발생한다면 그 발생한 이벤트에 따라 함수를 호출한다.
그리고 그 함수가 끝나면 다시 이벤트 루프로 돌아온다.

그리고 실행된 함수는 혼자서 일하고 끝나기도 하지만 작업을 위해 함수가 다른 함수를 부르는 일이 반드시 생긴다.

예를 하나 들어보겠다.

이벤트 루프에서 돌고 있다가 유저가 화면에 터치를 했다.

그러면 이벤트 루프는 UIApplication객체에 터치 이벤트가 있음을 알리는 메세지를 날린다.
*보통 Objective-C에서 메세지를 날린다고 하면 해당 객체에 함수를 호출한다고 보면 된다.

그럼 앱은 최상위 화면인 UIWindow에 메세지를 보내고,
UIWindow는 위에 올려진 뷰에 메세지를 보낼것이고,
올려진 뷰는 올려진 뷰위에 올려진 뷰에 메세지를 보내서
가장 상단에 올려진 뷰까지 메세지가 갔다올것이다.

즉 함수가 함수를 호출하는 상황이다.

그래서 그사이에 생성되는 오토릴리즈된 객체들은 해제되지 않고 있다가
일을 마치고 이벤트 루프로 돌아오면 전부 해제 처리되는 것이다.

결국 함수내에서 autorelease시킨 객체들이 함수 진행도중 해제 될까바 걱정할 일이 없다.

그럼 하나의 함수내에서 만들어 쓰고 버리는 객체들은 release를 사용할 일이 없다.
전부 오토릴리즈로 처리하면 되니깐...

그렇다. 근데 더 편하게 쓰게 도와주는것들이 있다.

NextStep시절부터 개발되어 오던 모든 클래스들은 한가지 규칙을 지키고 있다.
*객체를 돌려주는 함수는 반드시 오토릴리즈된 객체를 돌려준다*는 것이다.

위에서 언급했던 클래스 함수들.

한가지 예를 들어보자.

NSArray *array = [[[NSArray alloc] init] autorelease];

이것처럼 [[[ <-이거에 익숙해진 사람이라면 별 문제 없겠지만 이것들이 많아진다면 
그것도 나름데로 스트레스 이다.

NSArray같은 경우는 array라는 클래스 함수를 제공한다.
----------------------------------------
@interface NSArray (NSArrayCreation)

+(id)array;
----------------------------------------

함수로 돌려주는 객체는 반드시 오토릴리즈 된 객체이다 라는 규칙을 따르기 대문에
NSArray *array = [NSArray array];
이 코드는 위의 alloc init autorelease를 사용한 코드와 같은 의미 이다.

이렇게 기본적으로 Foundation에 내장되어 있는 NS계열의 객체들이나 아이폰부터 추가된 UIKit등 
모든 클래스들이 alloc이 아닌 특별히 제공하는 클래스 함수들은 전부 오토릴리즈 된 객체를 생성한다.

저자가 몇번 봤던 코드 중에는 이런것이 있다.
1: NSString *str = [[NSString alloc] initWithString:@"test"];
2: str = [str uppercaseString];
3: [str release];

이러면 에러가 나고 튕긴다는 문제였다.

1에서 오토릴리즈 안된 객체를 생성했다.

2에서 str에 기존의 문자열을 대문자로 바꾼 새로운 객체를 돌려주는 함수를 실행해서 돌려준 새로운 오토릴리즈된 객체의 주소를 다시 str에 지정했다.

이시점에서 기존의 str객체의 주소를 잃어버리게 되는것이다. 당연 메모리 누수가 난다.
그리고 str은 전부 대문자로 변환된 새로운 객체를 가지고 있게 된다.
하지만 오토릴리즈된 객체라 3에서 release를 날리면 당연 문제가 된다.
여기서 놓친 부분은 uppercaseString함수가 새로운 오토릴리즈된 객체를 만들어서 돌려준다는 점이다.

그럼 alloc의외에 클래스 생성 함수에 의해 만들어지는 객체들은 전부 오토릴리즈된 객체라는 것을 알았다. 그런데 아직도 이해가 안되는 부분이 있다.

NSArray같은 데이터 관리 객체는 어떻게 하는가?

NSArray보다 NSMutableArray를 예로 들어보겠다.

NSMutableArray에는 객체를 추가하는 함수가 있다.

addObject인데

하나 예를 들어보겠다.

NSString *str = @"test";
일단 문자열 객체를 준비한다.
NSMutableArray *array = [NSMutableArray array];
더이상 alloc init을 볼일이 없이 바로 오토릴리즈 된 배열 객체를 준비한다.

[array addObject:str];

array 객체에 str을 넣었다.
간단히 생각해보면 넣었다 라고 보겠지만
실제로 메모리 관리는 어떻게 될까?

NSMutableArray는 객체를 새로 추가 받는 함수가 실행되면 자동으로 그 객체에게 retain 함수를 날린다.

즉 여기서는 더이상 str을 쓸일이 없더라도 array는 str을 받은 이상 계속 가지고 있어야할 책임이 생기는 것이다.

리테인 카운트를 올려두었기 때문에 현재 코드에서 더이상 안쓴다 해도 객체는 유지된다.
하지만 별로 신경쓸 문제는 아니다.
NSMutableArray클래스는 객체를 받으면 책임 지기때문에 해제도한 책임 진다.

즉 array가 오토 릴리즈 상태이기때문에 나중에 해제 될때
가지고 있던 모든 객체에게 다 release를 날려주게 된다.

근데 넣어둔 객체 메모리 관리를 한답시고 직접 release를 날려버리면
해당 str객체는 해제 되겠지만 array는 객체가 있는줄 알고 
쓰레기 주소에 메세지를 때렸다가 베드 엑세스 에러가 나고만다.

결국 자기가 받은 객체는 자신이 책임 지는 구조이다.



그렇다면 클래스에 선언한 전역 변수는 어떻게 될까?

--------------------------------
@interface Test : UIView {
       NSString *title;
}

@end
--------------------------------

함수 내부에서 만들어 쓰고 끝나는 것들은 전부 오토릴리즈로 사용하겠지만 전역변수는 다르다. 더이상 안쓸때까지 retainCount를 하나 올려둬야 한다.

그리고 객체가 해제될때 호출되는 dealloc함수에서 해당 전역 변수의 객체들을 다 해제 시켜준다.

처음 생성할때 alloc init으로 가지고 있어도 되고, 아니면 다른 함수를 통해 오토릴리즈된 객체를 가지고 있다면 이럴대 retain을 사용하면 된다.











  

반응형

'Software > iOS & Objective-C' 카테고리의 다른 글

Objective C 기초정리  (0) 2013.01.23
멀티스레딩 <NSThread>  (0) 2011.12.30
Xcode & Objective-C - 기초개념 1.  (0) 2011.09.19
iPhone 개발 듀토리얼 - Window and view  (0) 2011.09.17
네트워크 in iPhone & iPad  (0) 2011.09.17