YYModel底層解析- Runtime
這段時間一直在忙新的需求,沒有時間來整理程式碼,發表自己技術部落格,今天我們來看一下YYModel的底層解析以及如何使用,希望對大家有所幫助!
一 概述
概括
YYModel是一個輕量級的JSON模型轉換庫,它的思路非常清晰程式碼風格也很好,所以還是建議大家看一下底層實現的邏輯,也可以從原始碼加深對Runtime的理解。
簡介
下面是YYModel第三方庫的一些程式碼結構。
YYModel的總共檔案只有5個檔案
除掉YYModel.h之外,只剩下了YYClassInfo和NSObject+YYModel兩個模組啦!
- YYClassInfo功能主要是將Runtime層級中的一些結構體封裝到NSObject中呼叫;
- NSObject+YYModel功能是提供呼叫的介面以及實現具體的模型轉換邏輯。
前面已經講到YYClassInfo主要功能是將Runtime層級的結構體封裝到NSObject層級以便呼叫。下面是YYClassInfo與Runtime層級對比:
二、詳細
1.YYClassIvarInfo
YYClassIvarInfo && objc_ivar
下面是YYClassIvarInfo
/** Instance variable information. */ @interface YYClassIvarInfo : NSObject @property (nonatomic, assign, readonly) Ivar ivar;///< ivar opaque struct @property (nonatomic, strong, readonly) NSString *name;///< Ivar's name @property (nonatomic, assign, readonly) ptrdiff_t offset;///< Ivar's offset @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< Ivar's type encoding @property (nonatomic, assign, readonly) YYEncodingType type;///< Ivar's type /** Creates and returns an ivar info object. @param ivar ivar opaque struct @return A new object, or nil if an error occurs. */ - (instancetype)initWithIvar:(Ivar)ivar; @end
緊接著我們看一下Runtime的objc_ivar表示變數的結構體
struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; // 變數名稱 char * _Nullable ivar_type OBJC2_UNAVAILABLE; // 變數型別 int ivar_offset OBJC2_UNAVAILABLE; // 變數偏移量 #ifdef __LP64__ // 如果已定義 __LP64__ 則表示正在構建 64 位目標 int space OBJC2_UNAVAILABLE; // 變數空間 #endif }
注:日常開發中,NSString型別的屬性會用copy修飾,看上面 YYClassIvarInfo中typeEncoding和name是用strong修飾。這是因為其內部先是通過Runtime方法拿到const char * 之後通過 stringWithUTF8String 方法之後轉為 NSString 的。所以 NSString 這類屬性在確定其不會在初始化之後出現被修改的情況下,使用 strong來修飾 做一次單純的強引用在效能上是比 copy 要高的。
YYClassMethodInfo && objc_method
下面是YYClassMethodInfo
@interface YYClassMethodInfo : NSObject @property (nonatomic, assign, readonly) Method method; ///< 方法 @property (nonatomic, strong, readonly) NSString *name; ///< 方法名稱 @property (nonatomic, assign, readonly) SEL sel; ///< 方法選擇器 @property (nonatomic, assign, readonly) IMP imp; ///< 方法實現,指向實現方法函式的函式指標 @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 方法引數和返回型別編碼 @property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< 返回值型別編碼 @property (nullable, nonatomic, strong, readonly) NSArray<nsstring *> *argumentTypeEncodings; ///< 引數型別編碼陣列 - (instancetype)initWithMethod:(Method)method; @end
YYClassMethodInfo則是對Rutime裡面的objc_method的封裝,緊接著我們看Runtime的objc_method結構體
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; // 方法名稱 char * _Nullable method_types OBJC2_UNAVAILABLE; // 方法型別 IMP _Nonnull method_imp OBJC2_UNAVAILABLE; // 方法實現(函式指標) }
YYClassPropertyInfo && property_t
YYClassPropertyInfo是對Runtime中property_t的封裝
@interface YYClassPropertyInfo : NSObject @property (nonatomic, assign, readonly) objc_property_t property; ///< 屬性 @property (nonatomic, strong, readonly) NSString *name; ///< 屬性名稱 @property (nonatomic, assign, readonly) YYEncodingType type; ///< 屬性型別 @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< 屬性型別編碼 @property (nonatomic, strong, readonly) NSString *ivarName; ///< 變數名稱 @property (nullable, nonatomic, assign, readonly) Class cls; ///< 型別 @property (nullable, nonatomic, strong, readonly) NSArray<nsstring *> *protocols; ///< 屬性相關協議 @property (nonatomic, assign, readonly) SEL getter; ///< getter 方法選擇器 @property (nonatomic, assign, readonly) SEL setter; ///< setter 方法選擇器 - (instancetype)initWithProperty:(objc_property_t)property; @end</nsstring *>
然後來看一下Runtime的property_t結構體
struct property_t { const char *name; // 名稱 const char *attributes; // 修飾 };
YYClassInfo && objc_class
YYClassInfo封裝了Runtime的objc_class,下面看一下YYClassInfo
YYClassInfo
@interface YYClassInfo : NSObject @property (nonatomic, assign, readonly) Class cls; ///< 類 @property (nullable, nonatomic, assign, readonly) Class superCls; ///< 超類 @property (nullable, nonatomic, assign, readonly) Class metaCls;///< 元類 @property (nonatomic, readonly) BOOL isMeta; ///< 元類標識,自身是否為元類 @property (nonatomic, strong, readonly) NSString *name; ///< 類名稱 @property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< 父類(超類)資訊 @property (nullable, nonatomic, strong, readonly) NSDictionary<nsstring *, yyclassivarinfo *> *ivarInfos; ///< 變數資訊 @property (nullable, nonatomic, strong, readonly) NSDictionary<nsstring *, yyclassmethodinfo *> *methodInfos; ///< 方法資訊 @property (nullable, nonatomic, strong, readonly) NSDictionary<nsstring *, yyclasspropertyinfo *> *propertyInfos; ///< 屬性資訊 - (void)setNeedUpdate; - (BOOL)needUpdate; + (nullable instancetype)classInfoWithClass:(Class)cls; + (nullable instancetype)classInfoWithClassName:(NSString *)className; @end
objc_class
// objc.h typedef struct objc_class *Class; // runtime.h struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; // isa 指標 #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; // 父類(超類)指標 const char * _Nonnull name OBJC2_UNAVAILABLE; // 類名 long version OBJC2_UNAVAILABLE; // 版本 long info OBJC2_UNAVAILABLE; // 資訊 long instance_size OBJC2_UNAVAILABLE; // 初始尺寸 struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 變數列表 struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表 struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 快取 struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 協議列表 #endif } OBJC2_UNAVAILABLE;
註解:下面是Runtime關於class的知識
下面是對應的講解。
YYClassInfo 的初始化
+ (instancetype)classInfoWithClass:(Class)cls { // 判空入參 if (!cls) return nil; // 單例快取 classCache 與 metaCache,對應快取類和元類 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); // 這裡把 dispatch_semaphore 當做鎖來使用(當訊號量只有 1 時) lock = dispatch_semaphore_create(1); }); // 初始化之前,首先會根據當前 YYClassInfo 是否為元類去對應的單例快取中查詢 // 這裡使用了上面的 dispatch_semaphore 加鎖,保證單例快取的執行緒安全 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); // 如果沒找到,才會去老實初始化 if (!info) { info = [[YYClassInfo alloc] initWithClass:cls]; if (info) { // 初始化成功 // 執行緒安全 dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); // 根據初始化資訊選擇向對應的類/元類快取注入資訊,key = cls,value = info CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info)); dispatch_semaphore_signal(lock); } } return info; }
下面總結一下初始化主要步驟:
- 首先建立單例快取,類快取和元類快取;
- 使用dispatch_semaphore 保證快取執行緒安全;
- 初始化操作之前首先快取中查詢是否已經向快取中註冊過的當前要初始化的YYClassInfo;
- 如果查詢快取物件,需要判斷物件是否需要更新以及其他相關操作;
- 如果沒有找到快取物件,就開始初始化;
- 初始化成功之後,向快取中註冊YYClassInfo例項。
2.NSObject+YYModel
NSObject+YYModel在YYModel主要任務是利用YYClassInfo層級封裝的類來執行JSON模型之間的轉換邏輯。下面是NSObject+YYModel講述的主要內容:
- 型別編碼的解析
- 資料結構的定義
- 遞迴模型的轉換
- 介面相關的程式碼
下面將部分講解
資料結構的定義
NSObject+YYModel重新定義了兩個類,來使用 YYClassInfo 中的封裝。
_YYModelPropertyMeta
@interface _YYModelPropertyMeta : NSObject { @package NSString *_name;///< 屬性名稱 YYEncodingType _type;///< 屬性型別 YYEncodingNSType _nsType;///< 屬性在 Foundation 框架中的型別 BOOL _isCNumber;///< 是否為 CNumber Class _cls;///< 屬性類 Class _genericCls;///< 屬性包含的泛型型別,沒有則為 nil SEL _getter;///< getter SEL _setter;///< setter BOOL _isKVCCompatible;///< 如果可以使用 KVC 則返回 YES BOOL _isStructAvailableForKeyedArchiver; ///< 如果可以使用 archiver/unarchiver 歸/解檔則返回 YES BOOL _hasCustomClassFromDictionary; ///< 類/泛型自定義型別,例如需要在陣列中實現不同型別的轉換需要用到 /* property->key:_mappedToKey:key_mappedToKeyPath:nil_mappedToKeyArray:nil property->keyPath:_mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil property->keys:_mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath_mappedToKeyArray:keys(array) */ NSString *_mappedToKey;///< 對映 key NSArray *_mappedToKeyPath;///< 對映 keyPath,如果沒有對映到 keyPath 則返回 nil NSArray *_mappedToKeyArray;///< key 或者 keyPath 的陣列,如果沒有對映多個鍵的話則返回 nil YYClassPropertyInfo *_info;///< 屬性資訊,詳見上文 YYClassPropertyInfo && property_t 章節 _YYModelPropertyMeta *_next; ///< 如果有多個屬性對映到同一個 key 則指向下一個模型屬性元 } @end
_YYModelMeta
@interface _YYModelMeta : NSObject { @package YYClassInfo *_classInfo; /// Key:被對映的 key 與 keyPath, Value:_YYModelPropertyMeta. NSDictionary *_mapper; /// Array<_YYModelPropertyMeta>, 當前模型的所有 _YYModelPropertyMeta 陣列 NSArray *_allPropertyMetas; /// Array<_YYModelPropertyMeta>, 被對映到 keyPath 的 _YYModelPropertyMeta 陣列 NSArray *_keyPathPropertyMetas; /// Array<_YYModelPropertyMeta>, 被對映到多個 key 的 _YYModelPropertyMeta 陣列 NSArray *_multiKeysPropertyMetas; /// 對映 key 與 keyPath 的數量,等同於 _mapper.count NSUInteger _keyMappedCount; /// 模型 class 型別 YYEncodingNSType _nsType; // 忽略 ... } @end
三、使用
1.簡單Model與JSON相互轉換
#import <Foundation/Foundation.h> //"time":"2018-07-04 12:13:52", //"ftime":"2018-07-04 12:13:52", //"context":"快件已簽收 簽收人: 他人代收 感謝使用圓通速遞,期待再次為您服 @interface IOALogisticsDetailModel : NSObject @property(nonatomic,copy)NSString*time; @property(nonatomic,copy)NSString*ftime; @property(nonatomic,copy)NSString*context; @end #import "IOALogisticsDetailModel.h" @implementation IOALogisticsDetailModel @end
下面是運用
- (NSArray <IOALogisticsDetailModel *>*)setupOrderWithArray:(NSArray <NSDictionary *>*)array{ NSMutableArray <IOALogisticsDetailModel *>*modelArray = [NSMutableArray arrayWithCapacity:array.count]; for(NSDictionary *dic in array){ IOALogisticsDetailModel *model = [IOALogisticsDetailModel yy_modelWithDictionary:dic]; if (!model) continue; [modelArray addObject:model]; } return modelArray; }
紅色部分就是應用。
如果需要Model轉為JSON如下
#import <Foundation/Foundation.h> @interface IOAOrderAftersaleRequestModel : NSObject @property(nonatomic,copy)NSString *order_sn; @property(nonatomic,copy)NSString *rec_id; @property(nonatomic,copy)NSString*is_type; @property(nonatomic,assign)NSInteger refund_count; @property(nonatomic,copy)NSString *content; @property(nonatomic,copy)NSString *return_attachs; @property(nonatomic,copy)NSString *shop_id; @property(nonatomic,copy)NSString *reason; @property(nonatomic,assign)float total; @end #import "IOAOrderAftersaleRequestModel.h" @implementation IOAOrderAftersaleRequestModel @end
下面是運用
//提交退貨商品 @interface IOAOrderAftersaleRequest:IOARequest @property (nonatomic,strong)IOAOrderAftersaleRequestModel *requestModel; @end //提交退貨商品 @implementation IOAOrderAftersaleRequest - (id)requestArgument{ NSMutableDictionary *dic = [IOAApiManager getParametersWithService:@"App.Order.SetOrderAftersaleGoodsrefundsList"]; NSDictionary *temDic = [self.requestModel yy_modelToJSONObject]; [dic addEntriesFromDictionary:temDic]; return dic; }
2.Model屬性名與JSON中key不同
// JSON: { "n":"Harry Pottery", "p": 256, "ext" : { "desc" : "A book written by J.K.Rowing." }, "ID" : 100010 } // Model: @interface Book : NSObject @property NSString *name; @property NSInteger page; @property NSString *desc; @property NSString *bookID; @end @implementation Book //返回一個 Dict,將 Model 屬性名對對映到 JSON 的 Key。 + (NSDictionary *)modelCustomPropertyMapper { return @{@"name" : @"n", @"page" : @"p", @"desc" : @"ext.desc", @"bookID" : @[@"id",@"ID",@"book_id"]}; }
3.Model包含Model
// JSON { "author":{ "name":"J.K.Rowling", "birthday":"1965-07-31T00:00:00+0000" }, "name":"Harry Potter", "pages":256 } // Model: 什麼都不用做,轉換會自動完成 @interface Author : NSObject @property NSString *name; @property NSDate *birthday; @end @implementation Author @end @interface Book : NSObject @property NSString *name; @property NSUInteger pages; @property Author *author; //Book 包含 Author 屬性 @end @implementation Book @end
下面在我們專案中的使用
#import <Foundation/Foundation.h> #import "IOAOrder.h" @interface IOAOrderGroup : NSObject @property(nonatomic,copy)NSString *order_id; @property(nonatomic,copy)NSString *parent_sn; @property(nonatomic,copy)NSString *order_sn; @property(nonatomic,copy)NSString *order_status; @property(nonatomic,copy)NSString *refund_status; @property(nonatomic,copy)NSString *return_status; @property(nonatomic,copy)NSString *pay_status; @property(nonatomic,copy)NSString *total_amount; @property(nonatomic,copy)NSString *company_name; @property(nonatomic,copy)NSString *company_logo; @property(nonatomic,copy)NSString *shop_id; @property(nonatomic,copy)NSString *stroe_id; @property(nonatomic,copy)NSString *order_amount; @property(nonatomic,assign)int store_id; @property (nonatomic,strong)NSArray<IOAOrder *> *goods_list; @end #import "IOAOrderGroup.h" #import <YYModel/YYModel.h> @implementation IOAOrderGroup + (NSDictionary *)modelContainerPropertyGenericClass{ return @{@"goods_list":[IOAOrder class]}; } @end #import <Foundation/Foundation.h> @interface IOAOrder : NSObject @property(nonatomic,copy)NSString *rec_id; @property(nonatomic,copy)NSString *order_id; @property(nonatomic,copy)NSString *brand_name; @property(nonatomic,copy)NSString *goods_id; @property(nonatomic,copy)NSString *goods_name; @property(nonatomic,copy)NSString *goods_sn; @property(nonatomic,copy)NSString *goods_num; @property(nonatomic,copy)NSString *market_price; @property(nonatomic,copy)NSString *goods_price; @property(nonatomic,copy)NSString *cost_price; @property(nonatomic,copy)NSString *member_goods_price; @property(nonatomic,copy)NSString *total_price; @property(nonatomic,copy)NSString *give_integral; @property(nonatomic,copy)NSString *spec_key; @property(nonatomic,copy)NSString *unit; @property(nonatomic,copy)NSString *spec_key_name; @property(nonatomic,copy)NSString *bar_code; @property(nonatomic,copy)NSString *is_comment; @property(nonatomic,copy)NSString *prom_type; @property(nonatomic,copy)NSString *prom_id; @property(nonatomic,copy)NSString *is_send; @property(nonatomic,copy)NSString *delivery_id; @property(nonatomic,copy)NSString *add_time; @property(nonatomic,copy)NSString *update_time; @property(nonatomic,copy)NSString *image_url; @property(nonatomic,assign)BOOL selected; @end #import "IOAOrder.h" @implementation IOAOrder @end View Code
4.白名單黑名單
@interface User @property NSString *name; @property NSUInteger age; @end @implementation Attributes // 如果實現了該方法,則處理過程中會忽略該列表內的所有屬性 + (NSArray *)modelPropertyBlacklist { return @[@"test1", @"test2"]; } // 如果實現了該方法,則處理過程中不會處理該列表外的屬性。 + (NSArray *)modelPropertyWhitelist { return @[@"name"]; } @end