IOS面試考察(一):runtime相關問題

IOS面試考察(一):runtime相關問題git

IOS面試考察(九):性能優化相關問題github

@[TOC]面試

1. IOS面試考察(一):runtime相關問題

Runtime簡介圖

1.1 runtime相關問題

runtime是iOS開發最核心的知識了,若是下面的問題都解決了,那麼對runtime的理解已經很深了。 runtime已經開源了,這有一份別人調試好可運行的源碼objc-runtime,也能夠去官網找objc4objective-c

官方的代碼下載下來要讓它運行起來,是須要花費一點時間去填坑的。我這裏提供一下已經填好坑,能夠直接編譯運行的objc4_750代碼:swift

objc_756代碼結構

先思考一下下面的這些問題,看你能回答出多少:數組

  1. runtime怎麼添加屬性、方法等
  2. runtime 如何實現 weak 屬性
  3. runtime如何經過selector找到對應的IMP地址?(分別考慮類方法和實例方法)
  4. 使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?
  5. _objc_msgForward函數是作什麼的?直接調用它將會發生什麼?
  6. 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?
  7. 簡述下Objective-C中調用方法的過程(runtime)
  8. 什麼是method swizzling(俗稱黑魔法)

咱們先來簡單瞭解一下runtime的3個簡單問題:緩存

  1. 什麼是runtime?
  2. 爲啥要用runtime?
  3. runtime有什麼做用?
    1. 什麼是runtime?
  1. runtime本質上是一套比較底層的C語言,C++ ,彙編組成的API。咱們有稱爲運行時,在runtime的底層不少實現爲了性能效率方面,都直接用匯編代碼。
  2. 咱們平時編寫的OC代碼,須要runtime來建立類和對象,進行消息發送和轉發,其實最終會轉換成Runtime的C語言代碼。
  3. runtime是將數據類型的肯定由編譯時推遲到了運行時。
    1. 爲啥要用runtime?
  1. OC是一門動態語言,它會將一些工做放在代碼的運行時纔去處理而並不是編譯時,所以編譯器是不夠,咱們還須要一個運行時系統來處理編譯後的代碼。
  2. runtime基本是用C語言和彙編語言寫的,蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都高度的保持一致。
    1. runtime有什麼做用?
  1. 消息傳遞、轉發<消息機制>
  2. 訪問私有變量 --eg:(UITextFiled 的修改)
  3. 交換系統方法 --eg:(攔截--防止button連續點擊)
  4. 動態增長方法
  5. 爲分類增長屬性
  6. 字典轉模型 -- eg:(YYModel, MJModel)

接下來咱們來給出runtime面試問題的解答,可能不完善,歡迎補充。安全

1.1.1 runtime怎麼添加屬性、方法等

ivar表示成員變量 class_addIvar class_addMethod class_addProperty class_addProtocol class_replaceProperty性能優化

1.1.1.1 動態添加屬性

  • 需求:給NSObject添加一個name屬性,動態添加屬性 -> runtime

思路:bash

  1. 給NSObject添加分類,在分類中添加屬性。問題:@property在分類中做用:僅僅是生成get,set方法聲明,不會生成get,set方法實現和下劃線成員屬性,因此要在.m文件實現setter/getter方法,用static保存下滑線屬性,這樣一來,當對象銷燬時,屬性沒法銷燬。
  2. 用runtime動態添加屬性:本質是讓屬性與某個對象產生一段關聯 使用場景:給系統的類添加屬性時。

實現代碼以下:

#import <objc/message.h>
@implementation NSObject (Property)
//static NSString *_name; //經過這樣去保存屬性無法作到對象銷燬,屬性也銷燬,static依然會讓屬性存在緩存池中,因此須要動態的添加成員屬性
// 只要想調用runtime方法,思考:誰的事情
-(void)setName:(NSString *)name
{
    // 保存name
    // 動態添加屬性 = 本質:讓對象的某個屬性與值產生關聯
    /* object:保存到哪一個對象中 key:用什麼屬性保存 屬性名 value:保存值 policy:策略,strong,weak objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>) */
    objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// _name = name;

}

- (NSString *)name
{
    return objc_getAssociatedObject(self, "name");
// return _name;

}
@end
複製代碼
1.1.1.1.1 自動生成屬性

開發中,從網絡數據中解析出字典數組,將數組轉爲模型時,若是有幾百個key須要用,要寫不少@property成員屬性,下面提供一個萬能的方法,可直接將字典數組轉爲所有@property成員屬性,打印出來,這樣直接複製在模型中就行了。

  • 自動生成屬性代碼以下:
#import "NSDictionary+PropertyCode.h"
@implementation NSDictionary (PropertyCode)

//經過這個方法,自動將字典轉成模型中須要用的屬性代碼

// 私有API:真實存在,可是蘋果沒有暴露出來,不給你。如BOOL值,不知道類型,打印得知是__NSCFBoolean,可是沒法敲出來,只能用NSClassFromString(@"__NSCFBoolean")

// isKindOfClass:判斷下是不是當前類或者子類,BOOL是NSNumber的子類,要先判斷BOOL
- (void)createPropetyCode
{
    // 模型中屬性根據字典的key
    // 有多少個key,生成多少個屬性
    NSMutableString *codes = [NSMutableString string];
    // 遍歷字典
    [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        NSString *code = nil;
        

// NSLog(@"%@",[value class]);
        
        if ([value isKindOfClass:[NSString class]]) {
          code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key];
        } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
            code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key];
        } else if ([value isKindOfClass:[NSNumber class]]) {
             code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key];
        } else if ([value isKindOfClass:[NSArray class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key];
        } else if ([value isKindOfClass:[NSDictionary class]]) {
            code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key];
        }
        
        // 拼接字符串
        [codes appendFormat:@"%@\n",code];

    }];
    
    NSLog(@"%@",codes);
    
}

@end
複製代碼
1.1.1.1.2 KVC字典轉模型
  • 需求:就是在開發中,一般後臺會給你不少數據,可是並非每一個數據都有用,這些沒有用的數據,需不須要保存到模型中。 有不少第三方框架都是基於這些原理去實現的,如YYModel, MJModel.

實現代碼以下:

@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict{
    // 建立模型
    Status *s = [[self alloc] init];
    
    // 字典value轉模型屬性保存
    [s setValuesForKeysWithDictionary:dict];

// s.reposts_count = dict[@"reposts_count"];
    // 4️⃣MJExtension:能夠字典轉模型,並且能夠不用與字典中屬性一一對應,runtime,遍歷模型中有多少個屬性,直接去字典中取出對應value,給模型賦值
    
    // 1️⃣setValuesForKeysWithDictionary:方法底層實現:遍歷字典中全部key,去模型中查找對應的屬性,把值給模型屬性賦值,即調用下面方法:
    /* [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { // source // 這行代碼纔是真正給模型的屬性賦值 [s setValue:dict[@"source"] forKey:@"source"]; //底層實現是: 2️⃣ [s setValue:dict[@"source"] forKey:@"source"]; 1.首先會去模型中查找有沒有setSource方法,直接調用set方法 [s setSource:dict[@"source"]]; 2.去模型中查找有沒有source屬性,source = dict[@"source"] 3.去查找有沒有_source屬性,_source = dict[@"source"] 4.調用對象的 setValue:forUndefinedKey:直接報錯 [s setValue:obj forKey:key]; }]; */
    return s;
}
     

// 3️⃣用KVC,不想讓系統報錯,重寫系統方法思想:
// 1.想給系統方法添加功能
// 2.不想要系統實現
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
}


@end
複製代碼
1.1.1.1.3 MJExtention的底層實現
#import "NSObject+Model.h"
#import <objc/message.h>

// class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>) 獲取屬性列表

@implementation NSObject (Model)

/** 字典轉模型 @param dict 傳入須要轉模型的字典 @return 賦值好的模型 */

+ (instancetype)modelWithDict:(NSDictionary *)dict

{
    id objc = [[self alloc] init];

    //思路: runtime遍歷模型中屬性,去字典中取出對應value,在給模型中屬性賦值
    // 1.獲取模型中全部屬性 -> 保存到類
    // ivar:下劃線成員變量 和 Property:屬性
    // 獲取成員變量列表
    // class:獲取哪一個類成員變量列表
    // count:成員變量總數
    //這個方法獲得一個裝有成員變量的數組
    //class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
    
    int count = 0;
    // 成員變量數組 指向數組第0個元素
    Ivar *ivarList = class_copyIvarList(self, &count);


    // 遍歷全部成員變量
    for (int i = 0; i < count; i++) {
        
        // 獲取成員變量 user
        Ivar ivar = ivarList[i];
        // 獲取成員變量名稱,即將C語言的字符轉爲OC字符串
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 獲取成員變量類型,用於獲取二級字典的模型名字
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // 將type這樣的字符串@"@\"User\"" 轉成 @"User"
        type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""];
        type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        
        // 成員變量名稱轉換key,即去掉成員變量前面的下劃線
        NSString *key = [ivarName substringFromIndex:1];
        
        // 從字典中取出對應value dict[@"user"] -> 字典
        id value = dict[key];
        
        // 二級轉換
        // 而且是自定義類型,才須要轉換
        if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典才須要轉換
           
            Class className = NSClassFromString(type);
            
            // 字典轉模型
            value = [className modelWithDict:value];
        }
        
        // 給模型中屬性賦值 key:user value:字典 -> 模型
        if (value) {
            [objc setValue:value forKey:key];
        }
        
    }
    
    return objc;

}

@end
複製代碼
1.1.1.1.4 自動序列化
  • 用runtime提供的函數遍歷Model自身全部屬性,並對屬性進行encode和decode操做。

在Model的基類中重寫方法:

- (id)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        unsigned int outCount;
        Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) {
            Ivar ivar = ivars[i];
            NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            [self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder {
    unsigned int outCount;
    Ivar * ivars = class_copyIvarList([self class], &outCount); for (int i = 0; i < outCount; i ++) {
        Ivar ivar = ivars[i];
        NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        [aCoder encodeObject:[self valueForKey:key] forKey:key];
    }
}
複製代碼

1.1.1.2 動態添加方法

  • 開發使用場景:若是一個類方法很是多,加載類到內存的時候也比較耗費資源,須要給每一個方法生成映射表,可使用動態給某個類,添加方法解決。

可能的面試題:有沒有使用performSelector,其實主要想問你有沒有動態添加過方法。

動態添加方法代碼實現:

@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
複製代碼

1.1.1.3 類,對象的關聯對象

OC的分類容許給分類添加屬性,但不會自動生成getter、setter方法 因此常規的僅僅添加以後,調用的話會crash。

關聯對象不是爲類\對象添加屬性或者成員變量(由於在設置關聯後也沒法經過ivarList或者propertyList取得) ,而是爲類添加一個相關的對象,一般用於存儲類信息,例如存儲類的屬性列表數組,爲未來字典轉模型的方便。

runtime關聯對象的API:

//關聯對象
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)
複製代碼
  • 給分類添加屬性:
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 給系統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
複製代碼
  • 給對象添加關聯對象: 好比alertView,通常傳值,使用的是alertView的tag屬性。咱們想把更多的參數傳給alertView代理:
/** * 刪除點擊 * @param recId 購物車ID */
- (void)shopCartCell:(BSShopCartCell *)shopCartCell didDeleteClickedAtRecId:(NSString *)recId
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"確認要刪除這個寶貝" delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"肯定", nil];
    /* objc_setAssociatedObject方法的參數解釋: 第一個參數: id object, 當前對象 第二個參數: const void *key, 關聯的key,是c字符串 第三個參數: id value, 被關聯的對象的值 第四個參數: objc_AssociationPolicy policy關聯引用的規則 */
    // 傳遞多參數
    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];
    }
}
複製代碼

1.1.2 runtime 如何實現 weak 屬性

  • 首先要搞清楚weak屬性的特色:

weak策略代表該屬性定義了一種「非擁有關係」 (nonowning relationship)。爲這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign相似;然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)

  • 那麼runtime如何實現weak變量的自動置nil?

runtime對註冊的類,會進行佈局,會將 weak 對象放入一個 hash 表中。用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會調用對象的 dealloc 方法,假設 weak 指向的對象內存地址是a,那麼就會以a爲key,在這個 weak hash表中搜索,找到全部以a爲key的 weak 對象,從而設置爲 nil。

  • weak屬性須要在dealloc中置nil麼?

在ARC環境不管是強指針仍是弱指針都無需在 dealloc 設置爲 nil , ARC 會自動幫咱們處理 即使是編譯器不幫咱們作這些,weak也不須要在dealloc中置nil 在屬性所指的對象遭到摧毀時,屬性值也會清空

objc// 模擬下weak的setter方法,大體以下
- (void)setObject:(NSObject *)object{   

  objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);

 [object cyl_runAtDealloc:^{ _object = nil; }];
 }

複製代碼

先看下 runtime 裏源碼的實現:具體完整實現參照 objc/objc-weak.h

/** * The internal structure stored in the weak references table. * It maintains and stores * a hash set of weak references pointing to an object. * If out_of_line==0, the set is instead a small inline array. */
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
   DisguisedPtr<objc_object> referent;
   union {
       struct {
           weak_referrer_t *referrers;
           uintptr_t        out_of_line : 1;
           uintptr_t        num_refs : PTR_MINUS_1;
           uintptr_t        mask;
           uintptr_t        max_hash_displacement;
       };
       struct {
           // out_of_line=0 is LSB of one of these (don't care which)
           weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
       };
   };
};

/** * The global weak references table. Stores object ids as keys, * and weak_entry_t structs as their values. */
struct weak_table_t {
   weak_entry_t *weak_entries;
   size_t    num_entries;
   uintptr_t mask;
   uintptr_t max_hash_displacement;
};
複製代碼

咱們能夠設計一個函數(僞代碼)來表示上述機制:

objc_storeWeak(&a, b)函數:

objc_storeWeak函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak修飾的屬性變量(a)的內存地址(&a)做爲value,註冊到 weak 表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak表中刪除,

你能夠把objc_storeWeak(&a, b)理解爲:objc_storeWeak(value, key),而且當keynil,將valuenil

在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。

而若是a是由 assign 修飾的,則: 在 b 非 nil 時,a 和 b 指向同一個內存地址,在 b 變 nil 時,a 仍是指向該內存地址,變野指針。此時向 a 發送消息極易崩潰。

下面咱們將基於objc_storeWeak(&a, b)函數,使用僞代碼模擬「runtime如何實現weak屬性」:

// 使用僞代碼模擬:runtime如何實現weak屬性
 id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
 objc_destroyWeak(&obj1);
複製代碼

下面對用到的兩個方法objc_initWeakobjc_destroyWeak作下解釋:

整體說來,做用是: 經過objc_initWeak函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak函數釋放該變量(obj1)。

下面分別介紹下方法的內部實現:

objc_initWeak函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak函數。

obj1 = 0;
obj_storeWeak(&obj1, obj);
複製代碼

也就是說:weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的)。 而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak函數。objc_storeWeak(&obj1, 0); 前面的源代碼與下列源代碼相同。

// 使用僞代碼模擬:runtime如何實現weak屬性
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變爲0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製代碼

objc_storeWeak 函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從 weak 表中刪除,

使用僞代碼是爲了方便理解,下面咱們「真槍實彈」地實現下:

  • 如何讓不使用weak修飾的@property,擁有weak的效果。

咱們從setter方法入手:

(注意如下的 cyl_runAtDealloc 方法實現僅僅用於模擬原理,若是想用於項目中,還須要考慮更復雜的場景,想在實際項目使用的話,可使用ChenYilong 寫的一個庫 CYLDeallocBlockExecutor

- (void)setObject:(NSObject *)object
{
   objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
   [object cyl_runAtDealloc:^{
       _object = nil;
   }];
}
複製代碼

也就是有兩個步驟:

(1) 在setter方法中作以下設置:

objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);

複製代碼

(2), 在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。作到這點,一樣要藉助 runtime:

//要銷燬的目標對象
id objectToBeDeallocated;
//能夠理解爲一個「事件」:當上面的目標對象銷燬時,同時要發生的「事件」。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
                        someUniqueKey,
                        objectWeWantToBeReleasedWhenThatHappens,
                        OBJC_ASSOCIATION_RETAIN);
複製代碼

知道了思路,咱們就開始實現 cyl_runAtDealloc 方法,實現過程分兩部分:

  • 第一部分:建立一個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助 block 執行「事件」。
// 這個類,能夠理解爲一個「事件」:當目標對象銷燬時,同時要發生的「事件」。藉助block執行「事件」。

typedef void (^voidBlock)(void);

@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end

@interface CYLBlockExecutor() {
   voidBlock _block;
}
@implementation CYLBlockExecutor

- (id)initWithBlock:(voidBlock)aBlock
{
   self = [super init];
   if (self) {
       _block = [aBlock copy];
   }
   return self;
}

- (void)dealloc
{
   _block ? _block() : nil;
}

@end
複製代碼
  • 第二部分:核心代碼:利用runtime實現cyl_runAtDealloc方法
// 利用runtime實現cyl_runAtDealloc方法

#import "CYLBlockExecutor.h"

const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;

@interface NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block;
@end

@implementation NSObject (CYLRunAtDealloc)

- (void)cyl_runAtDealloc:(voidBlock)block
{
   if (block) {
       CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];
       
       objc_setAssociatedObject(self,
                                runAtDeallocBlockKey,
                                executor,
                                OBJC_ASSOCIATION_RETAIN);
   }
}

@end
複製代碼

使用方法: 導入#import "CYLNSObject+RunAtDealloc.h",而後就可使用了:

NSObject *foo = [[NSObject alloc] init];

[foo cyl_runAtDealloc:^{
   NSLog(@"正在釋放foo!");
}];
複製代碼

1.1.2.1 runtime如何實現weak變量的自動置nil?

runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。

咱們能夠設計一個函數(僞代碼)來表示上述機制:

objc_storeWeak(&a, b)函數:

  • objc_storeWeak函數把第二個參數--賦值對象(b)的內存地址做爲鍵值key,將第一個參數--weak修飾的屬性變量(a)的內存地址(&a)做爲value,註冊到 weak 表中。若是第二個參數(b)爲0(nil),那麼把變量(a)的內存地址(&a)從weak表中刪除,

  • 你能夠把objc_storeWeak(&a, b)理解爲:objc_storeWeak(value, key),而且當keynil,將valuenil

  • 在b非nil時,a和b指向同一個內存地址,在b變nil時,a變nil。此時向a發送消息不會崩潰:在Objective-C中向nil發送消息是安全的。

  • 而若是a是由assign修飾的,則: 在b非nil時,a和b指向同一個內存地址,在b變nil時,a仍是指向該內存地址,變野指針。此時向a發送消息極易崩潰。

下面咱們將基於objc_storeWeak(&a, b)函數,使用僞代碼模擬「runtime如何實現weak屬性」:

id obj1;
 objc_initWeak(&obj1, obj);
/*obj引用計數變爲0,變量做用域結束*/
 objc_destroyWeak(&obj1);
複製代碼

下面對用到的兩個方法objc_initWeakobjc_destroyWeak作下解釋:

整體說來,做用是: 經過objc_initWeak函數初始化「附有weak修飾符的變量(obj1)」,在變量做用域結束時經過objc_destoryWeak函數釋放該變量(obj1)。

下面分別介紹下方法的內部實現:

objc_initWeak函數的實現是這樣的:在將「附有weak修飾符的變量(obj1)」初始化爲0(nil)後,會將「賦值對象」(obj)做爲參數,調用objc_storeWeak函數。

obj1 = 0;
obj_storeWeak(&obj1, obj);
複製代碼

也就是說:weak 修飾的指針默認值是 nil (在Objective-C中向nil發送消息是安全的) 而後obj_destroyWeak函數將0(nil)做爲參數,調用objc_storeWeak函數。

objc_storeWeak(&obj1, 0);
複製代碼
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計數變爲0,被置nil ... */
objc_storeWeak(&obj1, 0);
複製代碼

objc_storeWeak函數把第二個參數--賦值對象(obj)的內存地址做爲鍵值,將第一個參數--weak修飾的屬性變量(obj1)的內存地址註冊到 weak 表中。若是第二個參數(obj)爲0(nil),那麼把變量(obj1)的地址從weak表中刪除。

1.1.3 runtime如何經過selector找到對應的IMP地址?(分別考慮類方法和實例方法)

  1. 每個類對象中都一個對象方法列表(對象方法緩存)
  2. 類方法列表是存放在類對象中isa指針指向的元類對象中(類方法緩存)
  3. 方法列表中每一個方法結構體中記錄着方法的名稱,方法實現,以及參數類型,其實selector本質就是方法名稱,經過這個方法名稱就能夠在方法列表中找到對應的方法實現.
  4. 當咱們發送一個消息給一個NSObject對象時,這條消息會在對象的類對象方法列表裏查找
  5. 當咱們發送一個消息給一個類時,這條消息會在類的Meta Class對象的方法列表裏查找

元類,就像以前的類同樣,它也是一個對象,全部的元類都使用根元類(繼承體系中處於頂端的類的元類)做爲他們的類。這就意味着全部NSObject的子類(大多數類)的元類都會以NSObject的元類做爲他們的類,根據這個規則,全部的元類使用根元類做爲他們的類,根元類的元類則就是它本身。也就是說基類的元類的isa指針指向他本身。

1.1.4 使用runtime Associate方法關聯的對象,須要在主對象dealloc的時候釋放麼?

不管在MRC下仍是ARC下均不須要, 被關聯的對象在生命週期內要比對象自己釋放的晚不少,它們會在被 NSObject -dealloc 調用的object_dispose()方法中釋放 補充:對象的內存銷燬時間表,分四個步驟

  1. 調用 -release :引用計數變爲零 對象正在被銷燬,生命週期即將結束. 不能再有新的 __weak 弱引用,不然將指向 nil. 調用 [self dealloc]
  2. 父類調用 -dealloc 繼承關係中最直接繼承的父類再調用 -dealloc 若是是 MRC 代碼 則會手動釋放實例變量們(iVars) 繼承關係中每一層的父類 都再調用 -dealloc
  3. NSObject 調 -dealloc 只作一件事:調用 Objective-C runtime 中object_dispose() 方法
  4. 調用 object_dispose() 爲 C++ 的實例變量們(iVars)調用 destructors 爲 ARC 狀態下的 實例變量們(iVars) 調用 -release 解除全部使用 runtime Associate方法關聯的對象 解除全部 __weak 引用 調用 free()

1.1.5 _objc_msgForward函數是作什麼的?直接調用它將會發生什麼?

_objc_msgForward是IMP類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward會嘗試作消息轉發。 直接調用_objc_msgForward是很是危險 的事,這是把雙刃刀,若是用很差會直接致使程序Crash,可是若是用得好,能作不少很是酷的事 JSPatch就是直接調用_objc_msgForward來實現其核心功能的 詳細解說參見這裏的第一個問題解答

咱們能夠這樣建立一個_objc_msgForward對象:IMP msgForwardIMP = _objc_msgForward;

咱們知道objc_msgSend在「消息傳遞」中的做用。 在「消息傳遞」過程當中,objc_msgSend的動做比較清晰:

  1. 首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),
  2. 若是沒找到,則向父類的 Class 查找。
  3. 若是一直查找到根類仍舊沒有實現,則用_objc_msgForward函數指針代替 IMP 。
  4. 最後,執行這個 IMP 。

咱們能夠在objc4_750源碼中的objc-runtime-new.mm文件中搜索_objc_msgForward裏面有一個lookUpImpOrForward的說明

/*********************************************************************** * lookUpImpOrForward. * The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails) * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) * Most callers should use initialize==YES and cache==YES. * inst is an instance of cls or a subclass thereof, or nil if none is known. * If cls is an un-initialized metaclass then a non-nil inst is faster. * May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret. * If you don't want forwarding at all, use lookUpImpOrNil() instead. **********************************************************************/
複製代碼

objc-runtime-new.mm文件裏與_objc_msgForward有關的三個函數使用僞代碼展現下:

id objc_msgSend(id self, SEL op, ...) {
    if (!self) return nil;
	IMP imp = class_getMethodImplementation(self->isa, SEL op);
	imp(self, op, ...); //調用這個函數,僞代碼...
}
 
//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; //_objc_msgForward 用於消息轉發
    return imp;
}
 
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
 
    Class curClass = cls;
    IMP imp = nil;
    do { //先查緩存,緩存沒有時重建,仍舊沒有則向父類查詢
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass);
 
    return imp;
}
複製代碼

雖然Apple沒有公開_objc_msgForward的實現源碼,可是咱們仍是能得出結論:

  1. _objc_msgForward是一個函數指針(和 IMP 的類型同樣),是用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward會嘗試作消息轉發。
  2. objc_msgSend在「消息傳遞」中的做用: 在「消息傳遞」過程當中,objc_msgSend的動做比較清晰: (1) 首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存), (2) 若是沒找到,則向父類的 Class 查找。 (3) 若是一直查找到根類仍舊沒有實現,則用_objc_msgForward函數指針代替 IMP 。 (4) 最後,執行這個 IMP

爲了展現消息轉發的具體動做,這裏嘗試向一個對象發送一條錯誤的消息,並查看一下_objc_msgForward是如何進行轉發的。

首先開啓調試模式、打印出全部運行時發送的消息: 能夠在代碼裏執行下面的方法:(void)instrumentObjcMessageSends(YES);

由於該函數處於 objc-internal.h 內,而該文件並不開放,因此調用的時候先聲明,目的是告訴編譯器程序目標文件包含該方法存在,讓編譯經過:

OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
複製代碼

或者斷點暫停程序運行,並在 gdb 中輸入下面的命令:call (void)instrumentObjcMessageSends(YES)

以第二種爲例,操做以下所示:

lldb調式

以後,運行時發送的全部消息都會打印到/tmp/msgSend-xxxx文件裏了。

終端中輸入命令前往:open /private/tmp

找到日誌輸入文件

可能看到有多條,找到最新生成的,雙擊打開

在模擬器上執行執行如下語句(這一套調試方案僅適用於模擬器,真機不可用,關於該調試方案的拓展連接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一個對象發送一條錯誤的消息:

向一個對象發送一條錯誤的消息
你能夠在/tmp/msgSend-xxxx(我這一次是/tmp/msgSend-9805)文件裏,看到打印出來:
/tmp/msgSend-xxxx日誌

結合《NSObject官方文檔》,排除掉 NSObject 作的事,剩下的就是_objc_msgForward消息轉發作的幾件事:

  1. 調用resolveInstanceMethod:方法 (或 resolveClassMethod:)。容許用戶在此時爲該 Class 動態添加實現。若是有實現了,則調用並返回YES,那麼從新開始objc_msgSend流程。這一次對象會響應這個選擇器,通常是由於它已經調用過class_addMethod。若是仍沒實現,繼續下面的動做。
  2. 調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。若是獲取到,則直接把消息轉發給它,返回非 nil 對象。不然返回 nil ,繼續下面的動做。注意,這裏不要返回 self ,不然會造成死循環。
  3. 調用methodSignatureForSelector:方法,嘗試得到一個方法簽名。若是獲取不到,則直接調用doesNotRecognizeSelector拋出異常。若是能獲取,則返回非nil:建立一個 NSlnvocation 並傳給forwardInvocation:。
  4. 調用forwardInvocation:方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裏面了,並返回非nil。
  5. 調用doesNotRecognizeSelector: ,默認的實現是拋出異常。若是第3步沒能得到一個方法簽名,執行該步驟。

上面前4個方法均是模板方法,開發者能夠override,由 runtime 來調用。最多見的實現消息轉發:就是重寫方法3和4,吞掉一個消息或者代理給其餘對象都是沒問題的。 也就是說_objc_msgForward在進行消息轉發的過程當中會涉及如下這幾個方法:

  1. resolveInstanceMethod:方法 (或 resolveClassMethod:)。
  2. forwardingTargetForSelector:方法
  3. methodSignatureForSelector:方法
  4. forwardInvocation:方法
  5. doesNotRecognizeSelector: 方法
    消息轉發流程圖
  • 下面回答下第二個問題「直接_objc_msgForward調用它將會發生什麼?」

直接調用_objc_msgForward是很是危險的事,若是用很差會直接致使程序Crash,可是若是用得好,能作不少很是酷的事。

就好像跑酷,幹得好,叫「耍酷」,幹很差就叫「做死」。

正如前文所說:_objc_msgForwardIMP 類型,用於消息轉發的:當向一個對象發送一條消息,但它並無實現的時候,_objc_msgForward會嘗試作消息轉發。

如何調用_objc_msgForward? _objc_msgForward隸屬 C 語言,有三個參數 :

-- _objc_msgForward參數 類型
1 所屬對象 id類型
2 方法名 SEL類型
3 可變參數 可變參數類型

首先了解下如何調用 IMP 類型的方法,IMP類型是以下格式:

爲了直觀,咱們能夠經過以下方式定義一個 IMP類型 :

typedef void (*voidIMP)(id, SEL, ...)
複製代碼

一旦調用_objc_msgForward,將跳過查找 IMP 的過程,直接觸發「消息轉發」,

若是調用了_objc_msgForward,即便這個對象確實已經實現了這個方法,你也會告訴objc_msgSend:「我沒有在這個對象裏找到這個方法的實現」

想象下objc_msgSend會怎麼作?一般狀況下,下面這張圖就是你正常走objc_msgSend過程,和直接調用_objc_msgForward的先後差異:

搞笑

1.1.6 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何?

不能向編譯後獲得的類中增長實例變量;能向運行時建立的類中添加實例變量;

由於編譯後的類已經註冊在runtime中,類結構體中的objc_ivar_list 實例變量的鏈表和instance_size實例變量的內存大小已經肯定,同時runtime 會調用class_setIvarLayout 或 class_setWeakIvarLayout來處理strong weak引用,因此不能向存在的類中添加實例變量 運行時建立的類是能夠添加實例變量,調用 class_addIvar函數,可是得在調用objc_allocateClassPair以後,objc_registerClassPair以前,緣由同上。

1.1.7 簡述下Objective-C中調用方法的過程(runtime)

Runtime 鑄就了Objective-C 是動態語言的特性,使得C語言具有了面向對象的特性,在程序運行期建立,檢查,修改類、對象及其對應的方法,這些操做均可以使用runtime中的對應方法實現。

Objective-C是動態語言,每一個方法在運行時會被動態轉爲消息發送,即:objc_msgSend(receiver, selector),整個過程介紹以下:

  1. objc在向一個對象發送消息時,runtime庫會根據對象的isa指針找到該對象實際所屬的類
  2. 而後在該類中的方法列表以及其父類方法列表中尋找方法運行
  3. 若是,在最頂層的父類(通常也就NSObject)中依然找不到相應的方法時,程序在運行時會掛掉並拋出異常unrecognized selector sent to XXX
  4. 可是在這以前,objc的運行時會給出三次拯救程序崩潰的機會。這三次機會分別是: (1)動態方法解析過程當中的:對象方法動態解析(+(BOOL)resolveInstanceMethod:(SEL)sel)和 類方法動態解析 (+(BOOL)resolveClassMethod:(SEL)sel) (2)若是動態解析失敗,則會進入消息轉發流程,消息轉發又分爲:快速轉發和慢速轉發兩種方式。 (3)快速轉發的實現是 forwardingTargetForSelector,讓其餘能響應要查找消息的對象來幹活。 (4)慢速轉發的實現是 methodSignatureForSelectorforwardInvocation 的結合,提供了更細粒度的控制,先返回方法簽名給 Runtime,而後讓 anInvocation 來把消息發送給提供的對象,最後由 Runtime 提取結果真後傳遞給原始的消息發送者。 (5)若是在3次挽救機會:resolveInstanceMethodforwardingTargetForSelectorforwardInvocation都沒有處理時,就會報unrecognized selector sent to XXX異常。此時程序會崩潰。

消息轉發流程圖

  • 關於resolveInstanceMethod 方法又稱爲對象方法動態解析,它的流程大體以下:
  1. 檢查是否實現了 +(BOOL)resolveInstanceMethod:(SEL)sel 類方法,若是沒有實現則直接返回(經過 cls->ISA() 是拿到元類,由於類方法是存儲在元類上的對象方法)
  2. 若是當前實現了 +(BOOL)resolveInstanceMethod:(SEL)sel 類方法,則經過 objc_msgSend 手動調用該類方法。
  3. 完成調用後,再次查詢 cls 中的 imp
  4. 若是 imp 找到了,則輸出動態解析對象方法成功的日誌。
  5. 若是 imp 沒有找到,則輸出雖然實現了 +(BOOL)resolveInstanceMethod:(SEL)sel,而且返回了 YES

對應源碼以下:

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製代碼
  • 若是對象方法動態解析未實現,實際會沿着isa指針,去調用_class_resolveClassMethod 走類方法動態解析流程:
  1. 判斷是不是元類,若是不是,直接退出。
  2. 檢查是否實現了 +(BOOL)resolveClassMethod:(SEL)sel 類方法,若是沒有實現則直接返回(經過 cls- 是由於當前 cls 就是元類,由於類方法是存儲在元類上的對象方法)
  3. 若是當前實現了 +(BOOL)resolveClassMethod:(SEL)sel 類方法,則經過 objc_msgSend 手動調用該類方法,注意這裏和動態解析對象方法不一樣,這裏須要經過元類和對象來找到類,也就是 _class_getNonMetaClass
  4. 完成調用後,再次查詢 cls 中的 imp
  5. 若是 imp 找到了,則輸出動態解析對象方法成功的日誌。
  6. 若是 imp 沒有找到,則輸出雖然實現了 +(BOOL)resolveClassMethod:(SEL)sel,而且返回了 YES,但並無查找到 imp 的日誌

對應源碼以下:

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
複製代碼
  • 關於forwardingTargetForSelector方法 又稱爲快速消息轉發:
  1. forwardingTargetForSelector 是一種快速的消息轉發流程,它直接讓其餘對象來響應未知的消息。
  2. forwardingTargetForSelector 不能返回 self,不然會陷入死循環,由於返回 self 又回去當前實例對象身上走一遍消息查找流程,顯然又會來到 forwardingTargetForSelector
  3. forwardingTargetForSelector 適用於消息轉發給其餘能響應未知消息的對象,也就是最終返回的內容必須和要查找的消息的參數和返回值一致,若是想要不一致,就須要走其餘的流程。

快速消息轉發是經過彙編來實現的,根據 lookUpImpOrForward 源碼咱們能夠看到當動態解析沒有成功後,會直接返回一個 _objc_msgForward_impcache。 在源碼中全局搜索_objc_msgForward_impcache 能夠定位到objc-msg-arm64.s 彙編源碼 以下:

STATIC_ENTRY __objc_msgForward_impcache

	// No stret specialization.
	b	__objc_msgForward

	END_ENTRY __objc_msgForward_impcache

	
	ENTRY __objc_msgForward

	adrp	x17, __objc_forward_handler@PAGE
	ldr	p17, [x17, __objc_forward_handler@PAGEOFF]
	TailCallFunctionPointer x17
	
	END_ENTRY __objc_msgForward
複製代碼
  • 關於 forwardInvocation 對應慢速消息轉發 methodSignatureForSelector 方法簽名:
  1. forwardInvocation 方法有兩個任務: (1): 查找能夠響應 inInvocation 中編碼的消息的對象。對於全部消息,此對象沒必要相同。 (2): 使用 anInvocation 將消息發送到該對象。anInvocation 將保存結果,運行時系統將提取結果並將其傳遞給原始發送者。
  2. forwardInvocation 方法的實現不只僅能夠轉發消息。forwardInvocation還能夠用於合併響應各類不一樣消息的代碼,從而避免了必須爲每一個選擇器編寫單獨方法的麻煩。forwardInvocation 方法在對給定消息的響應中還可能涉及其餘幾個對象,而不是僅將其轉發給一個對象。
  3. NSObjectforwardInvocation 實現:只會調用 dosNotRecognizeSelector:方法,它不會轉發任何消息。所以,若是選擇不實現`forwardInvocation,將沒法識別的消息發送給對象將引起異常。

下面舉例來講明: 假設咱們調用[dog walk]方法,那麼它會經歷以下過程:

  1. 編譯器會把[dog walk]轉化爲objc_msgSend(dog,SEL),SEL爲@selector(walk)。
  2. Runtime會在dog對象所對應的Dog類的方法緩存列表裏查找方法的SEL
  3. 若是沒有找到,則在Dog類的方法分發表查找方法的SEL。(類由對象isa指針指向,方法分發表即methodList)
  4. 若是沒有找到,則在其父類(設Dog類的父類爲Animal類)的方法分發表裏查找方法的SEL(父類由類的superClass指向)
  5. 若是沒有找到,則沿繼承體系繼續下去,最終到達NSObject類。
  6. 若是在234的其中一步中找到,則定位了方法實現的入口,執行具體實現
  7. 若是最後仍是沒有找到,會面臨兩種狀況:``(1) 若是是使用[dog walk]的方式調用方法````(2) 使用[dog performSelector:@selector(walk)]的方式調用方法`

若是是一個沒有定義的方法,那麼它會經歷動態方法解析,消息轉發流程

  • 對象如何找到對應的方法去調用
  1. 根據對象的isa去對應的類查找方法,isa:判斷去哪一個類查找對應的方法 指向方法調用的類
  2. 根據傳入的方法編號SEL,裏面有個哈希列表,在列表中找到對應方法Method(方法名)
  3. 根據方法名(函數入口)找到函數實現,函數實如今方法區

1.1.8 什麼是method swizzling(俗稱黑魔法)

簡單說就是進行方法交換

  1. 在Objective-C中調用一個方法,實際上是向一個對象發送消息,查找消息的惟一依據是selector的名字。利用Objective-C的動態特性,能夠實如今運行時偷換selector對應的方法實現,達到給方法掛鉤的目的
  2. 每一個類都有一個方法列表,存放着方法的名字和方法實現的映射關係,selector的本質其實就是方法名,IMP有點相似函數指針,指向具體的Method實現,經過selector就能夠找到對應的IMP
    IMP地址指向
  3. 交換方法的幾種實現方式: (1) 利用 method_exchangeImplementations 交換兩個方法的實現 (2)利用 class_replaceMethod 替換方法的實現 (3)利用 method_setImplementation 來直接設置某個方法的IMP
    方法交換
  • 方法交換實際應用:有一個需求:好比我有個項目,已經開發2年,以前都是使用UIImage去加載圖片,組長想要在調用imageNamed,就給我提示,是否加載成功。

有三種方法解決這個需求問題:

  1. 解決方式 自定義UIImage類,缺點:每次用要導入本身的類
  2. 解決方法:UIImage分類擴充一個這樣方法,缺點:須要導入,沒法寫super和self,會幹掉系統方法,解決:給系統方法加個前綴,與系統方法區分,如:xmg_imageNamed:
  3. 交互方法實現,步驟: 1.提供分類 2.寫一個有這樣功能方法 3.用系統方法與這個功能方法交互實現,在+load方法中實現

若是用方法2,每一個調用imageNamed方法的,都要改爲xmg_imageNamed:才能擁有這個功能,很麻煩。解決:用runtime交換方法 就比較好。

注意:在分類必定不要重寫系統方法,就直接把系統方法幹掉,若是真的想重寫,在系統方法前面加前綴,方法裏面去調用系統方法

思想:何時須要自定義,系統功能不完善,就自定義一個這樣類,去擴展這個類

方法交換實現代碼以下:

/#import "UIImage+Image.h"
/#import <objc/message.h>
@implementation UIImage (Image)
// 加載類的時候調用,確定只會調用一次

 +(void)load
{
    // 交互方法實現xmg_imageNamed,imageNamed
    /** 獲取類方法名 @param Class cls,#> 獲取哪一個類方法 description#> @param SEL name#> 方法編號 description#> @return 返回Method(方法名) class_getClassMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>) */
    /** 獲取對象方法名 @param Class cls,#> 獲取哪一個對象方法 description#> @param SEL name#> 方法編號 description#> @return 返回Method(方法名) class_getInstanceMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>) */
    
   Method imageNameMethod = class_getClassMethod(self, @selector(imageNamed:));
    Method xmg_imageNameMethod = class_getClassMethod(self, @selector(xmg_imageNamed:));
    //用runtime對imageNameMethod和xmg_imageNameMethod方法進行交換
    method_exchangeImplementations(imageNameMethod, xmg_imageNameMethod);
}
//外界調用imageNamed:方法,實際上是調用下面方法,調用xmg_imageNamed就是調用imageNamed:
+ (UIImage *)xmg_imageNamed:(NSString *)name
{
    //已經把xmg_imageNamed換成imageNamed,因此下面實際上是調用的imageNamed:
   UIImage *image = [UIImage xmg_imageNamed:name];
    
    if (image == nil) {
        NSLog(@"加載失敗");
    }
    return image;
}
@end
複製代碼

1.2 結構模型

1.2.1 類結構,消息轉發相關

上面主要是關於runtime流程的一些問題,接下來會有更加深刻的須要深刻理解objc4相關的源碼。其中關於消息轉發機制多是最多見的問題了。

  • 關於類結構,消息轉發相關,你可能被問到的問題:
  1. 介紹下runtime的內存模型(isa、對象、類、metaclass、結構體的存儲信息等)
  2. 爲何要設計metaclass
  3. class_copyIvarList & class_copyPropertyList區別
  4. class_rw_t 和 class_ro_t 的區別
  5. category如何被加載的,兩個category的load方法的加載順序,兩個category的同名方法的加載順序
  6. category & extension區別,能給NSObject添加Extension嗎,結果如何
  7. 消息轉發機制,消息轉發機制和其餘語言的消息機制優劣對比
  8. 在方法調用的時候,方法查詢-> 動態解析-> 消息轉發 以前作了什麼
  9. IMP、SEL、Method的區別和使用場景
  10. load、initialize方法的區別什麼?在繼承關係中他們有什麼區別
  11. 說說消息轉發機制的優劣

1.2.1.1 類結構 isa指針相關問題

1.2.1.1.1 說一下對 isa 指針的理解, 對象的isa 指針指向哪裏?isa 指針有哪兩種類型?
  • isa 等價於 is kind of

實例對象 isa 指向類對象 類對象指 isa 向元類對象 元類對象的 isa 指向元類的基類

  • isa 有兩種類型

純指針,指向內存地址 NON_POINTER_ISA,除了內存地址,還存有一些其餘信息

  • isa源碼分析 在Runtime源碼查看isa_t是共用體。簡化結構以下:
union isa_t 
{
    Class cls;
    uintptr_t bits;
    # if __arm64__ // arm64架構
#   define ISA_MASK        0x0000000ffffffff8ULL //用來取出33位內存地址使用(&)操做
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; //0:表明普通指針,1:表示優化過的,能夠存儲更多信息。
        uintptr_t has_assoc         : 1; //是否設置過關聯對象。若是沒設置過,釋放會更快
        uintptr_t has_cxx_dtor      : 1; //是否有C++的析構函數
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 內存地址值
        uintptr_t magic             : 6; //用於在調試時分辨對象是否未完成初始化
        uintptr_t weakly_referenced : 1; //是否有被弱引用指向過
        uintptr_t deallocating      : 1; //是否正在釋放
        uintptr_t has_sidetable_rc  : 1; //引用計數器是否過大沒法存儲在ISA中。若是爲1,那麼引用計數會存儲在一個叫作SideTable的類的屬性中
        uintptr_t extra_rc          : 19; //裏面存儲的值是引用計數器減1

#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__ // arm86架構,模擬器是arm86
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

# else
#   error unknown architecture for packed isa
# endif

}
複製代碼
  • 繼續查看結構體objc_class的定義:
typedef struct objc_class *Class; struct objc_class {

    Class isa  OBJC_ISA_AVAILABILITY;   //isa指針,指向metaclass(該類的元類)

#if !__OBJC2__

    Class super_class   //指向objc_class(該類)的super_class(父類)

    const char *name    //objc_class(該類)的類名

    long version        //objc_class(該類)的版本信息,初始化爲0,能夠經過runtime函數class_setVersion和class_getVersion進行修改和讀取

    long info           //一些標識信息,如CLS_CLASS表示objc_class(該類)爲普通類。ClS_CLASS表示objc_class(該類)爲metaclass(元類)

    long instance_size  //objc_class(該類)的實例變量的大小

    struct objc_ivar_list *ivars //用於存儲每一個成員變量的地址 struct objc_method_list **methodLists //方法列表,與info標識關聯 struct objc_cache *cache //指向最近使用的方法的指針,用於提高效率 struct objc_protocol_list *protocols //存儲objc_class(該類)的一些協議 #endif } OBJC2_UNAVAILABLE; typedef struct objc_object *id; struct objc_object {

    Class isa  OBJC_ISA_AVAILABILITY;

};


複製代碼

struct objc_classs結構體裏存放的數據稱爲元數據(metadata),經過成員變量的名稱咱們能夠猜想裏面存放有指向父類的指針、類的名字、版本、實例大小、實例變量列表、方法列表、緩存、遵照的協議列表等,這些信息就足夠建立一個實例了,該結構體的第一個成員變量也是isa指針,這就說明了Class自己其實也是一個對象,咱們稱之爲類對象,類對象在編譯期產生用於建立實例對象,是單例。

objec_object(對象)中isa指針指向的類結構稱爲objec_class(該對象的類),其中存放着普通成員變量與對象方法 (「-」開頭的方法)。

objec_class(類)中isa指針指向的類結構稱爲metaclass(該類的元類),其中存放着static類型的成員變量與static類型的方法 (「+」開頭的方法)。

元類(metaclass):在oc中,每個類實際上也是一個對象。也有一個isa指針。由於類也是一個對象,因此也必須是另一個類的實例,這個類就是元類(metaclass),即元類的對象就是這個類,元類保存了類方法的列表

在oc語言中,每個類實際上也是一個對象。每個類也有一個isa指針。每個類也能夠接收消息。由於類也是一個對象,因此也是另一個類的實例,這個類就是元類(metaclass)。元類也是一個對象,全部的元類的isa指針都會指向一個根元類。根元類的isa指針又會指向他本身,這樣就造成了一個閉環。

以下圖展現了類的繼承和isa指向的關係:

isa指針走位圖

isa指針的指向

  1. 一個objc對象的isa指針指向他的類對象,類對象的isa指針指向他的元類,元類的isa指針指向根元類,全部的元類isa都指向同一個根元類,根元類的isa指針指向根元類自己。根元類super class父類指向NSObject類對象。根metaclass(元類)中的superClass指針指向根類,由於根metaclass(元類)是經過繼承根類產生的。
  2. 實例對象的isa指針, 指向他的類對象,類對象的isa 指針, 指向他的元類。系統判斷一個對象屬於哪一個類,也是經過這個對象的isa指針的指向來判斷。對象中的成員變量,存儲在對象自己,對象的實例方法,存儲在他的isa 指針所指向的對象中。
  3. 對象在調用減號方法的時候,系統會在對象的isa指針所指向的類對象中找方法,這一段在kvo的實現原理中就能看到,kvo的實現原理就是系統動態的生成一個類對象,這個類是監聽對象的類的子類,在生成的子類中重寫了監聽屬性的set方法,以後將監聽對象的isa指針指向系統動態生成的這個類,當監聽對象調用set方法時,因爲監聽對象的isa指針指向的是剛剛動態生成的類,因此在執行的set方法也是子類中重寫的set方法,這就是kvo的實現原理。同理,咱們也能夠經過runtime中的方法設置某個對象isa指針指向的類對象,讓對象調用一些本來不屬於他的方法。
  • 類對象(class object):
  1. 類對象由編譯器建立。任何直接或間接繼承了NSObject的類,它的實例對象中都有一個isa指針,指向它的類對象。這個類對象中存儲了關於這個實例對象所屬的類的定義的一切:包括變量,方法,遵照的協議等等。所以,類對象能訪問全部關於這個類的信息,利用這些信息能夠產生一個新的實例,可是類對象不能訪問任何實例對象的內容。類對象沒有本身的實例變量。

例如:咱們建立p對象:Person * p = [Person new] ;

在建立一個對象p以前,在堆內存中就先存在了一個類(Person)(類對象),類對象在編繹時系統會爲咱們自動建立。在類第一次加載進內存時建立。

建立一個對象以後,在堆內存中會建立了一個p對象,該對象包含了一個isa指針的成員變量(第一個屬性),isa指針則指向在堆裏面存在的類對象, 在棧內存裏建立了一個該類的指針p,p指針指向的是isa地址,isa指向Person.

  • 對象方法的調用:

OC調用方法,運行的時候編譯器會將代碼轉化爲objc_msgSend(obj, @selector (selector)),在objc_msgSend函數中首先經過obj(對象)的isa指針找到obj(對象)對應的class(類)。在class(類)中先去cache中經過SEL(方法的編號)查找對應method(方法),若cache中未找到,再去methodLists中查找,若methodists中未找到,則去superClass中查找,若能找到,則將method(方法)加入到cache中,以方便下次查找,並經過method(方法)中的函數指針跳轉到對應的函數中去執行。若是仍然找不到,則繼續經過 super_class向上一級父類結構體中查找,直至根class

  • 類方法的調用:
  1. 當咱們調用某個類方法時,它首先經過本身的isa指針指向的objc_class中的isa指針找到元類,並從其methodLists中查找該類方法,若是找不到則會經過元類)的super_class指針找到父類的元類結構體,而後從methodLists中查找該方法,若是仍然找不到,則繼續經過super_class向上一級父類結構體中查 找,直至根元類;
  2. C語言函數編譯的時候就會決定調用哪一個函數,OC是一種動態語言,他會盡量把代碼的從編譯連接推遲到運行時,這就是oc運行時多態。 給一個對象發送消息,並不會當即執行,而是在運行的時候在去尋找他對應的實現而OC的函數,屬於動態調用過程,在編譯期並不能決定真正調用哪一個函數,只有在真正運行時纔會根據函數的名稱找到對應的函數來調用。

1.3 內存管理相關

參考:www.jianshu.com/p/8345a79fd… juejin.im/post/5e0dbb… github.com/ChenYilong/… www.jianshu.com/p/0bf8787db…

相關文章
相關標籤/搜索