iOS Runtime基礎

RuntimeObject-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指針指向根元類

Object-C類圖.jpg

消息發送

  • 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中有一個是采用這種方案實現動態首頁的。另外,網上也有對這個問題的詳細描述。下面這篇鏈接是比較好的一篇。

iOS應用架構談 組件化方案
CTMediator

  • 關于組建化,本人更偏向于蘑菇街的方案。原因是這個方案url的編碼更自由一點,對應關系可以自定義。另外,有蘑菇街的實踐也是一個考慮原因。

蘑菇街 App 的組件化之路
MGJRouter

對象關聯

  • Category可以添加方法,但是怎么樣添加屬性呢?答案是通過對象關聯的方法。

  • Category中的屬性,只會生成settergetter方法,不會生成成員變量

  • 關聯的屬性和一個全局變量關聯,那個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;

字典與模型互轉

  • 在類的頭文件中定義的屬性,不包括額外定義的成員變量

  • 使用屬性列表函數,而不是成員列表函數

// 屬性列表,不是成員變量列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
// 獲取屬性的名字
const char *property_getName(objc_property_t property) ;
  • 可以使用KVC,也可以使用objc_msgSend調用gettersetter函數進行值的讀取和設置

字典轉模型,自動歸檔等推薦的第三方庫為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文件中定義,是基類中的函數。

繼承自NSObject的不常用又很有用的函數(2)

方法交換

  • 這個是有使用場景的,最常見的場景是“統計埋點”

iOS動態性(二)可復用而且高度解耦的用戶統計埋點實現

  • 這項技術有一個專門的名字叫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); 
  • 可以考慮用來解決崩潰的問題,比如下面的文章

使用method-swizzling讓程序更健壯

  • 方法交換需要放在 +(void)load方法中,并且要用dispatch_once進行保護。道理很簡單,交換偶數次不就被還原了嗎?
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,774評論 0 9
  • objc_getAssociatedObject返回與給定鍵的特定對象關聯的值。ID objc_getAssoci...
    有一種再見叫青春閱讀 1,643評論 0 7
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 792評論 0 1
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,636評論 33 466
  • 今天去廣州出了個差,計劃好的東西又泡了湯,其實這件事我去不去關系不大,但人生就是這么無奈,自己權力不到,說話沒...
    aweness閱讀 47評論 0 0