好文,转自:http://nathanli.cn/2015/12/14/objective-c-元编程实践-分类动态属性/
背景
分类,在 iOS 开发中,是常常需要用到的。在分类里添加属性也是常有的事,但分类中无法添加实例变量,编译器也无法为提供分类中属性的 getter
和 setter
方法了。一般而言,需要手动来实现这两个方法,如果只是用来存储变量的话,关联对象很容易做到这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@interface
NSObject
(
db_sqlite
)
@property
(
nonatomic
,
assign
)
int
db_rowid
;
@end
@implementation
NSObject
(
db_sqlite
)
-
(
int
)
db
_
rowid
{
return
[
objc_getAssociatedObject
(
self
,
_cmd
)
intValue
]
;
}
-
(
void
)
setDb_rowid
:
(
int
)
db
_
rowid
{
objc_setAssociatedObject
(
self
,
@selector
(
db_rowid
)
,
@
(
db_rowid
)
,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
;
}
@end
|
这是很常见的实现方式。
要是再给这个分类多加几个属性,也就得再多加几个这样的 getter
、setter
方法,无法也就是方法名字、关联参数不一样罢了,这可真是个体力活呀!要是能像普通类属性那样就好了,自动给生成这两个方法,想想就爽。
要想做到自动生成这两个方法,可以从两个方面入手:
1、编码期
2、运行期
编码期。在写代码的时候要做到自动生成方法,可以写一个 XCode 插件,一按某些快捷键,相应代码就自动生成了,这有点类似于 Eclipse。插件的讨论不在本文范围内。
运行期。在编码阶段只需少量代码,具体的方法则在运行期动态生成。本文研究怎么在运行期动态生成这些所需要的方法。本文最终生成的代码在:https://github.com/NathanLi/iOSCategoryPropertyDynamicSupport
需求
简单点说,就是在运行时能生成分类中属性相应的 getter
setter
方法,也就是模仿类中普通 @property 定义的属性。这样的需求太泛,咱们先来细化一下:
1、只生成分类中的,我想要的 getter
和 setter
方法体;
2、属性类型:支持基本数据类型、对象、结构体,且自动存取;
3、支持 @property
定义中的 assign
、strong
、copy
、weak
;
4、支持 @property
中的自定义的方法名;
5、支持 KVC
;
6、支持 KVO
;
7、本条不是需求,而是简单的设定:不支持原子即 atomic,只支持 nonatomic。
实现
1、确定要动态生成方法的属性
这里根据属性的名字来确定是否需要动态生成方法,就以 nl_
为前辍就好了:
1
2
|
@property
(
nonatomic
,
strong
)
id
nl_object
;
|
由于是在分类中,且没有定义相应的方法,所以会有警告:
1
2
3
|
Property
'nl_object'
requires
method
'nl_object'
to
be
defined
-
use
@dynamic
or
provide
a
method
implementation
in
this
category
Property
'nl_object'
requires
method
'setNl_object:'
to
be
defined
-
use
@dynamic
or
provide
a
method
implementation
in
this
category
|
在分类实现里加个 @dynamic
就好了:
1
2
|
@dynamic
nl_double
;
|
2、消息
@dynamic
告诉编译器,这两个方法有没有实现你都不用管,就当作它们存在就行了。那么问题来了,这两个方法明明没有实现,却依然能够调用呢?
这根消息发送机制有关。在 Objective-C中,消息不会与方法实现绑定,而是在运行时才关联起来的。
编译器会把所有消息转换为一个函数调用:objc_msgSend
。这个函数总得知道是 谁
发送了 哪条
消息吧,所以它最少也有两个参数——消息的接收者 和 消息名(即选择子 SEL
),比如下面这个方法:
1
2
|
[
receiver
message
]
|
编译器就会把它变成这个样子:
1
2
|
objc_msgSend
(
receiver
,
message
)
|
如果消息有参数的话,就会直接传这个函数,所以这个函数的参数个数不定:
1
2
|
objc_msgSend
(
receiver
,
selector
,
arg1
,
arg2
,
.
.
.
)
|
objc_msgSend
的工作就是消息的动态绑定:
1、根据 selecotr
和 receiver
找到对应的函数实现(也就是函数地址)。不同的类可以有相同的方法,但是它们所对应的函数地址是不一样的。
2、调用找到的函数,并传入相应的参数:receiver
、selector
、arg1
…。
3、返回调用的函数的返回值。
来看下实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@implementation
NLPerson
-
(
instancetype
)
init
{
if
(
self
=
[
super
init
]
)
{
[
self
setName
:
@"name"
]
;
}
return
self
;
}
-
(
void
)
setName
:
(
NSString
*
)
name
{
_name
=
name
;
}
@end
|
所对应的函数代码是这样的:
1
2
3
4
5
6
7
8
9
10
11
|
static
instancetype
_I_NLPerson_init
(
NLPerson
*
self
,
SEL
_cmd
)
{
if
(
self
.
.
.
)
{
objc_msgSend
(
(
id
)
self
,
sel_registerName
(
"setName:"
)
,
"name"
)
;
}
return
self
;
}
static
void
_I_NLPerson_setName_
(
NLPerson
*
self
,
SEL
_cmd
,
NSString
*name
)
{
.
.
.
}
|
可以看到, [self setName:@"name"]
,最后变成了 objc_msgSend
函数调用。这个函数最终会根据 self
和 setName:
找到函数 _I_NLPerson_setName_
并调用。被调用的函数包含三个参数,分别是调用者、SEL
(_cmd
)和方法参数。
正如上那个函数看到的,每个方法都有一个选择子这个参数:_cmd
,所以才能这么打印方法名:
1
2
3
4
5
|
-
(
void
)
setName
:
(
NSString
*
)
name
{
NSLog
(
@"%s"
,
sel_getName
(
_cmd
)
)
;
_name
=
name
;
}
|
SEL
实际上就是一个字符串:cahr *
,所以咱们将 SEL
简单理解为方法名也并无不可。刚刚说到了,objc_msgSend
会根据 SEL
找到对应的函数地址,来看看它是怎么找的。
实际上,OC 中的所有对象和类,最后都被处理为结构体。对象结构体中,会有一个 isa
指针,指向自己的类结构体。而类结构体有很多类信息,其中两个:
1、指向 superclass
的指针。
2、类分发表。这个表里存储了方法名 selector
与所对应的函数地址 address
。
如上面的 NLPeson
类中的分发表:
selector | addrss |
---|---|
init | _I_NLPerson_init |
setName: | _I_NLPerson_setName_ |
… | … |
消息传递框架图:
当发送一个消息给一个对象时,首先会去这个对象的 isa
所指向的类结构体里的分发表中寻找 selector
,如果找不到的话,objc_msgSend
会根据 superclass
指针找到下一个结构体里寻找 selector
,直到 NSObject
。只要它找到了 selector
,就会调用相应的函数了。意思就是说,先通过消息名,找到函数地址,再调用。这就是所谓的消息运行时动态绑定。
3、动态增加方法
如果给对象发送了一个未知的消息,如果这个对象无法响应或转发的话,就会调用 doesNotRecognizeSelector:
方法,这个方法会抛出 NSInvalidArgumentException
异常。如果你不想让一个让别人调用你的类的 copy
或 init
方法的话,可以这么做:
1
2
3
4
|
-
(
id
)
copy
{
[
self
doesNotRecognizeSelector
:
_cmd
]
;
}
|
但在调用这个方法之前,系统还是给了我们处理的机会。实现 resolveInstanceMethod:
方法,能动态地给实例方法和类方法添加一个实现。
Objective-C 中的方法所对应的函数最少有两个参数:self
和 _cmd
。如下所示:
1
2
3
4
|
void
dynamicMethodIMP
(
id
self
,
SEL
_cmd
)
{
// implementation ....
}
|
可以用 C 函数 class_addMethod
将其作为一个方法动态加到一个类中:
1
2
3
4
5
6
7
8
9
10
11
|
@implementation
MyClass
+
(
BOOL
)
resolveInstanceMethod
:
(
SEL
)
aSEL
{
if
(
aSEL
==
@selector
(
resolveThisMethodDynamically
)
)
{
class_addMethod
(
[
self
class
]
,
aSEL
,
(
IMP
)
dynamicMethodIMP
,
"v@:"
)
;
return
YES
;
}
return
[
super
resolveInstanceMethod
:aSEL
]
;
}
@end
|
4、属性元数据、类型编码(Type Encodings)
要能动态生成属性的方法,首先得知道属性的一些基本信息:类型、方法名、是 weak
还是 strong
等。这些数据都可以在运行时获得到。要用到的技术是:类型编码(Type Encdoings)。
类型编码是 runtime 的辅助工具。编译器会将类型用字符串来表示。可以用 @encode
得到这个字符串:
1
2
3
4
|
char
*buf1
=
@encode
(
int
)
;
// buf1 --> "i"
char
*buf2
=
@encode
(
long
long
)
;
// buf2 --> "q"
char
*buf3
=
@encode
(
unsigned
int
)
;
// buf2 --> "I"
|
编码表如下:
这是描述类型的数据。那描述属性的呢?
编译器会将类、分类和协议中的属性以元数据信息存起来。有一系列的 C 函数来访问这些数据。
属性元数据是用结构体 Property
来描述的:
1
2
|
typedef
struct
objc_property
*Property
;
|
可以用 class_copyPropertyList
和 protocol_copyPropertyList
来分别获取类(包含分类)中和协议中的属性:
1
2
3
|
objc_property_t
*class_copyPropertyList
(
Class
cls
,
unsigned
int
*outCount
)
objc_property_t
*protocol_copyPropertyList
(
Protocol
*proto
,
unsigned
int
*outCount
)
|
比如下面声明的这个类:
1
2
3
4
5
6
7
8
|
@interface
Person
: NSObject
@property
(
nonatomic
,
assign
)
float
age
;
@end
.
.
.
@dynamic
age
;
|
可以这么来获取它的属性列表:
1
2
3
4
|
id
PersonClass
=
objc_getClass
(
"Person"
)
;
unsigned
int
outCount
;
objc_property_t
*properties
=
class_copyPropertyList
(
PersonClass
,
&
outCount
)
;
|
除了一次性获得所有属性列表外,还有方法 class_getProperty
和 protocol_getProperty
可以通过属性名获取单个属性:
1
2
3
|
objc_property_t
class_getProperty
(
Class
cls
,
const
char
*name
)
objc_property_t
protocol_getProperty
(
Protocol
*proto
,
const
char
*name
,
BOOL
isRequiredProperty
,
BOOL
isInstanceProperty
)
|
获取到属性结构体后,就可以拿到这个属性的名字和元信息:
1
2
3
|
const
char
*property_getName
(
objc_property_t
property
)
// 获取属性的名字
const
char
*property_getAttributes
(
objc_property_t
property
)
// 获取属性的元信息
|
property_getAttributes
能获取到属性的很多信息,包括刚看到的类型编码、getter
和setter
方法名、对应的实例变量名等等。打印所有属性的元信息例子:
1
2
3
4
5
6
7
8
9
|
id
PersonClass
=
objc_getClass
(
"Person"
)
;
unsigned
int
outCount
,
i
;
objc_property_t
*properties
=
class_copyPropertyList
(
PersonClass
,
&
outCount
)
;
for
(
i
=
0
;
i
<
outCount
;
i
++
)
{
objc_property_t
property
=
properties
[
i
]
;
fprintf
(
stdout
,
"%s %s\n"
,
property_getName
(
property
)
,
property_getAttributes
(
property
)
)
;
// 输出:age Tf,D,N
}
|
property_getAttributes
获取到的 Tf,D,N
是什么意思呢?Tf
,是以 T
开头,后面的字符串 f
表示类型编码;D
表示 @dynamic
;N
表示 nonatomic
。这些都是属性本身的信息,以 ,
分割。这些字符串的规则是这样的:
Code | 意义 |
---|---|
R | readonly |
C | copy |
& | assigned (retain). |
N | nonatomic |
Gname | 以 G 开头是的自定义的 Getter 方法名。(如:GcustomGetter 名字是:customGetter). |
Sname | 以 S 开头是的自定义的 Setter 方法名。(如:ScustoSetter: 名字是: ScustoSetter:). |
D | @dynamic |
W | __weak |
来看看下面这个例子,你就全理解了:
5、属性解析
直接使用属性的元数据可不太好用,用一个对象来描述它会好很多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
typedef
NS_ENUM
(
NSUInteger
,
NLPropertyPolicy
)
{
NLPropertyPolicyAssign
,
NLPropertyPolicyStrong
,
NLPropertyPolicyCopy
,
NLPropertyPolicyWeak
,
}
;
@interface
NLPropertyDescriptor
: NSObject
/**
* @brief 属性名
*/
@property
(
nonatomic
,
copy
,
readonly
)
NSString
*name
;
/**
* @brief getter 方法名
*/
@property
(
nonatomic
,
copy
,
readonly
)
NSString
*getterName
;
/**
* @brief setter 方法名
*/
@property
(
nonatomic
,
copy
,
readonly
)
NSString
*setterName
;
/**
* @brief 变量名
*/
@property
(
nonatomic
,
copy
,
readonly
)
NSString
*variableName
;
/**
* @brief 属性类型编码
*/
@property
(
nonatomic
,
copy
,
readonly
)
NSString
*typeEncoding
;
/**
* @brief 属性类型
*/
@property
(
nonatomic
,
assign
,
readonly
)
NLPropertyPolicy
propertyPolicy
;
/**
* @brief 初始化
*/
-
(
instancetype
)
initWithObjcProperty
:
(
objc_property_t
)
objcProperty
;
@end
|
将属性的各项特性都存起来,想要的时候直接拿就好了,这就比 objc_property_t
好用多了。下面是初始化方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
-
(
instancetype
)
initWithObjcProperty
:
(
objc_property_t
)
objcProperty
{
if
(
self
=
[
super
init
]
)
{
_propertyPolicy
=
NLPropertyPolicyAssign
;
const
char
*cPropertyName
=
property_getName
(
objcProperty
)
;
_name
=
[
[
NSString
stringWithCString
:cPropertyName
encoding
:NSUTF8StringEncoding
]
copy
]
;
_getterName
=
[
_name
copy
]
;
_variableName
=
[
@"_"
stringByAppendingString
:
_name
]
;
(
{
// default setter name.
NSString
*firstChar
=
[
[
_name
substringToIndex
:
1
]
uppercaseString
]
;
NSString
*subjectName
=
[
_name
substringFromIndex
:
1
]
?
:
@""
;
subjectName
=
[
subjectName
stringByAppendingString
:
@":"
]
;
_setterName
=
[
[
NSString
stringWithFormat
:
@"set%@%@"
,
firstChar
,
subjectName
]
copy
]
;
}
)
;
const
char
*cPropertyAttributes
=
property_getAttributes
(
objcProperty
)
;
NSString
*sPropertyAttributes
=
[
NSString
stringWithCString
:cPropertyAttributes
encoding
:NSUTF8StringEncoding
]
;
NSArray
*attributes
=
[
sPropertyAttributes
componentsSeparatedByString
:
@","
]
;
[
attributes
enumerateObjectsUsingBlock
:
^
(
NSString
*
_Nonnull
obj
,
NSUInteger
idx
,
BOOL
*
_Nonnull
stop
)
{
if
(
idx
==
0
)
{
// 第一个一定是类型编码
_typeEncoding
=
[
obj
copy
]
;
}
if
(
[
obj
hasPrefix
:
@"G"
]
)
{
// getter 方法名
NSString
*getterName
=
[
obj
substringFromIndex
:
1
]
;
_getterName
=
[
getterName
copy
]
;
}
else
if
(
[
obj
hasPrefix
:
@"S"
]
)
{
// setter 方法名
NSString
*setterName
=
[
obj
substringFromIndex
:
1
]
;
_setterName
=
[
setterName
copy
]
;
}
else
if
(
[
obj
hasPrefix
:
@"V"
]
)
{
// 变量名
NSString
*variableName
=
[
obj
substringFromIndex
:
1
]
;
_variableName
=
[
variableName
copy
]
;
}
else
if
(
[
obj
isEqualToString
:
@"&"
]
)
{
_propertyPolicy
=
NLPropertyPolicyStrong
;
}
else
if
(
[
obj
isEqualToString
:
@"C"
]
)
{
_propertyPolicy
=
NLPropertyPolicyCopy
;
}
else
if
(
[
obj
isEqualToString
:
@"W"
]
)
{
_propertyPolicy
=
NLPropertyPolicyWeak
;
}
else
if
(
[
obj
isEqualToString
:
@"R"
]
)
{
// readonly
_setterName
=
nil
;
}
}
]
;
}
return
self
;
}
|
可以通过 class_copyPropertyList
获取到一个类中的所有属性结构体,也就能拿到所有属性的元数据。但大部分属性咱们是不感兴趣的,只对 @dynamic
以及以 nl_
为前辍的属性感兴趣。那就写一个分类方法,用来获取对咱们有用的所有属性数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
@interface
NSObject
(
nl_dynamicPropertyStore
)
/**
* @brief 判断是否应该自动生成方法的属性
*/
+
(
BOOL
)
nl_validDynamicProperty
:
(
_Nonnull
objc_property_t
)
objProperty
;
/**
* @brief 所有需要动态增加 getter、setter 方法的属性描述器
*/
+
(
NSArray
<
NLPropertyDescriptor
*
>
*
_Nullable
)
nl_dynamicPropertyDescriptors
;
@end
@implementation
NSObject
(
nl_dynamicPropertyStore
)
+
(
BOOL
)
nl_validDynamicProperty
:
(
objc_property_t
)
objProperty
{
const
char
*propertyAttributes
=
property_getAttributes
(
objProperty
)
;
// 必须是 @dynamic
static
char
*const
staticDynamicAttribute
=
",D,"
;
if
(
strstr
(
propertyAttributes
,
staticDynamicAttribute
)
==
NULL
)
{
return
NO
;
}
// 名字得以 “nl_” 为前辍
const
char
*propertyName
=
property_getName
(
objProperty
)
;
static
char
*const
staticPropertyNamePrefix
=
"nl_"
;
if
(
strstr
(
propertyName
,
staticPropertyNamePrefix
)
!=
propertyName
)
{
return
NO
;
}
return
YES
;
}
+
(
NSArray
*
)
nl
_
dynamicPropertyDescriptors
{
NSMutableArray
*descriptors
=
objc_getAssociatedObject
(
self
,
_cmd
)
;
if
(
nil
==
descriptors
)
{
unsigned
int
outCount
,
index
;
descriptors
=
[
NSMutableArray
arrayWithCapacity
:outCount
]
;
objc_setAssociatedObject
(
self
,
_cmd
,
descriptors
,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
;
// 获取到本类所有的属性结构体,并转换为属性描述器
objc_property_t
*properties
=
class_copyPropertyList
(
[
self
class
]
,
&
outCount
)
;
for
(
index
=
0
;
index
<
outCount
;
++
index
)
{
objc_property_t
property
=
properties
[
index
]
;
if
(
[
self
nl_validDynamicProperty
:property
]
)
{
NLPropertyDescriptor
*descriptor
=
[
[
NLPropertyDescriptor
alloc
]
initWithObjcProperty
:property
]
;
[
descriptors
addObject
:descriptor
]
;
}
}
free
(
properties
)
;
}
return
descriptors
;
}
@end
|
getter
和 setter
方法里的数据总得存储在某个地方吧,用字典来存储是比较理想的做法。就在 nl_dynamicPropertyStore
这个分类里定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@interface
NSObject
(
nl_dynamicPropertyStore
)
/**
* @brief 用来存储自动生成的 `getter`、`setter` 操作的数据
*/
@property
(
nonatomic
,
strong
,
readonly
)
NSMutableDictionary
*
_Nullable
nl_dynamicPropertyDictionary
;
@end
@implementation
NSObject
(
nl_dynamicPropertyStore
)
-
(
NSMutableDictionary
*
)
nl
_
dynamicPropertyDictionary
{
NSMutableDictionary
*dynamicProperties
=
objc_getAssociatedObject
(
self
,
_cmd
)
;
if
(
!
dynamicProperties
)
{
dynamicProperties
=
[
NSMutableDictionary
dictionaryWithCapacity
:
2
]
;
objc_setAssociatedObject
(
self
,
_cmd
,
dynamicProperties
,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
;
}
return
dynamicProperties
;
}
@end
|
6、自动生成 getter
、setter
方法
要用到的知识都已经介绍完了,接着就看看怎么来自动生成方法了。
前面介绍过,当发送了一个没有实现过的消息时,我们在 resolveInstanceMethod:
方法中为其添加实现。这个方法在 NSObject
类中定义,在这里,不可能继承它来实现我们想要的功能。我们可以在 NSObject
的分类中写一个新的方法来替代原有的这个方法实现,这叫“方法调配”(method swizzling),这常常用于给原有方法增加新的功能。
方法是动态绑定的,只有在运行时经过查找 后,才知道这条消息所对应的函数。方法,也就是一个名字,加上一个与之关联的函数。所谓方法调配,也就是将两个方法各自关联的函数互相交换一行而已。比如,nameA–>funcationA(nameA 是方法名,funcationA 是关联的方法实现函数), nameB–>funcationB,经过方法调配后,nameA–>funcationB,nameB–>funcationA。那么此时 [obj nameA] 这个消息,实现上调用的是 funcationB。
方法调配的核心函数是 method_exchangeImplementations
,它就是交换两个方法的实现的,代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@interface
NSObject
(
nl_dynamicPropertySupport
)
@end
@implementation
NSObject
(
nl_dynamicSupport
)
+
(
void
)
load
{
Method
resolveInstanceMethod
=
class_getClassMethod
(
self
,
@selector
(
resolveInstanceMethod
:
)
)
;
Method
nl_resolveInstanceMethod
=
class_getClassMethod
(
self
,
@selector
(
nl_resolveInstanceMethod
:
)
)
;
if
(
resolveInstanceMethod
&&
nl_resolveInstanceMethod
)
{
// method swizzling
method_exchangeImplementations
(
resolveInstanceMethod
,
nl_resolveInstanceMethod
)
;
}
}
#pragma mark - swizzle +resolveInstanceMethod
+
(
BOOL
)
nl_resolveInstanceMethod
:
(
SEL
)
sel
{
// 最后记得调用原有的实现
return
[
self
nl_resolveInstanceMethod
:sel
]
;
}
|
经过调配之后,原本调用 resolveInstanceMethod
最后执行的是 nl_resolveInstanceMethod
方法体。由于是给 resolveInstanceMethod
增加新的功能,所以在自定义的方法实现了自己的逻辑后,再调用原有的实现。那接下来就将增加方法的逻辑放在这里。
要添加方法,得先把这些方法所对应的函数定义出来。由于 getter
、setter
方法的参数个数和返回值个数都是一致的,所以它们对应的函数并不与属性名相关。而且所有属性的方法都有一个共同的参数:SEL
,我们可以用这个参数来对数据进行存储。这里以对象、int、CGRect类型为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
@interface
NSObject
(
nl_dynamicPropertyStore
)
/**
* 获取该选择子对应的属性名
*/
+
(
NSString
*
)
nl_dynamicPropertyNameWithSelctor
:
(
SEL
)
selector
{
return
[
[
self
nl_descriptorWithSelector
:selector
]
name
]
;
}
/**
* 获取该选择子对应的属性描述器
*/
+
(
NLPropertyDescriptor
*
)
nl_descriptorWithSelector
:
(
SEL
)
selector
{
for
(
NLPropertyDescriptor
*descriptor
in
[
self
nl_dynamicPropertyDescriptors
]
)
{
NSString
*selectorName
=
NSStringFromSelector
(
selector
)
;
if
(
[
descriptor
.
getterName
isEqualToString
:selectorName
]
||
[
descriptor
.
setterName
isEqualToString
:selectorName
]
)
{
return
descriptor
;
}
}
return
nil
;
}
@end
// 对象类型的属性的 setter 方法的实现
void
__NL__object_dynamicSetterIMP
(
id
self
,
SEL
_cmd
,
id
arg
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
[
[
self
nl_dynamicPropertyDictionary
]
setObject
:arg
forKey
:propertyName
]
;
}
// 对象类型的属性的 getter 方法的实现
id
__NL__object_dynamicGetterIMP
(
id
self
,
SEL
_cmd
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
return
[
[
self
nl_dynamicPropertyDictionary
]
objectForKey
:propertyName
]
;
}
// int类型的属性的 setter 方法的实现
void
__NL__int_dynamicSetterIMP
(
id
self
,
SEL
_cmd
,
int
arg
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
[
[
self
nl_dynamicPropertyDictionary
]
setObject
:
@
(
arg
)
forKey
:propertyName
]
;
}
// int类型的属性的 getter 方法的实现
int
__NL__int_dynamicGetterIMP
(
id
self
,
SEL
_cmd
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
return
[
[
[
self
nl_dynamicPropertyDictionary
]
objectForKey
:propertyName
]
intValue
]
;
}
// CGRect类型的属性的 setter 方法的实现
void
__NL__cgrect_dynamicSetterIMP
(
id
self
,
SEL
_cmd
,
CGRect
arg
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
[
[
self
nl_dynamicPropertyDictionary
]
setObject
:
[
NSValue
valueWithCGRect
:arg
]
forKey
:propertyName
]
;
}
// CGRect类型的属性的 getter 方法的实现
CGRect
__NL__cgrect_dynamicGetterIMP
(
id
self
,
SEL
_cmd
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
return
[
[
[
self
nl_dynamicPropertyDictionary
]
objectForKey
:propertyName
]
CGRectValue
]
;
}
|
方法的各个实现都有了,接下来的工作根据未实现的方法名,找到对应的函数,再把这个函数加到方法中去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
+
(
BOOL
)
nl_resolveInstanceMethod
:
(
SEL
)
sel
{
NSArray
<
NLPropertyDescriptor
*
>
*propertyDescriptors
=
[
self
nl_dynamicPropertyDescriptors
]
;
for
(
NLPropertyDescriptor
*propertyDescriptor
in
propertyDescriptors
)
{
BOOL
didAddMethod
=
[
self
nl_addMethodWithDescriptor
:propertyDescriptor
selector
:sel
]
;
if
(
didAddMethod
)
{
return
YES
;
}
}
// 最后记得调用原有的实现
return
[
self
nl_resolveInstanceMethod
:sel
]
;
}
+
(
BOOL
)
nl_addMethodWithDescriptor
:
(
NLPropertyDescriptor
*
)
desciptor
selector
:
(
SEL
)
sel
{
NSString
*selName
=
NSStringFromSelector
(
sel
)
;
if
(
[
desciptor
.
setterName
isEqualToString
:selName
]
)
{
BOOL
addedSetter
=
[
self
nl_addSetterMethodWithDescriptor
:desciptor
]
;
return
addedSetter
;
}
if
(
[
desciptor
.
getterName
isEqualToString
:selName
]
)
{
BOOL
addedGetter
=
[
self
nl_addGetterMethodWithDescriptor
:desciptor
]
;
return
addedGetter
;
}
return
NO
;
}
// 添加 setter 方法实现
+
(
BOOL
)
nl_addSetterMethodWithDescriptor
:
(
NLPropertyDescriptor
*
)
desciptor
{
IMP
setterIMP
=
NULL
;
if
(
[
desciptor
isIntType
]
)
{
setterIMP
=
(
IMP
)
__NL__int_dynamicSetterIMP
;
}
if
(
[
desciptor
isObjectType
]
)
{
setterIMP
=
(
IMP
)
__NL__object_dynamicSetterIMP
;
}
if
(
[
desciptor
isRectType
]
)
{
setterIMP
=
(
IMP
)
__NL__cgrect_dynamicSetterIMP
;
}
if
(
setterIMP
!=
NULL
)
{
class_addMethod
(
self
,
NSSelectorFromString
(
desciptor
.
setterName
)
,
setterIMP
,
"v@:"
)
;
return
YES
;
}
return
NO
;
}
// 添加 getter 方法实现
+
(
BOOL
)
nl_addGetterMethodWithDescriptor
:
(
NLPropertyDescriptor
*
)
desciptor
{
SEL
selector
=
NSSelectorFromString
(
desciptor
.
getterName
)
;
if
(
[
desciptor
isIntType
]
)
{
class_addMethod
(
self
,
selector
,
(
IMP
)
__NL__int_dynamicGetterIMP
,
"i@:"
)
;
return
YES
;
}
NSString
*typeEncoding
=
[
desciptor
typeEncoding
]
;
if
(
[
typeEncoding
hasPrefix
:
@"T"
]
)
{
typeEncoding
=
[
typeEncoding
substringFromIndex
:
1
]
;
}
const
char
*cFuncationTypes
=
[
[
typeEncoding
stringByAppendingString
:
@"@:"
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
;
if
(
[
desciptor
isObjectType
]
)
{
{
class_addMethod
(
self
,
selector
,
(
IMP
)
__NL__object_dynamicGetterIMP
,
cFuncationTypes
)
;
return
YES
;
}
if
(
[
desciptor
isRectType
]
)
{
class_addMethod
(
self
,
selector
,
(
IMP
)
__NL__cgrect_dynamicGetterIMP
,
cFuncationTypes
)
;
return
YES
;
}
return
NO
;
}
|
class_addMethod
函数声明:BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
。cls
是要添加方法的类;name
是要添加方法实现的名字;imp
是要添加方法对应的实现,types
是用类型编码描述该方法参数的字符串,而方法的函数必定会有参数:self
(对象,类型编码是@
)和_cmd
(选择子,类型编码是:
),所以这个type
字符串中必定包含 “@:” 子串,这个子串前的字符是这个方法的返回值,其后面的字符是该方法的其它参数。
实验一把:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@interface
ViewController
(
nl_ex
)
@property
(
nonatomic
,
assign
)
int
nl_int
;
@property
(
nonatomic
,
strong
)
id
nl_object
;
@end
@implementation
ViewController
(
nl_ex
)
@dynamic
nl_object
;
@dynamic
nl_int
;
@end
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
self
.
nl_int
=
20
;
self
.
nl_object
=
[
UIView
new
]
;
fprintf
(
stdout
,
"nl_int = %d\n"
,
self
.
nl_int
)
;
fprintf
(
stdout
,
"nl_object = %s\n"
,
[
[
self
.
nl_object
description
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
)
;
// 输出: nl_int = 20
// nl_object = new nl_object string
}
|
完全没问题,奖励自己一把先。
7、添加 KVO 支持
KVO 还不简单,在 setter
实现里加上 willChangeValueForKey:
和 didChangeValueForKey:
就好了:
1
2
3
4
5
6
7
|
void
__NL__object_dynamicSetterIMP
(
id
self
,
SEL
_cmd
,
id
arg
)
{
NSString
*propertyName
=
[
[
self
class
]
nl_dynamicPropertyNameWithSelctor
:
_cmd
]
;
[
self
willChangeValueForKey
:propertyName
]
;
[
[
self
nl_dynamicPropertyDictionary
]
setObject
:arg
forKey
:propertyName
]
;
[
self
didChangeValueForKey
:propertyName
]
;
}
|
再来验证一把:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-
(
void
)
observeValueForKeyPath
:
(
NSString
*
)
keyPath
ofObject
:
(
id
)
object
change
:
(
NSDictionary
<
NSString
*
,
id
>
*
)
change
context
:
(
void
*
)
context
{
id
value
=
[
object
valueForKeyPath
:keyPath
]
;
fprintf
(
stdout
,
"observe %s = %s\n"
,
[
keyPath
cStringUsingEncoding
:NSUTF8StringEncoding
]
,
[
[
value
description
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
)
;
}
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
[
self
addObserver
:self
forKeyPath
:
@"nl_object"
options
:NSKeyValueObservingOptionNew
context
:nil
]
;
self
.
nl_object
=
[
UIView
new
]
;
fprintf
(
stdout
,
"nl_object = %s\n"
,
[
[
self
.
nl_object
description
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
)
;
}
|
会打印出什么?
可惜,什么也不会打印,而会崩溃:
1
2
3
4
5
6
7
8
9
10
11
12
|
2015
-
12
-
14
00
:
10
:
48.700
CategoryPropertyDynamicSupport
[
1707
:
100735
]
*
*
*
Terminating
app
due
to
uncaught
exception
'NSInvalidArgumentException'
,
reason
:
'*** setObjectForKey: key cannot be nil'
*
*
*
First
throw
call
stack
:
(
0
CoreFoundation
0x00000001063cce65
__exceptionPreprocess
+
165
1
libobjc
.
A
.
dylib
0x0000000105e45deb
objc_exception_throw
+
48
2
CoreFoundation
0x00000001062ca6e2
-
[
_
_
NSDictionaryM
setObject
:forKey
:
]
+
1042
3
CategoryPropertyDynamicSupport
0x0000000105579e44
__NL__object_dynamicSetterIMP
+
260
4
CategoryPropertyDynamicSupport
0x0000000105582689
-
[
ViewController
viewDidLoad
]
+
1561
5
UIKit
0x0000000106fdef98
-
[
UIViewController
loadViewIfRequired
]
+
1198
6
UIKit
0x0000000106fdf2e7
-
[
UIViewController
view
]
+
27
.
.
.
|
log 显示 __NL__object_dynamicSetterIMP
函数里的 [[self nl_dynamicPropertyDictionary] setObject:arg forKey:propertyName];
崩溃,原因是 propertyName
等于 nil
。propertyName
不是选择子所对应的属性名吗,这个属性明明存在的呀,怎么为会空呢?
看看下面的代码:
1
2
3
4
5
6
7
8
|
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
[
self
addObserver
:self
forKeyPath
:
@"nl_object"
options
:NSKeyValueObservingOptionNew
context
:nil
]
;
Class
class
=
[
self
class
]
;
// ViewController
Class
kvoClass
=
object_getClass
(
self
)
;
// NSKVONotifying_ViewController
Class
kvoSuperClass
=
class_getSuperclass
(
kvoClass
)
// ViewController
|
原因就在这里,在 addObserver:...
后,咱们这个对象所属的类就已经不是原来的那个类了,而是原来的类的子类了。系统不过重写了 -class
方法,让人看起来还是原来的类的样子。咱们之前的 nl_dynamicPropertyDescriptors
只包含了当前类的属性,显然不对。这里把父类的属性也加进去:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
+
(
NSArray
*
)
nl
_
dynamicPropertyDescriptors
{
NSMutableArray
*descriptors
=
objc_getAssociatedObject
(
self
,
_cmd
)
;
if
(
nil
==
descriptors
)
{
unsigned
int
outCount
,
index
;
descriptors
=
[
NSMutableArray
arrayWithCapacity
:outCount
]
;
objc_setAssociatedObject
(
self
,
_cmd
,
descriptors
,
OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
;
// 获取到本类所有的属性结构体,并转换为属性描述器
objc_property_t
*properties
=
class_copyPropertyList
(
[
self
class
]
,
&
outCount
)
;
for
(
index
=
0
;
index
<
outCount
;
++
index
)
{
objc_property_t
property
=
properties
[
index
]
;
if
(
[
self
nl_validDynamicProperty
:property
]
)
{
NLPropertyDescriptor
*descriptor
=
[
[
NLPropertyDescriptor
alloc
]
initWithObjcProperty
:property
]
;
[
descriptors
addObject
:descriptor
]
;
}
}
free
(
properties
)
;
if
(
self
!=
[
NSObject
class
]
)
{
// 加上父类的属性描述器
[
descriptors
addObjectsFromArray
:
[
class_getSuperclass
(
self
)
nl_dynamicPropertyDescriptors
]
]
;
}
}
return
descriptors
;
}
|
再来验证一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-
(
void
)
observeValueForKeyPath
:
(
NSString
*
)
keyPath
ofObject
:
(
id
)
object
change
:
(
NSDictionary
<
NSString
*
,
id
>
*
)
change
context
:
(
void
*
)
context
{
id
value
=
[
object
valueForKeyPath
:keyPath
]
;
fprintf
(
stdout
,
"observe %s = %s\n"
,
[
keyPath
cStringUsingEncoding
:NSUTF8StringEncoding
]
,
[
[
value
description
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
)
;
}
-
(
void
)
viewDidLoad
{
[
super
viewDidLoad
]
;
[
self
addObserver
:self
forKeyPath
:
@"nl_object"
options
:NSKeyValueObservingOptionNew
context
:nil
]
;
self
.
nl_object
=
[
UIView
new
]
;
fprintf
(
stdout
,
"nl_object = %s\n"
,
[
[
self
.
nl_object
description
]
cStringUsingEncoding
:NSUTF8StringEncoding
]
)
;
// 输出:observe nl_object = <UIView: 0x7f8a6b82e820; frame = (0 0; 0 0); layer = <CALayer: 0x7f8a6b82e6b0>
// nl_object = <UIView: 0x7f8a6b82e820; frame = (0 0; 0 0); layer = <CALayer: 0x7f8a6b82e6b0>>
}
|
验证通过。
8、结束
代码过多,KVC 和 weak 的支持属于细枝末节,这里就不一一介绍了,想看看完整的代码的话,这里:https://github.com/NathanLi/iOSCategoryPropertyDynamicSupport。
虽然现在 Objective-C 在 Swift 面前已经显得过时,但这 runtime 知识此时了解却也还是有些价值的。这里只是简单的介绍了一个属性相关的知识,实际上可玩的东西很多,比如 ORM (如 LKDBHelper) 等等。