【iOS】让NSLog打印字典显示得更好看(解决中文乱码并显示成JSON格式)

前言

文章的初衷很简单,是为了能够正常显示打印出字典里面的中文。因为默认情况下,直接打印字典的话,在Xcode控制台上,中文会是乱码的,需要Unicode转码才能看到中文。
比如打印下面的一个字典

NSDictionary *dict = @{
                       @"ArticleTitle":@"【iOS开发】打开另一个APP(URL Scheme与openURL)",
                       @"ArticleUrl":@"https://www.jianshu.com/p/0811ccd6a65d",
                       @"author":@{
                               @"nickName":@"谦言忘语",
                               @"blog":@"https://www.jianshu.com/u/cc2cf725ac0c",
                               @"work":@"iOS工程师"
                               }
                       };
NSLog(@"打印出的字典:%@",dict);

Xcode控制台上显示的是这样子的:


默认情况下Xcode打印字典,中文会显示乱码

WTF!谁能告诉我,这坨东西是什么玩意儿?!!!

其实还是可以知道这些Unicode编码是什么意思的。平常我遇到这种情况会复制那堆Unicode的代码到在线网站上进行转码查看。但是依然觉得不太方便。

使用在线网站进行Unicode转码

先看看结果

我终于无法忍受这么坑爹的中文显示了,查找一些资料、经过一系列尝试之后,终于找到一个比较满意的解决方案了。先看结果:


最终结果
2018-09-03 15:43:10.046 PrintBeautifulLog[4446:1265987] 打印出的字典:{
  "ArticleTitle" : "【iOS开发】打开另一个APP(URL Scheme与openURL)",
  "ArticleUrl" : "https:\/\/www.jianshu.com\/p\/0811ccd6a65d",
  "author" : {
    "work" : "iOS工程师",
    "blog" : "https:\/\/www.jianshu.com\/u\/cc2cf725ac0c",
    "nickName" : "谦言忘语"
  }
}

是不是顿时觉得神清气爽?中文出来了,而且格式也很好看,层次分明。
对了,是不是觉得这个格式似曾相似?
嘿嘿,没错,这个就是JSON格式。不信?我们拿去JSON在线格式化网站上验证下?

JSON格式验证

另外,使用po命令调试打印的时候也是一样的。
po命令调试时也能打印打印出JSON格式的Log

直接将文件拖入到工程中即可使用

这么神奇的效果?怎么做到的?嗯,很简单,直接将github仓库上的这两个分类拉入到工程中就可以了。什么代码都不用写。

直接将这两个分类拉入到工程中即可使用

怎么做到的?

其实代码很简单,简单到难以想象。分类里面就只有10多行代码。

//NSDictionry分类实现文件代码
#import "NSDictionary+Log.h"
@implementation NSDictionary (Log)
#ifdef DEBUG
//打印到控制台时会调用该方法
- (NSString *)descriptionWithLocale:(id)locale{
    return self.debugDescription;
}
//有些时候不走上面的方法,而是走这个方法
- (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level{
    return self.debugDescription;
}
//用po打印调试信息时会调用该方法
- (NSString *)debugDescription{
    NSError *error = nil;
    //字典转成json
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted  error:&error];
    //如果报错了就按原先的格式输出
    if (error) {
        return [super debugDescription];
    }
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    return jsonString;
}
#endif
@end

接下来解释下这段代码:

  • NSLog打印字典(NSDictionary)和数组(NSArray)的时候的时候会走- (NSString *)descriptionWithLocale:(id)locale来决定打印的字符串。打印其他对象(比如NSString类型)的时候会走- (NSString *)description方法。所以现在我们需要重写NSDictionary的- (NSString *)descriptionWithLocale:(id)locale方法来得到我们想要的结果。
  • 在使用po命令调试的时候,会走- (NSString *)debugDescription方法,所以我们需要覆盖该方法来显示出我们想要的结果。
  • - (NSString *)descriptionWithLocale:(id)locale- (NSString *)debugDescription方法里面将字典转化为JSON字符串输入,就能同时在代码调试打印和使用po命令调试打印时都能得到我们想要的结果。
    NSError *error = nil;
    //字典转成json格式字符串
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted  error:&error];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    return jsonString;
  • 字典转化成字符串有可能会失败,所以失败的时候我们就以默认的格式输出。
if (error) {
    return [super debugDescription];
}
  • 在分类里面做了DEBUG预编译判断,只有在DEBUG模式下才会调用该方法,线上包(线上包采用Release模式)不会受到影响。
#ifdef DEBUG
//分类中的代码
#endif

嗯,NSArray分类里面的代码也是一毛一样的。所以打印NSArray也能像NSDictionary一样使用JSON格式输出,并且可以正常显示中文。不多说了。

除了 - (NSString *)descriptionWithLocale:(id)locale方法之外,还有一个- (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level方法。这两个方法功能是一样的,后者多了一个indent(缩进)参数。我测试过这两个方法的优先级,发现前后测试的结果有点矛盾,所以就懒得理,两个都实现了。

再看下其他解决NSLog打印字典时中文显示乱码的方式

还有其他的方式也能解决NSLog打印字典时显示乱码的问题。方法是一样的,增加字典和数组的分类,重写- (NSString *)descriptionWithLocale:(id)locale- (NSString *)debugDescription方法,修改Xcode输出字符串。不同之处在于输出字符串的处理方式。先看看常用的方式。

//NSDictionary分类实现文件代码
- (NSString *)descriptionWithLocale:(id)locale{
    return self.debugDescription;
}
- (NSString *)debugDescription {
    NSMutableString *strM = [NSMutableString stringWithString:@"{\n"];
    [self enumerateKeysAndObjectsUsingBlock:^(id key,id obj,BOOL *stop) {
        [strM appendFormat:@"\t%@ = %@;\n", key, obj];
    }];
    [strM appendString:@"}\n"];
    return strM;
}
//NSArray分类实现文件代码
- (NSString *)descriptionWithLocale:(id)locale{
    return self.debugDescription;
}
- (NSString *)debugDescription
{
    NSMutableString *strM = [NSMutableString stringWithString:@"(\n"];
    [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx,BOOL *stop) {
        [strM appendFormat:@"\t%@,\n", obj];
    }];
    [strM appendString:@")"];
    return strM;
}

这种方式是直接遍历字典中的key和value,中间加一个=拼接起来。然后所有的key/value对拼接成一个字符串。每个key/value对后面都加入一个换行符\n。最后在前后加上大括号{}括起来。这种方式可以解决中文显示乱码的问题,但是有一个比较不好的地方,就是缩进格式没有了(Xcode默认的格式是有缩进格式的)。不管里面有多少层嵌套,前面都是一样的间隔。在多层嵌套的时候看起来会不太爽。

遍历key/value对,重新拼接输出字符串

上面的方式无法处理缩进格式问题,我们之前提过,使用- (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level方法是有缩进参数的,所以可以使用这个方法可以将缩进格式搞出来。看了下感觉还不错。但是有个小缺点,使用po参数调试的时候就没有办法了。两个方法分写是在NSArray分类和NSDictionary分类里面实现的。代码如下:

//NSArray
- (NSString *)descriptionWithLocale:(nullable id)locale indent:(NSUInteger)level{
    
    NSMutableString *mStr = [NSMutableString string];
    NSMutableString *tab = [NSMutableString stringWithString:@""];
    for (int i = 0; i < level; i++) {
        [tab appendString:@"\t"];
    }
    [mStr appendString:@"(\n"];
    for (int i = 0; i < self.count; i++) {
        NSString *lastSymbol = (self.count == i + 1) ? @"":@",";
        id value = self[i];
        if ([value respondsToSelector:@selector(descriptionWithLocale:indent:)]) {
            [mStr appendFormat:@"\t%@%@%@\n",tab,[value descriptionWithLocale:locale indent:level + 1],lastSymbol];
        } else {
            [mStr appendFormat:@"\t%@%@%@\n",tab,value,lastSymbol];
        }
    }
    [mStr appendFormat:@"%@)",tab];
    return mStr;
}
//NSDictionary
- (NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level
{
    NSMutableString *mStr = [NSMutableString string];
    NSMutableString *tab = [NSMutableString stringWithString:@""];
    for (int i = 0; i < level; i++) {
        [tab appendString:@"\t"];
    }
    [mStr appendString:@"{\n"];
    NSArray *allKey = self.allKeys;
    for (int i = 0; i < allKey.count; i++) {
        id value = self[allKey[i]];
        NSString *lastSymbol = (allKey.count == i + 1) ? @"":@";";
        if ([value respondsToSelector:@selector(descriptionWithLocale:indent:)]) {
            [mStr appendFormat:@"\t%@%@ = %@%@\n",tab,allKey[i],[value descriptionWithLocale:locale indent:level + 1],lastSymbol];
        } else {
[mStr appendFormat:@"\t%@%@ = %@%@\n",tab,allKey[i],value,lastSymbol];
        }
    }
    [mStr appendFormat:@"%@}",tab];
    return mStr;
}

还有另外一种方式,这种方式的思想是,上面第一种方式没有缩进格式,看起来很不爽,但是系统默认的实现方式是有缩进格式的。只是中文显示有问题而已。那我直接把默认方式中要输出的字符串进行Unicode转化,将其转化为中文不就可以了?
具体代码就不贴了,有兴趣可以看下这篇文章
这种方式确实可行,跟原先的输出的唯一不同就是将Unicode字符串转化为了中文字符串显示。但是有一个缺点,那就是在将默认方式的Unicode字符串转化为中文字符串显示的时候,容易出问题。因为转码之前是需要暴力替换的,这个替换过程是很容易出问题的。比如如果字典的value字符串里面本来就有" "符号,那转码就出问题了。

更新(20180914)

之前的方式遇到字典数组里面有模型的情况容易出问题。
于是在将字典/数组转换成JSON字符串之前,先判断其是否能转换成JSON格式字符串,如果不能,就调用系统的原始实现。
由于要调用系统的原始实现,所以还使用了method swizzle交换了上面说的3个系统方法。具体可查看github代码。

更新(20230412)

在转JSON的时候options里面增加了NSJSONWritingSortedKeysNSJSONWritingWithoutEscapingSlashes,前者可以将json让key按照字母排序后输出,便于查找。后者可以去除value里的转义字符,看起来会更舒服。

//将obj转换成json字符串。如果失败则返回nil.
- (NSString *)convertToJsonString {
    
    //先判断是否能转化为JSON格式
    if (![NSJSONSerialization isValidJSONObject:self])  return nil;
    NSError *error = nil;
    
    NSJSONWritingOptions jsonOptions = NSJSONWritingPrettyPrinted;
    if (@available(iOS 11.0, *)) {
        //11.0之后,可以将JSON按照key排列后输出,看起来会更舒服
        jsonOptions =  jsonOptions | NSJSONWritingSortedKeys;
    }
    if (@available(iOS 13.0,*)) {
        //13.0之后,可以去除Json里面的转义字符
        jsonOptions =  jsonOptions | NSJSONWritingWithoutEscapingSlashes;
    }
    //核心代码,字典转化为有格式输出的JSON字符串
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:jsonOptions  error:&error];
    if (error || !jsonData) return nil;
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    return jsonString;
}

参考

代码已放在github上
iOS JSON数据NSLog小技巧
iOS 打印中文字典,数组,控制台输出中文,并保持缩进格式
iOS description方法和descriptionWithLocale:方法 解决中文现问题
xcode8控制台打印出字典和数组中的中文字符 解决中文乱码
iOS开发实战tips--让Xcode的控制台支持NSArray和NSDictionary的中文输出
从NSDictionary打印不出中文开始

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容