Edited at

NSArray MethodSwizzlingを使用しNSLogで日本語文字列を文字化け(正確に言うとUTF)させずに出力する

More than 5 years have passed since last update.

NSLog(@"%@", array);で配列内の要素をログに出力することができますが、日本語(マルチバイト)文字列を含むと文字化け(正確に言うとUTF)して出力されます。

これを文字化けせずに出力する方法です。


きっかけ

オブジェクトを文字列化するときの書式を変えるにてNSDateのdescriptionをMethodSwizzlingして書式を変えていたので、NSArrayも同じように解決するかなと思いやってみました。


実装

NSLog(@"%@", array);descriptionWithLocale:が暗黙的に呼ばれます。

ですのでdescriptionWithLocale:をMethodSwizzlingしその中でインデントを考慮した文字列を構築する必要があります。

以下の実装では、descriptionWithLocale:descriptionWithLocale:indent:をMethodSwizzlingしました。


main.m

#import <UIKit/UIKit.h>

#import <objc/runtime.h>
#import "AppDelegate.h"

int main(int argc, char *argv[])
{
@autoreleasepool {
NSArray *arr = @[@"あいうえお", @[@"アイウエオ", @"カキクケコ", @[@"唖伊兎絵尾", @"可来区毛個"], @"サシスセソ"], @"かきくけこ", @"さしすせそ", @"abcde"];
// 暗黙で descriptionWithLocale: が呼ばれる
NSLog(@"\n%@", arr); // 文字化けして出力される

Class arrayClass = [NSArray class];

// descriptionWithLocale: をMethodSwizzling
IMP imp = imp_implementationWithBlock(^NSString*(NSArray *arr, id locale) {
// 最初の括弧と改行
NSMutableString *mStr = [NSMutableString stringWithString:@"(\n"];
[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
if ([obj isKindOfClass:[NSArray class]]) {
// 配列が入れ子になっている場合は、descriptionWithLocale:indent: でインデントを考慮
[mStr appendFormat:@"%@,\n", [obj descriptionWithLocale:locale indent:2]];
} else {
// indent level1 のスペース
[mStr appendFormat:@" %@", [obj description]];
// 配列の末尾の要素かでコンマを入れるかを判断
if (idx == [arr count] - 1) {
[mStr appendString:@"\n"];
} else {
[mStr appendString:@",\n"];
}
}
}];
// 末尾の括弧
[mStr appendString:@")"];
return mStr;
});

SEL sel = @selector(descriptionWithLocale:);
class_replaceMethod(arrayClass, sel, imp, method_getTypeEncoding(class_getInstanceMethod(arrayClass, sel)));

// descriptionWithLocale:indent:をMethodSwizzling
imp = imp_implementationWithBlock(^NSString*(NSArray *arr, id locale, NSUInteger indent) {
NSMutableString *mStr = [NSMutableString string];
// indent level に応じてスペースを挿入
for (int i = 0; i < indent; i++) {
[mStr appendString:@" "];
}
[mStr appendString:@"(\n"];
[arr enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){
if ([obj isKindOfClass:[NSArray class]]) {
// さらに入れ子の場合は、indent level + 1 でインデントを考慮
[mStr appendFormat:@"%@,\n", [obj descriptionWithLocale:locale indent:indent + 1]];
} else {
// indent level に応じてスペースを挿入
for (int i = 0; i < indent; i++) {
[mStr appendString:@" "];
}
[mStr appendFormat:@"%@", [obj description]];
// 配列の末尾の要素かでコンマを入れるかを判断
if (idx == [arr count] - 1) {
[mStr appendString:@"\n"];
} else {
[mStr appendString:@",\n"];
}
}
}];
// indent level - 1 のスペースを挿入
for (int i = 0; i < indent - 1; i++) {
[mStr appendString:@" "];
}
[mStr appendString:@")"];
return mStr;
});

sel = @selector(descriptionWithLocale:indent:);
class_replaceMethod(arrayClass, sel, imp, method_getTypeEncoding(class_getInstanceMethod(arrayClass, sel)));

NSLog(@"\n%@", arr); // 文字化けせず出力される

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}


/* 通常のログ */

(
"\U3042\U3044\U3046\U3048\U304a",
(
"\U30a2\U30a4\U30a6\U30a8\U30aa",
"\U30ab\U30ad\U30af\U30b1\U30b3",
(
"\U5516\U4f0a\U514e\U7d75\U5c3e",
"\U53ef\U6765\U533a\U6bdb\U500b"
),
"\U30b5\U30b7\U30b9\U30bb\U30bd"
),
"\U304b\U304d\U304f\U3051\U3053",
"\U3055\U3057\U3059\U305b\U305d",
abcde
)

/* MethodSwizzling後 */
(
あいうえお,
(
アイウエオ,
カキクケコ,
(
唖伊兎絵尾,
可来区毛個
),
サシスセソ
),
かきくけこ,
さしすせそ,
abcde
)



  • ざざっとしか確認していませんが、NSArrayが入れ子のパターンでも問題なく出力されるはずです。なにか問題あるパターンがあれば修正して頂けると嬉しいです。


  • 再帰的にインデント処理を考慮した文字列を構築出来ればdescriptionWithLocale:indent:はMethodSwizzlingする必要ないと思います。

    ※ Blocksを再帰的に呼び出すものを書こうと思ったのですが、書き方がぱっとわからなかったのでそこらへんを解決した編集リクエストお待ちしています。