玩转iOS“宏定义”

玩转iOS“宏定义”

       宏定义在C类语言中很是重要,由于宏是一种预编译时的功能,所以其能够比运行时更高层面的对程序流程进行控制。在初学宏定义的时候,你们可能都会有这样一种感受:就是彻底替换么,太简单了。但若是你真这么想,那你就太天真了,不说本身编写宏,在Foundation框架中内置定义的许多宏要看明白也要费一番脑筋。本篇博客,总结了前辈的经验,同时收集了一些编写很是巧妙的宏进行分析,但愿能够帮助你们对宏定义有更加深入的理解,而且能够将心得应用于实际开发中。编程

1、准备

      宏的本质是预编译时的替换,在开始正文以前,咱们须要先介绍一种观察宏替换后结果的方法,这样帮助咱们更方便的对宏最终的结果进行验证与测试。Xcode开发工具自带查看预编译结果的功能,首先须要对工程编译一遍,以后选择工具栏中的Assistant选项,打开助手窗口,以下图所示:安全

以后选择窗口的Preprocess选项,便可打开预编译结果窗口,能够看到,宏被替换后的最终结果,以下图所示:框架

后面,咱们将使用这种方式来对编写的宏进行验证。函数

2、关于“宏定义”

      宏使用#define来进行定义,宏定义分为两种,一种是对象式宏,一种是函数式宏。对象式宏一般对来定义量值,在预编译时,直接将宏名替换成对应的量值,函数式宏在定义时能够设置参数,其做用与函数很相似。工具

例如,咱们能够将π的值定义成一个对象式宏,在使用的时候,用有意义的宏名要比直接使用π的字面值方便不少,例如:学习

#import <Foundation/Foundation.h>
#define PI 3.1415926
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = PI * 3;
        NSLog(@"%f", res);
    }
    return 0;
}

函数式宏要更加灵活一些,例如对圆面积计算的方法,咱们就能够将其定义成一个宏:开发工具

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1);
        NSLog(@"%f", res);
    }
    return 0;
}

如今,有了这个面积计算宏咱们能够更加方便的计算圆的面积了,看上去很完美,后面咱们就使用这个函数式宏为例,来深刻理解宏的原理。测试

3、从一个简单的函数式宏提及

     再来看下上面咱们编写的计算面积的宏,正常状况下好像没什么问题,可是须要注意,归根结底宏并非函数,若是彻底把其做为函数使用,咱们就可能会陷入一系列的陷阱中,好比这样使用:优化

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1 + 1);
        NSLog(@"%f", res);
    }
    return 0;
}

运行代码,运算的结果并非半径为2个圆的面积,哪里出了问题呢,咱们仍是先看下宏预编译后的结果:ui

CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;

一目了然了,因为运算符的优先级问题致使了运算顺序错误,在编程中,全部运算符优先级产生的问题均可以使用一种方式解决:用小括号。对CircleArea宏进行一下改造,以下:

#define CircleArea(r) (PI * (r) * (r))

对执行顺序进行了强制的控制,代码执行又恢复了正常,看上去好像是没有问题了,如今就满意了还为时过早,例以下面这样使用这个宏:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) PI * (r) * (r)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

运行,发现结果又错了,不只计算结果与咱们的预期不符,变量自加的的结果也不对了,咱们检查其展开的结果:

CGFloat res = 3.1415926 * (r++) * (r++);

原来问题出在这里,宏在展开的时候,将参数替换了两次,因为参数自己是一个自加表达式,因此被自加了两次,产生了问题,那么这个问题怎么解决呢,C语言中有一种颇有用的语法,即便用大括号定义代码块,代码块会将最后一条语句的执行结果返回,修改上面宏定义以下:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

此次程序又恢复的了正常。可是,若是若是在调用宏是变量的名字与宏内的临时变量产生了重名,灾难就又发生了,例如:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int _r = 1;
        CGFloat res = CircleArea(_r);
        NSLog(@"%f, %d", res, _r);
    }
    return 0;
}

运行上面代码,会发现宏内的临时变量没有被初始化成功。这确实难受,咱们在进一步,好比对临时变量的名字作一些手脚,将其命名为极其不容易重复的名字,其实系统内置的一个宏就是专门用来构造惟一性变量名的:__COUNTER__,这个宏是一个计数器,在编译的时候会自动进行累加,再次对咱们编写的宏进行改造,以下:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define PAST(A, B) A##B
#define CircleArea(r)   __CircleArea(r, __COUNTER__)
#define __CircleArea(r, v)      \
({                              \
    typeof(r) PAST(_r, v) = r;         \
    (PI * PAST(_r, v) * PAST(_r, v));     \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = CircleArea(_r);
        CGFloat res2 = CircleArea(_r);
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

这里改造后,咱们的宏就没有那么容易理解了,首先__COUNTER__在每次宏替换时都会进行自增,##是一种宏中专用的特殊符号,用来将参数拼接到一块儿,可是须要注意,使用##符号拼接的若是是另一个宏,则其会阻止宏的展开,所以咱们定义了一个转换宏PAST(A, B)来处理拼接。若是你一会儿不能理解为何这样就能够解决宏展开的问题,你只须要记住这样一条宏展开的原则:若是形参有使用#或##这种处理符号,则不会进行宏参数的展开,不然先展开宏参数,在展开当前宏。上面代码最终预编译的结果以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });
        CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

一个简单的计算圆面积的宏,为了安全,咱们就进行了这么多的处理,看来要用好宏,的确不容易。

4、编写宏时的好习惯

      经过前面的介绍,咱们知道,若是随随意意的编写一个宏是很是不负责任的,看上去好像没问题与在任何场景下使用都没有问题是彻底不一样的。在编写宏时,咱们能够刻意的去培养这样几个编码习惯:

  • 参数与计算结果要加小括号

      这条原则应该没必要多说了,前面的示例中就有演示,完整的添加小括号能够避免不少因为运算符优先级形成的异常问题。

  • 多语句功能性宏,要使用do-while包裹

      这条原则看上去有些莫名其妙,可是其很是重要,例如,咱们须要编写一个自定义的LOG宏,在进行打印时添加一些自定义的信息,你或许会这样写:

#define LOG(string)     \
NSLog(@"自定义的信息");   \
NSLog(string);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LOG(@"info")
    }
    return 0;
}

运行代码,目前貌似没有问题,可是若是其和if语句进行结合,可能问题就来了:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"info")
    }
    return 0;
}

运行代码,仍是有一行LOG信息被输出了,看下其预编译后的结果以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            NSLog(@"自定义的信息"); NSLog(@"info");
    }
    return 0;
}

找到问题了,因为if结构若是不加大括号进行规范,其默认做用域只有一句代码,多写大括号是不会出问题,所以编写多语句宏时,加上大括号是一个好习惯,以下:

#define LOG(string)     \
{NSLog(@"自定义的信息");   \
NSLog(string);}

这样解决了问题,可是并不完美,假设在使用时这样写:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO");
        else
            LOG(@"YES");
    }
    return 0;
}

结果发现仍是会报错,是因为分号捣的鬼,预编译结果以下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            {NSLog(@"自定义的信息"); NSLog(@"NO");};
        else
            {NSLog(@"自定义的信息"); NSLog(@"YES");};
    }
    return 0;
}

咱们知道,像if,while,for这种语法结构块的大括号后是不须要分号的,咱们为了兼容单行if语句因为宏的缘由被展开成多行的问题强行加了一个大括号上去,就产生这样的问题了,解决它的一个好方法是真的将多行的宏转化成单语句,do-whlie结构就能够实现这种效果,修改宏以下:

#define LOG(string)     \
do {NSLog(@"自定义的信息");   \
NSLog(string);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO")
        else
            LOG(@"YES");
    }
    return 0;
}

预编译后:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            do {NSLog(@"自定义的信息"); NSLog(@"NO");} while(0);
        else
            do {NSLog(@"自定义的信息"); NSLog(@"YES");} while(0);;
    }
    return 0;
}

如今,不管外面怎么使用,这个宏均可以正常工做了。

  • 对于不定参数的宏,借助##符号来拼接参数

      在定义函数时,咱们能够定义函数的参数为不定个数参数,定义函数式宏时也相似,使用符号"..."能够指定不定个数参数,例如对LOG宏进行调整,以下:

#define LOG(format, ...)     \
do {NSLog(@"自定义的信息");   \
NSLog(format, __VA_ARGS__);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d", NO)
        else
            LOG(@"%d", YES);
    }
    return 0;
}

__VA_ARGS__也是一个内置的宏符号,则做用是表明宏定义中的可变参数“...”,须要注意,若是按照上面的写法,若是咱们传入的可变参数为0个,会产生问题,其缘由也是因为多了一个逗号,例如:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d") // 这里会被预编译成NSLog(@"%d", )
        else
            LOG(@"%d", YES);
    }
    return 0;
}

解决方案是对可变参数进行一次##拼接,宏在使用##符号进行参数拼接时,若是后面的参数为空,其会自动将前面的逗号去掉,以下:

#define LOG(format, ...)     \
do {NSLog(@"自定义的信息");   \
NSLog(format, ##__VA_ARGS__);} while(0);

5、特殊的宏符号与经常使用内置宏

      有几个特殊的符号可让宏定义变得很是灵活,经常使用的特殊符号和特殊宏列举以下:

  • #

      井号的做用是将参数字符串化,例如:

#define Test(p) #p

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(abc); // 预编译后成为  "abc";
    }
    return 0;
}
  • ##

      双井号咱们前面有使用过,其做用是对参数进行拼接,例如:

#define Test(a,b) a##b

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(1,2); // 预编译后成为  12;
    }
    return 0;
}
  • __VA_ATGS__

      可变参数宏中专用,表示全部传入的可变参数。

  • __COUNTER__

      一个累加计数宏,经常使用来构造惟一变量名。

  • __LINE__

      记录LOG信息时,经常使用的一个内置宏,预编译时会将其替换为当前的行号。

  • __FILE__

      记录LOG信息时,经常使用的一个内置宏,预编译时会将其替换为当前文件的全路径。

  • __FILE_NAME__

      记录LOG信息时,经常使用的一个内置宏,预编译时会将其替换为当前的文件名。

  • __DATE__

      记录LOG信息时,经常使用的一个内置宏,预编译时会将其替换为当前日期。

  • __TIME__

      记录LOG信息时,经常使用的一个内置宏,预编译时会将其替换为当前时间。

6、宏的展开规则

      经过前面的介绍,对于应用宏咱们已经没有太大的问题,而且也了解了不少宏的使用技巧。这一小节将更深刻的对宏的替换规则进行讨论。宏自己是支持嵌套的,例如:

#define M1(A) M2(A)
#define M2(A) A
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1);
    }
    return 0;
}

上面代码中定义的两个宏基本上是没有意义的,M1宏替换后的结果是M2宏,M2宏最终被替换为参数自己,从这个例子能够看出,宏是能够嵌套递归展开的,可是递归展开是有原则,不会出现无限递归,例如:

#define M1(A) M2(A)
#define M2(A) M1(A)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1); // 最终展开为 M1(1)
    }
    return 0;
}

宏的展开须要符合下面原则:

  1. 在展开宏的过程当中会先将参数进行展开,若是使用##对参数进行了拼接或使用#进行了处理,则此参数不会被展开。
  2. 在宏的展开过程当中,若是替换列表中出现了要被展开的宏,则此宏不会被展开。

上面的展开原则提到了替换列表,宏在展开过程当中会维护一个替换列表,展开的过程当中须要从参数到宏自己,从外层宏到内层宏一层一层的替换,每次替换的时候都会将被替换的宏名放入维护的替换列表中,再下一轮替换中,若是再次出现替换列表中出现过的宏名,则不会被再次替换。以咱们上面的代码为例进行分析:

  1. 首先M1宏在第一轮替换后,被替换成了M2,此时替换列表中放入宏名M1。
  2. M2依然是一个宏名,第二轮对M2进行替换,将其替换为M1,再次将M2放入替换列表,此时替换列表中有宏名M1和M2。
  3. M1依然是宏名,可是替换列表中已经存在M1,此宏名再也不展开。

7、宏的妙用

      这一小节,咱们要转身成为鉴赏家,来对不少实用的宏的巧妙案例进行分析与鉴赏。从这些优秀的使用案例中,能够扩宽咱们对宏使用的思路。

  1. MIN与MAX

      Foundataion内置了一些经常使用的运算宏,如获取两个数的最大值、最小值、绝对值等等。以MAX宏为例,这个宏的编写基本涵盖了函数式宏全部要注意的点,以下:

#define __NSX_PASTE__(A,B) A##B
#if !defined(MAX)
    #define __NSMAX_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__b,L) : __NSX_PASTE__(__a,L); })
    #define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)
#endif

其中__NSMAX_IMPL__宏借助计数__COUNTER__和拼接__NSX_PASTE__宏来构造惟一的内部变量名,咱们前面提供的示例宏的写法也基本是参照这个系统宏来的。后面你们在编写函数式宏的时候,均可以参照下这个宏的实现。

2. NSAssert等

      NSAssert是断言宏,在开发调试中常常会使用断言来进行安全保障,这个宏的定义以下:

#define NSAssert(condition, desc, ...)	\
    do {				\
	__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
	if (__builtin_expect(!(condition), 0)) {		\
            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)

NSAssert宏定义中使用到了不定参数拼接消除逗号的技巧,而且是多行宏语句使用do-while进行优化的一个实践。

3. @weakify与@strongify

        weakify与strongify是ReactCocoa中经常使用的两个宏,用来处理循环引用问题。这两个宏的定义很是巧妙,以weakify宏为例,要看懂这个宏并非十分简单,首先与这个宏相关的宏定义列举以下:

#if DEBUG
#define rac_keywordify autoreleasepool {}
#else
#define rac_keywordify try {} @catch (...) {}
#endif

#define rac_weakify_(INDEX, CONTEXT, VAR) \
CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)

#define metamacro_argcount(...) \
metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at(N, ...) \
metamacro_concat(metamacro_at, N)(__VA_ARGS__)

#define metamacro_concat(A, B) \
metamacro_concat_(A, B)

#define metamacro_concat_(A, B) A ## B

#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

#define metamacro_head_(FIRST, ...) FIRST

其中rac_keywordify区分DEBUG和RELEASE环境,在DEBUG环境下,其其实是建立了一个无用的autoreleasepool,消除前面的@符号,在RELEASE环境下,其会建立一个try-catch结构,用来消除参数警告。metamacro_foreach_cxt宏比较复杂,其展开过程以下:

// 第一步: 原始宏
metamacro_foreach_cxt(rac_weakify_,, __weak, obj)
// 第二步: 展开metamacro_foreach_cxt
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(obj))(rac_weakify_,, __weak, obj)
// 第三步: 展开metamacro_argcount      
metamacro_concat(metamacro_foreach_cxt, metamacro_at(20, obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第四步: 展开metamacro_at       
metamacro_concat(metamacro_foreach_cxt,metamacro_concat(metamacro_at, 20)(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第五步:展开metamacro_concat       
metamacro_concat(metamacro_foreach_cxt,metamacro_at20(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// 第六步:展开metamacro_at20        
metamacro_concat(metamacro_foreach_cxt,metamacro_head(1))(rac_weakify_,, __weak, obj)
// 第七步:展开metamacro_head        
metamacro_concat(metamacro_foreach_cxt,metamacro_head_(1, 0))(rac_weakify_,, __weak, obj)
// 第八步:展开metamacro_head_      
metamacro_concat(metamacro_foreach_cxt,1)(rac_weakify_,, __weak, obj)
// 第九步:展开metamacro_concat        
metamacro_foreach_cxt1(rac_weakify_,, __weak, obj)
// 第十步:展开metamacro_foreach_cxt1
rac_weakify_(0, __weak, obj)
// 第十一步:展开rac_weakify_
__weak __typeof__(obj) metamacro_concat(obj, _weak_) = (obj);
// 第十二步:展开metamacro_concat        
__weak __typeof__(obj) obj_weak_ = (obj);

strongify宏的展开与之相似。

4. ParagraphStyleSet宏

      ParagraphStyleSet宏是YYLabel中提供的一个设置属性字符串ParagraphStyle相关属性的快捷方法,其中使用到的一个技巧是直接使用宏的形参做为属性名进行使用,使得各类属性的设置都使用同一个宏便可完成,其定义以下:

#define ParagraphStyleSet(_attr_) \
[self enumerateAttribute:NSParagraphStyleAttributeName \
                 inRange:range \
                 options:kNilOptions \
              usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \
                  NSMutableParagraphStyle *style = nil; \
                  if (value) { \
                      if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \
                          value = [NSParagraphStyle yy_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \
                      } \
                      if (value. _attr_ == _attr_) return; \
                      if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \
                          style = (id)value; \
                      } else { \
                          style = value.mutableCopy; \
                      } \
                  } else { \
                      if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \
                      style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \
                  } \
                  style. _attr_ = _attr_; \
                  [self yy_setParagraphStyle:style range:subRange]; \
              }];

8、结语

      宏看上去简单,可是真的用好用巧却并不容易,我想,最好的学习方式就是在实际应用中不断的使用,不断的琢磨与优化。若是能将宏的使用得心应手,必定会为你的代码能力带来质的提高。

专一技术,热爱生活,交流技术,也作朋友。

——珲少 QQ群:805263726