OC-runtime簡述

Runtime是什么

在C語言中,將代碼轉(zhuǎn)換成為可執(zhí)行文件,一般要經(jīng)歷三個步驟:
編譯--->鏈接--->運行
在鏈接的時候,對象的類型位谋、方法的實現(xiàn)已經(jīng)確認(rèn)好了.
而在OC中,卻將一下在編譯和鏈接所處理的工作放在了運行階段,也就是說, 一個編譯好的.ipa包,在程序沒有運行的時候,誰也不知道調(diào)用一個方法會發(fā)生什么,這也為熱修復(fù)提供了可能, 因此OC是一門動態(tài)語言.
這樣的設(shè)計使Objective-C變得靈活,我們可以在程序運行的時候,動態(tài)的修改一個方法的實現(xiàn),這個黑魔法就是runtime.
runtime就是iOS的一個庫,這個庫使我們在程序運行時,創(chuàng)建對象踪央、檢查對象、修改類和對象的方法.

Runtime是怎么工作的

首先要知道類和對象在OC 中是怎么定義的?

Class(類)的定義

在objc.h中,Class被定義為指向objc_class的指針定義如下:

typedef struct objc_class  *Class;

而objc_class是一個結(jié)構(gòu)體,在runtime 中定義如下:

struct objc_class {
    Class isa;                                // 實現(xiàn)方法調(diào)用的關(guān)鍵
    Class super_class;                        // 父類
    const char * name;                        // 類名
    long version;                             // 類的版本信息灾前,默認(rèn)為0
    long info;                                // 類信息,供運行期使用的一些位標(biāo)識
    long instance_size;                       // 該類的實例變量大小
    struct objc_ivar_list * ivars;            // 該類的成員變量鏈表
    struct objc_method_list ** methodLists;   // 方法定義的鏈表
    struct objc_cache * cache;                // 方法緩存
    struct objc_protocol_list * protocols;    // 協(xié)議鏈表
};

提示:在 Xcode 中,使用快捷鍵 command + shift + o,可以打開搜索窗口,輸入 objc_class 即可看到頭文件定義

可以看到一個類中保存了自身所有的:
成員變量(ivers)
所有方法(methodLists)
所有實現(xiàn)的協(xié)議(objc_protocol_list)
isa(指向指針)
cache(緩存)
...

OC中對象(objc)的定義

id 被定義為指向objc_object 的結(jié)構(gòu)體指針,說明objc_object就是用到的對象的定義.

typedef struct objc_object *id

在看objc_object 的定義, objc_object其實是一個結(jié)構(gòu)體, 只包含一個isa 指針, 而objc_object結(jié)構(gòu)體中唯一保存的就是Class 的isa(地址)

struct objc_object {
       Class isa;
}

總結(jié): 當(dāng)一個對象調(diào)用方法時, 對象會通過自身的isa 尋找對應(yīng)的objc_class, 然后在objc_class 結(jié)構(gòu)體methodLists找到調(diào)用的方法;

再說cache 存炮,因為調(diào)用方法的過程是個查找 methodLists 的過程撼玄,如果每次調(diào)用都去查找夺姑,效率會非常低。所以對于調(diào)用過的方法掌猛,會以 map 的方式保存在 cache 中盏浙,下次再調(diào)用就會快很多。

Meta Class 元類

調(diào)用一個對象的類方法的過程是怎么樣的?還有 objc_class 中也有一個 isa 指針废膘,它是干嘛用的竹海?

其實觀察 objc_class 和 objc_object 的定義,會發(fā)現(xiàn)兩者其實本質(zhì)相同(都包含 isa 指針)丐黄,只是 objc_class 多了一些額外的字段,相應(yīng)的,類也是一個對象,只是保存了一些字段.

既然說類也是對象,那么類的類型是什么呢?這里就引出了另外一個概念 —— Meta Class(元類).

在 Objective-C 中斋配,每一個類都有對應(yīng)的元類。而在元類的 methodLists 中灌闺,保存了類的方法鏈表许起,即所謂的「類方法」。并且類的 isa 指針指向?qū)?yīng)的元類菩鲜。因此上面的問題答案就呼之欲出园细,調(diào)用一個對象的類方法的過程如下:

  1. 通過對象的isa 指針找到對應(yīng)的Class;
    2.通過類的isa 找到Class對應(yīng)的 Meta Class;
  2. 在Meta Class 的methodLists, 找到對應(yīng)的方法, 然后執(zhí)行;

那么Meta Class(元類)也應(yīng)該是一個對象, OC 的設(shè)計者讓所有的元類的isa 指向基類的元類, 而基類的元類指向自己;

OC Method(方法調(diào)用)

Method 的定義:

// Method 被定義為一個 objc_method 指針
typedef struct objc_method *Method;

在看結(jié)構(gòu)體obje_method 怎么定義的:

##### objc_method
//在 objc_method 結(jié)構(gòu)體中,包含一個 SEL 和一個 IMP
struct objc_method {
       SEL method_name;
       char *method_types;
       IMP method_imp;
}
SEL定義

SEL 是一個指向 objc_selector 的指針接校,而 objc_selector 在頭文件中找不到明確的定義

typedef struct objc_selector *SEL;

我們來測試以下代碼:

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);          // 輸出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1);         // 輸出:viewDidLoad1

可以看到,SEL 不過是保存了方法名的一串字符,因此我們可以認(rèn)為,SEL 就是一個保存方法名的字符串.

由于一個 Method 只保存了方法的方法名猛频,并最終要根據(jù)方法名來查找方法的實現(xiàn),因此在Objective-C 中不支持下面這種定義.

-(void)setWidth:(int)width;
-(void)setWidth:(double)width;
IMP定義
typedef id (*IMP)(id, SEL, ...);

再來說 IMP ≈朊悖可以看到它是一個「函數(shù)指針.簡單來說鹿寻,「函數(shù)指針」就是用來找到函數(shù)地址,然后執(zhí)行函數(shù).
這里要注意诽凌, IMP 指向的函數(shù)的前兩個參數(shù)是默認(rèn)參數(shù)毡熏, id 和 SEL 。這里的 SEL 好理解侣诵,就是函數(shù)名,而 id 痢法,對于實例方法來說, self 保存了當(dāng)前對象的地址杜顺;對于類方法來說财搁, self 保存了當(dāng)前對應(yīng)類對象的地址,后面的省略號即是參數(shù)列表。

敲黑板: Method 建立了 SEL 和 IMP 的關(guān)聯(lián)躬络,當(dāng)對一個對象發(fā)送消息時尖奔,會通過給出的 SEL 去找到 IMP ,然后執(zhí)行.

在 Objective-C 中穷当,所有的方法調(diào)用提茁,都會轉(zhuǎn)化成向?qū)ο蟀l(fā)送消息。發(fā)送消息主要是使用 objc_msgSend 函數(shù); 看一下頭文件定義:

id objc_msgSend(id self, SEL op, ...);
// 譬如: 
[self loadView]; 
// 調(diào)用時就會變成:
objc_msgSend(self, @selector(loadView));

當(dāng)向一個對象發(fā)送消息時馁菜,會去這個類的 methodLists 中查找相應(yīng)的 SEL 茴扁,如果查不到,則通過 super_class 指針找到父類火邓,再去父類的 methodLists 中查找丹弱,層層遞進(jìn)。最后仍然找不到铲咨,才走拋異常流程;

當(dāng)一個方法找不到的時候躲胳,會走攔截調(diào)用和消息轉(zhuǎn)發(fā)流程。我們可以重寫 +resolveClassMethod: 和 +resolveInstanceMethod: 方法纤勒,在程序崩潰前做一些處理坯苹。通常的做法是動態(tài)添加一個方法,并返回 YES 告訴程序已經(jīng)成功處理消息摇天。如果這兩個方法返回 NO 粹湃,這個流程會繼續(xù)往下走.

Category(分類)

我們來看一下 Category 在頭文件中的定義:

typedef struct objc_category *Category;
struct objc_category {
    char * category_name;
    char * class_name;
    struct objc_method_list * instance_methods;
    struct objc_method_list * class_methods;
    struct objc_protocol_list * protocols;
}

Category 是一個指向 objc_category 結(jié)構(gòu)體的指針乾吻,在 objc_category 中包含對象方法列表铣缠、類方法列表崇棠、協(xié)議列表撒汉。從這里我們也可以看出蟋字, Category 支持添加對象方法承粤、類方法择葡、協(xié)議肴茄,但不能保存成員變量纯丸。

注意:在 Category 中是可以添加屬性的偏形,但不會生成對應(yīng)的成員變量、 getter 和 setter 觉鼻。因此俊扭,調(diào)用 Category 中聲明的屬性時會報錯。

我們可以通過「關(guān)聯(lián)對象」的方式來添加可用的屬性坠陈。具體操作如下:
1萨惑、在 UIViewController+Tag.h 文件中聲明 property 。

@property (nonatomic, strong) NSString *tag;

2仇矾、在 UIViewController+Tag.m 中實現(xiàn) getter 和 setter 咒钟。記得添加頭文件 #import。主要是用到 objc_setAssociatedObject 和 objc_getAssociatedObject 這兩個方法若未。

static void *tag = &tag;
@implementation UIViewController (Tag)
- (void)setTag:(NSString *)t {
    objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tag {
    return objc_getAssociatedObject(self, &tag);
}
@end

3朱嘴、在子類中調(diào)用。

// 子類 ViewController.m
- (void)testCategroy {
    self.tag = @"TAG";
    NSLog(@"%@", self.tag);   // 這里輸出:TAG
}

注意:當(dāng)一個對象被釋放后粗合, Runtime 回去查找這個對象是否有關(guān)聯(lián)的對象萍嬉,有的話,會將它們釋放掉隙疚。因此不需要我們手動去釋放壤追。

Runtime 常規(guī)操作

1. Method Swizzling 方法交換

首先來介紹一下被稱為「黑魔法」的 Method Swizzling,Method Swizzling 使我們有辦法在程序運行的時候,去修改一個方法的實現(xiàn)供屉。包括原生類(比如 UIKit 中的類)的方法行冰。首先來看下通常的寫法:

Method originalMethod = class_getInstanceMethod(class, (originalSelector));
Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector));
if (!class_addMethod((class),                                               
                     (originalSelector),                                 
                     method_getImplementation(swizzledMethod),  
                     method_getTypeEncoding(swizzledMethod))) {             
    method_exchangeImplementations(originalMethod, swizzledMethod);         
} else {                                                                    
    class_replaceMethod((class),                                            
                        (swizzledSelector),                                 
                        method_getImplementation(originalMethod),           
                        method_getTypeEncoding(originalMethod));            
}

簡單描述一下:先獲取 originalMethod 和 swizzledMethod 溺蕉。將 originalMethod 加到想要交換方法的類中(注意此時的 IMP 是 swizzledMethod 的 IMP ),如果加入成功悼做,就用 originalMethod 的 IMP 替換掉 swizzledMethod 的 IMP 疯特;如果加入失敗,則直接交換 originalMethod 和 swizzledMethod 的 IMP 肛走。
那么問題來了漓雅,為什么不直接用 method_exchangeImplementations 來交換就好?
因為可能會影響父類中的方法朽色。比如我們在一個子類中邻吞,去交換一個父類中的方法,而這個方法在子類中沒有實現(xiàn)葫男,這個時候父類的方法就指向了子類的實現(xiàn)抱冷,當(dāng)這個方法被調(diào)用的時候就會出問題。所以先采取添加方法的方式梢褐,如果添加失敗徘层,證明子類已經(jīng)實現(xiàn)了這個方法,直接用 method_exchangeImplementations 來交換利职;如果添加成功趣效,則說明沒有實現(xiàn)這個方法,采取先添加后替換的方式猪贪。這樣就能保證不影響父類了跷敬。
如果每次交換都寫這么多就太麻煩了,我們可以定義成一個宏热押,使用起來更方便西傀。

#define SwizzleMethod(class, originalSelector, swizzledSelector) {              \
    Method originalMethod = class_getInstanceMethod(class, (originalSelector)); \
    Method swizzledMethod = class_getInstanceMethod(class, (swizzledSelector)); \
    if (!class_addMethod((class),                                               \
                         (originalSelector),                                    \
                         method_getImplementation(swizzledMethod),              \
                         method_getTypeEncoding(swizzledMethod))) {             \
        method_exchangeImplementations(originalMethod, swizzledMethod);         \
    } else {                                                                    \
        class_replaceMethod((class),                                            \
                            (swizzledSelector),                                 \
                            method_getImplementation(originalMethod),           \
                            method_getTypeEncoding(originalMethod));            \
    }                                                                           \
}

在 +load 中調(diào)用:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));        
    });
}

注意:我們要保證方法只會被交換一次。因為 +load 方法原則上只會被調(diào)用一次桶癣,所以一般將 Method Swizzling 放在 +load 方法中執(zhí)行拥褂。但 +load 方法也可能被其他類手動調(diào)用,這時候就有可能會被交換多次牙寞,所以這里用 dispatch_once_t 來保證只執(zhí)行一次饺鹃。
那么上面的交換操作是否萬無一失了呢?還遠(yuǎn)遠(yuǎn)不夠间雀。
通常情況下上面的交換不會出什么問題悔详,但考慮下面一種場景。(注: ViewController 繼承自 UIViewController )
修改 UIViewController 中的 viewWillAppear: :

// UIViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(AA_viewWillAppear:));        
    });
}
- (void)AA_viewWillAppear:(BOOL)animated {
    NSLog(@"UIViewController");
    [self AA_viewWillAppear:animated];
}

修改 ViewController 中的 viewWillAppear: (注: ViewController 沒有重寫該方法):

// ViewController (Swizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class], @selector(viewWillAppear:), @selector(BB_viewWillAppear:));        
    });

}
- (void)BB_viewWillAppear:(BOOL)animated {
    NSLog(@"ViewController");
    [self BB_viewWillAppear:animated];
}

這里父類和子類同時對 viewWillAppear: 方法進(jìn)行交換惹挟,每次交換都加入一句輸出語句茄螃。則當(dāng) ViewController 調(diào)用 viewWillAppear: 時,我們期望輸出下面結(jié)果:
ViewController
UIViewController
大部分情況的確是這樣连锯,但也有可能只輸出:
ViewController
因為我們是在 +load 中做交換操作归苍,而子類的 +load 卻有可能先于父類執(zhí)行用狱。這樣造成的結(jié)果是,子類先拷貝父類的 viewWillAppear: 拼弃,并進(jìn)行交換夏伊,然后父類再進(jìn)行交換。但這個時候父類的交換結(jié)果并不會影響子類肴敛,也無法將 NSLog(@"UIViewController") 寫入子類的 viewWillAppear: 方法中,所以不會輸出吗购。
這里解決這個問題的思路是:在子類的 swizzledMethod 中医男,動態(tài)地去查找父類替換后方法的實現(xiàn)。每次調(diào)用都會去父類重新查找捻勉,而不是拷貝寫死在子類的新方法中镀梭。這樣子類 viewWillAppear: 方法的執(zhí)行結(jié)果就和 +load 的加載順序無關(guān)了。
至于怎么實現(xiàn)動態(tài)查找踱启,這里推薦 RSSwizzle 报账,這個庫不僅解決了上面提到的問題,還保證了 Method Swizzling 的線程安全埠偿,是一種更安全優(yōu)雅的解決方案透罢。簡單使用舉例:

RSSwizzleInstanceMethod([self class],
                        @selector(viewWillAppear:),
                        RSSWReturnType(void),
                        RSSWArguments(BOOL animated),
                        RSSWReplacement({
    NSLog(@"ViewController");
    RSSWCallOriginal(animated);
}), RSSwizzleModeAlways, NULL);
2.獲取所有屬性和方法

Runtime 中提供了一系列 API 來獲取 Class 的成員變量( Ivar )、屬性( Property )冠蒋、方法( Method )羽圃、協(xié)議( Protocol )等.

// 測試 打印屬性列表
- (void)testPrintPropertyList {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
}
// 測試 打印方法列表
- (void)testPrintMethodList {
    unsigned int count;
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i=0; i
        Method method = methodList[i];
        NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
    }
    free(methodList);
}
// 測試 打印成員變量列表
- (void)testPrintIvarList {
    unsigned int count;
    Ivar *ivarList = class_copyIvarList([self class], &count);
    for (unsigned int i=0; i
        Ivar myIvar = ivarList[i];
        const char *ivarName = ivar_getName(myIvar);
        NSLog(@"ivar----="">%@", [NSString stringWithUTF8String:ivarName]);
    }
    free(ivarList);
}
// 測試 打印協(xié)議列表
- (void)testPrintProtocolList {
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
    for (unsigned int i=0; i
        Protocol *myProtocal = protocolList[i];
        const char *protocolName = protocol_getName(myProtocal);
        NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
    }
    free(protocolList);
}
RunTime 的使用場景
1.面向切面編程(AOP)

AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程抖剿,通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)朽寞。AOP是OOP的延續(xù),是軟件開發(fā)中的一個熱點斩郎,也是Spring框架中的一個重要內(nèi)容脑融,是函數(shù)式編程的一種衍生范型。利用AOP可以對業(yè)務(wù)邏輯的各個部分進(jìn)行隔離缩宜,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低肘迎,提高程序的可重用性,同時提高了開發(fā)的效率.

畫重點锻煌,對業(yè)務(wù)邏輯進(jìn)行分離膜宋,降低耦合度。
假設(shè)現(xiàn)在有這樣一個需求炼幔,我們要對應(yīng)用中所有按鈕的點擊事件進(jìn)行上報秋茫,統(tǒng)計每個按鈕被點擊的次數(shù)。
首先我們要明確乃秀,統(tǒng)計功能應(yīng)該與業(yè)務(wù)無關(guān)肛著,即統(tǒng)計代碼不應(yīng)該與業(yè)務(wù)代碼耦合在一起圆兵。因此用上面「AOP」的思想來實現(xiàn)是合適的,而 Runtime 給我們提供了這樣一條途徑枢贿。因為當(dāng)按鈕點擊時殉农,會調(diào)用 sendAction:to:forEvent: 方法,所以我們可以使用 Method Swizzling 來修改該方法局荚,在其中添加上報的邏輯.

// UIButton+Swizzling.m
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        RSSwizzleInstanceMethod([self class],
                                @selector(sendAction:to:forEvent:),
                                RSSWReturnType(void),
                                RSSWArguments(SEL action, id target, UIEvent *event),
                                RSSWReplacement({
            NSString *name = NSStringFromClass([self class]);
            NSLog(@"UIButton+Swizzling:%@ 按鈕被點擊--上報", name);
            RSSWCallOriginal(action, target, event);
        }), RSSwizzleModeAlways, NULL);
    });
}

注意:盡管上面的需求也可以用繼承一個基類的方式來實現(xiàn)超凳,但是如果此時已經(jīng)有很多類繼承自 UIButton ,則修改起來會很麻煩耀态,其次我們也不能保證后續(xù)的所有按鈕都繼承這個基類轮傍。另外上面提到,統(tǒng)計邏輯不應(yīng)該和業(yè)務(wù)邏輯耦合首装,如果為了統(tǒng)計的需求去修改業(yè)務(wù)代碼创夜,也是不可取的(除非迫不得已)。因此上面利用 Method Swizzling 的方式更為合適仙逻,也更為簡潔驰吓。

2.dictionary 轉(zhuǎn)model

我們可以用 KVC 來實現(xiàn)字典轉(zhuǎn)模型,方法是調(diào)用 setValuesForKeysWithDictionary: 系奉。但這種方法要求 Model 的屬性和 NSDictionary 的 key 一一對應(yīng)檬贰,否則就會報錯。這里可以用 Runtime 配合 KVC 缺亮,來實現(xiàn)更靈活的字典轉(zhuǎn)模型偎蘸。

下面為 NSObject 添加一個分類,添加一個初始化方法瞬内,來看代碼:

// NSObject+JSONExtension.h
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
    self = [self init];
    if (self) {
        unsigned int count;
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i=0; i
            // 獲取屬性列表
            const char *propertyName = property_getName(propertyList[i]);
            NSString *name = [NSString stringWithUTF8String:propertyName];
            id value = [dictionary objectForKey:name];
            if (value) {
                // 注意這里用到 KVC
                [self setValue:value forKey:name];
            }
        }
        free(propertyList);
    }
    return self;
}

嘗試調(diào)用:

NSDictionary *info = @{@"title": @"標(biāo)題", @"count": @(1), @"test": @"hello"};
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
NSLog(@"%@", objectA.title);     // 輸出:標(biāo)題
NSLog(@"%ld", (long)objectA.count);         // 輸出:1

注意:在實際的應(yīng)用中迷雪,會有更多復(fù)雜的情況需要考慮,比如字典中包含數(shù)組虫蝶、對象等章咧。這里只是做個簡單示例.

3. 進(jìn)行歸解檔

「歸檔」是將對象序列化存入沙盒文件的過程,會調(diào)用 encodeWithCoder: 來序列化能真×扪希「解檔」是將沙盒文件中的數(shù)據(jù)反序列化讀入內(nèi)存的過程,會調(diào)用 initWithCoder: 來反序列化粉铐。

通常來說疼约,歸解檔需要對實例對象的各個屬性依次進(jìn)行歸檔和解檔,十分繁瑣且易出錯蝙泼。這里我們參照「字典轉(zhuǎn)模型」的例子程剥,通過獲取類的所有屬性,實現(xiàn)自動歸解檔汤踏。
觸發(fā)對象歸檔可以調(diào)用 NSKeyedArchiver 的 + archiveRootObject:toFile: 方法织鲸;觸發(fā)對象解檔可以調(diào)用 NSKeyedUnarchiver 的 + unarchiveObjectWithFile: 方法舔腾。
注: xib 文件在載入的時候,也會觸發(fā) initWithCoder: 方法搂擦,可見讀取 xib 文件也是一個解檔的過程稳诚。
首先在 NSObject 的分類中添加兩個方法:

// NSObject+JSONExtension.m
- (void)initAllPropertiesWithCoder:(NSCoder *)coder {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        id value = [coder decodeObjectForKey:name];
        [self setValue:value forKey:name];
    }
    free(propertyList);
}
- (void)encodeAllPropertiesWithCoder:(NSCoder *)coder {
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    for (unsigned int i=0; i
        const char *propertyName = property_getName(propertyList[i]);
        NSString *name = [NSString stringWithUTF8String:propertyName];
        id value = [self valueForKey:name];
        [coder encodeObject:value forKey:name];
    }
    free(propertyList);
}

在 NSObject 的子類中實現(xiàn)歸解檔方法:

// ObjectA.m
- (id)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        [self initAllPropertiesWithCoder:aDecoder];
    }
    return self;
}
-(void)encodeWithCoder:(NSCoder *)aCoder{
    [self encodeAllPropertiesWithCoder:aCoder];
}

嘗試調(diào)用:

NSDictionary *info = @{@"title": @"標(biāo)題11", @"count": @(11)};
NSString *path = [NSString stringWithFormat:@"%@/objectA.plist", NSHomeDirectory()];
// 歸檔
ObjectA *objectA = [[ObjectA alloc] initWithDictionary:info];
[NSKeyedArchiver archiveRootObject:objectA toFile:path];
// 解檔
ObjectA *objectB = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
NSLog(@"%@", objectB.title);      // 輸出:標(biāo)題11
NSLog(@"%ld", (long)objectB.count);       // 輸出:11

注:上面的代碼邏輯并不完善,只是做簡單示例用瀑踢。

4. 逆向開發(fā)

在「逆向開發(fā)」中扳还,會用到一個叫 class-dump 的工具。這個工具可以將已脫殼的 APP 的所有類的頭文件導(dǎo)出橱夭,為分析 APP 做準(zhǔn)備氨距。這里也是利用 Runtime 的特性,將存儲在mach-O文件中的 @interface 和 @protocol 信息提取出來徘钥,并生成對應(yīng)的 .h 文件衔蹲。

5. 熱修復(fù)

「熱修復(fù)」是一種不需要發(fā)布新版本肢娘,通過動態(tài)下發(fā)修復(fù)文件來修復(fù) Bug 的方式呈础。比如 JSPatch,就是利用 Runtime 強大的動態(tài)能力橱健,對出問題的代碼段進(jìn)行替換而钞。

源碼

請到 GitHub 上查看完整例子。

參考

iOS運行時(Runtime)詳解+Demo

OC最實用的runtime總結(jié)拘荡,面試臼节、工作你看我就足夠了!

iOS Runtime 詳解

Method swizzling的正確姿勢

Method Swizzling 安全性分析 和 RSSwizzle解決方案分析

【南峰子的技術(shù)博客】Objective-C Runtime 運行時系列

runtime 面試題
1. [self class]和[super class]返回的分別是什么珊皿?

[self class]會被編譯器轉(zhuǎn)化成 objc_msgSend(self, @selector(class))
[super class]會被編譯器轉(zhuǎn)化成 objc_msgSendSuper(super, @selector(class)).
super在這里只是一個結(jié)構(gòu)體(objc_super)网缝,objc_super結(jié)構(gòu)體重有一個 receiver的變量,也就是方法的接收者蟋定,這里的接收者也是當(dāng)前對象self粉臊。
所以這兩個消息傳遞的接收者都是當(dāng)前對象self,兩個方法的運行結(jié)果也就相同驶兜,都會返回當(dāng)前對象的class扼仲。

2. 講一下對象、類對象抄淑、元類跟元類的結(jié)構(gòu)體是如何關(guān)聯(lián)的屠凶?

1). 所有的OC對象的基礎(chǔ)結(jié)構(gòu)體都是objc_object,而類和元類的結(jié)構(gòu)體objc_class也是繼承于objc_object的肆资,所以類和元類其實也是一種“對象”.
2). objc_object有isa指針矗愧,對象、類對象郑原、元類對象正是通過isa指針相連.

3. 為什么對象方法沒有保存到對象的結(jié)構(gòu)體里贱枣,而是保存在類對象的結(jié)構(gòu)體里监署?

1).假如保存在對象中,當(dāng)我已經(jīng)創(chuàng)建一個對象后纽哥,再為這個類動態(tài)的添加一個對象方法钠乏,這時由于已經(jīng)創(chuàng)建的對象的內(nèi)存布局已經(jīng)確定,也就沒辦法添加這個方法了春塌,這個對象就會有問題晓避。也就無法實現(xiàn)動態(tài)添加方法的功能。

2). 將對象方法保存在類對象類只壳,那么所有的對象方法只要在類對象中保存一份即可俏拱,而不需要再每個對象里都保存一份,會節(jié)省大量的內(nèi)存吼句。

3). 正是因為方法保存在類對象中锅必,而每次調(diào)用時通過isa指針找到類對象再找到方法調(diào)用,才保證了OC語言的動態(tài)特性惕艳,方法可以動態(tài)添加搞隐、修改,否則就不能實現(xiàn)動態(tài)的效果远搪。

4). 在方法的實際調(diào)用中有這樣的過程:先找緩存->在找當(dāng)前類中的對象方法列表->再逐級查找父類的對象方法列表劣纲,通過把常用的方法放入緩存中,及提高了訪問速度谁鳍,也節(jié)省了不必要的內(nèi)存開銷癞季。通過把對象方法放在父類才能很好的實現(xiàn)這個功能,放在對象中實現(xiàn)的話倘潜,就非常浪費內(nèi)存绷柒。

4.class_ro_t和class_rw_t的區(qū)別? PS: ro == readonly, rw == readwrite

1). class_ro_t是class_rw_t的一個變量涮因,用于保存宿主類的成員變量列表信息废睦、協(xié)議列表、對象方法列表蕊退、屬性列表信息(這三個列表都是一位數(shù)組)郊楣,這些是在類注冊到runtime中就固定了,無法再修改.
2). class_rw_t的成員變量除了class_ro_t之外瓤荔,還有methods净蚤、protocols、property三個二維數(shù)組输硝,之所以是二維數(shù)組是因為一個類可以有多個分類今瀑,而每個分類都可以有多個方法、協(xié)議、屬性橘荠,所以就是二維數(shù)組屿附。class_rw_t是可以動態(tài)修改的,當(dāng)運行時動態(tài)的給一個類添加一個方法時哥童,這個方法就保存在methods中挺份。

這里再說明一下,類在編譯時初始化時會創(chuàng)建class_ro_t結(jié)構(gòu)體贮懈,里邊的內(nèi)容都是只讀的匀泊,在運行時初始化會創(chuàng)建class_rw_t,將class_rw_t的ro指針指向之前創(chuàng)建的class_ro_t結(jié)構(gòu)體朵你,并將class_ro_t中的方法列表添加到class_rw_t的methodList中各聘,在之后進(jìn)行方法查找時就不需要再查class_ro_t了.

5. SEL和IMP的區(qū)別?

SEL相當(dāng)于是方法名抡医,IMP則是函數(shù)指針躲因,在類對象的方法緩存哈希表中,SEL和IMP相當(dāng)于對應(yīng)著key和value.

6. 能否向編譯后的類中增加實例變量忌傻?能否向運行時創(chuàng)建的類中添加實例變量大脉?為什么?

1). 不能芯勘,當(dāng)一個類已經(jīng)編譯好了箱靴,它就已經(jīng)注冊到runtime中腺逛,其對象的內(nèi)存布局已經(jīng)確定荷愕,無法再修改。假如能修改的話棍矛,當(dāng)我已經(jīng)創(chuàng)建了一個對象A安疗,這時這個A的內(nèi)存大小和布局已經(jīng)確定,如果這時動態(tài)向類A中增加一個Int型的變量够委,則意味著新的對象的內(nèi)存大小和布局都要改變荐类,而這個對象A已經(jīng)確定下來無法改變,也就成了廢對象茁帽,這會引起嚴(yán)重的后果玉罐。

2). 但在運行時創(chuàng)建的類還沒注冊到runtime中,可以為其添加成員變量潘拨,因為此時的內(nèi)存布局還未確定下來。

7. 在運行時創(chuàng)建類的方法objc_allocateClassPair的方法名尾部為什么是pair(成對的意思)铁追?

一個是類對象季蚂,一個是元類對象,所以是pair

8. 談一談對消息機(jī)制的理解?

1). runtime的核心就是消息機(jī)制,而runtime又是OC語言的核心扭屁。當(dāng)我們正常通過OC語言調(diào)用一個方法[objc func]時算谈,并不是直接調(diào)用其函數(shù)實現(xiàn),而是向objc對象發(fā)送了一個消息料滥。至于具體的方法實現(xiàn)是什么然眼,在編譯時是并不知道的,只有在運行時才能確定下來葵腹。這就是消息機(jī)制罪治,而通過這種消息的方式,則體現(xiàn)了OC語言的動態(tài)屬性礁蔗,使用起來更加靈活觉义。

2). 具體在調(diào)用[objc func]時,是在調(diào)用obj對象的名稱為func的對象方法浴井,對象方法是保存在類對象中的晒骇,所以obj會通過其isa指針找到其類對象,然后先查方法緩存中是否已經(jīng)有此方法(具體查找方法為哈希查找)磺浙。如果沒找到的話洪囤,就在類對象的對象方法列表中查找func方法(如果方法列表已經(jīng)排過序就按二分法查找,如果沒排序就按遍歷查找撕氧,并且以先查找分類的方法瘤缩,后查找宿主類的方法)。如果還沒有查找到伦泥,就逐級查找父類的方法緩存和方法列表剥啤。如果找到func方法,就把其IMP緩存到obj的類對象的方法緩存中不脯。如果到根類都沒找到的話府怯,就進(jìn)入到消息轉(zhuǎn)發(fā)流程。

9.轉(zhuǎn)發(fā)消息的流程?

1). 動態(tài)解析環(huán)節(jié):查看當(dāng)前類有沒有實現(xiàn)動態(tài)解析
+(BOOL)resolveInstanceMethod:(SEL)selector 或者
+(BOOL)resolveClassMethod:(SEL)selector 如果已經(jīng)實現(xiàn)就調(diào)用防楷。使用這個方法解決問題的前提是牺丙,相關(guān)方法的實現(xiàn)代碼已經(jīng)存在,只是要在運行時動態(tài)的插到類里面复局。此方法可以用來實現(xiàn)@dynamic(阻止編譯器自動生成類方法)

2). 快速轉(zhuǎn)發(fā)環(huán)節(jié):如果沒實現(xiàn)就進(jìn)入到快速轉(zhuǎn)發(fā)流程 - (id)forwardingTargetForSelector:(SEL)selector;將當(dāng)前selector消息轉(zhuǎn)發(fā)到其他對象上冲簿。

3). 完整轉(zhuǎn)發(fā)環(huán)節(jié):如果還沒實現(xiàn)則進(jìn)入到最終的轉(zhuǎn)發(fā)流程
3.1 先調(diào)用-(NSMethodSignature *)methodSignatureForSelector:(SEL)selector,根據(jù)selector返回一個方法簽名亿昏,這個簽名按規(guī)則包含了返回值類型峦剔、傳參類型(最常見的“v@:”,v表示返回值為void類型,@表示參數(shù)為id類型龙优,:表示參數(shù)為SEL)
3.2 再調(diào)用 -(void)forwardInvocation:(NSInvocation *)invocation;可以進(jìn)行消息轉(zhuǎn)發(fā)羊异。如果已經(jīng)重寫了此方法事秀,那么即使沒有實現(xiàn)轉(zhuǎn)發(fā),也不會崩潰了野舶,因此消息轉(zhuǎn)發(fā)也常用來兼容crash情況易迹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市平道,隨后出現(xiàn)的幾起案子睹欲,更是在濱河造成了極大的恐慌,老刑警劉巖一屋,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窘疮,死亡現(xiàn)場離奇詭異,居然都是意外死亡冀墨,警方通過查閱死者的電腦和手機(jī)闸衫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來诽嘉,“玉大人蔚出,你說我怎么就攤上這事〕嬉福” “怎么了骄酗?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長悦冀。 經(jīng)常有香客問我趋翻,道長,這世上最難降的妖魔是什么盒蟆? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任踏烙,我火速辦了婚禮,結(jié)果婚禮上茁影,老公的妹妹穿的比我還像新娘宙帝。我一直安慰自己丧凤,他們只是感情好募闲,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著愿待,像睡著了一般浩螺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仍侥,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天要出,我揣著相機(jī)與錄音,去河邊找鬼农渊。 笑死患蹂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播传于,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼囱挑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沼溜?” 一聲冷哼從身側(cè)響起平挑,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎系草,沒想到半個月后通熄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡找都,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年唇辨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片能耻。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡助泽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嚎京,到底是詐尸還是另有隱情嗡贺,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布鞍帝,位于F島的核電站诫睬,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帕涌。R本人自食惡果不足惜摄凡,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蚓曼。 院中可真熱鬧亲澡,春花似錦、人聲如沸纫版。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽其弊。三九已至癞己,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間梭伐,已是汗流浹背痹雅。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留糊识,地道東北人绩社。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓摔蓝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親愉耙。 傳聞我的和親對象是個殘疾皇子项鬼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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