OC 自動生成分類屬性方法
標籤(空格分隔): Objective-C runtime iOS 分類 category
分類屬性方法自動生成編碼全過程。
背景
分類,在 iOS 開發中,是常常需要用到的。在分類裡新增屬性也是常有的事,但分類中無法新增例項變數,編譯器也無法為提供分類中屬性的 getter
和 setter
方法了。一般而言,需要手動來實現這兩個方法,如果只是用來儲存變數的話,關聯物件很容易做到這一點:
@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。外掛的討論不在本文範圍內。
需求
簡單點說,就是在執行時能生成分類中屬性相應的 getter
setter
方法,也就是模仿類中普通 @property 定義的屬性。這樣的需求太泛,咱們先來細化一下:
1、只生成分類中的,我想要的 getter
setter
方法體; 2、屬性型別:支援基本資料型別、物件、結構體,且自動存取;
3、支援
@property
定義中的 assign
、strong
、copy
、weak
; 4、支援
@property
中的自定義的方法名; 5、支援
KVC
; 6、支援
KVO
; 7、本條不是需求,而是簡單的設定:不支援原子即 atomic,只支援 nonatomic。
實現
1、確定要動態生成方法的屬性
這裡根據屬性的名字來確定是否需要動態生成方法,就以 nl_
為前輟就好了:
@property (nonatomic, strong) id nl_object;
由於是在分類中,且沒有定義相應的方法,所以會有警告:
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
就好了:
@dynamic nl_double;
2、訊息
@dynamic
告訴編譯器,這兩個方法有沒有實現你都不用管,就當作它們存在就行了。那麼問題來了,這兩個方法明明沒有實現,卻依然能夠呼叫呢?
這根訊息傳送機制有關。在 Objective-C中,訊息不會與方法實現繫結,而是在執行時才關聯起來的。
編譯器會把所有訊息轉換為一個函式呼叫:objc_msgSend
。這個函式總得知道是 誰
傳送了 哪條
訊息吧,所以它最少也有兩個引數——訊息的接收者 和 訊息名(即選擇子 SEL
),比如下面這個方法:
[receiver message]
編譯器就會把它變成這個樣子:
objc_msgSend(receiver, message)
如果訊息有引數的話,就會直接傳這個函式,所以這個函式的引數個數不定:
objc_msgSend(receiver, selector, arg1, arg2, ...)
objc_msgSend
的工作就是訊息的動態繫結:
1、根據 selecotr
和 receiver
找到對應的函式實現(也就是函式地址)。不同的類可以有相同的方法,但是它們所對應的函式地址是不一樣的。
2、呼叫找到的函式,並傳入相應的引數:receiver
、selector
、arg1
…。
3、返回呼叫的函式的返回值。
來看下例項:
@implementation NLPerson
- (instancetype)init {
if (self = [super init]) {
[self setName:@"name"];
}
return self;
}
- (void)setName:(NSString *)name {
_name = name;
}
@end
所對應的函式程式碼是這樣的:
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
,所以才能這麼列印方法名:
- (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
方法的話,可以這麼做:
- (id)copy {
[self doesNotRecognizeSelector:_cmd];
}
但在呼叫這個方法之前,系統還是給了我們處理的機會。實現 resolveInstanceMethod:
方法,能動態地給例項方法和類方法新增一個實現。
Objective-C 中的方法所對應的函式最少有兩個引數:self
和 _cmd
。如下所示:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
可以用 C 函式 class_addMethod
將其作為一個方法動態加到一個類中:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "[email protected]:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
4、屬性元資料、型別編碼(Type Encodings)
要能動態生成屬性的方法,首先得知道屬性的一些基本資訊:型別、方法名、是 weak
還是 strong
等。這些資料都可以在執行時獲得到。要用到的技術是:型別編碼(Type Encdoings)。
型別編碼是 runtime 的輔助工具。編譯器會將型別用字串來表示。可以用 @encode
得到這個字串:
char *buf1 = @encode(int); // buf1 --> "i"
char *buf2 = @encode(long long); // buf2 --> "q"
char *buf3 = @encode(unsigned int); // buf2 --> "I"
編碼表如下:
這是描述型別的資料。那描述屬性的呢?
編譯器會將類、分類和協議中的屬性以元資料資訊存起來。有一系列的 C 函式來訪問這些資料。
屬性元資料是用結構體 Property
來描述的:
typedef struct objc_property *Property;
可以用 class_copyPropertyList
和 protocol_copyPropertyList
來分別獲取類(包含分類)中和協議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
比如下面宣告的這個類:
@interface Person : NSObject
@property (nonatomic, assign) float age;
@end
...
@dynamic age;
可以這麼來獲取它的屬性列表:
id PersonClass = objc_getClass("Person");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(PersonClass, &outCount);
除了一次性獲得所有屬性列表外,還有方法 class_getProperty
和 protocol_getProperty
可以通過屬性名獲取單個屬性:
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
獲取到屬性結構體後,就可以拿到這個屬性的名字和元資訊:
const char *property_getName(objc_property_t property) // 獲取屬性的名字
const char *property_getAttributes(objc_property_t property) // 獲取屬性的元資訊
property_getAttributes
能獲取到屬性的很多資訊,包括剛看到的型別編碼、getter
和setter
方法名、對應的例項變數名等等。列印所有屬性的元資訊例子:
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 |
G`name` | 以 G 開頭是的自定義的 Getter 方法名。(如:GcustomGetter 名字是:customGetter). |
S`name` | 以 S 開頭是的自定義的 Setter 方法名。(如:ScustoSetter: 名字是: ScustoSetter:). |
D | @dynamic |
W | __weak |
來看看下面這個例子,你就全理解了:
5、屬性解析
直接使用屬性的元資料可不太好用,用一個物件來描述它會好很多。
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
好用多了。下面是初始化方法:
- (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_
為前輟的屬性感興趣。那就寫一個分類方法,用來獲取對咱們有用的所有屬性資料:
@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
這個分類裡定義:
@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
,它就是交換兩個方法的實現的,程式碼:
@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型別為例:
@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];
}
方法的各個實現都有了,接下來的工作根據未實現的方法名,找到對應的函式,再把這個函式加到方法中去:
+ (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, "[email protected]:");
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, "[email protected]:");
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
字串中必定包含 “@:” 子串,這個子串前的字元是這個方法的返回值,其後面的字元是該方法的其它引數。
實驗一把:
@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:
就好了:
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];
}
再來驗證一把:
- (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]);
}
會打印出什麼?
可惜,什麼也不會列印,而會崩潰:
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
不是選擇子所對應的屬性名嗎,這個屬性明明存在的呀,怎麼為會空呢?
看看下面的程式碼:
- (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
只包含了當前類的屬性,顯然不對。這裡把父類的屬性也加進去:
+ (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;
}
再來驗證一下:
- (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、結束
雖然現在 Objective-C 在 Swift 面前已經顯得過時,但這 runtime 知識此時瞭解卻也還是有些價值的。這裡只是簡單的介紹了一個屬性相關的知識,實際上可玩的東西很多,比如 ORM (如 LKDBHelper) 等等。