1. 程式人生 > >OC 自動生成分類屬性方法

OC 自動生成分類屬性方法

標籤(空格分隔): Objective-C runtime iOS 分類 category

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

背景

  分類,在 iOS 開發中,是常常需要用到的。在分類裡新增屬性也是常有的事,但分類中無法新增例項變數,編譯器也無法為提供分類中屬性的 gettersetter 方法了。一般而言,需要手動來實現這兩個方法,如果只是用來儲存變數的話,關聯物件很容易做到這一點:

@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

  這是很常見的實現方式。

  要是再給這個分類多加幾個屬性,也就得再多加幾個這樣的 gettersetter

方法,無法也就是方法名字、關聯引數不一樣罷了,這可真是個體力活呀!要是能像普通類屬性那樣就好了,自動給生成這兩個方法,想想就爽。

  要想做到自動生成這兩個方法,可以從兩個方面入手:
  1、編碼期
  2、執行期

  編碼期。在寫程式碼的時候要做到自動生成方法,可以寫一個 XCode 外掛,一按某些快捷鍵,相應程式碼就自動生成了,這有點類似於 Eclipse。外掛的討論不在本文範圍內。

需求

  簡單點說,就是在執行時能生成分類中屬性相應的 getter setter 方法,也就是模仿類中普通 @property 定義的屬性。這樣的需求太泛,咱們先來細化一下:

  1、只生成分類中的,我想要的 getter

setter方法體;
  2、屬性型別:支援基本資料型別、物件、結構體,且自動存取;
  3、支援 @property 定義中的 assignstrongcopyweak
  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、根據 selecotrreceiver 找到對應的函式實現(也就是函式地址)。不同的類可以有相同的方法,但是它們所對應的函式地址是不一樣的。
  2、呼叫找到的函式,並傳入相應的引數:receiverselectorarg1…。
  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 函式呼叫。這個函式最終會根據 selfsetName: 找到函式 _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 異常。如果你不想讓一個讓別人呼叫你的類的 copyinit 方法的話,可以這麼做:

- (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_copyPropertyListprotocol_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_getPropertyprotocol_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 能獲取到屬性的很多資訊,包括剛看到的型別編碼、gettersetter 方法名、對應的例項變數名等等。列印所有屬性的元資訊例子:

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 表示 @dynamicN 表示 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

  gettersetter 方法裡的資料總得儲存在某個地方吧,用字典來儲存是比較理想的做法。就在 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、自動生成 gettersetter 方法

  要用到的知識都已經介紹完了,接著就看看怎麼來自動生成方法了。

  前面介紹過,當傳送了一個沒有實現過的訊息時,我們在 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 增加新的功能,所以在自定義的方法實現了自己的邏輯後,再呼叫原有的實現。那接下來就將增加方法的邏輯放在這裡。

  要新增方法,得先把這些方法所對應的函式定義出來。由於 gettersetter 方法的引數個數和返回值個數都是一致的,所以它們對應的函式並不與屬性名相關。而且所有屬性的方法都有一個共同的引數: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 等於 nilpropertyName 不是選擇子所對應的屬性名嗎,這個屬性明明存在的呀,怎麼為會空呢?

  看看下面的程式碼:

- (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) 等等。