YYModel 源碼剖析:關注性能

范德薩 6年前發布 | 33K 次閱讀 源碼分析 iOS開發 移動開發

前言

json與模型的轉換框架很多,YYModel 一出,性能吊打同類組件,終于找了些時間觀摩了一番,確實收益頗多,寫下此文作為分享。

由于該框架代碼比較多,考慮到突出重點,壓縮篇幅,不會有太多筆墨在基礎知識上,很多展示源碼部分會做刪減,重點是在理解作者思維。讀者需要具備一定的 runtime 知識,若想閱讀起來輕松一些,最好自己打開源碼做參照。

一、框架的核心思路

使用過框架的朋友應該很熟悉如下的這些方法:

@interface NSObject (YYModel) + (nullable instancetype)yy_modelWithJSON:(id)json;

  • (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary;
  • (nullable id)yy_modelToJSONObject;
  • (nullable NSData *)yy_modelToJSONData; ......</code></pre>

    框架解決的問題,就是實現 json 和  OC對象 間的轉換,這個過程的核心問題就是  json數據 和  OC對象的成員變量 之間的映射關系。

    而這個映射關系,需要借助 runtime 來完成。只需要傳入一個  Class 類變量,框架內部就能通過 runtime 將該類的屬性以及方法查找出來,默認是將屬性名作為映射的 key,然后 json 數據就能通過這個映射的 key 匹配賦值(通過objc_msgSend)。

    若將 OC 對象轉換成 json 數據,只需要逆向處理一下。

    框架做的事情說起來是簡單的,不同開源庫實現的細節雖然不同,但是它們的核心思路很相似。

    二、類型編碼 Type-Encoding

    前面筆者提到,可以通過 runtime 獲取到某個類的所有屬性名字,達成映射。但是考慮到我們的 模型類 往往會定義很多種類型,比如:double、char、NSString、NSDate、SEL 、NSSet 等,所以需要將元數據 json(或者字典數據)轉換成我們實際需要的類型。

    但是,計算機如何知道我們定義的 模型類 的屬性是什么類型的呢?由此,引入類型編碼的概念——

    兩個關于類型編碼的官方文檔:

    文檔一

    文檔二

    Type-Encoding 是指定的一套類型編碼,在使用 runtime 獲取某個類的成員變量、屬性、方法的時候,能同時獲取到它們的類型編碼,通過這個編碼就能辨別這些成員變量、屬性、方法的數據類型(也包括屬性修飾符、方法修飾符等)。

    枚舉的處理

    關于類型編碼的具體細節請自行查閱文檔,本文不做講解。在 YYModel 的源碼中,作者使用了一個枚舉來對應不同的類型,見名知意,方便在框架中使用:

    typedef NS_OPTIONS(NSUInteger, YYEncodingType) {
      YYEncodingTypeMask       = 0xFF, ///< mask of type value
      YYEncodingTypeUnknown    = 0, ///< unknown
      YYEncodingTypeVoid       = 1, ///< void
      ......
      YYEncodingTypeCArray     = 22, ///< char[10] (for example)

    YYEncodingTypeQualifierMask = 0xFF00, ///< mask of qualifier YYEncodingTypeQualifierConst = 1 << 8, ///< const YYEncodingTypeQualifierIn = 1 << 9, ///< in ...... YYEncodingTypeQualifierOneway = 1 << 14, ///< oneway

    YYEncodingTypePropertyMask = 0xFF0000, ///< mask of property YYEncodingTypePropertyReadonly = 1 << 16, ///< readonly YYEncodingTypePropertyCopy = 1 << 17, ///< copy ...... YYEncodingTypePropertyDynamic = 1 << 23, ///< @dynamic };</code></pre>

    筆者并不是想把所有類型編碼貼出來看,所以做了省略。這個枚舉可能是多選的,所以使用了NS_OPTIONS而不是NS_ENUM(編碼規范)。

    可以看到該枚舉既包含了單選枚舉值,也包含了多選枚舉值,如何讓它們互不影響?

    YYEncodingTypeMask、YYEncodingTypeQualifierMask、YYEncodingTypePropertyMask將枚舉值分為三部分,它們的值轉換為二進制分別為:

    然后,這三部分其他枚舉的值,恰巧分布在這三個 mask 枚舉的值分成的三個區間。在源碼中,會看到如下代碼:

    YYEncodingType type; 
    if ((type & YYEncodingTypeMask) == YYEncodingTypeVoid) {...}

    通過一個 位與& 運算符,直接將高于YYEncodingTypeMask的值過濾掉,然后實現單值比較。

    這是一個代碼技巧,挺有意思。

    關于 Type-Encoding 轉換 YYEncodingType 枚舉的代碼就不解釋了,基本上根據官方文檔來的。

    三、將底層數據裝進中間類

    在 YYClassInfo 文件中,可以看到有這么幾個類:

    YYClassIvarInfo
    YYClassMethodInfo
    YYClassPropertyInfo
    YYClassInfo

    很明顯,他們是將 Ivar、Method、objc_property_t、Class 的相關信息裝進去,這樣做一是方便使用,二是為了做緩存。

    在源碼中可以看到:

    操作 runtime 底層類型的時候,由于它們不受 ARC 自動管理內存,所以記得用完了釋放(但是不要去釋放 const 常量),釋放之前切記判斷該內存是否存在防止意外crash。

    基本的轉換過程很簡單,不一一討論,下面提出一些值得注意的地方:

    YYClassPropertyInfo 記錄屬性<> 包裹部分

    @implementation YYClassPropertyInfo

  • (instancetype)initWithProperty:(objc_property_t)property { ... NSScanner scanner = [NSScanner scannerWithString:_typeEncoding]; ... NSMutableArray protocols = nil; while ([scanner scanString:@"<" intoString:NULL]) {

      NSString* protocol = nil;
      if ([scanner scanUpToString:@">" intoString: &protocol]) {
          if (protocol.length) {
              if (!protocols) protocols = [NSMutableArray new];
              [protocols addObject:protocol];
          }
      }
      [scanner scanString:@">" intoString:NULL];
    

    } _protocols = protocols; ... } @end</code></pre>

    在對 objc_property_t 的轉換中,作者查找了類型編碼中用<> 包裹的部分,然后把它們放進一個數組。這是作者做的一個基于用戶體驗的優化,比如一個屬性@property NSArray arr,框架就可以解析到 arr 容器內部是包含的 CustomObject 類型,而不需使用者專門寫方法去映射(當然可以自定義映射)。關于具體的邏輯后面會講到。

    YYClassInfo 結構

    @interface YYClassInfo : NSObject
    @property (nonatomic, assign, readonly) Class cls; ///< class object
    @property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object
    @property (nullable, nonatomic, assign, readonly) Class metaCls;  ///< class's meta class object
    @property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class
    @property (nonatomic, strong, readonly) NSString *name; ///< class name
    @property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info
    @property (nullable, nonatomic, strong, readonly) NSDictionary *ivarInfos; ///< ivars
    @property (nullable, nonatomic, strong, readonly) NSDictionary *methodInfos; ///< methods
    @property (nullable, nonatomic, strong, readonly) NSDictionary *propertyInfos; ///< properties
    ...

    可以看到,Class 類的成員變量、屬性、方法分別裝入了三個 hash 容器(ivarInfos/methodInfos/propertyInfos)。

    superClassInfo 指向父類,初始化時框架會循環向上查找,直至當前 Class 的父類不存在(NSObject 父類指針為 nil),這類似一個單向的鏈表,將有繼承關系的類信息全部串聯起來。這么做的目的,就是為了 json 轉模型的時候,同樣把父類的屬性名作為映射的 key。初始化 YYClassInfo 的代碼大致如下:

    - (instancetype)initWithClass:(Class)cls {
      if (!cls) return nil;
      self = [super init];
      ...
    //_update方法就是將當前類的成員變量列表、屬性列表、方法列表轉換放進對應的 hash
      [self _update];
    //獲取父類信息。 classInfoWithClass: 是一個獲取類的方法,里面有緩存機制,下一步會講到
      _superClassInfo = [self.class classInfoWithClass:_superCls];
      return self;
    }

    YYClassInfo 緩存

    作者做了一個類信息(YYClassInfo)緩存的機制:

    + (instancetype)classInfoWithClass:(Class)cls {
      if (!cls) return nil;
    //初始化幾個容器和鎖
      static CFMutableDictionaryRef classCache;
      static CFMutableDictionaryRef metaCache;
      static dispatch_once_t onceToken;
      static dispatch_semaphore_t lock;
      dispatch_once(&onceToken, ^{

      classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
      metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
      lock = dispatch_semaphore_create(1);
    

    }); //讀取緩存 dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); YYClassInfo info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void )(cls)); //更新成員變量列表、屬性列表、方法列表 if (info && info->_needUpdate) [info _update]; dispatch_semaphore_signal(lock); //若無緩存,將 Class 類信息轉換為新的 YYClassInfo 實例,并且放入緩存 if (!info) {

      info = [[YYClassInfo alloc] initWithClass:cls];
      if (info) {
          dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
          CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
          dispatch_semaphore_signal(lock);
      }
    

    } return info; }</code></pre>

    由于同一個類的相關信息在程序運行期間通常是相同的,所以使用 classCache(類hash) 和 metaCache(元類hash) 緩存已經通過 runtime 轉換為 YYClassInfo 的 Class,保證不會重復轉換 Class 類信息做無用功;考慮到 runtime 帶來的動態特性,作者使用了一個 bool 值判斷是否需要更新成員變量列表、屬性列表、方法列表,_update方法就是重新獲取這些信息。

    這個緩存機制能帶來很高的效率提升,是 YYModel 一個比較核心的操作。

    有幾個值得注意和學習的地方:

    1. 使用 static 修飾局部變量提升其生命周期,而又不改變其作用域,保證在程序運行期間局部變量不會釋放,又防止了其他代碼對該局部變量的訪問。

    2. 線程安全的考慮。在初始化 static 變量的時候,使用dispatch_once()保證線程安全;在讀取和寫入使用dispatch_semaphore_t信號量保證線程安全。

    四、一些工具方法

    在進入核心業務之前,先介紹一些 NSObject+YYModel.m 里面值得注意的工具方法。

    在工具方法中,經常會看到這么一個宏來修飾函數:

    #define force_inline __inline__ __attribute__((always_inline))

    它的作用是強制內聯,因為使用 inline 關鍵字最終會不會內聯還是由編譯器決定。對于這些強制內聯的函數參數,作者經常使用 __unsafe_unretained 來修飾,拒絕其引用計數+1,以減少內存開銷。

    將 id 類型轉換為 NSNumber

    static force_inline NSNumber YYNSNumberCreateFromID(__unsafe_unretained id value) {
      static NSCharacterSet dot;
      static NSDictionary *dic;
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{

      dot = [NSCharacterSet characterSetWithRange:NSMakeRange('.', 1)];
      dic = @{@"TRUE" :   @(YES),
              @"True" :   @(YES),
              @"true" :   @(YES),
              ...
              @"NIL" :    (id)kCFNull,
              @"Nil" :    (id)kCFNull,
              ...
    

    });

    if (!value || value == (id)kCFNull) return nil; if ([value isKindOfClass:[NSNumber class]]) return value; if ([value isKindOfClass:[NSString class]]) {

      NSNumber *num = dic[value];
      if (num) {
          if (num == (id)kCFNull) return nil;
          return num;
      }
      ...
    

    return nil; }</code></pre>

    這里的轉換處理的主要是 NSString 到 NSNumber 的轉換,由于服務端返回給前端的 bool 類型、空類型多種多樣,這里使用了一個 hash 將所有的情況作為 key 。然后轉換的時候直接從 hash 中取值,將查找效率最大化提高。

    NSString 轉換為 NSDate

    static force_inline NSDate YYNSDateFromString(__unsafe_unretained NSString string) {
      typedef NSDate (^YYNSDateParseBlock)(NSString string);
      #define kParserNum 34
      static YYNSDateParseBlock blocks[kParserNum + 1] = {0};
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{

      ...
      { /*
           Fri Sep 04 00:12:21 +0800 2015 // Weibo, 推ter
           Fri Sep 04 00:12:21.000 +0800 2015
           */
          NSDateFormatter *formatter = [NSDateFormatter new];
          formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
          formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
          NSDateFormatter *formatter2 = [NSDateFormatter new];
          formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
          formatter2.dateFormat = @"EEE MMM dd HH:mm:ss.SSS Z yyyy";
          blocks[30] = ^(NSString *string) { return [formatter dateFromString:string]; };
          blocks[34] = ^(NSString *string) { return [formatter2 dateFromString:string]; };
      }
    

    }); if (!string) return nil; if (string.length > kParserNum) return nil; YYNSDateParseBlock parser = blocks[string.length]; if (!parser) return nil; return parser(string); #undef kParserNum }</code></pre>

    在 NSDictionary 原數據轉模型的時候,會有將時間格式編碼的字符串原數據轉成 NSDate 類型的需求。

    此處作者有個巧妙的設計 —— blocks。它是一個長度為 kParserNum + 1 的數組,里面的元素是YYNSDateParseBlock 類型的閉包。

    作者將幾乎所有(此處代碼有刪減)的關于時間的字符串格式羅列出來,創建等量 NSDateFormatter 對象和閉包對象,然后將 NSDateFormatter 對象 放入閉包對象的代碼塊中返回轉換好的 NSDate 類型,最后將閉包對象放入數組,而 放入的下標即為字符串的長度

    實際上這也是 hash 思想,當傳入有效時間格式的 NSString 對象時,通過其長度就能直接取到 blocks 數組中的閉包對象,調用閉包傳入該字符串就能直接得到轉換后的 NSDate 對象。

    最后使用#undef解除 kParserNum 宏定義,避免外部的宏沖突。

    獲取 NSBlock 類

    static force_inline Class YYNSBlockClass() {
      static Class cls;
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{

      void (^block)(void) = ^{};
      cls = ((NSObject *)block).class;
      while (class_getSuperclass(cls) != [NSObject class]) {
          cls = class_getSuperclass(cls);
      }
    

    }); return cls; // current is "NSBlock" }</code></pre>

    NSBlock 是 OC 中閉包的隱藏跟類(繼承自 NSObject),先將一個閉包強轉為 NSObject 獲取其 Class 類型,然后循環查找父類,直到該 Class 的父類為 NSObject.class。

    五、輔助類 _YYModelPropertyMeta

    位于 NSObject+YYModel.m 中的輔助類 _YYModelPropertyMeta 是基于之前提到的 YYClassPropertyInfo 的二次解析封裝,結合屬性歸屬類添加了很多成員變量來輔助完成框架的核心業務功能,先來看一下它的結構:

    @interface _YYModelPropertyMeta : NSObject {
      @package
      NSString *_name;             ///< property's name
      YYEncodingType _type;        ///< property's type
      YYEncodingNSType _nsType;    ///< property's Foundation type
      BOOL _isCNumber;             ///< is c number type
      Class _cls;                  ///< property's class, or nil
      Class _genericCls;           ///< container's generic class, or nil if threr's no generic class
      SEL _getter;                 ///< getter, or nil if the instances cannot respond
      SEL _setter;                 ///< setter, or nil if the instances cannot respond
      BOOL _isKVCCompatible;       ///< YES if it can access with key-value coding
      BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
      BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:

    NSString _mappedToKey; ///< the key mapped to NSArray _mappedToKeyPath; ///< the key path mapped to (nil if the name is not key path) NSArray _mappedToKeyArray; ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys) YYClassPropertyInfo _info; ///< property's info _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key. } @end</code></pre>

    結合注釋可以看明白一部分的變量的含義,個別成員變量的作用需要結合另外一個輔助類 _YYModelMeta 來解析,后面再討論。

    _isStructAvailableForKeyedArchiver: 標識如果該屬性是結構體,是否支持編碼,支持編碼的結構體可以在源碼里面去看。

    _isKVCCompatible: 標識該成員變量是否支持 KVC。

    在該類的初始化方法中,有如下處理:

    @implementation _YYModelPropertyMeta

  • (instancetype)metaWithClassInfo:(YYClassInfo )classInfo propertyInfo:(YYClassPropertyInfo )propertyInfo generic:(Class)generic { // support pseudo generic class with protocol name if (!generic && propertyInfo.protocols) {

      for (NSString *protocol in propertyInfo.protocols) {
          Class cls = objc_getClass(protocol.UTF8String);
          if (cls) {
              generic = cls;
              break;
          }
      }
    

    } ...</code></pre>

    generic 即為容器的元素類型,若初始化時沒有傳入,這里會讀取屬性<> 包含的字符,轉換成對應的類。

    六、輔助類 _YYModelMeta

    _YYModelMeta 是核心輔助類:

    @interface _YYModelMeta : NSObject {
      @package
      YYClassInfo _classInfo;
      /// Key:mapped key and key path, Value:_YYModelPropertyMeta.
      NSDictionary _mapper;
      /// Array, all property meta of this model.
      NSArray _allPropertyMetas;
      /// Array, property meta which is mapped to a key path.
      NSArray _keyPathPropertyMetas;
      /// Array, property meta which is mapped to multi keys.
      NSArray *_multiKeysPropertyMetas;
      /// The number of mapped key (and key path), same to _mapper.count.
      NSUInteger _keyMappedCount;
      /// Model class type.
      YYEncodingNSType _nsType;

    BOOL _hasCustomWillTransformFromDictionary; BOOL _hasCustomTransformFromDictionary; BOOL _hasCustomTransformToDictionary; BOOL _hasCustomClassFromDictionary; } @end</code></pre>

    _classInfo記錄的 Class 信息;_mapper/_allPropertyMetas是記錄屬性信息(_YYModelPropertyMeta)的 hash 和數組;_keyPathPropertyMetas/_multiKeysPropertyMetas是記錄屬性映射為路徑和映射為多個 key 的數組;_nsType記錄當前模型的類型;最后四個 bool 記錄是否有自定義的相關實現。

    下面將 _YYModelMeta 類初始化方法分塊講解(建議打開源碼對照)。

    黑名單/白名單

    @implementation _YYModelMeta

  • (instancetype)initWithClass:(Class)cls { ... // Get black list NSSet *blacklist = nil; if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
      NSArray *properties = [(id)cls modelPropertyBlacklist];
      if (properties) {
          blacklist = [NSSet setWithArray:properties];
      }
    
    } // Get white list NSSet *whitelist = nil; if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
      NSArray *properties = [(id)cls modelPropertyWhitelist];
      if (properties) {
          whitelist = [NSSet setWithArray:properties];
      }
    
    } ...</code></pre>

    YYModel 是包含了眾多自定義方法的協議,modelPropertyBlacklist和modelPropertyWhitelist分別為黑名單和白名單協議方法。

    自定義容器元素類型

    @implementation _YYModelMeta
  • (instancetype)initWithClass:(Class)cls { ... // Get container property's generic class NSDictionary *genericMapper = nil; if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
      genericMapper = [(id)cls modelContainerPropertyGenericClass];
      if (genericMapper) {
          NSMutableDictionary *tmp = [NSMutableDictionary new];
          [genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
              if (![key isKindOfClass:[NSString class]]) return;
              Class meta = object_getClass(obj);
              if (!meta) return;
              if (class_isMetaClass(meta)) {
                  tmp[key] = obj;
              } else if ([obj isKindOfClass:[NSString class]]) {
                  Class cls = NSClassFromString(obj);
                  if (cls) {
                      tmp[key] = cls;
                  }
              }
          }];
          genericMapper = tmp;
      }
    
    } ...</code></pre>

    同樣是 YYModel 協議下的方法:modelContainerPropertyGenericClass,返回了一個自定義的容器與內部元素的 hash。比如模型中一個容器屬性@property NSArray *arr;,當你希望轉換過后它內部裝有CustomObject類型時,你需要實現該協議方法,返回{@"arr":@"CustomObject"}或者@{@"arr": CustomObject.class}(看上面代碼可知作者做了兼容)。

    當然,你可以指定模型容器屬性的元素,如:@property NSArray *arr;,若你未在上述協議中返回該屬性的映射關系,那么在將該屬性轉換成中間類 _YYModelPropertyMeta 時,會自動查找屬性的  type-ecoding  中的<> 的包裹部分,從而定位你的容器里面是什么元素。(可以查看前面對 _YYModelPropertyMeta 初始化方法的解析)

    查找該類的所有屬性

    @implementation _YYModelMeta
  • (instancetype)initWithClass:(Class)cls { ... NSMutableDictionary allPropertyMetas = [NSMutableDictionary new]; YYClassInfo curClassInfo = classInfo; //循環查找父類屬性,但是忽略跟類 (NSObject/NSProxy) while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
      for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
          if (!propertyInfo.name) continue;
    
    //兼容黑名單和白名單
          if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
          if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
    
    //將屬性轉換為中間類
          _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
                                                                  propertyInfo:propertyInfo
                                                                       generic:genericMapper[propertyInfo.name]];
          ...
    
    //記錄
          allPropertyMetas[meta->_name] = meta;
      }
    
    //指針向父類推進
      curClassInfo = curClassInfo.superClassInfo;
    
    } ...</code></pre>

    自定義映射關系

    @implementation _YYModelMeta
  • (instancetype)initWithClass:(Class)cls { ... if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {

      NSDictionary *customMapper = [(id )cls modelCustomPropertyMapper];
    

    //遍歷自定義映射的 hash

      [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
          _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
          if (!propertyMeta) return;
          [allPropertyMetas removeObjectForKey:propertyName];
    
          if ([mappedToKey isKindOfClass:[NSString class]]) {
              if (mappedToKey.length == 0) return;
              propertyMeta->_mappedToKey = mappedToKey;
              //1、判斷是否是路徑
              NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
              for (NSString *onePath in keyPath) {
                  if (onePath.length == 0) {
                      NSMutableArray *tmp = keyPath.mutableCopy;
                      [tmp removeObject:@""];
                      keyPath = tmp;
                      break;
                  }
              }
              if (keyPath.count > 1) {
                  propertyMeta->_mappedToKeyPath = keyPath;
                  [keyPathPropertyMetas addObject:propertyMeta];
              }
              //2、連接相同映射的屬性
              propertyMeta->_next = mapper[mappedToKey] ?: nil;
              mapper[mappedToKey] = propertyMeta;
    
          } else if ([mappedToKey isKindOfClass:[NSArray class]]) {
              ...
          }
      }];
    

    } ...</code></pre>

    modelCustomPropertyMapper協議方法是用于自定義映射關系,比如需要將 json 中的 id 字段轉換成屬性:@property NSString *ID;,由于系統是默認將屬性的名字作為映射的依據,所以這種業務場景需要使用者自行定義映射關系。

    在實現映射關系協議時,有多種寫法:

    + (NSDictionary *)modelCustomPropertyMapper {

       return @{@"name"  : @"n",
                @"page"  : @"p",
                @"desc"  : @"ext.desc",
                @"bookID": @[@"id", @"ID", @"book_id"]};
    

    }</code></pre>

    key 是模型中的屬性名字,value 就是對于 json(或字典)數據源的字段。特別的,可以使用“.”來鏈接字符形成一個路徑,也可以傳入一個數組,當映射的是一個數組的時候,json -> model 的時候會找到第一個有效的映射作為model屬性的值。比如上面代碼中,在數據源中找到ID字符,便會將其值給當前模型類的bookID屬性,忽略掉后面的映射(book_id)。

    性能層面,可以在代碼中看到兩個閃光點:

    1、判斷是否是路徑

    將映射的value拆分成keyPath數組,然后做了一個遍歷,當遍歷到@""空字符值時,深拷貝一份keyPath移除所有的@""然后break。

    這個操作看似簡單,實則是作者對性能的優化。通常情況下,傳入的路徑是正確的a.b.c,這時不需要移除@""。而當路徑錯誤,比如a..b.c、a.b.c.時,分離字符串時keyPath中就會有空值@""。由于componentsSeparatedByString方法返回的是一個不可變的數組,所以移除keyPath中的@""需要先深拷貝一份可變內存。

    作者此處的想法很明顯:在正常情況下,不需要移除,也就是不需要深拷貝keyPath增加內存開銷。

    若考慮到極致的性能,會發現此處做了兩個遍歷(一個拆分mappedToKey的遍歷,一個keyPath的遍歷),應該一個遍歷就能做出來,有興趣的朋友可能嘗試一下。

    不過此處的路徑不會很長,也就基本可以忽略掉多的這幾次遍歷了。

    2、連接相同映射的屬性

    之前解析 _YYModelPropertyMeta 類時,可以發現它有個成員變量_YYModelPropertyMeta *_next;,它的作用就可以從此處看出端倪。

    代碼中,mapper是記錄的所有屬性的 hash(由前面未貼出代碼得到),hash 的 key 即為映射的值(路徑)。作者做了一個判斷,若mapper中存在相同 key 的屬性,就改變了一下指針,做了一個鏈接,將相同映射 key 的屬性連接起來形成一個鏈表。

    這么做的目的很簡單,就是為了在 json 數據源查找到某個目標值時,可以移動_next指針,將所有的相同映射的屬性統統賦值,從而達到不重復查找數據源相同路徑值的目的。

    對象緩存

    + (instancetype)metaWithClass:(Class)cls {
      if (!cls) return nil;
      static CFMutableDictionaryRef cache;
      static dispatch_once_t onceToken;
      static dispatch_semaphore_t lock;
      dispatch_once(&onceToken, ^{

      cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
      lock = dispatch_semaphore_create(1);
    

    }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); _YYModelMeta meta = CFDictionaryGetValue(cache, (__bridge const void )(cls)); dispatch_semaphore_signal(lock); if (!meta || meta->_classInfo.needUpdate) {

      meta = [[_YYModelMeta alloc] initWithClass:cls];
      if (meta) {
          dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
          CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
          dispatch_semaphore_signal(lock);
      }
    

    } return meta; }</code></pre>

    _YYModelMeta 的緩存邏輯和 上文中 YYClassInfo 的緩存邏輯一樣,不多闡述。

    七、給數據模型屬性賦值 / 將數據模型解析成 json

    實際上上文已經將 YYModel 的大部分內容講解完了,可以說之前的都是準備工作。

    NSObject+YYModel.m 中有個很長的方法:

    static void ModelSetValueForProperty(__unsafe_unretained id model,

                                   __unsafe_unretained id value,
                                   __unsafe_unretained _YYModelPropertyMeta *meta) {...}</code></pre> 
    

    看該方法的名字應該很容易猜到,這就是將數據模型(model)中的某個屬性(meta)賦值為目標值(value)。具體代碼不貼了,主要是根據之前的一些輔助的類,利用objc_msgSend給目標數據 model 發送屬性的 setter 方法。代碼看起來復雜,實際上很簡單。

    相反地,有這樣一個方法將已經賦值的數據模型解析成 json:

    static id ModelToJSONObjectRecursive(NSObject *model) {...}

    實現都是根據前文解析的那些中間類來處理的。

    性能的優化

    直接使用objc_msgSend給對象發送消息的效率要高于使用 KVC,可以在源碼中看到作者但凡可以使用發送消息賦值處理的,都不會使用 KVC。

    八、從入口函數說起

    回到開頭,有幾個方法是經常使用的(當然包括 NSArray 和 NSDictionary 中的延展方法):

    + (nullable instancetype)yy_modelWithJSON:(id)json;

  • (nullable instancetype)yy_modelWithDictionary:(NSDictionary )dictionary;</code></pre>

    這些方法其實落腳點都在一個方法:

    - (BOOL)yy_modelSetWithDictionary:(NSDictionary )dic {
      if (!dic || dic == (id)kCFNull) return NO;
      if (![dic isKindOfClass:[NSDictionary class]]) return NO;
    //通過 Class 獲取 _YYModelMeta 實例
      _YYModelMeta modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
      ...
    /使用 ModelSetContext 結構體將以下內容裝起來:
    1、具體模型對象(self)
    2、通過模型對象的類 Class 轉換的 _YYModelMeta 對象(modelMeta) 3、json 轉換的原始數據(dic) / ModelSetContext context = {0}; context.modelMeta = (__bridge void )(modelMeta); context.model = (bridge void *)(self); context.dictionary = (bridge void *)(dic);

//執行轉換 if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) { CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); if (modelMeta->_keyPathPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } if (modelMeta->_multiKeysPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } } else { CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, CFRangeMake(0, modelMeta->_keyMappedCount), ModelSetWithPropertyMetaArrayFunction, &context); }

...
return YES;

}</code></pre>

這里使用 CF 框架下的函數是為提升執行效率。

至于ModelSetWithPropertyMetaArrayFunction和ModelSetWithDictionaryFunction的實現不復雜,不多解析。

九、組件對外提供的一些工具方法

作者很細心的提供了一些工具方法方便開發者使用。

拷貝

- (id)yy_modelCopy;

注意是深拷貝。

歸檔/解檔

- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;

  • (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;</code></pre>

    喜歡用歸解檔朋友的福音。

    hash 值

    - (NSUInteger)yy_modelHash;

    提供了一個現成的 hash 表算法,方便開發者構建 hash 數據結構。

    判斷相等

    - (BOOL)yy_modelIsEqual:(id)model;

    在方法實現中,當兩個待比較對象的 hash 值不同時,作者使用if ([self hash] != [model hash]) return NO;判斷來及時返回,提高比較效率。

    后語

    本文主要是剖析 YYModel 的重點、難點、閃光點,更多的技術實現細節請查閱源碼,作者的細節處理得很棒。

    從該框架中,可以看到作者對性能的極致追求,這也是作為一位合格的開發者應有的精神。不斷的探究實踐思考,才能真正的做好一件事。

    希望本文能讓讀者朋友對 YYModel 有更深的理解

    參考文獻:作者 ibireme 的博客   iOS JSON 模型轉換庫評測

    作者:indulge_in

    來自:https://www.jianshu.com/p/fe30e6bbc551

     

     

 本文由用戶 范德薩 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!