RunTime

一祖乳、數(shù)據(jù)結(jié)構(gòu):objc_object, objc_class, isa, class_data_bits_t, cache_t, method_t
屏幕快照 2019-09-03 09.19.27.png
  • objc_object (id)
    isa_t,關(guān)于isa操作相關(guān),弱引用相關(guān)秉氧,關(guān)聯(lián)對(duì)象相關(guān)眷昆,內(nèi)存管理相關(guān)


    屏幕快照 2019-09-03 11.17.48.png
  • objc_class(class) 繼承 objc_object
    Class superClass, cache_t cache,class_data_bits_bits


    屏幕快照 2019-09-03 11.18.46.png
  • isa指針,共用體 isa_t


    屏幕快照 2019-09-03 09.27.48.png
  • isa指向
    關(guān)于對(duì)象汁咏,其指向類對(duì)象亚斋。
    關(guān)于類對(duì)象,其指向元類對(duì)象攘滩。
    實(shí)例--(isa)-->class--(isa)-->MetaClass

  • cache_t
    用于快速查找方法執(zhí)行函數(shù)帅刊,是可增量擴(kuò)展的哈希表結(jié)構(gòu),是局部性原理的最佳運(yùn)用


    屏幕快照 2019-09-03 11.19.53.png
 struct cache_t {
    struct bucket_t *_buckets;//一個(gè)散列表轰驳,用來方法緩存厚掷,bucket_t類型弟灼,包含key以及方法實(shí)現(xiàn)IMP
    mask_t _mask;//分配用來緩存bucket的總數(shù)
    mask_t _occupied;//表明目前實(shí)際占用的緩存bucket的個(gè)數(shù)
}
struct bucket_t {
    private:
    cache_key_t _key;
    IMP _imp;
 }
  • class_data_bits_t:對(duì)class_rw_t的封裝
struct class_rw_t {
     uint32_t flags;
     uint32_t version;

     const class_ro_t *ro;

     method_array_t methods;
     property_array_t properties;
     protocol_array_t protocols;

     Class firstSubclass;
     Class nextSiblingClass;

     char *demangledName;
}

Objc的類的屬性级解、方法、以及遵循的協(xié)議都放在class_rw_t中田绑,class_rw_t代表了類相關(guān)的讀寫信息勤哗,是對(duì)class_ro_t的封裝,而class_ro_t代表了類的只讀信息掩驱,存儲(chǔ)了 編譯器決定了的屬性芒划、方法和遵守協(xié)議

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    #ifdef __LP64__
    uint32_t reserved;
    #endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
  };
  • method_t
    函數(shù)四要素:名稱,返回值欧穴,參數(shù)民逼,函數(shù)體
struct method_t {
  SEL name;           //名稱
  const char *types;//返回值和參數(shù)
  IMP imp;              //函數(shù)體
}
二、 對(duì)象涮帘,類對(duì)象拼苍,元類對(duì)象
  • 類對(duì)象存儲(chǔ)實(shí)例方法列表等信息。
  • 元類對(duì)象存儲(chǔ)類方法列表等信息调缨。
三疮鲫、消息傳遞
屏幕快照 2019-09-03 11.24.04.png
void objc_msgSend(void /* id self, SEL op, ... */ )

void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

消息傳遞的流程:緩存查找-->當(dāng)前類查找-->父類逐級(jí)查找

  • 調(diào)用方法之前,先去查找緩存弦叶,看看緩存中是否有對(duì)應(yīng)選擇器的方法實(shí)現(xiàn)俊犯,如果有,就去調(diào)用函數(shù)伤哺,完成消息傳遞(緩存查找:給定值SEL,目標(biāo)是查找對(duì)應(yīng)bucket_t中的IMP燕侠,哈希查找)
  • 如果緩存中沒有者祖,會(huì)根據(jù)當(dāng)前實(shí)例的isa指針查找當(dāng)前類對(duì)象的方法列表,看看是否有同樣名稱的方法 绢彤,如果找到咸包,就去調(diào)用函數(shù),完成消息傳遞(當(dāng)前類中查找:對(duì)于已排序好的方法列表杖虾,采用二分查找烂瘫,對(duì)于沒有排序好的列表,采用一般遍歷)
  • 如果當(dāng)前類對(duì)象的方法列表沒有奇适,就會(huì)逐級(jí)父類方法列表中查找坟比,如果找到,就去調(diào)用函數(shù)嚷往,完成消息傳遞(父類逐級(jí)查找:先判斷父類是否為nil葛账,為nil則結(jié)束,否則就繼續(xù)進(jìn)行緩存查找-->當(dāng)前類查找-->父類逐級(jí)查找的流程)
  • 如果一直查到根類依然沒有查找到皮仁,則進(jìn)入到消息轉(zhuǎn)發(fā)流程中籍琳,完成消息傳遞
四、消息轉(zhuǎn)發(fā)
+ (BOOL)resolveInstanceMethod:(SEL)sel;//為對(duì)象方法進(jìn)行決議
+ (BOOL)resolveClassMethod:(SEL)sel;//為類方法進(jìn)行決議
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法轉(zhuǎn)發(fā)目標(biāo)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
屏幕快照 2019-09-03 10.12.42.png

那么最后消息未能處理的時(shí)候贷祈,還會(huì)調(diào)用到

  • (void)doesNotRecognizeSelector:(SEL)aSelector這個(gè)方法趋急,我們也可以在這個(gè)方法中做處理,避免掉crash势誊,但是只建議在線上環(huán)境的時(shí)候做處理呜达,實(shí)際開發(fā)過程中還要把異常拋出來

  • 方法交換(Method-Swizzling)

 + (void)load
{
    Method test = class_getInstanceMethod(self, @selector(test));

    Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

    method_exchangeImplementations(test, otherTest);
}

應(yīng)用場(chǎng)景:替換系統(tǒng)的方法,比如viewDidLoad粟耻,viewWillAppear以及一些響應(yīng)方法查近,來進(jìn)行統(tǒng)計(jì)信息

  • 動(dòng)態(tài)添加方法
class_addMethod(self, sel, testImp, "v@:");

void testImp (void)
{
    NSLog(@"testImp");
}
  • @dynamic 動(dòng)態(tài)方法解析
    動(dòng)態(tài)運(yùn)行時(shí)語言將函數(shù)決議推遲到運(yùn)行時(shí)
    編譯時(shí)語言在編譯期進(jìn)行函數(shù)決議
  • [obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系?
    objc_msgSend()是[obj foo]的具體實(shí)現(xiàn)挤忙。在runtime中霜威,objc_msgSend()是一個(gè)c函數(shù),[obj foo]會(huì)被翻譯成這樣的形式objc_msgSend(obj, foo)册烈。
  • runtime是如何通過selector找到對(duì)應(yīng)的IMP地址的戈泼?
    緩存查找-->當(dāng)前類查找-->父類逐級(jí)查找
  • 能否向編譯后的類中增加實(shí)例變量?
    不能茄厘。 編譯后矮冬,該類已經(jīng)完成了實(shí)例變量的布局,不能再增加實(shí)例變量次哈。
    但可以向 動(dòng)態(tài)添加的 類中增加實(shí)例變量胎署。
五.應(yīng)用
1)動(dòng)態(tài)的遍歷一個(gè)類的所有成員變量,用于字典轉(zhuǎn)模型,歸檔解檔操作
- (void)viewDidLoad {    
                  [super viewDidLoad];    
                  /** 利用runtime遍歷一個(gè)類的全部成員變量     
                      1.導(dǎo)入頭文件<objc/runtime.h>     */    
                  unsigned int count = 0;   
                 /** Ivar:表示成員變量類型 */    
                  Ivar *ivars = class_copyIvarList([BDPerson class], &count);//獲得一個(gè)指向該類成員變量的指針   
                 for (int i =0; i < count; i ++) {        
                //獲得Ivar      
                  Ivar ivar = ivars[i];        //根據(jù)ivar獲得其成員變量的名稱--->C語言的字符串      
                  const char *name = ivar_getName(ivar);       
                   NSString *key = [NSString stringWithUTF8String:name];      
                  NSLog(@"%d----%@",i,key);
                }
            }
2)可以利用遍歷類的屬性窑滞,來快速的進(jìn)行歸檔操作琼牧;將從網(wǎng)絡(luò)上下載的json數(shù)據(jù)進(jìn)行字典轉(zhuǎn)模型恢筝。
注意:歸檔解檔需要遵守<NSCoding>協(xié)議,實(shí)現(xiàn)以下兩個(gè)方法
            - (void)encodeWithCoder:(NSCoder *)encoder{    
                //歸檔存儲(chǔ)自定義對(duì)象    
                unsigned int count = 0;  
                //獲得指向該類所有屬性的指針   
                objc_property_t *properties =     class_copyPropertyList([BDPerson class], &count);   
                for (int i =0; i < count; i ++) {        
                //獲得        
                objc_property_t property = properties[i];        //根據(jù)objc_property_t獲得其屬性的名稱--->C語言的字符串       
               const char *name = property_getName(property);   
               NSString *key = [NSString   stringWithUTF8String:name];       
               //      編碼每個(gè)屬性,利用kVC取出每個(gè)屬性對(duì)應(yīng)的數(shù)值            
               [encoder encodeObject:[self valueForKeyPath:key] forKey:key]; 
             }}
            
            - (instancetype)initWithCoder:(NSCoder *)decoder{    
                  //歸檔存儲(chǔ)自定義對(duì)象    
                    unsigned int count = 0;   
                 //獲得指向該類所有屬性的指針   
                   objc_property_t *properties = class_copyPropertyList([BDPerson class], &count);   
                   for (int i =0; i < count; i ++) {       
                   objc_property_t property = properties[i];        //根據(jù)objc_property_t獲得其屬性的名稱--->C語言的字符串       
                   const char *name = property_getName(property); 
                     NSString *key = [NSString stringWithUTF8String:name];        //解碼每個(gè)屬性,利用kVC取出每個(gè)屬性對(duì)應(yīng)的數(shù)值      
                   [self setValue:[decoder decodeObjectForKey:key] forKeyPath:key];  
            }   
             return self;
            }
3)交換方法
一.例如數(shù)組越界問題巨坊,防止系統(tǒng)崩潰(NSMutableArray 添加空值會(huì)出現(xiàn)崩潰)
① 新建一個(gè)分類撬槽,分類中引入頭文件,實(shí)現(xiàn)下列方法
+ (void)load{
                Method orginalMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(addObject:));
                Method newMethod = class_getInstanceMethod(NSClassFromString(@"__NSArrayM"), @selector(Mn_addObject:));
                
                method_exchangeImplementations(orginalMethod, newMethod);
            }
            
            - (void)Mn_addObject:(id)object{
                if (object) {
                    [self Mn_addObject:object];
                }
            }
②在項(xiàng)目文件中趾撵,正常使用侄柔,若添加空值,不會(huì)崩潰只會(huì)出現(xiàn)報(bào)警信息
NSMutableArray *arr = [NSMutableArray array];
        [arr addObject:nil];
二. 生命周期
①創(chuàng)建分類
//load方法會(huì)在類第一次加載的時(shí)候被調(diào)用
            //調(diào)用的時(shí)間比較靠前占调,適合在這個(gè)方法里做方法交換
+ (void)load{
                //方法交換應(yīng)該被保證暂题,在程序中只會(huì)執(zhí)行一次
                static dispatch_once_t onceToken;
                dispatch_once(&onceToken, ^{
                    //獲得viewController的生命周期方法的selector
                    SEL systemSel = @selector(viewWillAppear:);
                    //自己實(shí)現(xiàn)的將要被交換的方法的selector
                    SEL swizzSel = @selector(swiz_viewWillAppear:);
                    //兩個(gè)方法的Method
                    Method systemMethod = class_getInstanceMethod([self class], systemSel);
                    Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
                    //首先動(dòng)態(tài)添加方法,實(shí)現(xiàn)是被交換的方法究珊,返回值表示添加成功還是失敗
                    BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
                    if (isAdd) {
                        //如果成功薪者,說明類中不存在這個(gè)方法的實(shí)現(xiàn)
                        //將被交換方法的實(shí)現(xiàn)替換到這個(gè)并不存在的實(shí)現(xiàn)
                        class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
                    }else{
                        //否則,交換兩個(gè)方法的實(shí)現(xiàn)
                        method_exchangeImplementations(systemMethod, swizzMethod);
                    }
                });
            }
- (void)swiz_viewWillAppear:(BOOL)animated{
                //這時(shí)候調(diào)用自己剿涮,看起來像是死循環(huán)
                //但是其實(shí)自己的實(shí)現(xiàn)已經(jīng)被替換了
                [self swiz_viewWillAppear:animated];
                NSLog(@"swizzle");
            }

        ②在一個(gè)自己定義的viewController中重寫viewWillAppear言津,Run起來看看輸出吧!
            
            - (void)viewWillAppear:(BOOL)animated{
                [super viewWillAppear:animated];
                NSLog(@"viewWillAppear");
            }

4)關(guān)聯(lián)屬性
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
      OBJC_ASSOCIATION_ASSIGN = 0,  //相當(dāng)于屬性中的assign         
      OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,    //retain,monatomic
      OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  //copy,nonatomic
      OBJC_ASSOCIATION_RETAIN = 01401,    //retain 
      OBJC_ASSOCIATION_COPY = 01403     //copy    
  };
//添加關(guān)聯(lián)對(duì)象
        - (void)addAssociatedObject:(id)object{
            objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        //獲取關(guān)聯(lián)對(duì)象
        - (id)getAssociatedObject{
            return objc_getAssociatedObject(self, _cmd);
        }

        //樣例
        - (void)viewDidLoad {
            [super viewDidLoad];
            // Do any additional setup after loading the view, typically from a nib.
            UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
            btn.backgroundColor = [UIColor blackColor];
            btn.frame = CGRectMake(100, 100, 60, 30);
            [self.view addSubview:btn];
             objc_setAssociatedObject(btn, myBtnKey, @"mybtn", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];            
        }

        - (void)btnClick:(id)sender {
            NSString *str = objc_getAssociatedObject(sender, myBtnKey);
            /**
             *  CODE
             */
        }
5)方法攔截

       + (BOOL)resolveClassMethod:(SEL)sel;
        + (BOOL)resolveInstanceMethod:(SEL)sel;

_objc_msgForward是 IMP類型取试,用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對(duì)象發(fā)送一條消息悬槽,但它并沒有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會(huì)嘗試做消息轉(zhuǎn)發(fā)想括。
IMP msgForward = _objc_msgForward;
如果手動(dòng)調(diào)用objc_msgForward,將跳過查找IMP的過程陷谱,而是直接出發(fā)“消息轉(zhuǎn)發(fā)”,進(jìn)入如下流程:
1)+ (BOOL)resolveInstanceMethodL:(SEL)sel 實(shí)現(xiàn)方法瑟蜈,指定是否動(dòng)態(tài)添加方法。若返回NO渣窜,則進(jìn)入下一步铺根,若返回YES,則通過class_addMethod函數(shù)動(dòng)態(tài)地添加方法乔宿,消息得到處理位迂,此流程完畢。
2)在第一步返回的是NO時(shí)详瑞,就會(huì)進(jìn)入 -(id)forwardTargetForSelector:(SEL)aSelector 方法掂林,這是運(yùn)行時(shí)給我們的第二次機(jī)會(huì),用于指定哪個(gè)對(duì)象響應(yīng)這個(gè)selector坝橡。不能指定為self泻帮。若返回nil,表示沒有響應(yīng)者计寇,則會(huì)進(jìn)入第三不锣杂。若返回某個(gè)對(duì)象脂倦,則會(huì)調(diào)用該對(duì)象的方法。
3)若第二部返回的是nil元莫,則我們首先要通過 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 指定方法簽名赖阻,若返回nil,則表示不處理踱蠢。若返回方法簽名火欧,則會(huì)進(jìn)入下一步。
4)當(dāng)?shù)谌椒呕胤椒ê灻缶ソ兀蜁?huì)調(diào)用 -(void)forwardInvocation:(NSInvocation *)anInvocation 方法布隔,我們可以通過anInvocation對(duì)象做很多處理,比如修改實(shí)現(xiàn)方法稼虎,修改響應(yīng)對(duì)象等衅檀。
5)若沒有實(shí)現(xiàn) -(void)forwardInvocation:(NSInvocation *)anInvocation 方法,那么會(huì)進(jìn)入 -(void)doesNotRecognizeSelector:(SEL)aSelector方法霎俩。若我們沒有實(shí)現(xiàn)這個(gè)方法哀军,那么就會(huì)crash,然后提示打不到響應(yīng)的方法打却。到此杉适,動(dòng)態(tài)解析的流程就結(jié)束了。
6)runtime如何實(shí)現(xiàn)weak變量的自動(dòng)置nil柳击?
runtime對(duì)注冊(cè)的類會(huì)進(jìn)行布局猿推,對(duì)于weak對(duì)象會(huì)放入一個(gè)hash表中。用weak指向的對(duì)象內(nèi)存地址作為key捌肴,當(dāng)此對(duì)象的引用計(jì)數(shù)為0的時(shí)候會(huì)dealloc蹬叭。假如weak指向的對(duì)象內(nèi)存地址是a,那么就會(huì)以a為鍵状知,在這個(gè)weak表中搜索秽五,找到所有以a為鍵的weak對(duì)象,從而設(shè)置為nil饥悴。
weak修飾的指針默認(rèn)值是nil(在Objective-C中向nil發(fā)送消息是安全的)

2. 動(dòng)態(tài)特性:方法解析和消息轉(zhuǎn)發(fā)

沒有方法的實(shí)現(xiàn)坦喘,程序會(huì)在運(yùn)行時(shí)掛掉并拋出 unrecognized selector sent to … 的異常。但在異常拋出前西设,Objective-C 的運(yùn)行時(shí)會(huì)給你三次拯救程序的機(jī)會(huì):

  • Method resolution
  • Fast forwarding
  • Normal forwarding
2.1 動(dòng)態(tài)方法解析: Method Resolution

首先瓣铣,Objective-C 運(yùn)行時(shí)會(huì)調(diào)用 + (BOOL)resolveInstanceMethod:或者 + (BOOL)resolveClassMethod:,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)贷揽。如果你添加了函數(shù)并返回 YES棠笑, 那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程。還是以 foo 為例擒滑,你可以這么實(shí)現(xiàn):

void fooMethod(id obj, SEL _cmd)  
{
    NSLog(@"Doing foo");
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(foo:)){
        class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod];
}

這里第一字符v代表函數(shù)返回類型void腐晾,第二個(gè)字符@代表self的類型id叉弦,第三個(gè)字符:代表_cmd的類型SEL。這些符號(hào)可在Xcode中的開發(fā)者文檔中搜索Type Encodings就可看到符號(hào)對(duì)應(yīng)的含義藻糖,更詳細(xì)的官方文檔傳送門 在這里淹冰,此處不再列舉了。

2.2 快速轉(zhuǎn)發(fā): Fast Rorwarding

與下面2.3完整轉(zhuǎn)發(fā)不同巨柒,F(xiàn)ast Rorwarding這是一種快速消息轉(zhuǎn)發(fā):只需要在指定API方法里面返回一個(gè)新對(duì)象即可樱拴,當(dāng)然其它的邏輯判斷還是要的(比如該SEL是否某個(gè)指定SEL?)洋满。
消息轉(zhuǎn)發(fā)機(jī)制執(zhí)行前晶乔,runtime系統(tǒng)允許我們替換消息的接收者為其他對(duì)象。通過- (id)forwardingTargetForSelector:(SEL)aSelector方法牺勾。如果此方法返回的是nil 或者self,則會(huì)進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制(- (void)forwardInvocation:(NSInvocation *)invocation)正罢,否則將會(huì)向返回的對(duì)象重新發(fā)送消息。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(foo:)){
        return [[BackupClass alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

2.3 完整消息轉(zhuǎn)發(fā): Normal Forwarding

與上面不同驻民,可以理解成完整消息轉(zhuǎn)發(fā)翻具,是可以代替快速轉(zhuǎn)發(fā)做更多的事。

- (void)forwardInvocation:(NSInvocation *)invocation {
    SEL sel = invocation.selector;
    if([alternateObject respondsToSelector:sel]) {
        [invocation invokeWithTarget:alternateObject];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

forwardInvocation: 方法就是一個(gè)不能識(shí)別消息的分發(fā)中心回还,將這些不能識(shí)別的消息轉(zhuǎn)發(fā)給不同的消息對(duì)象裆泳,或者轉(zhuǎn)發(fā)給同一個(gè)對(duì)象,再或者將消息翻譯成另外的消息柠硕,亦或者簡(jiǎn)單的“吃掉”某些消息工禾,因此沒有響應(yīng)也不會(huì)報(bào)錯(cuò)。例如:我們可以為了避免直接閃退蝗柔,可以當(dāng)消息沒法處理時(shí)在這個(gè)方法中給用戶一個(gè)提示闻葵,也不失為一種友好的用戶體驗(yàn)。

其中诫咱,參數(shù)invocation是從哪來的笙隙?在forwardInvocation:消息發(fā)送前,runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:消息坎缭,并取到返回的方法簽名用于生成NSInvocation對(duì)象。所以重寫forwardInvocation:的同時(shí)也要重寫methodSignatureForSelector:方法签钩,否則會(huì)拋出異常掏呼。當(dāng)一個(gè)對(duì)象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某個(gè)消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過forwardInvocation:消息通知該對(duì)象铅檩。每個(gè)對(duì)象都繼承了forwardInvocation:方法憎夷,我們可以將消息轉(zhuǎn)發(fā)給其它的對(duì)象。

2.4 區(qū)別: Fast Rorwarding 對(duì)比 Normal Forwarding昧旨?

這兩個(gè)轉(zhuǎn)發(fā)都是將消息轉(zhuǎn)發(fā)給其它對(duì)象拾给,那么這兩個(gè)有什么區(qū)別祥得?

  • 需要重載的API方法的用法不同
    前者只需要重載一個(gè)API即可,后者需要重載兩個(gè)API蒋得。
    前者只需在API方法里面返回一個(gè)新對(duì)象即可级及,后者需要對(duì)被轉(zhuǎn)發(fā)的消息進(jìn)行重簽并手動(dòng)轉(zhuǎn)發(fā)給新對(duì)象(利用 invokeWithTarget:)。
  • 轉(zhuǎn)發(fā)給新對(duì)象的個(gè)數(shù)不同
    前者只能轉(zhuǎn)發(fā)一個(gè)對(duì)象额衙,后者可以連續(xù)轉(zhuǎn)發(fā)給多個(gè)對(duì)象饮焦。例如下面是完整轉(zhuǎn)發(fā):
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector==@selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector: aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL selector =[anInvocation selector];
    
    RunPerson *RP1=[RunPerson new];
    RunPerson *RP2=[RunPerson new];
    
    if ([RP1 respondsToSelector:selector]) {
        
        [anInvocation invokeWithTarget:RP1];
    }
    if ([RP2 respondsToSelector:selector]) {
        
        [anInvocation invokeWithTarget:RP2];
    }    
}

3. 應(yīng)用實(shí)戰(zhàn):消息轉(zhuǎn)發(fā)

3.1 特定奔潰預(yù)防處理

下面有一段因?yàn)闆]有實(shí)現(xiàn)方法而會(huì)導(dǎo)致奔潰的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test2ViewController";
    
    //實(shí)例化一個(gè)button,未實(shí)現(xiàn)其方法
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(50, 100, 200, 100);
    button.backgroundColor = [UIColor blueColor];
    [button setTitle:@"消息轉(zhuǎn)發(fā)" forState:UIControlStateNormal];
    [button addTarget:self
               action:@selector(doSomething)
     forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

為解決這個(gè)問題,可以專門創(chuàng)建一個(gè)處理這種問題的分類

  • NSObject+CrashLogHandle
#import "NSObject+CrashLogHandle.h"

@implementation NSObject (CrashLogHandle)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    //方法簽名
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"NSObject+CrashLogHandle---在類:%@中 未實(shí)現(xiàn)該方法:%@",NSStringFromClass([anInvocation.target class]),NSStringFromSelector(anInvocation.selector));
}

@end

因?yàn)樵赾ategory中復(fù)寫了父類的方法窍侧,會(huì)出現(xiàn)下面的警告:
解決辦法就是在Xcode的Build Phases中的資源文件里县踢,在對(duì)應(yīng)的文件后面 -w ,忽略所有警告伟件。

3.2 蘋果系統(tǒng)API迭代造成API不兼容的奔潰處理
3.2.1 兼容系統(tǒng)API迭代的傳統(tǒng)方案

隨著每年iOS系統(tǒng)與硬件的更新迭代硼啤,部分性能更優(yōu)異或者可讀性更高的API將有可能對(duì)原有API進(jìn)行廢棄與更替。與此同時(shí)我們也需要對(duì)現(xiàn)有APP中的老舊API進(jìn)行版本兼容斧账,當(dāng)然進(jìn)行版本兼容的方法也有很多種谴返,下面筆者會(huì)列舉常用的幾種:

  • 根據(jù)能否響應(yīng)方法進(jìn)行判斷
if ([object respondsToSelector: @selector(selectorName)]) {
    //using new API
} else {
    //using deprecated API
}

  • 根據(jù)當(dāng)前版本SDK是否存在所需類進(jìn)行判斷
if (NSClassFromString(@"ClassName")) {    
    //using new API
}else {
    //using deprecated API
}

  • 根據(jù)操作系統(tǒng)版本進(jìn)行判斷
#define isOperatingSystemAtLeastVersion(majorVersion, minorVersion, patchVersion)[[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: (NSOperatingSystemVersion) {
    majorVersion,
    minorVersion,
    patchVersion
}]

if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
    //using new API
} else {
    //using deprecated API
}

3.2.2 兼容系統(tǒng)API迭代的新方案

需求:假設(shè)現(xiàn)在有一個(gè)利用新API寫好的類,如下所示其骄,其中有一行可能因?yàn)檫\(yùn)行在低版本系統(tǒng)(比如iOS9)導(dǎo)致奔潰的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor whiteColor]];
    self.title = @"Test3ViewController";
    
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, 375, 600) style:UITableViewStylePlain];
    tableView.delegate = self;
    tableView.dataSource = self;
    tableView.backgroundColor = [UIColor orangeColor];
    
    // May Crash Line
    tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
    
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
    [self.view addSubview:tableView];
}

其中有一行會(huì)發(fā)出警告亏镰,Xcode也給出了推薦解決方案,如果你點(diǎn)擊Fix它會(huì)自動(dòng)添加檢查系統(tǒng)版本的代碼拯爽,如下圖所示:


屏幕快照 2019-09-03 17.25.31.png

方案1:手動(dòng)加入版本判斷邏輯

以前的適配處理索抓,可根據(jù)操作系統(tǒng)版本進(jìn)行判斷

if (isOperatingSystemAtLeastVersion(11, 0, 0)) {
    scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else {
    viewController.automaticallyAdjustsScrollViewInsets = NO;
}

方案2:消息轉(zhuǎn)發(fā)

在iOS11 Base SDK直接采取最新的API并且配合Runtime的消息轉(zhuǎn)發(fā)機(jī)制就能實(shí)現(xiàn)一行代碼在不同版本操作系統(tǒng)下采取不同的消息調(diào)用方式

  • UIScrollView+Forwarding.m
#import "UIScrollView+Forwarding.h"
#import "NSObject+AdapterViewController.h"

@implementation UIScrollView (Forwarding)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { // 1
    
    NSMethodSignature *signature = nil;
    if (aSelector == @selector(setContentInsetAdjustmentBehavior:)) {
        signature = [UIViewController instanceMethodSignatureForSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
    }else {
        signature = [super methodSignatureForSelector:aSelector];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation { // 2
    
    BOOL automaticallyAdjustsScrollViewInsets  = NO;
    UIViewController *topmostViewController = [self cm_topmostViewController];
    NSInvocation *viewControllerInvocation = [NSInvocation invocationWithMethodSignature:anInvocation.methodSignature]; // 3
    [viewControllerInvocation setTarget:topmostViewController];
    [viewControllerInvocation setSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)];
    [viewControllerInvocation setArgument:&automaticallyAdjustsScrollViewInsets atIndex:2]; // 4
    [viewControllerInvocation invokeWithTarget:topmostViewController]; // 5
}

@end

  • NSObject+AdapterViewController.m
#import "NSObject+AdapterViewController.h"

@implementation NSObject (AdapterViewController)

- (UIViewController *)cm_topmostViewController {
    UIViewController *resultVC;
    resultVC = [self cm_topViewController:[[UIApplication sharedApplication].keyWindow rootViewController]];
    while (resultVC.presentedViewController) {
        resultVC = [self cm_topViewController:resultVC.presentedViewController];
    }
    return resultVC;
}

- (UIViewController *)cm_topViewController:(UIViewController *)vc {
    if ([vc isKindOfClass:[UINavigationController class]]) {
        return [self cm_topViewController:[(UINavigationController *)vc topViewController]];
    } else if ([vc isKindOfClass:[UITabBarController class]]) {
        return [self cm_topViewController:[(UITabBarController *)vc selectedViewController]];
    } else {
        return vc;
    }
}

@end

當(dāng)我們?cè)趇OS10調(diào)用新API時(shí),由于沒有具體對(duì)應(yīng)API實(shí)現(xiàn)毯炮,我們將其原有的消息轉(zhuǎn)發(fā)至當(dāng)前棧頂U(kuò)IViewController去調(diào)用低版本API逼肯。
關(guān)于[self cm_topmostViewController];,執(zhí)行之后得到的結(jié)果可以查看如下:


屏幕快照 2019-09-03 17.28.03.png

方案2的整體流程:

  1. 為即將轉(zhuǎn)發(fā)的消息返回一個(gè)對(duì)應(yīng)的方法簽名(該簽名后面用于對(duì)轉(zhuǎn)發(fā)消息對(duì)象(NSInvocation *)anInvocation進(jìn)行編碼用)
  2. 開始消息轉(zhuǎn)發(fā)((NSInvocation *)anInvocation封裝了原有消息的調(diào)用桃煎,包括了方法名篮幢,方法參數(shù)等)
  3. 由于轉(zhuǎn)發(fā)調(diào)用的API與原始調(diào)用的API不同,這里我們新建一個(gè)用于消息調(diào)用的NSInvocation對(duì)象viewControllerInvocation并配置好對(duì)應(yīng)的target與selector
  4. 配置所需參數(shù):由于每個(gè)方法實(shí)際是默認(rèn)自帶兩個(gè)參數(shù)的:self和_cmd为迈,所以我們要配置其他參數(shù)時(shí)是從第三個(gè)參數(shù)開始配置
  5. 消息轉(zhuǎn)發(fā)

4. 總結(jié)

4.1 模擬多繼承

面試挖坑:OC是否支持多繼承三椿?好,你說不支持多繼承葫辐,那你有沒有模擬多繼承特性的辦法搜锰?

轉(zhuǎn)發(fā)和繼承相似,可用于為OC編程添加一些多繼承的效果耿战,一個(gè)對(duì)象把消息轉(zhuǎn)發(fā)出去蛋叼,就好像他把另一個(gè)對(duì)象中放法接過來或者“繼承”一樣。消息轉(zhuǎn)發(fā)彌補(bǔ)了objc不支持多繼承的性質(zhì),也避免了因?yàn)槎嗬^承導(dǎo)致單個(gè)類變得臃腫復(fù)雜狈涮。
雖然轉(zhuǎn)發(fā)可以實(shí)現(xiàn)繼承功能狐胎,但是NSObject還是必須表面上很嚴(yán)謹(jǐn),像respondsToSelector:和isKindOfClass:這類方法只會(huì)考慮繼承體系歌馍,不會(huì)考慮轉(zhuǎn)發(fā)鏈握巢。

Objective-C 中給一個(gè)對(duì)象發(fā)送消息會(huì)經(jīng)過以下幾個(gè)步驟:

  1. 在對(duì)象類的 dispatch table 中嘗試找到該消息。如果找到了骆姐,跳到相應(yīng)的函數(shù)IMP去執(zhí)行實(shí)現(xiàn)代碼镜粤;
  2. 如果沒有找到,Runtime 會(huì)發(fā)送 +resolveInstanceMethod: 或者 +resolveClassMethod: 嘗試去 resolve 這個(gè)消息玻褪;
  3. 如果 resolve 方法返回 NO肉渴,Runtime 就發(fā)送 -forwardingTargetForSelector: 允許你把這個(gè)消息轉(zhuǎn)發(fā)給另一個(gè)對(duì)象;
  4. 如果沒有新的目標(biāo)對(duì)象返回带射, Runtime 就會(huì)發(fā)送-methodSignatureForSelector: 和 -forwardInvocation: 消息同规。你可以發(fā)送 -invokeWithTarget: 消息來手動(dòng)轉(zhuǎn)發(fā)消息或者發(fā)送 -doesNotRecognizeSelector: 拋出異常。

本文Demo傳送門: MessageForwardingDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窟社,一起剝皮案震驚了整個(gè)濱河市券勺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灿里,老刑警劉巖关炼,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異匣吊,居然都是意外死亡儒拂,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門色鸳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來社痛,“玉大人,你說我怎么就攤上這事。” “怎么了回怜?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)包帚。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任统倒,我火速辦了婚禮,結(jié)果婚禮上氛雪,老公的妹妹穿的比我還像新娘。我一直安慰自己耸成,他們只是感情好报亩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布浴鸿。 她就那樣靜靜地躺著,像睡著了一般弦追。 火紅的嫁衣襯著肌膚如雪岳链。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天劲件,我揣著相機(jī)與錄音掸哑,去河邊找鬼。 笑死零远,一個(gè)胖子當(dāng)著我的面吹牛苗分,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播牵辣,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼摔癣,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了纬向?” 一聲冷哼從身側(cè)響起择浊,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逾条,沒想到半個(gè)月后琢岩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡师脂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年担孔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片危彩。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡攒磨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汤徽,到底是詐尸還是另有隱情娩缰,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布谒府,位于F島的核電站拼坎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏完疫。R本人自食惡果不足惜泰鸡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壳鹤。 院中可真熱鬧盛龄,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至匿值,卻和暖如春赠制,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挟憔。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工钟些, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绊谭。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓政恍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親龙誊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抚垃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 本文轉(zhuǎn)載自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex閱讀 758評(píng)論 0 1
  • 本文詳細(xì)整理了 Cocoa 的 Runtime 系統(tǒng)的知識(shí),它使得 Objective-C 如虎添翼趟大,具備了靈活的...
    lylaut閱讀 800評(píng)論 0 4
  • 文中的實(shí)驗(yàn)代碼我放在了這個(gè)項(xiàng)目中鹤树。 以下內(nèi)容是我通過整理[這篇博客] (http://yulingtianxia....
    茗涙閱讀 923評(píng)論 0 6
  • 我們常常會(huì)聽說 Objective-C 是一門動(dòng)態(tài)語言,那么這個(gè)「動(dòng)態(tài)」表現(xiàn)在哪呢逊朽?我想最主要的表現(xiàn)就是 Obje...
    Ethan_Struggle閱讀 2,192評(píng)論 0 7
  • 繼上Runtime梳理(四) 通過前面的學(xué)習(xí)罕伯,我們了解到Objective-C的動(dòng)態(tài)特性:Objective-C不...
    小名一峰閱讀 752評(píng)論 0 3