iOS深思篇 | 宏定义

11,334 阅读8分钟

一. 简介

是一种批量处理的称谓,简单来说就是根据定义好的规则替换一定的文本。替换过程在程序编译期,也因此大量使用宏会造成编译时间变长;而且替换过程不进行类型安全检查;还需要注意“边缘效应”;

比如#define N 1 + 2,使用时double a = N / 2, 预期1.5,结果是2,因为在处理过程中转化为double a = 1 + 2 / 2,所以建议使用宏时加括号表明是一个整体。

想要了解宏的话得先了解一下来源,OC从C语言演变来,自然也继承了C语言的优良传统,这里简单介绍一下,C语言中预处理命令,它包括三个方面:

  1. 宏定义:#define 指令定义一个宏,#undef指令删除一个宏定义。
  2. 文件包含:#include指令指定一个文件的内容被包含到程序中。
  3. 条件编译:#if,#ifdef,#ifndef,#elif,#else和#endif指令可以根据编译器可以测试的条件来将一段文本包含到程序中或排除在程序之外。

需要注意的是预处理命令都是以符号“#”开头。

1.1 宏的分类

大部分将宏按类型分为对象宏和函数宏,也有按传入参数分为带参数的宏和不带参数的宏。

1.1.1 对象宏
#define STATUS_HEIGHT 20
1.1.2 函数宏
#define MAX(X, Y)  ((X) > (Y) ? (X) : (Y))

1.1 宏定义与常量定义的区别

#defineconst都可用来修饰常量。

  • 编译器处理方式不同   define宏是在预处理阶段展开。   const常量是编译运行阶段使用。
  • 类型和安全检查不同   define宏没有类型,不做任何类型检查,仅仅是展开。   const常量有具体的类型,在编译阶段会执行类型检查。
  • 存储方式不同   define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)   const常量会在内存中分配(可以是堆中也可以是栈中)。
  • const可以节省空间,避免不必要的内存分配。
  • 提高了效率;编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
  • 宏替换只作替换,不做计算,不做表达式求解;

1.2 宏的一些用法

1.2.1 字符化

将传入的单字符参数名转换成字符,以一对单引用括起来.

#define STRING @#s    // 's'
1.2.2 字符串化

在宏参数前加个#,那么在宏体扩展的时候,宏参数会被扩展成字符串的形式。

#define NSSTRING #str  // "str"
1.2.3 连接

如果宏体所在标示符中有##,那么在宏体扩展的时候,宏参数会被直接替换到标示符中。

#define COMMAND(PREFIX, NAME)  PREFIX##NAME  
1.2.4 换行

遇到需要换行的可以用\号连接;

#define PRINT_IF(CONDITION) \
do { if (CONDITION) \
NSLog(@"print hello"); } \
while (0)
1.2.5 变参宏(_VA_ARGS

下面是OC中自定义Log的例子:

#ifdef DEBUG
#define Log(fmt, ...) NSLog((@"%s [Line %d] " fmt), __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#define Log(...)
#endif

需要注意的是: __VA_ARGS__ : 至少传一个参数 ##__VA_ARGS__ : 随便传几个参数

二. C语言的宏

下面是一些C语言的宏:

#define        定义一个预处理宏
#undef         取消宏的定义
#include       包含文件命令
#include_next  与#include相似, 但它有着特殊的用途
#if            编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef         判断某个宏是否被定义, 若已定义, 执行随后的语句
#ifndef        与#ifdef相反, 判断某个宏是否未被定义
#elif          若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
#else          与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
#endif         #if, #ifdef, #ifndef这些条件命令的结束标志.
#defined       与#if, #elif配合使用, 判断某个宏是否被定义
#line          标志该语句所在的行号
#              将宏参数替代为以参数值为内容的字符窜常量
##             将两个相邻的标记(token)连接为一个单独的标记
#pragma        说明编译器信息
#warning       显示编译警告信息
#error         显示编译错误信息

是不是感觉很熟悉,就算在iOS开发中也经常用到里面的内容。 除了基本的宏操作还有预定义宏,预定义宏是为了方便处理一些有用的信息,里面定义了一些预处理标识符,也就是预定义宏。预定义宏的名称都是以“__”(两条下划线)开头和结尾的,如果宏名是由两个单词组成,那么中间以“_”(一条下划线)进行连接。并且,宏名称一般都由大写字符组成。 下面是常见的预定义宏:

描 述
FUNTION获取当前函数名
DATE丐前源文件的编泽口期,用 “Mmm dd yyy”形式的字符串常量表示
FILE当前源文件的名称,用字符串常量表示
LINE当前源义件中的行号,用十进制整数常量表示,它可以随#line指令改变
TIME当前源文件的最新编译吋间,用“hh:mm:ss”形式的宁符串常量表示
STDC如果今前编泽器符合ISO标准,那么该宏的值为1,否则未定义
COUNTER无重复的计数器,从程序启动开始每次调用都会++,常用语宏中定义无重复的参数名称
func所在scope的函数名称,常见于log中

三. OC相关宏的扩展

3.1 系统相关宏

描 述
__has_include用来检查是否引入了某个文件
NS_ASSUME_NONNULL_BEGIN & NS_ASSUME_NONNULL_END在这两个宏之间的代码,所有简单指针对象都被假定为nonnull
__cplusplus识别是c代码还是c++代码
__has_feature(objc_arc)判断是否是ARC,否则为MRC
@available(iOS 11, *)当前iOS11是否满足需求
TARGET_IPHONE_SIMULATOR满足条件时,执行模拟器代码;否则执行非模拟器代码
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8_0设备系统大于8.0 以上的代码
NS_REQUIRES_SUPER申明子类如果重写该方法,必须调用该父类方法
FOUNDATION_EXPORT用于定义常量,在检测值是否相等时直接比较指针,效率比较快
NS_AVAILABLE_IOS(8_0)这个方法可以在iOS3.0及以后的版本中使用,如果在比5.0更老的版本中调用这个方法,就会引起崩溃
NS_DEPRECATED_IOS(2_0, 6_0)这个方法在iOS2.0引入,6.0被删除
NS_AVAILABLE(10_8, 6_0)这个宏告诉我们这方法分别随Mac OS 10.8和iOS 6.0被引入
NS_DEPRECATED(10_0, 10_6, 2_0, 4_0)这个方法随Mac OS 10.0和iOS 2.0被引入,在Mac OS 10.6和iOS 4.0后被废弃
NS_CLASS_AVAILABLE(10_11, 9_0)这个类分别随Mac OS 10.11和iOS9.0被引入
NS_ENUM_AVAILABLE(10_11, 9_0)这个枚举分别随Mac OS 10.11和iOS9.0被引入
__IPHONE_OS_VERSION_MAX_ALLOWED允许最大的iOS版本
__IPHONE_OS_VERSION_MIN_ALLOWED最低的iOS版本

3.2 自定义的宏

/**** UI尺寸 ****/
//获取屏幕宽度与高度
#define SCREEN_WIDTH   [UIScreen mainScreen].bounds.size.width
#define SCREENH_HEIGHT [UIScreen mainScreen].bounds.size.height
//根据6,7,8适配
#define ScaleWidth(width) (width / 375.0) * SCREEN_WIDTH
#define ScaleHeight(height) (height / 667.0) * SCREENH_HEIGHT
//是否是iPhoneX
#define k1IS_iPhoneX (SCREEN_WIDTH == 375.f && SCREENH_HEIGHT == 812.f)
#define k2IS_iPhoneX  ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)

//判断是否为X系列
#define IPHONE_X \
({BOOL isPhoneX = NO;\
if (@available(iOS 11.0, *)) {\
isPhoneX = [[UIApplication sharedApplication] delegate].window.safeAreaInsets.bottom > 0.0;\
}\

// 状态栏高度
#define kStatusBarHeight        (IPHONE_X ? 44.f : 20.f)
// 顶部导航栏高度
#define kNavigationBarHeight    44.f
// 顶部安全距离
#define kSafeAreaTopHeight      (IPHONE_X ? 88.f : 64.f)
// 底部安全距离
#define kSafeAreaBottomHeight   (IPHONE_X ? 34.f : 0.f)
// Tabbar高度
#define kTabbarHeight           49.f
// 去除上下导航栏剩余中间视图高度
#define ContentHeight           (kScreenHeight - kSafeAreaTopHeight - kSafeAreaBottomHeight - kTabbarHeight)


/**** 颜色 ****/
//随机颜色
#define ZBRandomColor [UIColor colorWithRed:arc4random_uniform(256)/255.0 green:arc4random_uniform(256)/255.0 blue:arc4random_uniform(256)/255.0 alpha:1.0]
//RGB
#define ZBRGBColor(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1.0]
//RGBA
#define ZBRGBAColor(r, g, b, a) [UIColor colorWithRed:(r)/255.0 green:(r)/255.0 blue:(r)/255.0 alpha:a]
//十六进制颜色
#define ZBRGBHex(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]
//十六进制颜色,透明度
#define ZBRGBHexAlpha(rgbValue,a) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:(a)]

/**** 系统相关 ****/
//app版本号
#define DEVICE_APP_VERSION      (NSString *)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]
//app Build版本号
#define DEVICE_APP_BUILD        (NSString *)[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]
//系统版本号(string)
#define DEVICE_OS_VERSION       [[UIDevice currentDevice] systemVersion]
//系统版本号(float)
#define DEVICE_OS_VERSION_VALUE [DEVICE_OS_VERSION floatValue]
//检测是否是竖屏状态
#define IsPortrait ([UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortrait || [UIApplication sharedApplication].statusBarOrientation == UIInterfaceOrientationPortraitUpsideDown)

/**** 沙盒目录文件 ****/
//temp
#define ZBPathTemp NSTemporaryDirectory()
//Document
#define ZBPathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]
//Cache
#define ZBPathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]

/**** 数据判空 ****/
//字符串是否为空
#define kStringIsEmpty(str) ([str isKindOfClass:[NSNull class]] || str == nil || [str length] < 1 ? YES : NO )
//数组是否为空
#define kArrayIsEmpty(array) (array == nil || [array isKindOfClass:[NSNull class]] || array.count == 0)
//字典是否为空
#define kDictIsEmpty(dic) (dic == nil || [dic isKindOfClass:[NSNull class]] || dic.allKeys == 0)
//是否是空对象
#define kObjectIsEmpty(_object) (_object == nil \
|| [_object isKindOfClass:[NSNull class]] \
|| ([_object respondsToSelector:@selector(length)] && [(NSData *)_object length] == 0) \
|| ([_object respondsToSelector:@selector(count)] && [(NSArray *)_object count] == 0))

/**** 常用缩写 ****/
#define kApplication            [UIApplication sharedApplication]
#define kKeyWindow              [UIApplication sharedApplication].keyWindow
#define kAppDelegate            [UIApplication sharedApplication].delegate
#define kUserDefaults           [NSUserDefaults standardUserDefaults]
#define kNotifCenter            [NSNotificationCenter defaultCenter]

/**** 其他 ****/
//弱引用
#define ZBWeak __weak typeof(self) weakSelf = self;
#define ZBWeakSelf(type)  __weak typeof(type) weak##type = type;
//强引用
#define ZBStrongSelf(type) __strong typeof(type) type = weak##type;

//角度转换弧度
#define ZBDegreesToRadian(x) (M_PI * (x) / 180.0)
//弧度转换角度
#define ZBRadianToDegrees(radian) (radian*180.0)/(M_PI)

//block判空回调
#define ZBBlockNotEmpt(block, ...)  if (block) { block(__VA_ARGS__); }

//.h头文件中的单例宏
#define ZBSingletonH(name) + (instancetype)shared##name;

//.m文件中的单例宏
#define ZBSingletonM(name) \
static id _instance;\
+ (instancetype)allocWithZone:(struct _NSZone *)zone{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [super allocWithZone:zone];\
});\
return _instance;\
}\
+ (instancetype)shared##name{\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{\
_instance = [[self alloc] init];\
});\
return _instance;\
}\
- (id)copyWithZone:(NSZone *)zone{\
return _instance;\
}

四. 总结

介绍了这么多宏的相关知识,最后总结下对宏的几点感想:

  1. 宏直接调用方法名或者常量名称的方式易于理解,可以减少重复代码,统一规范,方便修改;
  2. 使用太多宏会增加编译时长,而且还需注意“边缘效应”,防止发生不可预期的错误;
  3. 定义宏时应遵守规范,比如宏名和参数的括号间不能有空格;定义表达式要外面用括号包裹等;

暂时先说这么多,后续还将继续更新,最后欢迎大佬们下方吹水。

学习:

GCC Macros

宏--从入门到精通

【如何正确使用const,static,extern】|那些人追的干货

const常量与define宏定义的区别

宏定义的黑魔法 - 宏菜鸟起飞手册

C语言编译预处理和条件编译执行过程的理解

C语言宏定义的几个坑和特殊用法

C语言中宏定义的使用

深入理解C语言中宏定义