흑마술은 용법용량을 지켜 올바르게 사용해 주세요

iOS Advent Calendar 2016 24일째의 기사입니다.

소개



이제 Objective-C로 미안해.

Objective-C의 흑마술 「objc/runtime.h」



runtimeAPI는 메소드의 거동을 바꾸거나 매우 강력한 흑마술입니다.
흑마술에 대해서는 하기
[Objective-C] 런타임 API 메모

흑마술의 유효 활용



사용자 정보를 관리하는 User라는 모델 클래스가 있다고 가정합니다.

User.h
NS_ASSUME_NONNULL_BEGIN

@interface User : NSObject

@property (nonatomic, strong) NSString *userId;
@property (nonatomic, strong) NSString *userName;
@property (nonatomic, nullable, strong) NSString *address;
@property (nonatomic) NSInteger age;

- (instancetype)initWithUserData:(NSDictionary<NSString *, NSString *> *)userData;

@end

NS_ASSUME_NONNULL_END

User.m
#import "User.h"

@implementation User

- (instancetype)initWithUserData:(NSDictionary<NSString *, NSString *> *)userData {
    self = [super init];
    if (self) {
        _userId = userData[@"userId"];
        _userName = userData[@"userName"];
        _address = userData[@"address"];
        _age = [userData[@"age"] integerValue];
    }
    return self;
}

@end

이것을 그대로 NSLog로 표시하면 객체의 포인터가 표시됩니다.
- (void)createUser {
    NSDictionary *tarouData = @{@"userId": @"001"
                                , @"userName": @"田中太郎"
                                , @"address": @"東京都"
                                , @"age": @25};

    User *tarou = [[User alloc] initWithUserData:tarouData];
    NSLog(@"%@", tarou);

    NSDictionary *hanakoData = @{@"userId": @"002"
                                 , @"userName": @"山田花子"
                                 , @"age": @23};

    User *hanako = [[User alloc] initWithUserData:hanakoData];
    NSLog(@"%@", hanako);
}



포인터가 아니고, userName 이나 address 의 내용이 보고 싶을 때는 user.userName 라고 변수를 지정해 NSLog 를 내야 합니다.
조금 귀찮습니다.

그래서 objc/runtime.h


<objc/runtime.h> 를 가져오고 User 클래스의 description 메소드를 아래와 같이 기술합니다.

User.m
#import <objc/runtime.h>

- (NSString *)description {
    NSMutableString *description = [NSMutableString string];
    [description appendString:@"{\n"];

    unsigned int outCount, i;
     // 自身が持つPropertyの一覧を取得
    objc_property_t *properties = class_copyPropertyList([self class], &outCount);
    for (i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        // 文字列変換
        NSString *propertyName = [NSString stringWithUTF8String:name];
        // KVCを使ってPropertyの中身を取得
        NSString *propertyValue = [self valueForKey:propertyName];

        [description appendFormat:@"\t%@: %@", propertyName, propertyValue];
        [description appendString:@"\n"];
    }
    free(properties);

    [description appendString:@"}\n"];

    return description;
}

@end

그리고 이전 코드를 다시 실행하면 ,,

Property의 내용이 표시됩니다.
이것이라면 Property가 증가해도 자동으로 출력 항목이 증가하기 때문에 편리합니다.

여기에서 본제



저는 SES에서 Objective-C를 메인으로 개발하고 있습니다만, 지금의 현장에서 이런 코드를 만났습니다.

NSUSerDefaults+Category
@interface NSUserDefaults(Category)
+(void)switchStringForKey;
@end

@implementation NSUserDefaults(Category)
+(void)switchStringForKey {
    [self switchInstanceMethodFrom:@selector(stringForKey:) To:@selector(nonNilStringForKey:)];
}

- (NSString *)nonNilStringForKey:(NSString *)defaultName{
    NSString *str = [self nonNilStringForKey:defaultName];
    if (str) {
        return str;
    } else {
        NSLog(@"String nil key: %@", defaultName);
        return @"";
    }
}

 +(void)switchInstanceMethodFrom:(SEL)from To:(SEL)to {
    Method fromMethod = class_getInstanceMethod(self,from);
    Method toMethod   = class_getInstanceMethod(self,to  );
    method_exchangeImplementations(fromMethod, toMethod);
}

무엇을 하고 있는가 하면(자) NSUserDefaultsstringForKey: 의 메소드를 옮겨놓아(Method Swizzling)
절대로 nil이 반환되지 않도록하고 있습니다. ← 여기 중요

왜 이런 코드가 있는지 궁금했지만, 코드를 읽을수록 이유가 밝혀졌습니다.
자세한 것은 걸리지 않습니다만, 앱내 사용하는 캐릭터 라인을 한 번 모두 NSUserDefaults에 세트 해 각 화면에서 NSUserDefaults로부터 캐릭터 라인을 취득해 묘화 하고 있었습니다.
꽤 똥 코드 독특한 코드 네요.
그러한 처리가 되어 있으므로, StringForKey: 로 취득할 수 없었을 때에, 화면에 (null) 라고 표시되는 것을 피하기 위한 Method Swizzling 입니다.

검은 마술이 폭발했습니다.



그런 가운데 9월의 끝 정도에, 어떤 팀의 분들이 시끄러웠습니다.
분명히 iOS10 에서 카메라 롤을 보면 앱이 떨어지는 것 같습니다.
저도 도움으로 원인 조사를 실시하고 있었습니다만, 카메라 롤을 표시할 때에 신경이 쓰이는 로그를 발견했습니다.
2016-12-22 23:36:46.545 hogehoge[5750:242757] String nil key: com.apple.CoreData.Logging.ほげほげ
2016-12-22 23:36:46.545 hogehoge[5750:242757] String nil key: com.apple.CoreData.Logging.ふがふが
2016-12-22 23:36:46.550 hogehoge[5750:242757] String nil key: com.apple.CoreData.ほげふが

※ 표시 내용을 일부 흐리게 하고 있습니다

어라? 이거 검은 마술의 부분이지?



물건은 시험과 Method Swizzling 의 처리를 제외한 곳에 카메라 롤이 떨어지지 않게 되지 않습니까! ! !

뭐가 문제였는지



Apple 문서

Returns nil if the default does not exist or is not a string or number value.
Method Swizzling 로 옮겨놓은 처리는 본래 존재하지 않는 Value에 액세스 하면(자), nil 를 돌려주는 것이 됩니다.
그리고 카메라 롤을 표시할 때 UserDefaultsにアクセスしてnilかどうかで処理を分岐している箇所があり、その部分でクラッシュしていた로 예측됩니다.

어떻게 대책했는가



이번 예에서 말하면 기존의 처리를 바꾸어 버린 것이 직접적인 요인이었기 때문에 nonNilStringForKey 를 폐지해 원래대로 nil 를 돌려주도록 했습니다.
애초에 이번 사례는.
또 전체 화면에서 nonNilStringForKey 가 사용되고 있어 간단하게 치환을 할 수 없는 상태였으므로 나도 방치해 버렸습니다.
원래 StringForKey 의 사용법이 되어 있지 않다고 하는 곳은 있습니다만

끝에


NSUserDefaults 는 매우 강력한 함수이므로, 여러분 용법용량을 올바르게 지키고 흑마술을 사용합시다.

좋은 웹페이지 즐겨찾기