玩转iOS“宏定义”(二)

简介: 玩转iOS“宏定义”

四、编写宏时的好习惯


     通过前面的介绍,我们知道,如果随随意意的编写一个宏是非常不负责任的,看上去好像没问题与在任何场景下使用都没有问题是完全不同的。在编写宏时,我们可以刻意的去培养这样几个编码习惯:


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

     这条原则应该不必多说了,前面的示例中就有演示,完整的添加小括号可以避免很多由于运算符优先级造成的异常问题。


多语句功能性宏,要使用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);

五、特殊的宏符号与常用内置宏


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


#

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


#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信息时,常用的一个内置宏,预编译时会将其替换为当前时间。


六、宏的展开规则


     通过前面的介绍,对于应用宏我们已经没有太大的问题,并且也了解了很多宏的使用技巧。这一小节将更深入的对宏的替换规则进行讨论。宏本身是支持嵌套的,例如:


#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;

}

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


在展开宏的过程中会先将参数进行展开,如果使用##对参数进行了拼接或使用#进行了处理,则此参数不会被展开。

在宏的展开过程中,如果替换列表中出现了要被展开的宏,则此宏不会被展开。

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


首先M1宏在第一轮替换后,被替换成了M2,此时替换列表中放入宏名M1。

M2依然是一个宏名,第二轮对M2进行替换,将其替换为M1,再次将M2放入替换列表,此时替换列表中有宏名M1和M2。

M1依然是宏名,但是替换列表中已经存在M1,此宏名不再展开。

七、宏的妙用


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


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]; \

             }];

八、结语


     宏看上去简单,但是真的用好用巧却并不容易,我想,最好的学习方式就是在实际应用中不断的使用,不断的琢磨与优化。如果能将宏的使用驾轻就熟,一定会为你的代码能力带来质的提升。

目录
相关文章
|
iOS开发
iOS宏定义里面多参数输入
想实现类似NSLog那样支持多参数,和格式化等逻辑处理的宏
113 0
|
iOS开发
iOS常用的宏定义
iOS常用的宏定义
417 0
|
iOS开发
iOS 宏定义应用
iOS 宏定义应用
138 0
|
安全 开发工具 vr&ar
玩转iOS“宏定义”(一)
玩转iOS“宏定义”
362 0
玩转iOS“宏定义”(一)
|
API iOS开发
iOS宏定义的使用与规范
http://my.oschina.net/leejan97/blog/354904 宏定义在很多方面都会使用,例如定义高度、判断iOS系统、工具类,还有诸如文件路径、服务端api接口文档。为了对宏能够快速定位和了解其功能,我们最好在定义的时候将其放入特定的头文件中,下面我抛砖引玉,对一些常用的宏进行分类、分文件定义,希望对大家有所帮助。
738 0
|
26天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
94 1
|
1月前
|
设计模式 安全 Swift
探索iOS开发:打造你的第一个天气应用
【9月更文挑战第36天】在这篇文章中,我们将一起踏上iOS开发的旅程,从零开始构建一个简单的天气应用。文章将通过通俗易懂的语言,引导你理解iOS开发的基本概念,掌握Swift语言的核心语法,并逐步实现一个具有实际功能的天气应用。我们将遵循“学中做,做中学”的原则,让理论知识和实践操作紧密结合,确保学习过程既高效又有趣。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你打开一扇通往iOS开发世界的大门。
|
1月前
|
搜索推荐 IDE API
打造个性化天气应用:iOS开发之旅
【9月更文挑战第35天】在这篇文章中,我们将一起踏上iOS开发的旅程,通过创建一个个性化的天气应用来探索Swift编程语言的魅力和iOS平台的强大功能。无论你是编程新手还是希望扩展你的技能集,这个项目都将为你提供实战经验,帮助你理解从构思到实现一个应用的全过程。让我们开始吧,构建你自己的天气应用,探索更多可能!
54 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
1天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第28天】在这篇技术性文章中,我们将一起踏上一段探索iOS开发的旅程。无论你是刚入门的新手,还是希望提升技能的开发者,这篇文章都将为你提供宝贵的指导和灵感。我们将从基础概念开始,逐步深入到高级主题,如设计模式、性能优化等。通过阅读这篇文章,你将获得一个清晰的学习路径,帮助你在iOS开发领域不断成长。
15 2