iOS runtime探究(四): 從runtiem開始實踐Category添加屬性與黑魔法method swizzling

你要知道的runtime都在這里

轉載請注明出處 http://www.lxweimin.com/p/e2c0c67d39ed

本文主要講解runtime相關知識,從原理到實踐,由于包含內容過多分為以下五篇文章詳細講解,可自行選擇需要了解的方向:

本文是系列文章的第四篇文章從runtiem開始: 實踐Category添加屬性與黑魔法method swizzling,本文將會介紹比較常用的runtime關聯對象以及runtime對方法的處理和一個交換方法實現的方法。

關聯對象 Associated Object

如果我們想為系統的類添加一個方法可以采用類別的方式進行擴展,相對來說比較簡單,但如果要添加一個屬性或稱為成員變量,通常采用的方法就是繼承,這樣就比較繁瑣了,如果不想去繼承那就可以通過runtime來進行關聯對象操作。

使用runtime關聯對象添加屬性與我們自定義類時定義的屬性其實是兩個不同的概念,通過關聯對象添加屬性本質上是使用類別進行擴展,通過添加settergetter方法從而在訪問時可以使用點語法進行方法,在使用上與自定義類定義的屬性沒有區別。

具體需要使用的C函數如下:

//為一個實例對象添加一個關聯對象,由于是C函數只能使用C字符串,這個key就是關聯對象的名稱,value為具體的關聯對象的值,policy為關聯對象策略,與我們自定義屬性時設置的修飾符類似
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//通過key和實例對象獲取關聯對象的值
id objc_getAssociatedObject(id object, const void *key);
//刪除實例對象的關聯對象
void objc_removeAssociatedObjects(id object);

通過注釋和函數名不難發現上訴三個方法分別是設置關聯對象、獲取關聯對象和刪除關聯對象。

需要說明一下objc_AssociationPolicy,具體的定義如下:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
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. */
};

這些關鍵詞很眼熟,沒錯,就是property使用的修飾符,具體含義也與property修飾符相同,如果對propertyproperty修飾符等有疑問可以查閱本系列教程第三篇文章從runtime開始: 理解OC的屬性property或本博客另外兩篇關于property的講解文章:iOS @property探究(一): 基礎詳解iOS @property探究(二): 深入理解

說了這么多,接下來舉個具體的栗子,為一個已有類添加一個關聯對象。

@interface Person : NSObject

@property (nonatomic, copy) NSString* cjmName;
@property (nonatomic, assign) NSUInteger cjmAge;

@end

@implementation Person

@synthesize cjmName = _cjmName;
@synthesize cjmAge = _cjmAge;

@end

@interface NSArray (MyPerson)

- (void)setPerson:(Person*)person;
- (Person*)person;

@end

@implementation NSArray (MyPerson)

- (void)setPerson:(Person *)person {
    objc_setAssociatedObject(self, "_person", person, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (Person*)person {
    return objc_getAssociatedObject(self, "_person");
}
@end

這個栗子設置的關聯對象其實沒有任何實際意義,通過代碼可以看出,使用runtime為一個已有類添加屬性就是通過類別擴展gettersetter方法。

實例方法

在本系列文章的第二篇iOS runtime探究(二): 從runtime開始深入理解OC消息轉發機制,我們詳細介紹了runtime對方法的底層處理,以及發送消息和消息轉發機制,這里就不再贅述了,如有需要可以查看相關文章,本文會介紹OC層面對方法的相關操作,同時會介紹method swizzling的方法。

先來回顧一下實例方法相關的結構體和底層實現,有如下代碼:

@interface Person : NSObject

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) NSUInteger age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age;

- (void)showMyself;

- (void)helloWorld;

@end

@implementation Person

@synthesize name = _name;
@synthesize age = _age;

- (instancetype)initWithName:(NSString*)name age:(NSUInteger)age {
    if (self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}

- (void)showMyself {
    NSLog(@"Hello World, My name is %@ I\'m %ld years old.", self.name, self.age);
}

- (void)helloWorld {
    NSLog(@"Hello World");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        [p showMyself];
        unsigned int count = 0;
        Method *methodList = class_copyMethodList([p class], &count);
        for (int i = 0; i < count; i++) {
            SEL s = method_getName(methodList[i]);
            NSLog(@"%@", NSStringFromSelector(s));
            if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
                IMP imp = method_getImplementation(methodList[i]);
                imp();
            }
        }
    }
    return 0;
}

通過clang轉寫后可以找到如下與實例方法相關的定義:

struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
};

static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[7];
} _OBJC_$_INSTANCE_METHODS_Person __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        7,
        {{(struct objc_selector *)"initWithName:age:", "@32@0:8@16Q24", (void *)_I_Person_initWithName_age_},
        {(struct objc_selector *)"showMyself", "v16@0:8", (void *)_I_Person_showMyself},
        {(struct objc_selector *)"helloWorld", "v16@0:8", (void *)_I_Person_helloWorld},
        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Person_name},
        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_},
        {(struct objc_selector *)"age", "Q16@0:8", (void *)_I_Person_age},
        {(struct objc_selector *)"setAge:", "v24@0:8Q16", (void *)_I_Person_setAge_}}
};

上一篇文章iOS runtime探究(二): 從runtime開始深入理解OC消息轉發機制已經詳細介紹了上述結構體,這里不再贅述了。

通過上述代碼可以看出,一個實例方法在底層就是一個方法描述和一個C函數的具體實現,我們可以通過如下代碼獲取這個方法描述結構體:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        unsigned int count = 0;
        Method *methodList = class_copyMethodList([p class], &count);
        for (int i = 0; i < count; i++) {
            SEL s = method_getName(methodList[i]);
            NSLog(@"%@ %s", NSStringFromSelector(s), method_getTypeEncoding(methodList[i]));
            if ([NSStringFromSelector(s) isEqualToString:@"helloWorld"]) {
                IMP imp = method_getImplementation(methodList[i]);
                imp();
            }
        }
    }
    return 0;
}

首先看一下Method是什么,在objc/runtime.h中可以找到相關定義:

typedef struct objc_method *Method;

它是一個指向結構體struct objc_method的指針,這里的結構體struct objc_method其實就是前文中.cpp文件中的struct _objc_method結構體,通過class_copyMethodList方法就可以獲取到相關類的所有實例方法,具體函數聲明如下:

/** 
 * Describes the instance methods implemented by a class.
 * 
 * @param cls The class you want to inspect.
 * @param outCount On return, contains the length of the returned array. 
 *  If outCount is NULL, the length is not returned.
 * 
 * @return An array of pointers of type Method describing the instance methods 
 *  implemented by the class—any instance methods implemented by superclasses are not included. 
 *  The array contains *outCount pointers followed by a NULL terminator. You must free the array with free().
 * 
 *  If cls implements no instance methods, or cls is Nil, returns NULL and *outCount is 0.
 * 
 * @note To get the class methods of a class, use \c class_copyMethodList(object_getClass(cls), &count).
 * @note To get the implementations of methods that may be implemented by superclasses, 
 *  use \c class_getInstanceMethod or \c class_getClassMethod.
 */
OBJC_EXPORT Method *class_copyMethodList(Class cls, unsigned int *outCount) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

通過注釋可以看出,第一個參數是相關類的類對象(如有疑問可以查閱本系列文章的前兩篇文章),第二個參數是一個指向unsigned int的指針,用于指明Method的數量,通過該方法就能夠獲取到所有的實例方法,接下來可以通過method_getName方法獲取成員變量_cmd,這是一個選擇子selector可以通過方法NSStringFromSelector獲取到實例方法的名稱。通過方法method_getTypeEncoding就可以獲得函數類型method_type。通過方法method_getImplementation就可以獲取到實例方法的具體實現imp,這個具體實現就是我們自定義的實例方法的一個C函數,因此,如果該方法內不訪問任何其他實例變量并且沒有任何參數就可以直接執行該函數。

上述代碼的輸出結果如下:

2017-03-27 12:36:12.342715 OCTest[4135:952839] initWithName:age: @32@0:8@16Q24
2017-03-27 12:36:12.342795 OCTest[4135:952839] showMyself v16@0:8
2017-03-27 12:36:12.342843 OCTest[4135:952839] helloWorld v16@0:8
2017-03-27 12:36:12.342866 OCTest[4135:952839] Hello World
2017-03-27 12:36:12.342884 OCTest[4135:952839] .cxx_destruct v16@0:8
2017-03-27 12:36:12.342911 OCTest[4135:952839] name @16@0:8
2017-03-27 12:36:12.342929 OCTest[4135:952839] setName: v24@0:8@16
2017-03-27 12:36:12.342951 OCTest[4135:952839] age Q16@0:8
2017-03-27 12:36:12.342966 OCTest[4135:952839] setAge: v24@0:8Q16

我們也可以通過class_addMethod函數動態的為一個類添加實例方法,具體的栗子可以查看前文從runtime開始: 深入理解OC消息轉發機制這里不再贅述。

Method Swizzling

通過前面的介紹,我們知道一個實例方法在底層就是一個方法描述加上方法類型和具體的C函數實現,Foundation等框架都是閉源的,我們沒有辦法直接修改代碼,通常情況下可以通過繼承、類別、關聯屬性等手段添加屬性或實例方法,在某些情況下通過上述方法實現的代碼還是比較復雜或繁瑣。接下來本文將介紹一種方法用于交換兩個實例方法的實現,從而達到修改閉源代碼的效果,這個方法就是Method Swizzling

Method Swizzling方法的本質就是修改前文介紹的方法描述結構體,方法描述結構體struct _objc_method中有一個struct objc_selector類型的成員變量_cmd,這就是我們常用的selector選擇子,同時也有一個函數指針_imp,這個函數指針就指向實例方法的具體實現。了解了這些我們就可以手動修改selector對應的_imp,也就是修改實例方法的具體實現,下面舉個栗子:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] initWithName:@"Jiaming Chen" age:22];
        Method method1 = class_getInstanceMethod([p class], @selector(helloWorld));
        Method method2 = class_getInstanceMethod([p class], @selector(showMyself));
        method_exchangeImplementations(method1, method2);
        
        [p showMyself];
        [p helloWorld];
    }
    return 0;
}

上述代碼使用了一個C函數:

/** 
 * Exchanges the implementations of two methods.
 * 
 * @param m1 Method to exchange with second method.
 * @param m2 Method to exchange with first method.
 * 
 * @note This is an atomic version of the following:
 *  \code 
 *  IMP imp1 = method_getImplementation(m1);
 *  IMP imp2 = method_getImplementation(m2);
 *  method_setImplementation(m1, imp2);
 *  method_setImplementation(m2, imp1);
 *  \endcode
 */
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

通過注釋和函數名稱不難發現,該函數用于交換兩個方法的實現,也就是說前文講述的結構體struct _objc_method中的函數指針_imp被交換了,原來的選擇子@selector(helloWorld)對應著方法helloWorld的實現,原來的選擇子@selector(showMyself)對應著方法showMyself的實現。如下圖所示:

交換前

通過上述方法將兩個結構體的_imp成員變量進行了一次交換操作,也就是說選擇子@selector(helloWorld)對應著方法showMyself的實現,而選擇子@selector(showMyself)對應著方法helloWorld的實現,如下圖所示:

交換后

因此上述代碼的輸出結果如下:

2017-03-27 15:35:54.077598 OCTest[6061:1472928] Hello World
2017-03-27 15:35:54.077853 OCTest[6061:1472928] Hello World, My name is Jiaming Chen I'm 22 years old.

runtime強大到可以改變一個實例方法的具體實現,但是上面的例子好像并沒有什么用,沒有人會閑的沒事去交換兩個實例方法的實現。

考慮一個需求,現在需要為每一個頁面添加一個手勢用于執行某項固定操作,比如添加一個長按收拾,用戶可以在任意界面長按后彈出一個視圖或是執行某項操作,又比如需要統計每個視圖打開的次數,你可能會想到在每一個的視圖控制器的viewDidLoad方法中添加這個手勢或在viewDidAppear方法中進行統計操作,但是這樣太繁瑣了。你也可能想到通過繼承來實現上述方法,但是你就需要繼承UIViewControllerUITableViewControllerUINavigationController等,你在代碼中使用過的任意視圖控制器,這樣一看似乎也挺麻煩的而且代碼也不統一。
通過前面的學習我們可以通過使用類別加上Method Swizzling來實現在不修改使用方式的前提下執行自定義操作了。

具體栗子如下:

@interface UIViewController (MyUIViewController)

@end

@implementation UIViewController(MyUIViewController)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewWillAppear:);
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        
        SEL exchangeSelector = @selector(myViewWillAppear:);
        Method exchangeMethod = class_getInstanceMethod([self class], exchangeSelector);
        
        method_exchangeImplementations(originalMethod, exchangeMethod);
    });
}

- (void)myViewWillAppear:(BOOL)animated {
    [self myViewWillAppear:animated];
    NSLog(@"MyViewWillAppear %@", self);
}

@end

首先需要使用類方法load來進行實例方法實現的交換操作,因為load方法會保證在類第一次被加載的時候調用,這樣可以保證一定會執行方法交換操作。其次使用GCDdispatch_once來保證交換兩個實例方法的實現只進行一次。接下來通過前文介紹的方法來獲取自定義的myViewWillAppear:以及UIViewController的選擇子和具體的方法描述結構體,最后調用前文介紹的method_exchangeImplementations函數將兩個實例方法的實現進行交換就可以了。
可能你看到myViewWillAppear:方法會有疑問,這樣不就會導致遞歸調用嗎?需要注意的是,交換兩個方法的實現是在運行時進行的,當你調用myViewWillAppear:方法時,實際會執行viewWillAppear:的方法實現,因此不會導致遞歸調用。

備注

由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,751評論 0 9
  • 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的轉載 這篇文章完全是基于南峰子老師博客的...
    西木閱讀 30,578評論 33 466
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 755評論 0 2
  • 本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 772評論 0 1
  • 本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的...
    lylaut閱讀 816評論 0 4