[Swift][Objective-C] 클래스 메서드 load()를 활용하여 반복하는 초기화 작업을 줄이기
일반적으로 개발자는 특정 객체나 값을 초기화하는 코드를 AppDelegate
클래스의 UIApplicationDelegate
의 application(_:didFinishLaunchingWithOptions:)
함수에서 작성합니다.
그 이유는 애플리케이션의 시작 지점이기 때문입니다. 물론, main 함수에서 제어도 가능하지만, 대개 application(_:didFinishLaunchingWithOptions:)
함수가 시작 지점으로 사용합니다.
그렇다보니, 초기화 작업이 많은 코드가 들어가면서 복잡도가 증가하는 문제가 있습니다. 또한, 앱이 하나인 경우는 괜찮지만, 각 기능별이나 상품별 데모 앱이 존재하는 경우에는 무수히 많은 AppDelegate
클래스가 추가될 수 있고, 각 클래스에서 application(_:didFinishLaunchingWithOptions:)
함수에서 초기화 작업을 하는 코드가 중복되며 계속 늘어날 것입니다.
하지만, 각 기능별, 상품별 데모앱들이 개발 및 내부 배포에만 사용된다면, 초기화 작업을 런타임에서 자동으로 호출할 수 있는 (비)공식적인 방법을 사용하여 application(_:didFinishLaunchingWithOptions:)
함수에서 작성하는 초기화 코드의 양을 줄일 수 있습니다. 이렇게 하면 각 데모앱마다 작성해야 하는 초기화 코드가 줄어들게 됩니다.
Swift 언어에서는 런타임 활용에 제한이 있지만, Objective-C 언어는 더 다양한 기능을 Runtime을 이용해 구현할 수 있습니다.
NSObject의 클래스 메서드 load
NSObject
클래스의 클래스 메서드인 load()
는 해당 클래스가 메모리에 로드될 때 호출됩니다. 이러한 동작 방식으로, 클래스 메서드 load()
가 AppDelegate
의 application(_:didFinishLaunchingWithOptions:)
함수보다 먼저 호출된다는 의미입니다.
그러면 클래스 메서드 load
를 구현해봅시다.
/// FileName : AutoLoadClass.m
#import <Foundation/Foundation.h>
@interface AutoLoadClass : NSObject
@end
@implementation AutoLoadClass : NSObject
+ (void)load {
NSLog(@"Hello AutoLoadClass Loaded");
}
@end
해당 코드의 목적은 문서에서 설명한 것과 같이 클래스 메서드 load
가 동작하는지 확인하는 것입니다. 따라서 별도의 헤더 파일을 추가하지 않았습니다.
코드를 실행하면 출력 결과는 다음과 같습니다.
클래스 메서드 load
에 중단점을 설정하였을 때, 보여지는 Call Stack입니다.
AppDelegate
의 application(_:didFinishLaunchingWithOptions:)
함수보다 먼저 호출된 것을 확인할 수 있습니다.
그리고 Call Stack에서 load_images
, dyld4
관련 코드들이 보입니다.
클레스 메서드 load
가 호출된 정확한 부분을 찾도록 Call Stack에서 load_images를 눌러봅니다.
어셈블리어 코드를 확인할 수 있습니다.
가장 위의 어셈블리어 코드를 보면 “LOAD: +[%s load]\n” 이라는 문자열을 확인할 수 있습니다.
load_images
는 objc 관련 코드이므로, 해당 코드는 애플에서 공개한 apple-oss-distributions/objc4 저장소에서 확인할 수 있습니다.
objc4 저장소에서 “LOAD: +[%s load]\n” 문자열을 검색하여 다음과 같은 코드를 찾아낼 수 있었습니다. 코드
// FileName : runtime/objc-loadmethod.mm
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
여기에서 클래스 메서드인 load
가 호출된다는 것을 확인할 수 있었습니다.
그리고 call_class_loads
함수는 call_load_methods
함수에서 호출하고 있습니다. 코드
// FileName : runtime/objc-loadmethod.mm
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
lockdebug::assert_locked(&loadMethodLock);
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
call_load_methods
함수는 objc-runtime-new.mm
의 load_images
함수에서 호출하는 것을 확인할 수 있습니다. 코드
void
load_images(const char *path __unused, const struct mach_header *mh)
{
if (!didInitialAttachCategories && didCallDyldNotifyRegister) {
didInitialAttachCategories = true;
loadAllCategories();
}
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
클래스 메서드 load
가 호출될 때 Call Stack에 보여진 load_images 함수가 어떤 것인지, 어떻게 호출되는지 알게 되었습니다.
그리고 dyld4 관련 코드는 apple-oss-distributions/dyld 저장소에서 찾을 수 있습니다.
클래스 메서드 load 확장
클래스 메서드 load
가 언제 어떻게 호출되는지 알게 되었습니다. 클래스 메서드 load
에서는 간단한 작업을 수행하는 것이 좋습니다. 그렇지 않으면 AppDelegate
의 application(_:didFinishLaunchingWithOptions:)
함수가 늦게 호출됩니다.
만약 개발 및 내부에서만 사용하는 앱이라면, 클래스 메서드 load
에서 초기화 작업을 수행한다면, 기능 개발시 만드는 데모앱
의 AppDelegate
클래스의 application(_:didFinishLaunchingWithOptions:)
함수에서 초기화 작업을 수행하지 않아도 됩니다. 이러면 반복해서 작성하던 보일러 플레이트 코드를 더 이상 작성하지 않아도 됩니다.
위와 같은 상황에서는 4개의 AppDelegate
클래스의 application(_:didFinishLaunchingWithOptions:)
함수에서 초기화 작업을 수행해야합니다.
각 프레임워크에서 AutoLoadClass
클래스를 추가하고, 클래스 메서드 load
에서 Swift 코드의 초기화 작업을 수행합니다. 이렇게 하면 각 데모앱에서 보일러 플레이트 코드를 반복해서 작성할 필요 없이 초기화 작업을 런타임에서 자동으로 수행할 수 있습니다.
/// ModuleName : AFramework
/// FileName : Hello.swift
import Foundation
@objc
public class Hello: NSObject {
@objc
public static func world() {
print("Hello world")
}
}
/// ModuleName : AFramework
/// FileName : AutoLoadClass.m
#import <Foundation/Foundation.h>
#import <AFramework/AFramework-Swift.h>
@interface AutoLoadClass : NSObject
@end
@implementation AutoLoadClass : NSObject
+ (void)load {
[Hello world];
NSLog(@"Hello AutoLoadClass Loaded");
}
위 코드를 실행하면 Objective-C
에서 작성된 코드에서 Swift
클래스의 메서드를 호출하는 것을 확인할 수 있었습니다.
정리
NSObject
의 클래스 메서드load
를 적절히 활용하면 반복되는 코드를 줄이고, 초기화 작업 등을 효율적으로 처리할 수 있습니다.