自动格式化打印变量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致使的警告,使用了逗号运算符,这是宏定义使用中经常使用的一个运算符。

参考资料

相关文章
相关标签/搜索