黒魔術は用法用量を守って正しくお使いください

[iOS Advent Calendar 2016](http://qiita.com/advent-calendar/2016/ios) 24日目の記事です。
# はじめに
**今更Objective-Cでごめんなさい**

# Objctive-Cの黒魔術「objc/runtime.h」
runtimeAPIはメソッドの挙動を差し替えたりとても強力な**黒魔術**です。
黒魔術については下記
[[Objective-C] ランタイムAPIのメモ](http://qiita.com/edo_m18/items/0d0a07c93298b905f1a2)

## 黒魔術の有効活用
ユーザ情報を管理する`User`というモデルクラスがあったとします。

``` objective-c: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
```
``` objective-c: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`で表示するとオブジェクトのポインタが表示されます。

``` objective-c
- (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);
}
```
![NSLog.png](https://qiita-image-store.s3.amazonaws.com/0/61909/d1828151-605f-2689-1554-1bd3826b5ed6.png)

ポインタではなく、`userName`や`address`の中身がみたいときは`user.userName`と変数を指定してNSLogを出さないといけません。
少し面倒です。

### そこでobjc/runtime.h
`<objc/runtime.h>`をインポートし、`User`クラスの`description`メソッドを下記の様に記述します。

``` objective-c: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
```

そしてもう一度先程のコードを実行すると、、、
![NSLog2.png](https://qiita-image-store.s3.amazonaws.com/0/61909/7d6297e4-4086-fa21-c460-e5e95cc8e6c9.png)
**Propertyの中身が表示されます。**
これならPropertyが増えても自動で出力項目が増えるので便利です。

# ここから本題
私はSESでObjective-Cをメインに開発を行っているのですが、今の現場でこのようなコードに出会いました。

``` objc: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);
}
```

何をやっているかというと`NSUserDefaults`の`stringForKey:`のメソッドを置き換えて(**Method Swizzling**)
**絶対にnilが返らないようにしています。** ← ここ大事

なぜこのようなコードがあるのか疑問だったのですが、コードを読み解くにつれて理由がわかりました。
詳しくはかけないのですが、アプリ内使用する文字列を**一度すべてNSUserDefaultsにセットして**各画面でNSUserDefaultsから文字列を取得し描画していました。
なかなか~~糞なコード~~ユニークなコードですね。
そのような処理がになっているので、`StringForKey:`で取得できなかった際に、画面に`(null)`と表示されるのを避けるための`Method Swizzling`なのでしょう。

## 黒魔術が暴発しました
そんな中９月の終わりぐらいに、とあるチームの方たちが騒いでいました。
どうやら`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のドキュメント](https://developer.apple.com/reference/foundation/nsuserdefaults/1416700-stringforkey?language=objc)にもあるように
> 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`の使い方がなっていないというところはありますが~~

# 終わりに
`objc/runtime`はとても強力な関数なので、皆さん用法用量を正しく守って黒魔術を使用しましょう。