Runtime
是Object-C
的一種特性,本人并不感冒。不過這塊內容卻很流行,也是Object-C
動態特性的來源,被認為是比Swift
好的地方。
平時用不用是一回事,知道這些基礎知識還是有好處的,至少跟人聊的時候能打上話。
動態特性帶來了方便,但是同時也帶來了很大的安全隱患,在使用的時候盡量謹慎一點。
另外,這是底層的函數,像ARC
這種偷懶用的好特性就沒有了,要注意內存泄漏問題。(基本上無法避免)
Runtime全方位裝逼指南
RuntimeLearn
這篇文章寫得比較好,基礎概念寫得比較清晰,值得優先讀
iOS動態性(二)可復用而且高度解耦的用戶統計埋點實現
統計埋點確實是一個比較典型的應用,這篇文章寫得比較清楚
對象、類、元類
Object-C
是一種面向對象的語言。“一切皆對象”是本質的一點。Object-C
就是借助實例,類,元類三級結構來實現這一點的。isa
指針是Object-C
采用c
語言的"結構體"來實現面向對象的方法。實例的
isa
指向對應的類,類的isa
指向元類。元類的
isa
都指向根元類,根元類的isa
指向自己。類和元類除了
isa
指針,還有一個super_class
指針,指向父類(父元類)。根類的父類指向nil
元類保存靜態變量和靜態類方法
跟元類的父類指向根類,根類的
isa
指針指向根元類
消息發送
Objective-C
中的方法調用,不是簡單的方法調用,而是發送消息,也就是說,其實[receiver message]
會被編譯器轉化為:objc_msgSend(receiver, selector)
這個是
Objective-C
動態特性的本質;在函數調用之前插入一個消息轉發,按照對象(id)
,函數(SEL)
,參數三級結構實現動態特性。一些函數定義
void objc_msgSend(void /* id self, SEL op, ... */ );
typedef struct objc_selector *SEL;
typedef struct objc_object *id;
// 下面幾個都是將字符串轉換為函數指針SEL;根據使用場景選擇方便的
// 這個c字符串
SEL 變量名 = sel_registerName(const char *str); // 在c的模塊中推薦用
// 下面兩個是NSString
SEL 變量名 = NSSelectorFromString(NSString *aSelectorName); // 推薦用這個
SEL 變量名 = @selector(NSString *aSelectorName); // 這個用得比較多,不過難理解,不是很推薦
- 例子,有個類TestClass,有如下方法和調用
- (void)showSizeWithWidth:(float)aWidth andHeight:(float)aHeight{
NSLog(@"size is %.2f * %.2f",aWidth, aHeight);
}
TestClass *testObject = [[TestClass alloc] init];
[testObject showSizeWithWidth:110.5f andHeight:200.0f]
也可以用下面的調用方式:
((void (*) (id, SEL, float, float)) objc_msgSend) (testObject, sel_registerName("showSizeWithWidth:andHeight:"), 110.5f, 200.0f);
這個就是
Object-C
動態特性的來源。id、SEL
都是一些指向結構體的指針,objc_msgSend
的類型是void
,在具體使用的時候需要強制轉化為需要的類型。(參數類型,返回值類型都要考慮到)。這里有很大的安全隱患,代碼難懂,很容易出錯。所以本人一直不建議用。編譯器會根據情況在
objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret或 objc_msgSend_fpret
五個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用objc_msgSendSuper
方法,如果消息返回值是數據結構,就會調用objc_msgSendSuper_stret
方法,如果返回值是浮點數,則調用objc_msgSend_fpret
方法。
類的本質
-
Class
也是一個結構體指針類型
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
ivars:
指向該類的成員變量列表。大多數情況可以認為這個也是類的屬性列表。不過有說法認為有另外的屬性列表,只是這里看不出來。methodLists:
指向該類的實例方法列表,它將方法選擇器和方法實現地址聯系起來。這也是Category
實現的原理,同樣解釋了Category
不能添加屬性的原因。protocols:
指向該類的協議列表。
這里沒有單獨的屬性列表,給理解帶來了困難。如果都是基本類型,可以認為成員變量列表就是屬性列表,比如下面的“自動歸檔”部分處理的那樣。
另外一種說法是有單獨的屬性列表,只是這里沒有顯示出來。比如“字典轉模型”,就使用了屬性列表。
class_copyPropertyList和class_copyIvarList的區別
遠程調用
有些時候,比如首頁,顯示的內容由后臺決定,實現所謂的“千人千面”
這里的實現基礎,就是類
Class
、方法SEL
與字符串NSString
的互轉
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class _Nullable NSClassFromString(NSString *aClassName);
有了
Class
之后,就可以通過id object = [[Class alloc] init];
得到對象有對象和
SEL
之后,就可以通過函數執行
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
客戶端先把調用的類和方法在本地實現好,后臺發送類名和方法名的字符串,然后根據字符串,實現動態調用。
本人經歷過的四五個
App
中有一個是采用這種方案實現動態首頁的。另外,網上也有對這個問題的詳細描述。下面這篇鏈接是比較好的一篇。
- 關于組建化,本人更偏向于蘑菇街的方案。原因是這個方案
url
的編碼更自由一點,對應關系可以自定義。另外,有蘑菇街的實踐也是一個考慮原因。
對象關聯
Category
可以添加方法,但是怎么樣添加屬性呢?答案是通過對象關聯的方法。Category
中的屬性,只會生成setter
和getter
方法,不會生成成員變量關聯的屬性和一個全局變量關聯,那個
key
一般是一個靜態全局變量相關函數:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object); // 移除所有關聯屬性,不要輕易使用
// 屬性的修飾符,根據情況設置
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};
這方面的資料很多,使用也相對簡單,比如下面就有一篇:
iOS-OC-Runtime使用小談(objc_setAssociatedObject)
自動歸檔
- 自動歸檔主要是實現
NSCoding
協議
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end
- 相關函數
// 把成員變量當做屬性,獲取列表
Ivar *class_copyIvarList(Class cls, unsigned int *outCount);
// 獲得成員變量的名字,帶_前綴;這是c字符串
const char *ivar_getName(Ivar v) ;
// 通過KVC獲得成員變量的值
- (nullable id)valueForKey:(NSString *)key;
// 通過KVC設置成員變量的值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- 下面是一篇比較好的參考文章:
Runtime應用(三)實現NSCoding的自動歸檔和自動解檔
字典與模型互轉
在類的頭文件中定義的屬性,不包括額外定義的成員變量
使用屬性列表函數,而不是成員列表函數
// 屬性列表,不是成員變量列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
// 獲取屬性的名字
const char *property_getName(objc_property_t property) ;
- 可以使用
KVC
,也可以使用objc_msgSend
調用getter
和setter
函數進行值的讀取和設置
字典轉模型,自動歸檔等推薦的第三方庫為YYModel
,實際用過,確實很方便
YYModel
方法動態解析
給一個對象發消息,就是執行objc_msgSend(id, SEL, ...)
函數。使用很小心,id、SEL
都正確的情況下,當然沒問題。但是,如果出錯了呢?
是的,崩潰,崩潰信息一般如下:
unrecognized selector sent to instance ...
- 不是
SEL
找不到嗎?下面這個函數就是給機會,修改SEL
參數
+ (BOOL)resolveInstanceMethod:(SEL)sel;
- 這個對象沒有,其他對象可能有啊。下面這個函數就是給機會,修改
id
參數
- (id)forwardingTargetForSelector:(SEL)aSelector;
- 其他對象也沒有這個
SEL
,那么再給機會,id、SEL
都改,完全自定義。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- 最后,就會拋出異常,就是常說的崩潰
- (void)doesNotRecognizeSelector:(SEL)aSelector;
- 這塊內容具體的應用場景在實際工作中還沒有遇到過。上面這些函數都在
NSObject.h
文件中定義,是基類中的函數。
方法交換
- 這個是有使用場景的,最常見的場景是“統計埋點”
- 這項技術有一個專門的名字叫Method Swizzling,為什么這么叫,原因不清楚。
Objective-C的hook方案(一): Method Swizzling
- 主要用到的函數:
Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);
IMP method_getImplementation(Method m);
IMP method_setImplementation(Method m, IMP imp);
BOOL class_addMethod(Class cls, SEL name, IMP imp,
const char *types);
- 可以考慮用來解決崩潰的問題,比如下面的文章
- 方法交換需要放在
+(void)load
方法中,并且要用dispatch_once
進行保護。道理很簡單,交換偶數次不就被還原了嗎?