iOS - 斷言處理與調試

一直想寫一篇你關于斷言的文章, 今天有時間趕緊寫出來.
參考 Mattt 文章


一、Objective - C 中的斷言:

  • Objective - C 中的斷言處理使用的是 NSAssertionHandler :

每個線程擁有它自己的斷言處理器,它是 NSAssertionHandler 類的實例對象。當被調用時,一個斷言處理器打印一條包含方法和類名(或者函數名)的錯誤信息。然后它拋出一個 NSInternalInconsistencyException 異常。

  • 基礎類中定義了兩套斷言宏
    • NSAssert / NSCAssert
/** NSAssert */
#if !defined(_NSAssertBody)
#define NSAssert(condition, desc, ...)  \
    do {                \
    __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    if (!(condition)) {     \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
        [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
        object:self file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    }               \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)
#endif
/** NSCAssert */
#if !defined(_NSCAssertBody)
#define NSCAssert(condition, desc, ...) \
    do {                \
    __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    if (!(condition)) {     \
            NSString *__assert_fn__ = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \
            __assert_fn__ = __assert_fn__ ? __assert_fn__ : @"<Unknown Function>"; \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
        [[NSAssertionHandler currentHandler] handleFailureInFunction:__assert_fn__ \
        file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    }               \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)
#endif
  • NSParameterAssert / NSCParameterAssert
/** NSParameterAssert */
#define NSParameterAssert(condition) NSAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)
/** NSCParameterAssert */
#define NSCParameterAssert(condition) NSCAssert((condition), @"Invalid parameter not satisfying: %@", @#condition)
  • 這么做的意義在于兩點:
    • 第一個是蘋果對于斷言處理在 API 層面進行了區分:
      • NSAssertNSCAssert 用來處理一般情況的斷言
      • NSParameterAssertNSCParameterAssert 用來處理參數化的斷言
    • 第二個是區別是在 Objective - C 和 C 之間進行了區分這樣才有了:
      • NSAssertNSCAssert
      • NSParameterAssertNSCParameterAssert

二、使用 NSAssertionHandler

  • 從 Xcode 4.2 開始,發布構建默認關閉了斷言,它是通過定義 NS_BLOCK_ASSERTIONS 宏實現的。也就是說,當編譯發布版時,任何調用 NSAssert 等的地方都被有效的移除了。
    盡管基礎類庫的斷言宏在它們自己的權力下十分有用————雖然只用于開發之中。NSAssertionHandler 還提供了一套優雅地處理斷言失敗的方式來保留珍貴的現實世界的使用信息。

Pay Attension:

據說,許多經驗豐富的 Objective-C 開發者們告誡不要在生產環境中使用 NSAssertionHandler。基礎類庫中的斷言處理是用來在一定安全距離外來理解和感激的。請小心行事如果你決定在對外發布版的應用中使用它。

  • NSAssertionHandler 是一個很直接的類,帶有兩個需要在子類中實現的方法:-handleFailureInMethod:... (當 NSAssert / NSParameterAssert 失敗時調用)和 -handleFailureInFunction:... (當 NSCAssert / NSCParameterAssert 失敗時調用)。
  • 接下來看一個使用的實例
#pragram 第一步,創建一個繼承自NSAssertionHandler 的類:LoggingAssertionHandler 用來專門處理斷言
#import <Foundation/Foundation.h>
@interface LoggingAssertionHandler : NSAssertionHandler
@end
#import "LoggingAssertionHandler.h"
@implementation LoggingAssertionHandler
/** 重寫兩個失敗的回調方法,在這里執行我們想要拋出的錯誤(打印或者直接報錯) */
  - (void)handleFailureInMethod:(SEL)selector object:(id)object file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...{
    NSLog(@"NSAssert Failure: Method %@ for object %@ in %@#%li", NSStringFromSelector(selector), object, fileName, (long)line);
    NSException *e = [NSException
                      exceptionWithName: NSStringFromSelector(selector)
                      reason: format
                      userInfo: nil];
    @throw e;
}
  - (void)handleFailureInFunction:(NSString *)functionName file:(NSString *)fileName lineNumber:(NSInteger)line description:(NSString *)format, ...{
    NSLog(@"NSCAssert Failure: Function (%@) in %@#%li", functionName, fileName, (long)line);
}
@end
  • 每個線程都可以指定斷言處理器。 想設置一個 NSAssertionHandler 的子類來處理失敗的斷言,在線程的threadDictionary 對象中設置 NSAssertionHandlerKey 字段即可。

大部分情況下,你只需在

 -application:didFinishLaunchingWithOptions:

中設置當前線程的斷言處理器。

  • AppDelegate 中的處理
 - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSAssertionHandler *assertionHandler = [[LoggingAssertionHandler alloc] init];
  [[[NSThread currentThread] threadDictionary] setValue:assertionHandler
                                                 forKey:NSAssertionHandlerKey];
  // ...
  return YES;
}
  • 這樣我們就完成再當前線程中使用我們自定義的斷言處理器的配置,那么接下來,如果有和我們條件不同的情況都直接會回調對應著的那兩個失敗的方法,我們可以在那倆個方法中按自己的輸出意愿來處理你的話術。
  • 具體應用
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
 - (void)viewDidLoad {
    [super viewDidLoad];
    NSObject*mc = [NSObject new];
    mc = @2;
    NSAssert(mc == nil, @"我不為空了");
}
@end

根據輸出情況可以看到是完全按照我們所需要的來輸出的

2015-10-30 21:33:14.529 NSAssert[20537:678428] *** 
Terminating app due to uncaught exception 'viewDidLoad', reason: '我不為空了'

三、使用上的注意點

  • 仔細觀察 NSAssert 的宏定義 ,你會發現 self 的痕跡,有 self 的地方就一定要注意 block 容易產生的循環引用問題。
/** NSAssert */
#if !defined(_NSAssertBody)
#define NSAssert(condition, desc, ...)    \
do {                \
__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
if (!(condition)) {        \
       NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
       __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
   [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
   object:self file:__assert_file__ \
       lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
}                \
   __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
} while(0)
#endif
  • 接下來舉個例子:
/** 創建一個 preson 類 */
 #import <Foundation/Foundation.h>
typedef void(^mitchelBlock)(int num);
@interface person : NSObject
@property(nonatomic, copy)mitchelBlock block;
@end
#import "person.h"
@implementation person
 - (instancetype)init{
    if (self = [super init]) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            if (self.block) {
                self.block(1);
            }
        });
    }
    return self;
}
@end
/** ViewController 中的代碼 */
#import "ViewController.h"
#import "person.h"
@interface ViewController ()
@property(nonatomic, strong)person * aPerson;
@end
@implementation ViewController
 - (void)viewDidLoad {
    [super viewDidLoad];
    NSObject*mc = [NSObject new];
    mc = @2;
    self.aPerson = [person new];
    self.aPerson.block = ^(int num){
        NSAssert(mc == nil, @"我不為空了");
        NSLog(@"%d",num);
    };
}
@end

這樣我們就會看到 Block 中循環引用的警告啦:

屏幕快照 2015-10-30 下午9.48.17.png

那如果我想在 Block 中使用斷言怎么辦吶?用 NSCAssert 替換 NSAssertNSCParameterAssert 來替換 NSParameterAssert

 - (void)viewDidLoad {
    [super viewDidLoad];
    NSObject*mc = [NSObject new];
    mc = @2;
    self.aPerson = [person new];
    self.aPerson.block = ^(int num){
        NSCAssert(mc == nil, @"我不為空了");
        NSCParameterAssert(num>5);
    };
}
  • 這樣就 OK 了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容