由於objective-c是一門動態語言,也就是說只有編譯器是不夠的,還須要一個運行時系統(runtime system)來執行編譯後的代碼。這是整個objective-c運行框架的一塊基石。 runtime簡稱運行時。其中最主要的就是消息機制。對於編譯期語言,會在編譯的時候決定調用哪一個函數。對於OC的函數,是動態調用的,在編譯的時候並不能決定真正調用哪一個函數,只有在運行時纔會根據函數的名稱找到對應的函數來調用。
Objc 在三種層面上與 Runtime 系統進行交互: 1. 經過 Objective-C 源代碼 2. 經過 Foundation 框架的 NSObject 類定義的方法 3. 經過對 Runtime 庫函數的直接調用
蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。
都是運行時的頭文件,其中主要使用的函數定義在message.h和runtime.h這兩個文件中。
Cocoa 程序中絕大部分類都是 NSObject 類的子類,因此都繼承了 NSObject 的行爲。(NSProxy 類是個例外,它是個抽象超類)
-class
方法返回對象的類;-isKindOfClass:
和 -isMemberOfClass:
方法檢查對象是否存在於指定的類的繼承體系中(是不是其子類或者父類或者當前類的成員變量);-respondsToSelector:
檢查對象可否響應指定的消息;-conformsToProtocol:
檢查對象是否實現了指定協議類的方法;-methodForSelector:
返回指定方法實現的地址。Runtime 系統是具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下,這意味着咱們使用時只須要引入objc/Runtime.h
頭文件便可。c++
許多函數可讓你使用純 C 代碼來實現 Objc 中一樣的功能。除非是寫一些 Objc 與其餘語言的橋接或是底層的 debug 工做,你在寫 Objc 代碼時通常不會用到這些 C 語言函數。對於公共接口都有哪些,後面會講到。我將會參考蘋果官方的 API 文檔。objective-c
它是selector
在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實做用就和名字同樣,平常生活中,咱們經過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應的方法實現。它的數據結構是:數組
typedef struct objc_selector *SEL;
咱們能夠看出它是個映射到方法的 C 字符串,你能夠經過 Objc 編譯器器命令@selector()
或者 Runtime 系統的 sel_registerName
函數來獲取一個 SEL
類型的方法選擇器。緩存
id 是一個參數類型,它是指向某個類的實例的指針。定義以下:數據結構
typedef struct objc_object *id; struct objc_object { Class isa; };
以上定義,看到 objc_object
結構體包含一個 isa 指針,根據 isa 指針就能夠找到對象所屬的類。框架
typedef struct objc_class *Class;
Class
實際上是指向 objc_class
結構體的指針。objc_class
的數據結構以下:iphone
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;
從 objc_class
能夠看到,一個運行時類中關聯了它的父類指針、類名、成員變量、方法、緩存以及附屬的協議。函數
其中 objc_ivar_list
和 objc_method_list
分別是成員變量列表和方法列表:性能
// 成員變量列表 struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; // 方法列表 struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; }
Method 表明類中某個方法的類型優化
typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
objc_method
存儲了方法名,方法類型和方法實現:
SEL
method_types
是個 char 指針,存儲方法的參數類型和返回值類型method_imp
指向了方法的實現,本質是一個函數指針Ivar
是表示成員變量的類型。
typedef struct objc_ivar *Ivar; struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; char *ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
其中 ivar_offset
是基地址偏移字節
IMP在objc.h中的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP
這個函數指針就指向了這個方法的實現。
若是獲得了執行某個實例某個方法的入口,咱們就能夠繞開消息傳遞階段,直接執行方法,這在後面 Cache
中會提到。
你會發現 IMP
指向的方法與 objc_msgSend
函數類型相同,參數都包含 id
和 SEL
類型。每一個方法名都對應一個 SEL
類型的方法選擇器,而每一個實例對象中的 SEL
對應的方法實現確定是惟一的,經過一組 id
和 SEL
參數就能肯定惟一的方法實現地址。
而一個肯定的方法也只有惟一的一組 id
和 SEL
參數。
Cache 定義以下:
typedef struct objc_cache *Cache struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE; };
Cache 爲方法調用的性能進行優化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找可以響應的方法,由於每次都要查找效率過低了,而是優先在 Cache 中查找。
Runtime 系統會把被調用的方法存到 Cache 中,若是一個方法被調用,那麼它有可能從此還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 同樣。
typedef struct objc_property *Property; typedef struct objc_property *objc_property_t;//這個更經常使用
能夠經過class_copyPropertyList
和 protocol_copyPropertyList
方法獲取類和協議中的屬性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
#import <Foundation/Foundation.h> @interface Person : NSObject /** 姓名 */ @property (strong, nonatomic) NSString *name; /** age */ @property (assign, nonatomic) int age; /** weight */ @property (assign, nonatomic) double weight; @end
以上是一個 Person 類,有3個屬性。讓咱們用上述方法獲取類的運行時屬性。
unsigned int outCount = 0; objc_property_t *properties = class_copyPropertyList([Person class], &outCount); NSLog(@"%d", outCount); for (NSInteger i = 0; i < outCount; i++) { NSString *name = @(property_getName(properties[i])); NSString *attributes = @(property_getAttributes(properties[i])); NSLog(@"%@--------%@", name, attributes); }
一些 Runtime 術語講完了,接下來就要說到消息了。體會蘋果官方文檔中的 messages aren’t bound to method implementations until Runtime。消息直到運行時纔會與方法實現進行綁定。
這裏要清楚一點,objc_msgSend
方法看清來好像返回了數據,其實objc_msgSend
從不返回數據,而是你的方法在運行時實現被調用後纔會返回數據。下面詳細敘述消息發送的步驟(以下圖):
經過上圖能夠看出,一個實例對象`struct objc_object`的isa指針指向它的`struct objc_class`類對象,類對象的isa指針指向它的元類;`super_class`指針指向了父類的`類對象`,而`元類`的`super_class`指針指向了父類的`元類`。
方法調用的本質,就是讓對象發送消息。objc\_msgSend,只有對象才能發送消息,所以以objc開頭。使用消息機制前提,必須導入#import <objc/message.h> 消息機制簡單使用:
// 建立person對象 Person *p = [[Person alloc] init]; // 調用對象方法 [p eat]; // 本質:讓對象發送消息 objc_msgSend(p, @selector(eat)); // 調用類方法的方式:兩種 // 第一種經過類名調用 [Person eat]; // 第二種經過類對象調用 [[Person class] eat]; // 用類名調用類方法,底層會自動把類名轉換成類對象調用 // 本質:讓類對象發送消息 objc_msgSend([Person class], @selector(eat));
咱們能夠經過clang來查看代碼生成的CPP代碼。 例如: clang 將oc main.m文件轉成c++ main\_cpp文件代碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main_cpp.cpp
交換兩個方法的實現通常寫在類的load方法裏面,由於load方法會在程序運行前加載一次,而initialize方法會在類或者子類在 第一次使用的時候調用,當有分類的時候會調用屢次。
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 需求:給imageNamed方法提供功能,每次加載圖片就判斷下圖片是否加載成功。 // 步驟一:先搞個分類,定義一個能加載圖片而且能打印的方法+ (instancetype)imageWithName:(NSString *)name; // 步驟二:交換imageNamed和imageWithName的實現,就能調用imageWithName,間接調用imageWithName的實現。 UIImage *image = [UIImage imageNamed:@"123"]; } @end @implementation UIImage (Image) // 加載分類到內存的時候調用 + (void)load { // 交換方法 // 獲取imageWithName方法地址 Method imageWithName = class_getClassMethod(self, @selector(imageWithName:)); // 獲取imageWithName方法地址 Method imageName = class_getClassMethod(self, @selector(imageNamed:)); // 交換方法地址,至關於交換實現方式 method_exchangeImplementations(imageWithName, imageName); } // 不能在分類中重寫系統方法imageNamed,由於會把系統的功能給覆蓋掉,並且分類中不能調用super. // 既能加載圖片又能打印 + (instancetype)imageWithName:(NSString *)name { // 這裏調用imageWithName,至關於調用imageName UIImage *image = [self imageWithName:name]; if (image == nil) { NSLog(@"加載空的圖片"); } return image; } @end
使用方式一:給分類添加屬性
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 給系統NSObject類動態添加屬性name NSObject *objc = [[NSObject alloc] init]; objc.name = @"小碼哥"; NSLog(@"%@",objc.name); } @end // 定義關聯的key static const char *key = "name"; @implementation NSObject (Property) - (NSString *)name { // 根據關聯的key,獲取關聯的值。 return objc_getAssociatedObject(self, key); } - (void)setName:(NSString *)name { // 第一個參數:給哪一個對象添加關聯 // 第二個參數:關聯的key,經過這個key獲取 // 第三個參數:關聯的value // 第四個參數:關聯的策略 objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end
使用方式二:給對象添加關聯對象。
/** * 刪除點擊 * @param recId 購物車ID */ - (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"確認要刪除這個寶貝" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"肯定", nil]; // 傳遞多參數 objc_setAssociatedObject(alert, "suppliers_id", @"1", OBJC_ASSOCIATION_RETAIN_NONATOMIC); objc_setAssociatedObject(alert, "warehouse_id", @"2", OBJC_ASSOCIATION_RETAIN_NONATOMIC); alert.tag = [recId intValue]; [alert show]; } /** * 肯定刪除操做 */ - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (buttonIndex == 1) { NSString *warehouse_id = objc_getAssociatedObject(alertView, "warehouse_id"); NSString *suppliers_id = objc_getAssociatedObject(alertView, "suppliers_id"); NSString *recId = [NSString stringWithFormat:@"%ld",(long)alertView.tag]; } }
簡單使用:
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. Person *p = [[Person alloc] init]; // 默認person,沒有實現eat方法,能夠經過performSelector調用,可是會報錯。 // 動態添加方法就不會報錯 [p performSelector:@selector(eat)]; } @end @implementation Person // void(*)() // 默認方法都有兩個隱式參數, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 當一個對象調用未實現的方法,會調用這個方法處理,而且會把對應的方法列表傳過來. // 恰好能夠用來判斷,未實現的方法是否是咱們想要動態添加的方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 動態添加eat方法 // 第一個參數:給哪一個類添加方法 // 第二個參數:添加方法的方法編號 // 第三個參數:添加方法的函數實現(函數地址) // 第四個參數:函數的類型,(返回值+參數類型) v:void @:對象->self :表示SEL->_cmd class_addMethod(self, @selector(eat), eat, "v@:"); } return [super resolveInstanceMethod:sel]; } @end
// Ivar:成員變量 如下劃線開頭 // Property:屬性 + (instancetype)modelWithDict:(NSDictionary *)dict { id objc = [[self alloc] init]; // runtime:根據模型中屬性,去字典中取出對應的value給模型屬性賦值 // 1.獲取模型中全部成員變量 key // 獲取哪一個類的成員變量 // count:成員變量個數 unsigned int count = 0; // 獲取成員變量數組 Ivar *ivarList = class_copyIvarList(self, &count); // 遍歷全部成員變量 for (int i = 0; i < count; i++) { // 獲取成員變量 Ivar ivar = ivarList[i]; // 獲取成員變量名字 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; // 獲取成員變量類型 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; // @\"User\" -> User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; // 獲取key NSString *key = [ivarName substringFromIndex:1]; // 去字典中查找對應value // key:user value:NSDictionary id value = dict[key]; // 二級轉換:判斷下value是不是字典,若是是,字典轉換層對應的模型 // 而且是自定義對象才須要轉換 if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) { // 字典轉換成模型 userDict => User模型 // 轉換成哪一個模型 // 獲取類 Class modelClass = NSClassFromString(ivarType); value = [modelClass modelWithDict:value]; } // 給模型中屬性賦值 if (value) { [objc setValue:value forKey:key]; } } return objc; }
1.咱們發現。load 的加載比main 還要早,因此若是咱們再load方法裏面作了耗時的操做,那麼必定會影響程序的啓動時間,因此在load裏面必定不要寫耗時的代碼。
2.不要在load裏面取加載對象,由於咱們再load調用的時候根本就不肯定咱們的對象是否已經初始化了,因此不要去作對象的初始化
分類中的同名方法,源碼中是按照逆序加載的,也就是說後編譯的分類方法會覆蓋前面全部的同名的方法,分類還有一個特性就是,無論把聲明寫在主類仍是分類,只要分類中實現了就能夠找到。
+initialize本質爲objc/_msgSend,若是子類沒有實現initialize則會去父類查找,若是分類中實現,那麼會覆蓋主類,和runtime消息轉發邏輯同樣
1.initialize 會在類第一次接收到消息的時候調用 2.先調用父類的 initialize,而後調用子類。 3.initialize 是經過 objc_msgSend 調用的 4.若是子類沒有實現 initialize,會調用父類的initialize(父類可能被調用屢次) 5.若是分類實現了initialize,會覆蓋本類的initialize方法