本文是我在学习并理解了 RAC宏分析8:@keypath 之后写的,其实大量搬运了原文的内容,并在其基础上加了一点自己的理解。
作为一个刚入门的iOS开发者,真的是很多地方都不懂呀,上周组内同事review代码的时候,指出写KVO
的时候用@keypath
而不是写字符串,我当然是知道写字符串是不安全的,但是对于@keypath
自己没用过所以不知道有这么个东西,也不知道怎么用,当时尝试着写了一下,有.
提示,也build通过了,就觉得这个东西原来是这么用的:observe后面填入你要观察的对象,@keypath()
就是一直.
到你要观察的那个property
为止,还挺方便的。
[self.KVOController observe:audio
keyPath:@keypath(audio.property1.property2)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
//do something...
}];
直到今天在代码里发现其他人对于@keypath
的用法和我不一样,没见到任何一个和我相同用法的代码的时候,我慌了,因为在把上面的代码合入之后,期间我使用@keypath
,虽然都编译通过了,但是跑起来的时候都crash了,我是这样用的👇,观察property2
的property3
属性
[self.KVOController observe:self.property1.property2
keyPath:@keypath(self.property1.property2.property3)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
//do something...
}];
而别人是这样用的👇
[self.KVOController observe:self.property1.property2
keyPath:@keypath(self.property1.property2, property3)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
//do something...
}];
当时我就惊出了一身冷汗,完了,今日头条被我干crash了吗?赶紧打了一个最新的主分支包,测了一下,没crash?!为啥,功能也正常?!为啥表现的一切正常?悬着的心虽然放下了,但是内心有太多疑惑,我的写法看起来似乎是不正确的,但是为什么跑起来却表现正常呢?为什么后来的@keypath
也那么写却都crash了呢?大家为什么都是像这样分开写的呢👇
@keypath(self.property1.property2, property3)
经过一番查找和学习 RAC宏分析8:@keypath,RAC宏,算是对@keypath
有了一个认识,也感受到自己需要学习的东西太多了,还需要更加努力💪。
其实自己合入的那段audio
的@keypath
写的是正确的,@keypath
有两种写法,大家常用的那种是比较安全的一种用法,可以确保被观察obj
以及其被观察的property
之间的.
关系正确。而我那种写法却是运气好正好写对了,之后的写法都不对就crash了。。。
//常用用法
@keypath(obj, path)
//不常用用法
@keypath(path)
下面分析一下这个@keypath
#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
@keypath
可以根据参数个数来匹配使用keypath1
还是keypath2
,因为我的写法只有一个参数,所以使用了keypath1
,在讲解keypath1
之前,首先要复习一下逗号运算符
,逗号运算符
,从第一个表达式开始运算,直到最后一个表达式,并以最后一个表达式的结果作为整个运算的结果。
//求c的值
int a=3,b=4;
int c;
//先执行a,得3,再执行a+b,得3+4=7,以7作为整个运算的结果,并赋值给变量c
//因此c=7
c=(a,a+b);
而对于keypath1
来讲,输出的则是表达式最右边的一项strchr(# PATH, '.') + 1
,所以我们先讲右边这一项,左边这一项主要用来做.
语法提示,后面讲。
第一部分#PATH
,这个#
操作符可以在预处理阶段将后面的宏参数展开成一个C的字符串常量,比如:
#define ZZ_STR(s) # s
NSString *str = @ZZ_STR(大家好);
NSLog(@"%@", str);//输出 : 大家好
@
, @keypath
加这个@
是因为宏定义keypath
生成了一个C字符串,加上@
即OC字符串,就像上面的ZZ_STR(s) # s
=> @ZZ_STR(s)
=> @#s
=> @"s表示的字符串"
。
关于keypath1
的strchr(# PATH, '.')
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
strchar
函数是C中<string.h>
中定义的函数,用来查找某字符在字符串中首次出现的位置,其原型为:
char * strchr (const char *str, int c);
【参数】str
为字符串,c
为要查找的字符。strchr()
将会找出 str
字符串中第一次出现的字符 c
的地址,然后将该地址返回。
注意:字符串 str
的结束标志 NUL
也会被纳入检索范围,所以 str
的组后一个字符也可以被定位。
【返回值】如果找到指定的字符则返回该字符所在地址,否则返回 NULL
。
返回的地址是字符串在内存中随机分配的地址再加上你所搜索的字符在字符串位置。设字符在字符串中首次出现的位置为i
,那么返回的地址可以理解为 str + i
。
示例:
#include <stdio.h>
#include <string.h>
int main(int argc, const char * argv[]) {
const char *s = "Hello world, hello china!";
char *p = strchr(s, 'e');
printf("%p\n", s); // 0x100000f8c
printf("%p\n", p); // 0x100000f8d
printf("%s\n", p); // ello world, hello china!
return 0;
}
也就是说,strchr(str, 'c')
, 返回的是一个C字符串,这个字符串从找到str
中为c
的字符开始往后:
#define ZZ_KEY_PATH(PATH) \
strchr(# PATH, '.')
int main(int argc, const char * argv[]) {
printf("%s\n", strchr("Hello.china.shanghai", '.')); // .china.shanghai
printf("%s\n", ZZ_KEY_PATH(Hello.china.shanghai)); // .china.shanghai
return 0;
}
再来看keypath1
的宏定义:
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
我们先把它简化一下为:
#define keypath1(PATH) \
strchr(# PATH, '.') + 1)
#include <stdio.h>
#include <string.h>
#define keypath1(PATH) \
strchr(# PATH, '.') + 1
int main(int argc, const char * argv[]) {
printf("%s\n", keypath1(Hello.china.shanghai)); // china.shanghai
return 0;
}
关于代码提示:
代码提示其实就是用到了宏里面逗号左边那一项
(void)(NO && ((void)PATH, NO))
//首先计算括号内,其实也主要是括号内这部分起到.语法的作用
//PATH这个表达式就是property1.property2.....
((void)PATH, NO)
//对于keypath2,是同样的道理,在宏里使用了.语法,使得编译器可以对表达式作出语法提示和补全
((void)OBJ.PATH, NO))
关于void和NO
加void
是为了防止逗号表达式warning, 例如:
int a = 0;
int b = 1;
int c = (a,b);
//由于a没有被用到,所以会有警告。但是写成如下的样子就不会出现警告了:
int c = ((void)a,b);
所以上面加了几个void
就是为了防止出现warning。
加NO
是C语言判断条件短路表达式。增加NO &&
以后,预编译的时候看见了NO
,就会很快的跳过判断条件。即:
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
其最终结果相当于:# PATH
其实对于NO
我还是稍微有点不确定,我认为这应该只是为了在预编译的时候提高一点点速度吧,毕竟在实际运算的时候会先计算括号内部的((void)OBJ.PATH, NO))
来达到语法提示的目的。
最后回顾一下自己写的@keypath为什么有的没有crash有的却crash了
//没crash,@keypath(audio.property1.property2) 根据keypath1解析
//取第一个.操作符后面并作为C字符串返回,即:@"property1.property2",正好OK
[self.KVOController observe:audio
keyPath:@keypath(audio.property1.property2)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
//do something...
}];
//crash了,@keypath(self.property1.property2.property3) 根据keypath1解析
//取第一个.操作符后面并作为C字符串返回,即:@"property1.property2.property3"
//而对于self.property1.property2,哪里来的@"property1.property2.property3"这样的path啊,可不就crash了嘛
[self.KVOController observe:self.property1.property2
keyPath:@keypath(self.property1.property2.property3)
options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
//do something...
}];
以上是本人对@keypath
的粗浅理解,如果有什么地方不对还望指出。