5

NSError NSLocalizedDescription 自动生成

 3 years ago
source link: https://xiangwangfeng.com/2017/05/19/NSError-NSLocalizedDescription-%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

NSError NSLocalizedDescription 自动生成

19 May 2017

在 iOS 中我们常常会使用 NSError 来封装错误信息,相比于单纯的错误码,NSError 包含更多的信息。主要有

  • domain 错误发生域
  • code 错误码
  • userInfo 详细信息

但这也意味着它的使用更繁琐些,一个简单的 NSError 初始化方法往往是这样

NSError *error = [NSError errorWithDomain:NIMLocalErrorDomain
                                     code:NIMLocalErrorCodeInvalidParam
                                 userInfo:nil];

这里我们定义了自己的字符串常量 NIMLocalErrorDomain 来表示错误发生域和 NIMLocalErrorCode 枚举表示对应的错误码。但一个懒得要死的程序员显然对这种繁琐的初始化方法不感冒,然后就引入了宏定义使自己少写几行代码:

#define NIMLocalError(x)         ([NSError errorWithDomain:NIMLocalErrorDomain \
                                                      code:(x) \
                                                  userInfo:nil])

这样我们就可以使用 NSError *error = NIMLocalError(NIMLocalErrorCodeInvalidParam) 这种稍微简单的写法。

这是前几天我开脑洞之前云信这边的 NSError 的标准写法。

但是这样写法有个比较大的问题,当上层开发或者我们的客户在开发过程中碰到错误后,他只能看到 NSErrordomaincode 信息(这也是我们在使用苹果某些 API 时常常会碰到的问题,只有 doamincodethe operation could not be completed 的描述),于是我们只能拿着 domaincode 去 google 一把或者查阅文档来确定真正的错误信息。

一种改进的方法是调整宏定义的写法,将宏定义调整为

#define NIMLocalError(code,reason) ([NSError errorWithDomain:NIMLocalErrorDomain \
                                                        code:(code) \
                                                    userInfo:@{NSLocalizedDescriptionKey : (reason)}])

这样我们就可以调整为 NSError *error = NIMLocalError(NIMLocalErrorCodeInvalidParam,@"参数错误")。但这样的做法仍有两个问题

  • 不同文件中相同错误需要重复填入相同的描述信息。
  • 对于服务器只下发错误码而没有描述信息的情况无法处理。

一种简单解决方案是将错误码和错误信息的映射关系打包到一个 plist 文件或是写死在源码中,后续通过错误码反查描述信息并填充 NSError。大致的代码如下

- (NSError *)error:(NSString *)domain
              code:(NSInteger)code
{
    NSString *message = [self.messages objectForKey:@(code)];
    return message ? [NSError errorWithDomain:domain
                                   	     code:code
                                     userInfo:@{NSLocalizedDescriptionKey : message}] :
                     [NSError errorWithDomain:domain
                                   	     code:code
                                     userInfo:nil];          
}

接下来就是苦力活:每添加一个错误码就需要同时在描述文件中添加对应的描述信息。

而实际情况是:所有错误码枚举,我们都已添加对应的注释,也就是错误码的描述信息。那么问题就来了:为什么我们还需要单独维护一份描述文件呢,直接使用注释信息不就好了么?

换而言之,我们为什么不直接把枚举定义的头文件当成一种描述文件,直接从中提取注释作为错误信息呢?似乎这是非常困难的事情,需要引入 Objective-C 语法解析库,进而提取语法树。而事实上,由于使用 VVDocument 或是 Xcode 自带生成注释方法生成的枚举注释格式非常单一,直接通过简单的行扫描就可以完成。

通过 VVDocument 生成的注释格式如下

/**
  *  注释内容
 */

那么我们只需要检查当前行在 trim 后是否以 * 开头即可确定是否为注释内容行,并加以提取。 而对于枚举表达式,由于 Objective-C 的特殊性,所有枚举往往都会有自己的固定前缀,如

 typedef NS_ENUM(NSInteger, NIMRemoteErrorCode) {
    /**
     *  客户端版本错误
     */
    NIMRemoteErrorCodeInvalidVersion      = 201,
    /**
     *  密码错误
     */
    NIMRemoteErrorCodeInvalidPass         = 302,
    /**
     *  CheckSum校验失败
     */
    NIMRemoteErrorCodeInvalidCRC          = 402,
    /**
     *  非法操作或没有权限
     */
    NIMRemoteErrorCodeForbidden           = 403,
}

那我们就可以使用前缀进行匹配提取。具体实现如下

func parseComments(items : [String],domain :String,prefix : String) -> [String:String] {
    var results = [String:String]()
    
    var comment : String? = nil
    for item in items {
        if item.contains(prefix) {
            let exp = item.replacingOccurrences(of: ",", with: " ")
            let tokens = exp.components(separatedBy: "=")

            if tokens.count == 2 {
                let enumName = tokens[0].replacingOccurrences(of: " ", with: "")
                let enumValue = tokens[1].replacingOccurrences(of: " ", with: "")
                if let value = comment , let _ = Int(enumValue) {
                    results[enumName] = value
                    let key = domain + "." + enumValue
                    results[key] = enumName
                }
            }
        }
        else if item.trimmingCharacters(in: .whitespaces).hasPrefix("* ") {
            let word = item.replacingOccurrences(of: "*", with: "").replacingOccurrences(of: " ", with: "")
            comment = word
        }
    }
    return results
}

通过简单的匹配(都不需要使用正则,跑)我们就获取了对应关系。最后通过在 Xcode 中设置相应的 RunScript 就可以使得生成描述源码的过程优先在编译前执行。

xcrunscript.jpg

如图,至此整个流程就已完整:我们通过 nim_error_generator 读取并解析 NIMGlobalDefs.hNIMAVChatDefs.h 中相应枚举信息,并最终将映射关系写入 NIMErrorManager.mm,后者在生成 NSError 的时候会根据传入的 codedomain 查找对应的描述信息,并自动填入到 userinfo


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK