Objective-C之runtime漫談

runtime簡介

由於objective-c是一門動態語言,也就是說只有編譯器是不夠的,還須要一個運行時系統(runtime system)來執行編譯後的代碼。這是整個objective-c運行框架的一塊基石。
runtime簡稱運行時。其中最主要的就是消息機制。對於編譯期語言,會在編譯的時候決定調用哪一個函數。對於OC的函數,是動態調用的,在編譯的時候並不能決定真正調用哪一個函數,只有在運行時纔會根據函數的名稱找到對應的函數來調用。

runtime的做用

Objc 在三種層面上與 Runtime 系統進行交互:
   1.  經過 Objective-C 源代碼
   2.  經過 Foundation 框架的 NSObject 類定義的方法
   3.  經過對 Runtime 庫函數的直接調用

runtime源碼

蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。

頭文件

都是運行時的頭文件,其中主要使用的函數定義在message.h和runtime.h這兩個文件中。

經過 Foundation 框架的 NSObject 類定義的方法

Cocoa 程序中絕大部分類都是 NSObject 類的子類,因此都繼承了 NSObject 的行爲。(NSProxy 類是個例外,它是個抽象超類)
  • -class方法返回對象的類;
  • -isKindOfClass:-isMemberOfClass: 方法檢查對象是否存在於指定的類的繼承體系中(是不是其子類或者父類或者當前類的成員變量);
  • -respondsToSelector: 檢查對象可否響應指定的消息;
  • -conformsToProtocol:檢查對象是否實現了指定協議類的方法;
  • -methodForSelector: 返回指定方法實現的地址。

經過對 Runtime 庫函數的直接調用

Runtime 系統是具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下,這意味着咱們使用時只須要引入objc/Runtime.h頭文件便可。c++

許多函數可讓你使用純 C 代碼來實現 Objc 中一樣的功能。除非是寫一些 Objc 與其餘語言的橋接或是底層的 debug 工做,你在寫 Objc 代碼時通常不會用到這些 C 語言函數。對於公共接口都有哪些,後面會講到。我將會參考蘋果官方的 API 文檔。objective-c

Runtime的術語的數據結構

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 類)。selector 是方法選擇器,其實做用就和名字同樣,平常生活中,咱們經過人名辨別誰是誰,注意 Objc 在相同的類中不會有命名相同的兩個方法。selector 對方法名進行包裝,以便找到對應的方法實現。它的數據結構是:數組

typedef struct objc_selector *SEL;

咱們能夠看出它是個映射到方法的 C 字符串,你能夠經過 Objc 編譯器器命令@selector() 或者 Runtime 系統的 sel_registerName 函數來獲取一個 SEL 類型的方法選擇器。緩存

id

id 是一個參數類型,它是指向某個類的實例的指針。定義以下:數據結構

typedef struct objc_object *id;
struct objc_object { Class isa; };

以上定義,看到 objc_object 結構體包含一個 isa 指針,根據 isa 指針就能夠找到對象所屬的類。框架

Class

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_listobjc_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

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

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

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL, ...);

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息以後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。

若是獲得了執行某個實例某個方法的入口,咱們就能夠繞開消息傳遞階段,直接執行方法,這在後面 Cache 中會提到。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 idSEL 類型。每一個方法名都對應一個 SEL 類型的方法選擇器,而每一個實例對象中的 SEL 對應的方法實現確定是惟一的,經過一組 idSEL 參數就能肯定惟一的方法實現地址。

而一個肯定的方法也只有惟一的一組 idSEL 參數。

Cache

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 同樣。

Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更經常使用

能夠經過class_copyPropertyListprotocol_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 從不返回數據,而是你的方法在運行時實現被調用後纔會返回數據。下面詳細敘述消息發送的步驟(以下圖):
image

深刻代碼理解instance、class object、metaclass

isa指針結構圖

經過上圖能夠看出,一個實例對象`struct objc_object`的isa指針指向它的`struct objc_class`類對象,類對象的isa指針指向它的元類;`super_class`指針指向了父類的`類對象`,而`元類`的`super_class`指針指向了父類的`元類`。

runtime的應用

發送消息

方法調用的本質,就是讓對象發送消息。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

字典轉模型KVC實現

// 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;
}

+ load 和 + initialize 原理講解

+load 總結

  • load 方法調用在main以前,而且不須要咱們初始化,程序啓動就會把全部文件加載
  • 主類的調用優先於分類,分類的調動優先於當前類優先於分類
  • 主類和分類的調用順序跟編譯順序無關
  • 分類之間加載,也就是平級以前加載取決於編譯順序,誰先編譯就先加載誰

注意事項

1.咱們發現。load 的加載比main 還要早,因此若是咱們再load方法裏面作了耗時的操做,那麼必定會影響程序的啓動時間,因此在load裏面必定不要寫耗時的代碼。
2.不要在load裏面取加載對象,由於咱們再load調用的時候根本就不肯定咱們的對象是否已經初始化了,因此不要去作對象的初始化

調用順序延伸(category)

分類中的同名方法,源碼中是按照逆序加載的,也就是說後編譯的分類方法會覆蓋前面全部的同名的方法,分類還有一個特性就是,無論把聲明寫在主類仍是分類,只要分類中實現了就能夠找到。

+ initialize

+initialize本質爲objc/_msgSend,若是子類沒有實現initialize則會去父類查找,若是分類中實現,那麼會覆蓋主類,和runtime消息轉發邏輯同樣

initialize總結

1.initialize 會在類第一次接收到消息的時候調用 2.先調用父類的 initialize,而後調用子類。 3.initialize 是經過 objc_msgSend 調用的 4.若是子類沒有實現 initialize,會調用父類的initialize(父類可能被調用屢次) 5.若是分類實現了initialize,會覆蓋本類的initialize方法

相關文章
相關標籤/搜索