你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> OC 自動生成分類屬性方法

OC 自動生成分類屬性方法

編輯:IOS開發基礎

分類屬性方法自動生成編碼全過程。

背景

  分類,在 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。插件的討論不在本文范圍內。

  運行期。在編碼階段只需少量代碼,具體的方法則在運行期動態生成。本文研究怎麼在運行期動態生成這些所需要的方法。本文最終生成的代碼在: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_ 為前辍就好了:

@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_


  消息傳遞框架圖:

blob.png

當發送一個消息給一個對象時,首先會去這個對象的 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, "v@:");
          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"

  編碼表如下:

blob.png

  這是描述類型的數據。那描述屬性的呢?

  編譯器會將類、分類和協議中的屬性以元數據信息存起來。有一系列的 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
意義
Rreadonly
C
copy
&
assigned (retain).Nnonatomic
Gname
以 G 開頭是的自定義的 Getter 方法名。(如:GcustomGetter 名字是:customGetter).
Sname
以 S 開頭是的自定義的 Setter 方法名。(如:ScustoSetter: 名字是: ScustoSetter:).
D
@dynamic
W
__weak

  來看看下面這個例子,你就全理解了:

blob.png

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 * _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 *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 字符串中必定包含 “@:” 子串,這個子串前的字符是這個方法的返回值,其後面的字符是該方法的其它參數。

  實驗一把:

@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 *)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 *)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 = 
}

  驗證通過。

8、結束

代碼過多,KVC 和 weak 的支持屬於細枝末節,這裡就不一一介紹了,想看看完整的代碼的話,這裡:https://github.com/NathanLi/iOSCategoryPropertyDynamicSupport。

雖然現在 Objective-C 在 Swift 面前已經顯得過時,但這 runtime 知識此時了解卻也還是有些價值的。這裡只是簡單的介紹了一個屬性相關的知識,實際上可玩的東西很多,比如 ORM (如 LKDBHelper) 等等。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved