在我初學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
xxxx
後來接觸到各類各樣的Debug Log,主要利用__LINE__
和__func__
能夠很方便定位到輸出的位置,可是依然還存在前面3個問題。另外LLDB能夠很方便獲取變量值,但在變量較多或須要連續打印的狀況下也不夠方便快捷。
那個時候我就產生了一個想法,能不能本身寫一個Debug Log,解決上面這些困擾個人問題?這就是我開發HMLog
的初衷,源碼僅有一個HMLog.h
文件。github
項目源碼及demo:github.com/chenhuimao/…objective-c
如下用法都可在HMLogDemo項目中找到。markdown
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中的一個例子,用截圖展現: 框架
HMPrint
則基於printf
:ide
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
:函數
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
分別控制HMLog
和HMPrint
的是否生效,默認生效,不生效狀況下調用沒有任何效果。如只須要在Debug模式下開啓HMPrint
:
// Only enable HMPrint in Debug configuration
#ifdef DEBUG
#define HMPrintEnable 1
#else
#define HMPrintEnable 0
#endif
#import "HMLog.h"
複製代碼
控制頭部字符串(注意能夠重用FUNC
和LINE
,或者不使用):
#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:
複製代碼
控制每一個變量輸出的前綴。例如只須要展現下標,則按下面的方式定義:
// 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:
複製代碼
默認狀況下不支持CGVector
和CLLocationCoordinate2D
類型,能夠額外匹配須要格式化的類型:
#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
複製代碼
HMLog
項目的實現僅有一個文件HMLog.h
,能夠在pch文件導入,也能夠在每一個須要的文件分別導入。全部可選參數都應該在#import "HMLog.h"
以前定義好HMLogTypeExtension
進行擴展首先考慮1個變量的狀況,即HMLog
只能傳入1個變量。
要格式化1個變量,就要先知道這個變量的類型。變量能夠經過__typeof__
獲取類型,好比咱們經常這樣用__weak __typeof__(self) weakSelf = self;
。
取得變量類型後,還須要比較判斷,以後才能把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
是編譯器指令,並不影響運行時效率,上面的代碼塊中的形式更加直觀。
要把這個功能寫成一個通用函數,那如何表示任意的類型?換句話說,若是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個變量了。
接下來要考慮的是如何同時打印多個變量。
按照前面的思路,打印多個變量,須要把每一個變量(value)
和變量的類型編碼@encode(__typeof__(value))
都傳給可變參數函數_MyLog
,另外還須要一個數量count
表示一共有幾組變量及其類型編碼。爲了實現這個需求,這裏使用了獲取宏參數個數以及遞歸宏的技巧,請閱讀完這篇文章,瞭解C語言宏定義使用總結與遞歸宏。
最後補充好細節,使變量名稱化爲字符串做爲提示標籤,定製化使用的可選參數,這就完成了HMLog
。
總體思路很清晰,源碼也只有一個200多行的HMLog.h
文件,難點基本上只有遞歸宏的使用。除此以外值得一提的還有兩點:
float actual = (float)va_arg(v, double);
,這是由於有個規則叫默認參數提高,還有一些char、short等類型也是如此。[result appendFormat:@"%@%@\n", ((void)(valueString), HMLogPrefix(i, valueString)), obj];
這行代碼,爲了消除宏HMLogPrefix(i, valueString)
可能沒有用到valueString致使的警告,使用了逗號運算符,這是宏定義使用中經常使用的一個運算符。