Objective-C의 메서드 실행

Objective-C의 메서드 실행

Objective-C는 ‘런타임’이라고 불리는 기능에 의존하는 언어이며 동적이라는 의미는 다음과 같은 의미를 가진다.

  • Dynamic typing: 실행 도중에 오브젝트의 타입이 결정됨
  • Dynamic binding: 코드 상의 구문이 어떤 메서드를 동작시킬 지 실행 도중에 결정됨
  • Dynamic loading: 어떤 모듈이 실행 도중 메모리에 올릴지 실행 도중에 결정됨

다른 언어들도 이러한 기능을 지원하지만 Objective-C는 기존의 언어와 다른 형식으로 그것을 지원한다. C++과 Objective-C에서 나오는 Dynamic binding 간의 차이에서 그것을 알 수 있다.

  • 정적 바인딩(Static binding): 실행하는 메서드의 위치를 컴파일 타임에 미리 결정되는 것

  • 동적 바인딩(Dynamic binding): 실행하는 메서드의 위치를 실행 도중에 결정하는 것. 객체지향 언어가 구현하는 Polymorphism을 이루는데 필수적인 요소이다.

C++에서의 동적 바인딩

  • 가상 메소드 테이블(virtual method table) 또는 vtable을 두어 함수들에 대한 포인터들을 저장
  • 컴파일러는 각 클래스에 분리된 vtable을 생성하며 객체가 생성되면 이 vtable의 포인터(vpointer)를 객체의 숨겨진 멤버로서 추가함
  • 저장된 포인터들은 실행 기간 도중에 정확한 함수를 가리키게 되어 가상 함수를 호출할 경우 호출 순서가 vpointer -> vtable -> 클래스 선택 -> func() 가 됨
  • 동적 바인딩을 하고 싶은 메서드의 앞에 virtual 키워드를 명시

Objective-C에서의 동적 바인딩

Objective-C에서의 메서드 호출 순서를 보면 다음과 같다.

  • 메서드 실행 시에 대괄호를 사용하는데 이는 결국 메시지를 보내는 것이다. 이 메시지 안에는실행하고자 하는 메서드를 지정하는 셀렉터 가 있으며 그 뒤로 인자들이 전달된다.

    • Objective-C는 C++과 다르게 컴파일러는 이러한 메시지 구문에 대하여 정적 바인딩을 하지 않고 동적 바인딩이 이루어진다.
    • Dynamic typing, binding을 지원하는 코드들을 컴파일러가 덧붙이는데 이들이 하는 역할을 런타임 이라고 함
  • 오브젝트가 메시지를 받으면 런타임은 그 오브젝트가 어떤 클래스의 오브젝트인지 살펴봄

    • isa 포인터를 사용하여 어떤 클래스인지 파악
  • 어떤 클래스인지 파악했다면 해당 클래스가 구현하는 메서드들의 정보가 담긴 Dispatch table 이라는 가상의 테이블을 통해 해당 메서드에 대한 정보를 얻음

    • 해당 클래스에 구현되어 있지 않고 상위 클래스에 구현되어 있다면 super_class 포인터를 통해 상위 클래스 오브젝트의 가상 테이블을 확인한다.
  • Objective-C의 Method 타입이 전달받은 셀렉터를 통해 실제로 실행될 메서드와 매칭시켜주고 메서드를 실행

    • C++과 달리 Objective-C에서는 셀렉터와 메서드가 나뉘어져 있어 실행 도중에 전달 받은 셀렉터에 대해 어떤 메서드가 대응되어야 하는지 임의의 시점에서 변경이 가능함. 이점이 가장 큰 차이점!

Objective-C에서 런타임은 그 오브젝트가 어떤 클래스인지 어떻게 알 수 있을까?

isa 포인터

Objective-C의 모든 오브젝트는 isa라는 멤버 변수를 가지고 있다. isa 포인터는 클래스 오브젝트이자 해당 클래스를 가리키는 포인터이다. 따라서 인스턴스가 메시지를 날리면 런타임은 해당 인스턴스가 가지고 있는, 정확히는 타입 자체가 가지고 있는 isa 포인터를 보고 어떤 클래스인지 알 수 있게 된다.

어떤 클래스인지 알게 되면 Dispatch table을 통해 해당 메서드에 대한 정보를 얻는데, 만약 상위 클래스에만 정의되어 있어 하위 클래스가 정보를 가지고 있지 않다면 어떻게 메서드를 실행할까?

super_class 포인터

isa 포인터와 마찬가지로 클래스 오브젝트이지만 해당 클래스가 아닌 상속 받은 클래스 오브젝트를 가리키는 포인터이다. 모든 오브젝트가 멤버 변수로 가지고 있기 때문에 오버라이딩하지 않은 상위 클래스의 메서드를 실행할 경우 상위 클래스의 오브젝트 정보를 얻어 계속해서 메서드를 찾을 수 있다.

런타임은 셀렉터에 대응되는 메서드를 찾을 때가지 계속해서 상위 클래스 정보를 얻어 검색을 계속하며 이는 아무것도 상속받지 않은 ‘루트 클래스’에 도달할 때까지 계속된다.

  • Cocoa 프로그래밍 환경에서 루트 클래스는 대부분 NSObject이다.

runtime.h

이러한 정보들은 objc/runtime.h 에 정의되어 있으며 다음과 같이 확인할 수 있다. 먼저 Objective-C에서 가장 기본 단위인 object는 다음과 같이 정의되고 있다.

1
2
3
4
5
6
7
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

object는 Class 타입의 isa 포인터를 멤버 변수로 가지는 struct로 정의되고 있으며 이 덕분에 런타임이 어떤 객체가 어떤 클래스인지에 대한 정보를 알 수 있다.

다음으로 Class 타입을 보면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

마찬가지로 어떤 클래스인지 알 수 있는 isa 포인터를 가지고 있다. 추가적으로 상위 클래스의 대한 정보를 나타내는 super_class 포인터를 가지는 것을 알 수 있다. 또한 objc_method_list 를 통해 구현하는 메서드에 대한 정보를 모아놓고 있는 것을 짐작할 수 있다.

Objective-C에서는 위에서 말했듯이 메서드를 실행하려면 객체에 메시지를 보내는 형태로 이루어지는데, 이 때 호출부에서 실행하고자 하는 메서드를 지정하는 셀렉터와 실제 구현되는 메서드는 차이가 있다. 이 둘 사이를 연결시켜주는 것이 Method 구조체이다.

1
2
3
4
5
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

SEL은 셀렉터 타입으로 보통 문자열의 포인터이고, IMP는 구현 타입으로 실제로 구현, 실행될 메서드의 포인터를 담고 있다. 객체가 메시지를 받으면 어떤 셀렉터가 들어있는지 알고, 이에 해당하는 구현 함수를 실행되게 한다.

Swift에서는…?

Swift 런타임은 Objective-C처럼 공개되어 있지 않아 어떻게 구현되어 있는지 정확히 알 수는 없다. 하지만 개발자들의 연구 결과는 다음과 같다고 한다.

  • Swift에서는 C++과 같은 vtable을 통해 메서드의 구현을 저장한다.
  • 무조건적인 동적 바인딩이 아니기 때문에 Objective-C의 동작 속도보다 빠르다.
  • Swift에서 Dynamic dispatch를 사용하려면 dynamic이나 @objc 키워드를 사용해 Objective-C의 런타임이 사용할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
override func viewDidLoad() {
super.viewDidLoad()
swizzleViewDidAppear()
}

private func swizzleViewDidAppear() {
// 메서드 명으로 클래스 내의 vtable에서 selector 이름을 가진 Method를 얻는다.
guard let original = class_getInstanceMethod(ViewController.self, NSSelectorFromString("viewDidAppear:")),
let swizzled = class_getInstanceMethod(ViewController.self, NSSelectorFromString("newViewDidAppear:")) else {
return
}
// 두 method의 구현을 바꾼다.
method_exchangeImplementations(original, swizzled)
}

@objc func newViewDidAppear(_ animated: Bool) {
print("swizzled!")
}

Reference