메모리 관리

현대 컴퓨터 구조에서는 프로그램이 메모리에 올라간 상태로 명령어가 하나씩 실행된다.
모든 객체 인스턴스는 메모리에 만들어진다.
그 중에서 객체에 대한 메모리 관리는 필수적이면서, 귀찮은 숙제이다.

메모리 사용을 최적화하는 과정은 CPU 사용률에 직간접적으로 영향을 주고, 배터리 소모량에도 영향을 미친다.

메모리와 객체

OS가 관리하는 프로세스는 이론적으로

  1. 32bit -> 4GB : (2^32)
  2. 64bit -> 18EB : (2^64)macOS는 메인 메모리상의 사용하지 않는 공간을 '페이지' 단위로 나눠서 하드디스크에 백업(Swapping)하는 기능을 제공한다.
    반면에 iOS는 하드디스크가 없고, 대신 플래시 메모리를 사용하기 때문에, 늘 메모리가 부족하기 마련이다.
  3. 크기를 가지는 가상 주소 공간에 접근할 수 있다.

iOS에서 읽고-쓰는 데이터는 프로그램을 실행하는 동안 사라지지 않지만, 사용하지 않는 읽기전용 데이터는 페이지를 저장하고 메모리상에서 지운다.
더 나아가, 읽고쓰는 데이터들의 총합이 일정 수준 이상 많아지면, 메모리 부족 경고를 보내고, 그래도 부족하면 앱을 강제 종료시켜서 메모리를 확보한다.

  • applicationDidReceiveMemoryWarning(_:)
    메소드를 구현하여서, app이 죽지 않게 메모리를 확보하는 작업을 할 수 있다.

macOS와 iOS를 비롯한 OS는 프로세스 주소 공간보다 물리 메모리(실제 메모리)가 부족하기 떄문에, 가상 메모리 방식을 사용한다.
물리 메모리보다 더 큰 가상 메모리를 다루기 이ㅜ해서, CPU와 MMU에서 일정한 크기를 가신 '페이지' 단위로 나눠 메모리를 관리한다.
기본적으로 크기는 4KB를 사용하며, Page Fault가 발생하면 -> 디스크에서 4KB 단위씩 새 페이지를 읽는다.

객체 인스턴스 생성

객체를 구현할 때, 우선적으로 고민해야 하는 것은 '객체 생명주기' 를 예측 가능하게 만드는 것이다.
보통 클래스 코드를 작성할 때, 생명주기와 관련해서 생성자 메서드를 가장 먼저 구현한다.

Pen *aPen = [[Pen alloc] init];

Pen 클래스에 +alloc 메세지를 보내면 힙 공간에 객체 인스턴스가 만들어진다.
+alloc 메서드 구현 내용을 수도코드로 보면 이렇다.

+alloc{
    id newObject = malloc(self->clsSizeInstance, 0);
    newObject->isa = self;
    return newObject;
}

해당 클래스의 메타 클래스에 명시된 속성 데이터 타입 크기를 확인해서 clsSizeInstance 크기만큼 힙 메모리를 할당한다.
메모리를 할당하는 단위를 16Byte이다.
If) 4Byte를 요청하면 -> 16Byte 할당, 20Byte 요청 -> 32Byte 할당
64bit 커널을 기준으로 994Byte ~ 128KB -> 512Byte 단위로 할당, 그 이상 -> 4KB를 할당

+alloc 메서드는 _allocWithZone: 을 호출하고, class_createInstance() 런타임 API를 호출하는데,
이 함수 내부에서 calloc() 함수를 호출한다.
calloc() 함수 : malloc() 과는 달리 객체 크기만큼 메모리를 할당한 다음, 할당된 메모리 공간을 0으로 채워준다.

메모리 할당 단위

메모리에 TINY, SMALL, LARGE 단위의 메모리 조각들이 구분없이 만들어지면, 외부 파편화가 발생할 것이다.
따라서 힙 메모리 공간을 영역별로 구분한다.

-??> JAVA GC에서는 Compact 작업이 있는데, 없는건가?

메모리 영역과 가상메모리

UIImage 클래스의 +imageNamed: 메서드로 이미지를 생성할 경우, 의도하지 않아도 시스템 내부에 이미지를 캐싱한다.

객체 인스턴스 소멸

소멸자 메서드는 생성과 달리 객체가 소멸되기 직전에 호출된다.
객체 내부에서 생성한 객체 인스턴스가 있다면, 먼저 해제해주도록 도와준다.
-> 그래야 불필요한 객체 인스턴스들이 남지 않기 떄문이다.

요약

요즘 단말기기의 대부분은 커널에서 관리하는 가상 메모리 크기도 커지고, Zone 관리도 필요가 없어졌다.
따라서 프로그램 데이터 구조를 강제로 줄이거나 조정해야 하는 제약 사항이 사라졌다.
그럼에도 객체 인스턴스를 메모리에 생성해서 소멸할 때까지의 과정을 메모리 관리 측면에서 이해하고 있으면 효율적인 것은 변함없다.
객체 인스턴스가 아니더라도 이미지나 DB Cache를 위해 내부적으로 할당하는 메모리 공간에 대해 종합적으로 고려해야 한다.


참조 계산

객체 인스턴스가 메모리에 생성되고 소멸되기까지를 '객체의 생명주기' 라고 부른다
어떤 객체는 생명주기가 짧고, 어떤 객체는 App과 같이 긴 경우도 있다.
이렇게 만들어진 객체는 처음에 필요해서 만든 객체가 아니라, 전혀 다른 객체가 메세지를 보내기 위해 참조하기도 한다.

특정 객체가 다른 객체를 참조하는 경우, 참조할 객체가 메모리에 존재하는지 아니면 사라졌는지 판단할 필요가 있다.
Apple은 객체 인스턴스를 확인하기 위해 "참조 계산" 방식을 제공한다.
-> Reference Counting 이 ARC가 동작할 수 있는 바탕이다.

코코아 프레임워크가 제공하는 모든 객체는 "참조 카운터" 공간이 있다.
해당 객체의 참조 횟수를 계산한 값을 기록하는 공간이다.
객체가 만들어질 때, 해당 객체를 참조하는 포인터로 인해서 1로 설정
다른 객체들이 추가로 강한 참조(retain)할 때, +1
참조하고 있던 객체들이 끊으면(release)할 때, -1 로 동작한다.

Apple에서도 Java JVM처럼 GC를 지원했었다.
사라진 원인은 알아보쟈...

객체 소유권

다른 객체를 참조한다는 것을 C 스타일로 표현하면, "다른 객체의 힙 메모리 주소를 포인터 변수에 담고 있는 것" 을 의미한다

    Pen *aPen = [[Pen alloc] init];
    Pen *bPen = aPen;
    [aPen release];
    bPen.color = [UIColor yellowColor]; -> bPen이 객체 인스턴스를 retain하지 않았기 떄문에 nil 크래시

객체 소유권 규칙

코코아에서 사용하는 일반적인 객체 소유권 규칙은 다음과 같다.

  • 특정 객체를 새로 만드는 경우는 소유권을 갖는다.
  • 다른 객체가 생성한 객체를 참조하기 전에, 소유권을 요청해서 받아야 한다.
  • 소유권을 얻는 객체를 더 이상 참조하지 않으면, 소유권을 반환한다.
  • 소유권을 갖고 있지 않는 객체를 반환하면 안된다.

이 규칙을 코코아 프레임워크 용어로 다시 설명해보자.

  • '소유권을 갖는다' -> 참조 횟수를 1 증가한다.
  • '소유권을 반환한다' -> 참조 횟수를 1 감소한다.
    Pen *aPen = [[Pen alloc] init];
    Pen *bPen = [aPen retain];
    Pen *cPen = [bPen copy];
    [aPen release];
    bPen.color = ...
    cPen.color = ...
    [cPen release];
    [bPen release];

객체 메서드와 소유권 규칙

  1. alloc, new, copy, mutableCopy 계열 메서드로 특정 객체를 생성하거나 복사하는 경우 새로운 객체 인스턴스를 만든다
    • 그리고 참조 횟수를 1로 설정하고 소유권을 갖는다.
  2. 다른 객체가 이미 만들어 놓은 객체 인스턴스를 참조하는 경우에는 retain 메서드를 사용해서 객체 소유권을 요청한다.
    • 그리고 참조 횟수를 1 증가한다.
  3. 1, 2에서 소유권을 얻는 객체를 더 이상 참조하지 않는 경우, release 또는 autorelease 메서드를 사용해서 객체 소유권을 반환한다.
    • 참조 횟수를 1 감소한다.
  4. 소유권을 갖고 있지 않은 객체는 반환하면 안된다.
    • 1, 2에서 설명한 메서드로 소유권을 요청한 적이 없거나, 이미 소유권을 반환한 경우에는 release/autorelease 메세지를 보내면 안됨

자동 반환 목록(autorelease list)

앞에서 객체 소유권을 반환할 때, release나 autorelease 메서드를 사용한다고 했다.
특정 객체에게 release 메세지를 보내면 참조 횟수가 1 감소하고, 0이 되면 그 즉시 dealloc 메서드를 호출하고 메모리를 반환한다

어떤 객체는 생성하고 소유권이 없는 상태에서 다른 객체가 사용할 때까지, 일정 시간동안 메모리를 반환하지 않고 남아있어야 하는 경우가 있다.
이런 경우를 대비해 '자동 반환 목록 동작'에 대해 알아보자.
자동 반환 목록은 일정 시간 뒤에, 반환할 객체 목록을 만들어서 관리해준다.

Objectice-C 객체 인스턴스는 힙 메모리에 만들어지지만, 함수 범위나 문법적으로 특정 범위가 정해진 변수들은 C 언어처럼 자동 변수 스택에 생겼다가 사라진다.

temp 객체는 alloc, init 메서드로 만들어졌고, 소유권 규칙에 따라 소유권을 갖는다.
당연히 temp 객체 메모리를 해제할 때, release 메세지를 보내야 한다.
하지만 release 메세지를 보내서 소유권을 즉시 반환하는 대신, autorelease 메세지를 보내면, "자동 반환 목록" 에 객체를 등록할 수 있다.
그리고 주석 부분에 해당하는 다른 작업을 처리하고, autoreleasePool 객체가 drain 메서드를 처리하면서 "자동 반환 대상"인 객체를 차례대로 release 시킨다.

간편한 메서드와 자동 반환 대상

코코아 프레임워크 객체 중에는 객체를 생성하면서, "자동 반환 목록"에 추가하는 객체가 존재한다.
객체 팩토리 메서드 중에서 객체를 생성하기에 편리하도록 준비된 특별한 메서드는 객체를 '생성'하고 '초기화'한 다음 "자동 반환 목록"에 등록까지 해준다.
간편한 메서드(convenience methods)의 클래스 이름 형태는 코코아 클래스 이름에서 NS 접두어가 없고 소문자로 시작한다.
ex) NSString 클래스는 +string~ 형태로 시작하는 +stringWithFormat:, +stringWithString: 같은 메서드가 바로 간편한 메서드다.
NSNumber에서는 +numberWith~ , NSArray에서는 +array~ 메서드가 간편한 메서드이다.

NSString* aString = [[NSString alloc] initWithFormat: @"%08d", myNumber];

위 코드는 다음과 같이 풀어쓸 수 있다.
동작은 동일하나 소유권이 aString 객체를 생성한 객체에 있는 것이 아니라, "자동 반환 목록"으로 넘어간다.

NSString* aString = [[[NSString alloc] initWithFormat: @"%08d", myNumber] autorelease];

자동 반환 대상을 관리하는 AutoreleasePool 클래스에 대한 객체는 스레드마다 하나씩 생성해서 소유권을 갖고 있다가, 스레드가 끝날 때, 같이 소멸되도록 권장한다.
main() 함수 내에는 AuroreleasePool 객체가 이미 들어있다. -> 메인 스레드에는 별도로 생성할 필요 X

자동 반환 목록 사용 시 주의 사항

AutoreleasePool 객체는 대부분 NSRunLoop 클래스와 함께 동작하는데, 코드 흐름상 반복해서 객체를 생성하는 경우에는 자동 반환 목록에 있는 객체를 반환하는 시점이 되기도 전에 너무 많이 쌓이는 현상 이 생길 수 있다.

객체 그래프

OOP로 프로그래밍을 하다보면 여러 객체 인스턴스가 만들어지고, 객체들끼리 참조 관계가 생긴다.
이런 객체들끼리 관계를 네트워크 그래프로 표현하면 -> "객체 그래프"이다.

순환 참조 문제

객체 그래프를 확인해야 하는 이유는 객체끼리 순환 참조가 있는지 여부를 확인하기 위해서다.

요약

특정 객체에 대한 참조 관계를 관리하기 위해, 참조 횟수를 가감하여 계산하는 방식은 객체 포인터 변수를 참조할 때, 위험 요소를 줄이는 방법이다.
메모리 관리를 위해 참조 계산을 실수없이 처리하려면, 객체 소유권 개념을 정확히 파악해야 한다.
즉시 반환할 객체가 아니라면, auroreleasePool을 사용하는게 좋다.


객체 초기화

객체 인스턴스를 메모리에 할당한 직후, 객체 내부 변수를 초기 값으로 지정하기 위해 init 메서드를 사용한다.
NSObject에 구현된 -init 기본 초기화 메서드는 상속받아 만들어진 모든 객체에서 기본 초기화 메서드로 사용한다.

특정 객체가 포함하는 하위 객체는 상위 객체 인스턴스가 만들어지면서 동시에 만들어진다.
이런 소유권을 갖는 하위 객체는 대부분 초기화 메서드에서 만들어진다.
객체 내부 자원에 해당하는 인스턴스 변수를 준비한다는 측면에서 초기화 메서드는 중요하다.
If 상속시) Objective-C 에서 -init 초기화 메서드를 명시적으로 구현하지 않으면, 상속받은 상위 객체에 구현된 초기화 메서드를 호출한다.

여러 초기화 메서드

NSObject 클래스에 선언되어 있는 기본 초기화 메서드는 -(id)init 형태이다.
상속받아 만드는 클래스는 init 메서드를 다음과 같은 형태로 오버라이드해서 구현한다.

-(id)init{
    self = [super init];
    if(self != nil){
        //인스턴스 변수 초기화
    }
    return self;
}

기본 동작이 상속받은 부모 객체의 인스턴스 내부 변수와 리소스를 초기화하고, 자신의 내부 리소스를 초기화하는 순서를 권장하기 때문이다.
초기화 메서드가 기본값이 아닌 외부 데이터에 의존해서 초기값을 설저앻야 한다면, 추가적으로 초기화 메서드를 추가해야 한다.
메서드 명은 -init으로 시작하면 된다.

인스턴스타입(instanceType)
id 타입은 Objective-C에서 모든 객체를 표현할 수 있는 다이내믹 타입이다 -> AnyObject와 비슷
id 타입으로 리턴받은 객체는 타입 정보가 부족해서 특정 메세지를 보낼 수 있는지 없는지 컴파일러가 판단하기 어려움
실제로 메세지를 보내면 리턴받은 객체가 해당 메세지를 받아서 처리할 수 없어 앱이 죽기도 한다
최신 런타임 구조에서는 이런 생성/초기화 관련 메서드 리턴 타입을 id -> instanceType으로 변경했다.

Example

@interface AObject: NSObject
+ (instancetype)factoryMethodA;
+ (id)factoryMethodB;
@end

@implementation AObject
+ (instancetype)factoryMethodA {return [[[self class] alloc] init];}
+ (id)factoryMethodB {return [[[self class] alloc] init];}
@end

void aa(){
    NSUInteger x, y;

    //AObject instancetype
    x = [[AObject factoryMethodA] count]; // Warning -> AObject may not respond to 'count'
    // id
    y = [[AObject factoryMethodB] count]; // Not Warning
}

초기화 메서드 구현하기

코코아 프레임워크에서 -init 계열 메서드를 구현하기 위해 권장하는 가이드이다.

  1. 상속받은 Super Class의 초기화 메서드를 먼저 호출한다.
  2. Super Class 초기화 메서드 리턴 값을 확인해서 nil이면, 내부 리소스 초기화를 하지않고 그대로 nil을 리턴한다.
  3. 내부 리소스를 초기화하면서 객체느 copy나 retain 메서드를 호출해서 소유권을 갖는다.
  4. 인스턴스 변수들을 적정한 값으로 초기화하고 나면 self를 리턴한다.
  5. 인스턴스 변수들 초기화 과정에서 오류가 발생한 경우에는 self를 해제하고 nil 리턴
  6. self가 아닌 객체 인스턴스를 리턴하는 경우라 하더라고 self를 해제해야 한다.

Failable init in Swift

class A{
    var a: Int
    //Swift의 실패가능한 init은 실패 시에는 nil을 반환하지만, 성공시에는 값을 반환하지 않는다.
    //사용하는 이유 : 실수로 만들어지는 객체들로 인한 메모리 낭비를 방지, 예상하지 못한 상황 방지
    init?(value: Int){
        if value < 0{
            return nil
        }
        self.a = value
    }
}

객체 초기화 관련한 문제

한번 -init 계열 메서드로 객체 인스턴스를 초기화한 후, 또다시 -init 계열 메서드를 호출하면 안된다. -> exception 발생

초기화 하는 객체가 +alloc 메서드를 통해 정삭적으로 메모리에 생선한 객체가 아닌 경우도 조심해야 한다.
해당 객체 인스턴스가 하나만 존재해야 하는 싱글톤 인스턴스인 경우도 있고, 내부 인스턴스 변수 객체 중 싱글톤 형태로 존재하는 경우도 있다.

싱글톤 프로퍼티를 갖고 있거나, 클래스 자체가 싱글톤으로 사용된다면 주의

마지막으로 초기화 메서드가 실패한 경우를 대비해야만 한다.
-initWithString: 메서드에 인자 값 문자열이 nil인 경우가 있을 수 있고,
-initWithArray: 메서드에 인자값이 NSArray 대신 NSDictionary일 수도 있다.
이렇게 인자 값이 nil이거나 원하지 않는 객체가 들어오면, 새로 할당한 객체는 반환하고 nil을 리턴해야 한다.

Objectibe-C 기반으로 만든 맥용 Framework : Cocoa Framework

이후에 iOS용으로 나온 Framework는 터치 이벤트가 추가되었다고 하여 Cocoa Touch Framework라고 한다.

 

Objective-C를 포함한, 모든 OOP에서는 프로그램을 구성하는 단위를 Object 단위로 구분한다.

그렇다면 '객체'라는 단어는 "어떤 의미인지", "왜 주체는 없고 객체만 있는지" 에 대한 궁금증에서 시작하자.

1장에서는 Class와 인스턴스의 개념을 설명하고, 객체의 정체성, 등가성, 예외성을 확인한다.

그리고 객체 사이의 관계를 살펴보고, 마지막으로 Objective-C 런타임 구조에 대해 설명한다.

 

클래스와 객체 인스턴스

OOP는 절차 중심 프로그래밍과 대비되며, 객체 안에 속성 메서드 형태로 변수와 함수를 구현하고 프로그램을 구성하는 객체들끼리 메세지를 주고 받아 협력하는 형태로 동작한다.

 

첫번째 OOP 언어인 '시뮬라' 이후로, 대부분의 OOP에서는 객체, 클래스 개념을 사용한다.

문제 해결을 위해 추상화한 코드 = 클래스

실제로 프로그램을 실행하는 시점에, 메모리에 구체화된 실체 = 인스턴스

 

 

객체에 대한 철학

객체라는 단어는 주체(나)를 중심으로 하는 1인칭 시점/서양 철학에서 비롯됐다.

객체를 다루는 방식 : "자연에 실존하는 객체를 그에 대응되는 형체가 없는 개념과 언어로 추상화시켜 생각하는 방식"

 

객체 중심 프로그래밍(OOP)

랑그(langue) : 같은 언어를 사용하는 사람들끼리 생각하는 방식에 대한 원칙(단어의 의미, 문법)

파롤(parole) : 실제 대화나 상황에 따라서 표현이나 발음이 달라지는 것

 

랑그의 예 : 스택(Stack) - 위로 쌓고, 위에서부터 차례대로 꺼내는 원칙/방식

파롤의 예 : 이런 '스택' 개념을 각각 다른 언어로 구현

 

이전에 '프로그램 코드가 동작하는 순서'를 중요하게 생각했던 절차 중심 프로그래밍 패러다임과

'실생활에서 사고하는 방식과 비슷한 객체 개념을 차용하여, 객체의 역할과 책임 그리고 관계를 생각하는' 객체 중심 프로그래밍 패러다임을 생각하는 방식에서부터 차이가 있다.

 

프로그래밍 언어로 코드를 작성하는 방법과 객체 중심 패러다임을 알더라도, 클래스 단위로 코드를 표현하기에는 여러 어려움이 있다.

-> 이러한 어려움을 해결하고자 하는 원칙이 바로 "SOLID"

 

SOLID

  • 변경에 유연
  • 이해하기 쉬움
    • SOLID에 대한 지식이 있어야 -> 어렵고 사람마다 알고있는 개념이 약간씩 다르다
  • 명확/깔끔한 책임 구조, 높은 응집력, 낮은 의존성, 유지보수성 등의 장점

 

SRP : https://wlgusdn700.tistory.com/95?category=930441

  • 하나의 모듈은 하나의 일(책임)을 맡아야 한다.

OCP : https://wlgusdn700.tistory.com/96?category=930441 

  • 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다

LSP : https://wlgusdn700.tistory.com/97?category=930441

  • Sub Class, Struct는 Super Class 혹은 Protocol 로 교체할 수 있어야 한다.

ISP : https://wlgusdn700.tistory.com/98?category=930441 

  • 인터페이스(프로토콜)을 통해 기능/책임을 더 세분화, 사용하지 않는 책임에 의존관계 X

DIP : https://wlgusdn700.tistory.com/99?category=930441 

  • 고차원 모듈은 저차원 모듈에 의존하면 안된다.(추상적인 것은 구체적인 것에 의존하면 안된다)
  • 자주 변경되는 클래스에 의존하지 말자
  • OCP와 비슷한데, 인터페이스(프로토콜)을 통함

Objective-C 객체

+ 로 시작하는 메서드 : 클래스 메서드

- 로 시작하는 메서드 : 인스턴스 메서드

 

헤더 파일 내부 인터페이스(@interface) 영역에 선언한 메서드만 객체 외부에서 접근 가능

구현 영역(@implementation)에 선언한 메서드는 내부에서만 접근할 수 있고, 외부로는 감춰짐(캡슐화)

 

클래스 명세와 객체 인스턴스

 

출처 : Cocoa Internals

  1. aPen이라는 포인터는 aPen 고유의 객체 인스턴스를 가르킨다.
  2. aPen 고유의 객체 인스턴스는 Pen 클래스의 코드를 공유한다.

 

Objective-C 2.0 이후 변화

Class는 내부적으로 objc_Class 라는 구조체의 포인터이다

 

OS X 10.5 와 iOS 2.0 이후에 적용된 Objective-C 2.0부터는 최신 런타임 구조를 따른다

레거시 런타임 : 클래스의 구조가 바뀌면, 무조건 새로 컴파일

최신 런타임 : 인스턴스 변수와 메서드 변경해도, 재컴파일 X

최신 런타임 적용 이후, isa 포인터 사라짐

따라서 객체의 클래스를 알아내기 위해서, NSObject에 선언된 -(Class)class 메세지를 보낸다

 

Swift Native Object

스위프트에서는 Objective-C와 호환되는 객체를 쓸수도 있고, 네이티브 객체를 쓸수도 있다.

스위프트 언어는 SIL이라는 스위프트 중간 언어를 거쳐가면서 기계코드로 변형된다.

 

 

Swift ~ Objective-C 호환 객체

NSObject를 최상위 클래스로 지정해야 한다.

NSObject를 상속받은 Class로 생성한 객체는 내부에서 Objective-C로 자동으로 변환되고, Objective-C 런타임에서 동작한다.

 

 

요약

객체를 표현하기 위해서는 객체 중심으로 생각하는 과정이 중요

객체는 늘 객관적이어야 함

객체를 표현한 코드는 '나 혼자만의 것이 아님'

 

 


 

객체 정체성과 등가성

객체들은 필요한 시점에 객체 인스턴스가 만들어진다

어떤 클래는 인스턴스 없이 자체로 존재한다

 

Objective-C 객체와 메모리 구조

Text Segment : 프로그램 코드

Data Segment : 고정 값이 정해진 전역변수

BSS Segment : 초기값을 0으로 할당하는 전역변수

 

Objective-C에서 객체인스턴스는 항상 HEAP 에 만들어지며, 해당 힙 메모리 주소를 Stack 영역에 할당한 포인터로 참조해서 접근한다.

 

"포인터 변수에 담긴 메모리 주소"와 "해당 주소의 객체 인스턴스가 실제로 유효한지"를 포인터 주소만으로는 판단할 수 없다.

객체 포인터 변수는 이미 해제된 객체 주소를 저장하고 있는 Dangling Pointer 일 수도 있기 때문

Objective-C에서의 객체 생성 방법

  1. +new 하나만 호출하는 방법
    1. new = alloc + init
  2. +alloc 메서드와 -init 메서드를 두 단계에 걸쳐서 호출하는 방법

객체 정체성

객체 인스턴스가 HEAP 영역에 만들어지면서, 객체는 각각 고유한 메모리 영역을 차지한다.

Pen *aPen = [Pen new];

Pen *bPen = [Pen new];

생성한 2개의 펜은 서로 다른 객체이며, 고유한 정체성(메모리 구조)를 갖는다

동일한 객체 정체성

만약, bPen = aPen; 을 했다고 상상해보자.

메모리 그림은 변경될 것이다

또한, 같은 메모리 주소를 가르키기 때문에, aPen == bPen 도 true다

이때, bPen의 인스턴스를 차지하는 메모리 주소는 어떻게 될까 -> 2장 '메모리 관리'에서 살펴본다

 

객체 등가성

객체 인스턴스는 각각 고유한 메모리 영역을 차지하기 때문에, 동일한 속성에 대해서도 각자의 메모리 영역에 데이터를 보관한다.

 

만약 cPen이라는 인스턴스를 생성하고 aPen과 같은 color, position을 설정했다고 가정하자.

-> aPen과 cPen은 내부 속성이 모두 같기 때문에, 객체 등가성(Equality)를 갖는다

-> 등가성을 비교할 때는 aPen == cPen 비교문은 성립하지 않는다.

-> 대신 해당 Class에 -isEqual: 메서드를 오버라이드해서 모든 속성이 동일한지를 비교한다.

-> 특히 NSString 계열 클래스는 -isEqualToString: 메서드를 사용해서 동일한 문자열인지 비교하기를 권장한다.

 

객체 예외성

모든 코코아 객체 인스턴스가 Heap 영역에 생성되는 것은 아니다

특이하게 Heap 이 아니라 Text와 Data 영역에 생기는 경우가 있다.

 

NSString* aPenName = @"BluePen";

NSString* bPenName = @"BluePen";

 

NSString 클래스는 NSObject를 상속받는 코코아 클래스 중 유일하게 전역 변수로 선언할 수 있다

Heap 영역이 아닌, Text 영역에 "BluePen" 값을 저장하고, aPenName 변수는 전역 변수 형태로 Data 영역에 만들어진다.

 

더 특이한 점은 bPenName 객체처럼 aPenName 변수와 동일한 문자열 "BluePen"을 반복해서 사용하는 경우,

같은 Text 영역을 사용하고 bPenName 객체 인스턴스를 전역 변수 형태로 할당한다는 것이다.

다시 말해 aPenName 과 bPenName 은 동일한 정체성(같은 메모리 주소)을 갖게 된다. 

-> 이런 방식을 문자열 인터닝(string interning)이라 한다

 

Swift 문자열

스위프트에서 String 객체는 네이티브 문자열 객체를 만들 수 있고, NSString을 연결해서 쓸 수도 있다.

네이티브 문자열 객체는 내부적으로 인터닝을 시키는 NSString 객체와는 다르게, Text 영역에 있는 문자열을 OpaquePointer 형태 포인터 그대로 연결하는 방식을 사용한다.

-> 따라서, 네이티브 문자열이 NSString보다 조금 더 가볍다고 할 수 있다.

 

-hash 메서드

앞에서 말한 것처럼, -isEqual: 메서드를 재구현한 경우라면, 반드시 -hash 메서드도 다시 구현해야 한다.

왜냐하면 NSDictionary 같은 Collection 객체는 -isEqual: 메서드 대신 -hash를 사용하기 때문이다

최상위 객체인 NSObject에 기본적으로 구현된 -hash 메서드는 객체 정체성 기준이 되는 self 메모리 포인터 값을 NSUInteger 타입 숫자로 바꿔줄 뿐이다.

따라서, 객체 정체성이 다르지만 등가성이 성립하는 경우를 위해, -hash 메서드 결과 값도 고유한 값이어야만 한다.

 

Swift Hashable Protocol

스위프트에서는 모든 타입에 Hashable 프로토콜을 구현해야 한다.

Hashable Protocol은 앞서 설명한 -hash와 -isEqual 메서드에 해당하는 hashValue() 함수와 ==() 함수가 필수적으로 구현해야 하는 함수로 지정되어 있다.

 

요약

모든 객체는 메모리에 자리를 잡으면 고유한 정체성을 갖게 된다.

고유한 객체 중, 모든 속성이 같게 되는 등가성 관계도 생길 수 있다.

등가성, 정체성을 비교하기 위해서는 위에서 말한 메서드들을 재정의 해야 한다.

 


객체 사이 관계

메타 클래스

앞서 객체 인스턴스마다 isa Pointer가 있어서 해당 객체의 Class를 알 수 있다.

Pen Class는 인스턴스 메서드 목록과 코드를 갖고 있으며, Pen Class의 Meta Class는 클래스 메서드 목록과 코드를 갖고 있다.

 

상속

OOP 언어가 갖는 특징은 

  1. 추상화한 클래스 명세
  2. 객체 인스턴스의 활용
  3. 캡슐화
  4. 상속
  5. 다형성

으로 요약할 수 있다.

이 중, 객체 사이 관계와 가장 밀접한 특징을 상속이다.

상속받은 모든 Class들은 super를 따라가다 보면 상위 Class가 나온다.

Root Class의 super는 nil 이다

보다시피 Meta Class도 super 관계가 존재한다.

 

 

is-a, has-a 관계

aPen is-a Pen

Pen is-a NSObject

표현들은 true이다.

 

is-a : 인스턴스와 클래스의 관계(클래스와 Meta 클래스의 관계 포함) -> isa 사용

has-a : Sub/Super 클래스의 관계 -> super 사용

 

has-a 관계는 강한 참조 결합성을 갖는 구성 관계(composition)

약한 참조 결합설을 갖는 집합 관계(aggregation)으로 구분한다

 

  • 구성 관계
    • 참조하는 객체(referrer)가 사라질 때, 하위 객체들도 같이 사라짐
    • 참조하는 객체와 하위 객체가 동일한 생명주기를 갖는다
  • 집합 관계
    • 참조하는 객체가 사라지더라도, 하위 객체는 사라지지 않는다

자세한 것은 3.1 (ARC) 장에서 알아본다.

 

요약

상속 관계는 객체 사이 관계를 바꾸기 위한 유지보수나 리팩터링이 어려운 밀결합 형태이다.

따라서, 객체를 상속해서 확장하는 방식보다는 카테고리로 객체를 확장하도록 권하고 있다

자세한 내용은 7.3.1 팩토리 추상화 패턴 장을 참조

 

 


Objective-C 런타임

앞서 설명한 클래스와 메타 클래스를 메모리에 로딩하는 것은 Objective-C 런타임이 담당한다.

런타임 : 1. 실행 중, 객체에게 보내는 메세지를 처리한 메서드를 찾거나, 2. 객체 메모리 관리, 동적 타입 변환 등을 수행하는 C 함수 라이브러리

 

기존 런타임과 최신 런타임

기존 32bit 방식에서 동작하는 레거시 런타임

62bit 방식에서 동작하는 최신 런타임 이 존재한다.

 

최신 런타임에서는 Objective-C 2.0에 추가한 프로퍼티, 빠른 탐색, ARC, 블록 기능 을 위한 개선이 추가됐다

 

메세지 디스패치

Objective-C는 객체의 메서드를 직접 호출하지 않고, 객체에 메세지를 보내는 방식으로 동작한다

 

객체에 메세지는 보내는 과정

  1. 클래스에 메서드를 선언
    1. 리턴 값, 메서드명, 인자값 타입, 변수명 을 순서대로 명시
    2. - (void) replacePen:(Pen *) pen1 withPen:(Pen *) pen2;
  2. 객체 인스턴스에서 메서드를 호출하고 싶다면, 인스턴스에게 메세지를 보낸다
    1. [aPenHolder replacePen:apen withPen:bPen];
  3. 컴파일러는 이 코드를 보고 메서드 이름을 replacePen:withPen: 이라고 판단한다.
  4. 컴파일을 하면서 objc_msgSend() 런타임 API를 사용하는 코드로 변경
    1. objc_msgSend(aPenHolder, @selector(replacePen:withPen:), aPen, bPen);
  5. 실행 중 런타임은 objc_msgSend()를 실행하면서 메세지로 어떤 메서드를 실행할 지, 메세지 디스패치 과정을 통해 찾는다

어떤 클래스의 메서드를 실행할 지, Selector를 선택한 후에는 해당 메서드의 메모리 주소를 내부 캐시에 저장

 

 

요약

Objective-C가 가지는 장접들은 모두 런타임에서 발현된다.

또한 런타임 API를 사용하면, 실행 중 클래스나 객체의 구조, 함수를 바꾸는 동작이 가능하다 -> 리플렉션(reflection)

 

Bundle

  • 실행 가능한 코드와 리소스를 갖고 있는 디렉토리
  • 가지고 있는 내용 - info.plist, assets, string 등
  • Framework Bundle은 Dynamic Library에서 사용하는 코드와 라소스를 포함
  • 모든 Application Bundle은 App에 대한 정보가 담긴 info.plist를 가짐

Package

Finder가 사용자에게 단일 파일로 보여주는 디렉토리

 

 

NSBundle

  • Bundle에 포함된 코드와 리소스에 대한 표현
  • NSBundle은 Objective-C 를 통해 만들어진 Foundation Class이며, Bundle 디렉토리 내에 포함된 리소스에 쉽게 접근할 수 있도록 제공하는 객체

Main Bundle

Bundle은 여러가지가 있다.

그 중 mainBundle은 App이 실행되는 코드가 있는 Bundle 디렉토리에 접근할 수 있게 도와주는 Bundle

 

 

Copy Bundle Resource

  • mainBundle에 들어가 있는 리소스 파일들은 빌드할 때, 추가됨
    • Xcode의 Build Phase에 존재 - "Copy Bundle Resources"
    • 해당 항목에서 App에 추가할 리소스를 결정
    • 해당 항목이 없는 리소스는 Project Navigator에 추가되어 있어도 실제로 Bundle을 통해 접근 불가

 

Framework

공유자원(Dynamic Library, nib, 이미지 등)을 단일 패키지로 담고 있는 디렉토리

Framework는 Library와 달리 리소스를 포함할 수 있고, Framework를 모듈 배포를 통해 Library보다 많은 활용 가능

Framework는 파일 시스템의 Bundle로 패키징되어, Core Foundation Bundle Service를 이용 가능

NSBundle class로 접근 가능

 

 

 

각각의 기능으로 분리되어 있는 프로젝트들은 공통의 리소스를 가질 수 있다.

Color, Image, Lottie의 JSON 등을 포함한다.

각각의 프로젝트들은 공통의 리소스를 사용하여 개발해야 하기 때문에, 리소스 Framework를 만들어 관리할 수 있다.

중복되는 이미지도 없고, 리소스 Framework를 import만 하면 된다.

 

Image, Color를 리소스 Framework으로 관리

먼저 리소스 Framework를 만든다. 그리고 이미지를 Images.assets에 추가한다.

 

해당 이미지들은 외부에서 코드로 불러와 사용하는 경우가 있기 때문에, 이미지를 외부에서 접근할 수 있는 코드를 만든다.

Resource Framework는 R 이라는 타입으로 접근하여 사용할 것이다.

 

먼저 R 타입을 만들자.

Framework로 만들었기 때문에, 이미지를 불러올 때 Resource Framework의 Bundle 위치를 알기 위해 내부에서 사용할 Bundle을 만들었다.

이제 이미지를 외부에서 접근할 수 있는 코드를 R.Image.[이미지이름] 형태를 따르도록 만든다.

 

 

이제 외부에서는 다음과 같이 이미지를 불러올 수 있다.

 

Color도 Image와 마찬가지로 만들 수 있다.

 

 

Storyboard, Xib를 리소스 Framework에서 관리

iOS 개발시, 항상 논쟁이 되는 주제가 있다. 바로 View를 Storyboard vs Xib로 작성하는 것이다.

 

Storyboard 파일을 갖기 위해서는 Dynamic Framework를 만들어야하는데, 각 기능마다 Framework로 만들게 되면 Framework 개수가 빠른 속도로 늘어날 뿐만 아니라, 기능을 더 작게 나눠 한 화면을 Framework로 만들면 어마어마할 것이다.

 

따라서 Storyboard, Xib를 리소스 Framework에서 관리하면 된다.

리소스 Framework에서 관리하면 화면 단위의 Framework를 Static으로 만들어도 Bundle의 위치가 리소스 Framework이기 때문에 문제가 없다.

그리고 Storyboard, Xib에서 이미지와 Color를 지정해도 Framework 자신의 내부에서 가져오기 때문에 문제가 없다.

 

그렇다면 다른 Framework에 있는 UIViewController Class와 ViewController View를 어떻게 연결할까?

 

위 사진처럼 Module을 선택하고 해당 UIViewController Class 선택하면 된다.

 

사용은

 

 

 

 

 

 

 

 

+ Recent posts