自動格式化打印變量HMLog介紹

前言

在我初學iOS的時候,常常須要NSLog打印用於調試,有時候還須要打印多個變量:html

NSLog(@"xxxx frame=%@ tag=%ld isHidden=%d", NSStringFromCGRect(view.frame), view.tag, view.isHidden);
複製代碼

僅考慮把NSLog用來調試輸出,那寫種代碼就太麻煩了,主要存在着這樣幾個問題:git

  • 須要格式化,如%@%ld%d
  • 須要設置標籤,以便知道輸出值對應的變量,如frame=tag=isHidden=
  • 須要類型轉換,如NSStringFromCGRect
  • 有時候還須要寫一些指定字符串以便於在Console輸出中搜索或過濾,如xxxx

後來接觸到各類各樣的Debug Log,主要利用__LINE____func__能夠很方便定位到輸出的位置,可是依然還存在前面3個問題。另外LLDB能夠很方便獲取變量值,但在變量較多或須要連續打印的狀況下也不夠方便快捷。
那個時候我就產生了一個想法,能不能本身寫一個Debug Log,解決上面這些困擾個人問題?這就是我開發HMLog的初衷,源碼僅有一個HMLog.h文件。github

基本用法

項目源碼及demo:github.com/chenhuimao/…objective-c

如下用法都可在HMLogDemo項目中找到。markdown

HMLog

HMLog最終是基於NSLog輸出,根據前面的例子,使用HMLog的代碼和輸出是這樣的:app

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 30, 40)];
view.tag = 333;
HMLog(view.frame);
HMLog(view.frame, view.tag, view.isHidden);

// 輸出以下
// 2020-10-17 15:49:33.356890+0800 HMLogDemo[85956:1573131] 
// ================ -[ViewController viewDidLoad] [45] ================
// 0: view.frame = NSRect: {{10, 20}, {30, 40}}
// 2020-10-17 15:49:33.357017+0800 HMLogDemo[85956:1573131] 
// ================ -[ViewController viewDidLoad] [46] ================
// 0: view.frame = NSRect: {{10, 20}, {30, 40}}
// 1: view.tag = 333
// 2: view.isHidden = NO
複製代碼

Demo中的一個例子,用截圖展現: example框架

HMPrint

HMPrint則基於printfide

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 30, 40)];
view.tag = 333;
HMPrint(view.frame);
HMPrint(view.frame, view.tag, view.isHidden);

// 輸出以下
// ================ -[ViewController viewDidLoad] [45] ================
// 0: view.frame = NSRect: {{10, 20}, {30, 40}}
// 
// ================ -[ViewController viewDidLoad] [46] ================
// 0: view.frame = NSRect: {{10, 20}, {30, 40}}
// 1: view.tag = 333
// 2: view.isHidden = NO
複製代碼

HMFormatString

若是隻是須要自動格式化的目標字符串,能夠使用HMFormatString函數

self.displayLab.text = HMFormatString(self.view.frame, self.view.tag, @selector(viewDidLoad));
printf("%s", self.displayLab.text.UTF8String);

// ================ -[ViewController getFormatString1] [80] ================
// 0: self.view.frame = NSRect: {{0, 0}, {414, 896}}
// 1: self.view.tag = 0
// 2: @selector(viewDidLoad) = SEL: viewDidLoad
複製代碼

可選參數

全部可選參數都應該在#import "HMLog.h"以前定義好oop

HMLogEnable / HMPrintEnable

分別控制HMLogHMPrint的是否生效,默認生效,不生效狀況下調用沒有任何效果。如只須要在Debug模式下開啓HMPrint

// Only enable HMPrint in Debug configuration
#ifdef DEBUG
#define HMPrintEnable 1
#else
#define HMPrintEnable 0
#endif
#import "HMLog.h"
複製代碼

HMLogHeaderFormatString(FUNC, LINE)

控制頭部字符串(注意能夠重用FUNCLINE,或者不使用):

#define HMLogHeaderFormatString(FUNC, LINE) \ [NSString stringWithFormat:@"%s ????? %s:\n", FUNC, FUNC]
#import "HMLog.h"
...

HMPrint(self.navigationItem.title);
HMPrint(self.view.bounds.size, self.view.alignmentRectInsets, self.title, self.automaticallyAdjustsScrollViewInsets, self.navigationController, [self class], @selector(viewDidAppear:));

// 輸出以下
// -[ViewController print2] ????? -[ViewController print2]:
// 0: self.navigationItem.title = HMLogDemo
// 
// -[ViewController print2] ????? -[ViewController print2]:
// 0: self.view.bounds.size = NSSize: {414, 896}
// 1: self.view.alignmentRectInsets = {0, 0, 0, 0}
// 2: self.title = (null)
// 3: self.automaticallyAdjustsScrollViewInsets = YES
// 4: self.navigationController = <UINavigationController: 0x7fe108018400>
// 5: [self class] = ViewController
// 6: @selector(viewDidAppear:) = SEL: viewDidAppear:
複製代碼

HMLogPrefix(index, valueString)

控制每一個變量輸出的前綴。例如只須要展現下標,則按下面的方式定義:

// Only show index prefix
#define HMLogPrefix(index, valueString) [NSString stringWithFormat:@"%d: ", index]
#import "HMLog.h"
...

HMPrint(self.navigationItem.title);
HMPrint(self.view.bounds.size, self.view.alignmentRectInsets, self.title, self.automaticallyAdjustsScrollViewInsets, self.navigationController, [self class], @selector(viewDidAppear:));

// ================ -[ViewController print2] [79] ================
// 0: HMLogDemo
// 
// ================ -[ViewController print2] [80] ================
// 0: NSSize: {414, 896}
// 1: {0, 0, 0, 0}
// 2: (null)
// 3: YES
// 4: <UINavigationController: 0x7fab8c82be00>
// 5: ViewController
// 6: SEL: viewDidAppear:
複製代碼

HMLogTypeExtension

默認狀況下不支持CGVectorCLLocationCoordinate2D類型,能夠額外匹配須要格式化的類型:

#define HMLogTypeExtension \ else if (strcmp(type, @encode(CGVector)) == 0) { \ CGVector actual = (CGVector)va_arg(v, CGVector); \ obj = NSStringFromCGVector(actual); \ } else if (strcmp(type, @encode(CLLocationCoordinate2D)) == 0) { \ CLLocationCoordinate2D actual = (CLLocationCoordinate2D)va_arg(v, CLLocationCoordinate2D); \ obj = [NSString stringWithFormat:@"latitude: %lf, longitude: %lf", actual.latitude, actual.longitude]; \ }

#import <CoreLocation/CoreLocation.h>
#import "HMLog.h"
...

CGVector vector = CGVectorMake(110, 119);
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(22.512145, 113.9155);
HMPrint(vector, coordinate);

// ================ -[CustomizeFormatViewController log] [65] ================
// 0: vector = {110, 119}
// 1: coordinate = latitude: 22.512145, longitude: 113.915500
複製代碼

使用注意

  • 項目須要Foundation和UIKit框架。C語言標準爲gnu99,Xcode項目的C Language Dialect選項設置爲gnu99或gnu11
  • HMLog項目的實現僅有一個文件HMLog.h,能夠在pch文件導入,也能夠在每一個須要的文件分別導入。全部可選參數都應該在#import "HMLog.h"以前定義好
  • 一次調用最多支持20個變量
  • 沒有支持全部的數據類型,默認支持的類型參考源碼,能夠使用HMLogTypeExtension進行擴展

設計思路

只考慮1個變量

首先考慮1個變量的狀況,即HMLog只能傳入1個變量。

  1. 要格式化1個變量,就要先知道這個變量的類型。變量能夠經過__typeof__獲取類型,好比咱們經常這樣用__weak __typeof__(self) weakSelf = self;

  2. 取得變量類型後,還須要比較判斷,以後才能把id類型格式化爲"%@",把long類型格式化爲"%ld"。類型如何作判斷呢?if(long == id)顯然是不行的,OC類型編碼@encode會返回一個char *字符串,這樣就能夠利用strcmp函數作比較了:

    NSString *format;
    if (strcmp(@encode(__typeof__(self.view)), @encode(id)) == 0) {
        format = @"%@";
    } else if (strcmp(@encode(__typeof__(self.view)), @encode(long)) == 0) {
        format = @"%ld";
    } else if ...
    複製代碼

    參考蘋果的文檔,也能夠寫成if (strcmp(@encode(__typeof__(self.view)), "@") == 0)的形式。不過HMLog並無採用這種簡化的形式,@encode是編譯器指令,並不影響運行時效率,上面的代碼塊中的形式更加直觀。

  3. 要把這個功能寫成一個通用函數,那如何表示任意的類型?換句話說,若是value是NSObject對象能夠用id value表示,但若是value多是任何類型,id該換成什麼?這個時候,可變參數函數派上用場了,可變參數最後的...,是不須要寫明數據類型的,這樣能夠把變量(value)和變量的類型編碼@encode(__typeof__(value))同時傳入進去,同時利用宏把一個變量替換爲這兩種形式:

    #define MyLog(value) _MyLog(__func__, __LINE__, @encode(__typeof__(value)), (value))
    
    static void _MyLog(const char *func, int line, ...) {
        NSMutableString *result = [[NSMutableString alloc] init];
        [result appendFormat:@"\n===== %s [%d] =====\n", func, line];
        
        va_list v;
        va_start(v, line);
        char *type = va_arg(v, char *);
        if (strcmp(type, @encode(id)) == 0) {
            id actual = (id)va_arg(v, id);
            [result appendFormat:@"id: %@\n", actual];
        } else if (strcmp(type, @encode(long)) == 0) {
            long actual = (long)va_arg(v, long);
            [result appendFormat:@"long: %ld\n", actual];
        }
        va_end(v);
        NSLog(@"%@", result);
    }
    
    // 能夠愉快地打印id和long類型了
    MyLog(self.view);
    MyLog(self.view.tag);
    複製代碼

    把上面的例子的條件語句補充好須要的類型,MyLog宏就能夠打印任意類型的1個變量了。

考慮多個變量

接下來要考慮的是如何同時打印多個變量。

  1. 按照前面的思路,打印多個變量,須要把每一個變量(value)和變量的類型編碼@encode(__typeof__(value))都傳給可變參數函數_MyLog,另外還須要一個數量count表示一共有幾組變量及其類型編碼。爲了實現這個需求,這裏使用了獲取宏參數個數以及遞歸宏的技巧,請閱讀完這篇文章,瞭解C語言宏定義使用總結與遞歸宏

  2. 最後補充好細節,使變量名稱化爲字符串做爲提示標籤,定製化使用的可選參數,這就完成了HMLog

總體思路很清晰,源碼也只有一個200多行的HMLog.h文件,難點基本上只有遞歸宏的使用。除此以外值得一提的還有兩點:

  • float類型的值,經過va_arg獲取值先傳入double類型,而後再強制類型轉換爲float類型:float actual = (float)va_arg(v, double);,這是由於有個規則叫默認參數提高,還有一些char、short等類型也是如此。
  • [result appendFormat:@"%@%@\n", ((void)(valueString), HMLogPrefix(i, valueString)), obj];這行代碼,爲了消除宏HMLogPrefix(i, valueString)可能沒有用到valueString致使的警告,使用了逗號運算符,這是宏定義使用中經常使用的一個運算符。

參考資料

相關文章
相關標籤/搜索