你要知道的runtime都在這里
轉載請注明出處 http://www.lxweimin.com/p/e2c0c67d39ed
本文主要講解runtime
相關知識,從原理到實踐,由于包含內容過多分為以下五篇文章詳細講解,可自行選擇需要了解的方向:
- 從runtime開始: 理解面向對象的類到面向過程的結構體
- 從runtime開始: 深入理解OC消息轉發機制
- 從runtime開始: 理解OC的屬性property
- 從runtime開始: 實踐Category添加屬性與黑魔法method swizzling
- 從runtime開始: 深入weak實現機理
本文是系列文章的第四篇文章從runtiem開始: 實踐Category添加屬性與黑魔法method swizzling,本文將會介紹比較常用的runtime
的關聯對象
以及runtime
對方法的處理和一個交換方法實現的方法。
關聯對象 Associated Object
如果我們想為系統的類添加一個方法可以采用類別的方式進行擴展,相對來說比較簡單,但如果要添加一個屬性或稱為成員變量,通常采用的方法就是繼承,這樣就比較繁瑣了,如果不想去繼承那就可以通過runtime
來進行關聯對象操作。
使用runtime
的關聯對象
添加屬性與我們自定義類時定義的屬性其實是兩個不同的概念,通過關聯對象
添加屬性本質上是使用類別
進行擴展,通過添加setter
和getter
方法從而在訪問時可以使用點語法進行方法,在使用上與自定義類定義的屬性沒有區別。
具體需要使用的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修飾符
相同,如果對property
或property修飾符
等有疑問可以查閱本系列教程第三篇文章從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
為一個已有類添加屬性就是通過類別擴展getter
和setter
方法。
實例方法
在本系列文章的第二篇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
方法中進行統計操作,但是這樣太繁瑣了。你也可能想到通過繼承來實現上述方法,但是你就需要繼承UIViewController
、UITableViewController
、UINavigationController
等,你在代碼中使用過的任意視圖控制器,這樣一看似乎也挺麻煩的而且代碼也不統一。
通過前面的學習我們可以通過使用類別加上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
方法會保證在類第一次被加載的時候調用,這樣可以保證一定會執行方法交換操作。其次使用GCD
的dispatch_once
來保證交換兩個實例方法的實現只進行一次。接下來通過前文介紹的方法來獲取自定義的myViewWillAppear:
以及UIViewController
的選擇子和具體的方法描述結構體,最后調用前文介紹的method_exchangeImplementations
函數將兩個實例方法的實現進行交換就可以了。
可能你看到myViewWillAppear:
方法會有疑問,這樣不就會導致遞歸調用嗎?需要注意的是,交換兩個方法的實現是在運行時進行的,當你調用myViewWillAppear:
方法時,實際會執行viewWillAppear:
的方法實現,因此不會導致遞歸調用。
備注
由于作者水平有限,難免出現紕漏,如有問題還請不吝賜教。