iOS進(jìn)階之詳解Runtime運(yùn)行時(shí)機(jī)制

runtime簡(jiǎn)介

runtime簡(jiǎn)稱運(yùn)行時(shí),OC是運(yùn)行時(shí)機(jī)制,也就是在運(yùn)行時(shí)才做一些處理,是一套比較底層的純C語(yǔ)言API, 屬于1個(gè)C語(yǔ)言庫(kù), 包含了很多底層的C語(yǔ)言API。

在我們平時(shí)編寫的OC代碼中, 程序運(yùn)行過程時(shí), 最終都是轉(zhuǎn)成了runtime的C語(yǔ)言代碼。例如:C語(yǔ)言在編譯的時(shí)候就知道要調(diào)用哪個(gè)方法函數(shù),而OC在編譯的時(shí)候并不知道要調(diào)用哪個(gè)方法函數(shù),而是推遲到運(yùn)行的時(shí)候才知道調(diào)用的方法函數(shù)名稱,來(lái)找到對(duì)應(yīng)的方法函數(shù)進(jìn)行調(diào)用。

runtime消息傳遞流程

在OC語(yǔ)言中,任何類的定義都是對(duì)象。類和類的實(shí)例(對(duì)象)沒有任何本質(zhì)上的區(qū)別,任何對(duì)象都有isa指針。

一、isa指針

打開Runtime源碼,全局搜索isa_t,鎖定objc-private.h類,里面有這么一段代碼,isa指針是isa_t的實(shí)例。代碼如下:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      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;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

  • arm64之前,isa僅僅是一個(gè)指針,保存著對(duì)象或類對(duì)象內(nèi)存地址,在arm64架構(gòu)之后,apple對(duì)isa進(jìn)行了優(yōu)化,變成了一個(gè)聯(lián)合體union結(jié)構(gòu),同時(shí)使用位域來(lái)存儲(chǔ)更多的信息。

  • 對(duì)象是通過isa的bits進(jìn)行位運(yùn)算,取出響應(yīng)位置的值。runtime中的isa是被聯(lián)合體位域優(yōu)化過的,

  • 它不單單是指向類對(duì)象了,而是把64位中的每一位都運(yùn)用了起來(lái),其中的shiftcls為33位,代表了類對(duì)象的地址,其他的位都有各自的用處。

那么我來(lái)看看這個(gè)isa內(nèi)部元素代表的是什么含義?

  • nonpointer:表示是否對(duì)isa指針開啟指針優(yōu)化

    • 0:不開啟,表示純isa指針。
    • 1開啟,不單單是類對(duì)象的地址,isa中包含了類信息和對(duì)象的引用計(jì)數(shù)等。
  • has_assoc:表示是否有關(guān)聯(lián)對(duì)象標(biāo)識(shí)位

    • 0表示:沒有,沒有關(guān)聯(lián)對(duì)象會(huì)釋放的更快
    • 1表示:有
  • has_cxx_dtor:表示該對(duì)象是否有C++或者Objc的析構(gòu)器

    • 如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有,則可以更快的釋放對(duì)象
  • shiftcls:表示存儲(chǔ)類指針class的值。

    • 開啟指針優(yōu)化的情況下,在arm64 架構(gòu)中有33位用來(lái)存儲(chǔ)類指針。
  • magic:固定值為0xd2

    • 用于在調(diào)試時(shí)分辨對(duì)象是否完成初始化
  • weakly_referenced:表示對(duì)象是否被指向或者曾經(jīng)指向一個(gè) ARC 的弱引用變量。

    • 沒有弱引?用的對(duì)象可以更更快釋放。
  • deallocating:標(biāo)志對(duì)象是否正在釋放內(nèi)存

    • 0 表示沒有 1表示正在釋放內(nèi)存
  • has_sidetable_rc:表示當(dāng)對(duì)象的引用計(jì)數(shù)大于10,以至于無(wú)法存儲(chǔ)在isa指針中時(shí),用散列表去計(jì)數(shù)。

  • extra_rc:表示該對(duì)象的引用計(jì)數(shù),實(shí)際上是引用計(jì)數(shù)值減1

    • 例如如果對(duì)象的引用計(jì)數(shù)為10,那么extra_rc為9。如果引用計(jì)數(shù)?于10, 則需要使?到has_sidetable_rc

二、類對(duì)象(objc_class)

在OC中的類是用Class類型來(lái)表示的,它實(shí)際上是一個(gè)指向objc_class結(jié)構(gòu)體的指針。

typedef struct objc_class *Class;

查看objc/runtime.h中objc_class結(jié)構(gòu)體的定義如下:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

struct objc_class結(jié)構(gòu)體定義了很多變量如下:

  • Class isa : 實(shí)現(xiàn)方法調(diào)用的關(guān)鍵

  • Class super_class : 父類

  • const char * name : 類名

  • long version : 類的版本信息,默認(rèn)為0

  • long info : 類信息,供運(yùn)行期使用的一些位標(biāo)識(shí)

  • long instance_size : 該類的實(shí)例變量大小

  • struct objc_ivar_list * ivars : 該類的成員變量鏈表

  • struct objc_method_list ** methodLists : 方法定義的鏈表

  • struct objc_cache * cache : 方法緩存

  • struct objc_protocol_list * protocols : 協(xié)議鏈表

objc_class結(jié)構(gòu)體的第一個(gè)成員變量也是isa指針,這就說明了Class本身其實(shí)也是一個(gè)對(duì)象,因此我們稱之為類對(duì)象,類對(duì)象在編譯期產(chǎn)生用于創(chuàng)建實(shí)例對(duì)象,是單例。

三、實(shí)例(objc_object)

實(shí)例對(duì)象的isa指針指向類對(duì)象,類對(duì)象的isa指針指向元類。類對(duì)象和元類的結(jié)構(gòu)都是objc_class類型。

typedef struct objc_object *id;

struct objc_object {
    /*
     *實(shí)例對(duì)象的isa指針指向類對(duì)象
     *實(shí)質(zhì)就是指向objc_class結(jié)構(gòu)體的指針
     */
    Class isa; 
};

  • id表示泛型對(duì)象,指向objc_object結(jié)構(gòu)體的指針。objc_object 就是我們平時(shí)常用的實(shí)例對(duì)象的定義,它只包含一個(gè)isa指針。
    • 一個(gè)對(duì)象唯一保存的信息就是它的 Class 的地址
    • 當(dāng)我們調(diào)用一個(gè)對(duì)象的方法時(shí),它會(huì)通過 isa 去找到對(duì)應(yīng)的objc_class,然后再在objc_class的 methodLists 中找到我們調(diào)用的方法,然后執(zhí)行。

四、元類(Meta Class)

在 Objective-C 中,類也被設(shè)計(jì)為一個(gè)對(duì)象。
結(jié)構(gòu)體中也包含isa指針,這個(gè)指針指向元類。
調(diào)用一個(gè)對(duì)象的類方法的過程如下:

  • 通過對(duì)象的 isa 指針找到對(duì)應(yīng)的類。
  • 通過類的 isa 指針找到對(duì)應(yīng)元類。
  • 在元類的 methodLists 中,找到對(duì)應(yīng)的方法,然后執(zhí)行。

元類也是一個(gè)對(duì)象,也有一個(gè)isa指針,isa指針指向哪里呢?

為了不讓這種結(jié)構(gòu)無(wú)限延伸下去,Objective-C 的設(shè)計(jì)者讓所有的元類的isa指向基類(比如 NSObject)的元類。而基類的元類的 isa 指向自己。這樣就形成了一個(gè)完美的閉環(huán)。

因此消息傳遞整個(gè)結(jié)構(gòu)圖可以如下表示:

runtime_消息傳遞流程.png

五、objc_msgSend

當(dāng)對(duì)象調(diào)用方法時(shí)候,[objc test] 會(huì)轉(zhuǎn)換成

id objc_msgSend(id objc, SEL op, ...);
    
// 發(fā)送消息給無(wú)參數(shù),無(wú)返回值的方法
((void (*)(id, SEL)) objc_msgSend)(self, NSSelectorFromString(@"functionName"));

// 發(fā)送消息給有參數(shù),有返回值的方法
((NSString *(*)(id, SEL, NSString *)) objc_msgSend)(self, NSSelectorFromString(@"functionName"), @"parameter");

objc_msgSend 會(huì)默認(rèn)傳入 id 和 SEL,分別對(duì)應(yīng)兩個(gè)隱含參數(shù), self 和 _cmd,其中 _cmd 指向方法本身。

六、objc_method_list(方法列表)、objc_cache(方法緩存)

* struct objc_method_list ** methodLists : 方法定義的鏈表
* struct objc_cache * cache : 方法緩存

methodLists:存著該對(duì)象的實(shí)例方法(類對(duì)象的話存放這類方法)

cache的作用就是:

  • 因?yàn)閙ethodLists方法有很多,如果每次調(diào)用方法都去遍歷一遍methodLists的話,效率很低。所以objc_cache方法緩存存在的意義。
  • 當(dāng)methodLists遍歷找到方法,找到該方法之后,cache會(huì)以map的方式把method_name作為key,method_imp作為value給存起來(lái)。
  • 下次當(dāng)你在再調(diào)用該方法的時(shí)候,會(huì)先從cache中去找。

七、objc_method 方法

typedef struct objc_method *Method;
struct objc_method {
    
    SEL method_name                                          OBJC2_UNAVAILABLE; //方法名
    char *method_types                                       OBJC2_UNAVAILABLE; //方法類型
    IMP method_imp //方法實(shí)現(xiàn)
}    
    

在objc_method結(jié)構(gòu)體中中,SEL和IMP都是Method的屬性。

一、SEL(objc_selector)
/// 代表一個(gè)方法的不透明類型
typedef struct objc_selector *SEL;
//發(fā)送消息
id objc_msgSend(id self, SEL op, ...);

objc_msgSend函數(shù)第二個(gè)參數(shù)類型為SEL,selector是方法選擇器,可以理解為區(qū)分方法的 ID(唯一標(biāo)識(shí)符),而這個(gè)ID的數(shù)據(jù)結(jié)構(gòu)是SEL:

@property SEL selector;

可以看到selector是SEL的一個(gè)實(shí)例。其實(shí)selector就是個(gè)映射到方法的C字符串,selector既然是一個(gè)string,我覺得應(yīng)該是類似className+method的組合,命名規(guī)則有兩條:

  • 同一個(gè)類,selector不能重復(fù)
  • 不同的類,selector可以重復(fù)

這就是我們OC沒有重載的功能,一個(gè)方法不能有兩個(gè)方法名一樣,哪怕傳的參數(shù)個(gè)數(shù)和類型都不一樣。就是因?yàn)閟elector是通過方法名來(lái)找到對(duì)應(yīng)方法的。

二、IMP

IMP表示: 指向一個(gè)方法實(shí)現(xiàn)的指針。

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

就是指向最終實(shí)現(xiàn)程序的內(nèi)存地址的指針。

在iOS的Runtime中,Method通過selector和IMP兩個(gè)屬性,實(shí)現(xiàn)了快速查詢方法及實(shí)現(xiàn),相對(duì)提高了性能,又保持了靈活性。

八、Category(objc_category)

Category是表示一個(gè)指向分類的結(jié)構(gòu)體的指針,結(jié)構(gòu)體如下:

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

  • name:是指 class_name 而不是 category_name。
  • cls:要擴(kuò)展的類對(duì)象,編譯期間是不會(huì)定義的,而是在Runtime階段通過name對(duì) 應(yīng)到對(duì)應(yīng)的類對(duì)象。
  • instanceMethods:category中所有給類添加的實(shí)例方法的列表。
  • classMethods:category中所有添加的類方法的列表。
  • protocols:category實(shí)現(xiàn)的所有協(xié)議的列表。
  • instanceProperties:表示Category里所有的properties
    這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實(shí)例變量的原因,不過這個(gè)和一般的實(shí)例變量是不一樣的。

從上面的category_t的結(jié)構(gòu)體中可以看出,分類中可以添加實(shí)例方法,類方法,甚至可以實(shí)現(xiàn)協(xié)議,添加屬性,不可以添加成員變量。

Category的加載處理過程是怎樣的呢?

  1. 通過Runtime加載某個(gè)類的所有Category數(shù)據(jù)
  2. 把所有Category的方法、屬性、協(xié)議數(shù)據(jù),合并到一個(gè)大數(shù)組中
  3. 后面參與編譯的Category數(shù)據(jù),會(huì)在數(shù)組的前面
  4. 將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議),插入到類原來(lái)數(shù)據(jù)的前面

最后對(duì)象發(fā)送消息,整個(gè)執(zhí)行流程是什么樣的?

八、Runtime消息傳遞

一個(gè)對(duì)象的方法像這樣[obj work],編譯器轉(zhuǎn)成消息發(fā)送objc_msgSend(obj, work),Runtime時(shí)執(zhí)行的流程是這樣的:

  • 首先通過obj的isa指針找到它的類對(duì)象
  • class的methodlist找到work函數(shù)
  • 如果class中沒找到work函數(shù)方法,會(huì)繼續(xù)類對(duì)象的superclass(父類)中去找,一直找到父類是根類
  • 一旦知道work這個(gè)函數(shù)方法就去執(zhí)行的work的實(shí)現(xiàn)IMP

如果只是這么簡(jiǎn)單的消息傳遞,效率就非常低了。往往一個(gè)class只有20%的函數(shù)會(huì)被經(jīng)常調(diào)用,可能占總調(diào)用次數(shù)的80%。每個(gè)消息都需要遍歷一次objc_method_list并不合理。如果把經(jīng)常被調(diào)用的函數(shù)緩存下來(lái),那可以大大提高函數(shù)查詢的效率。objc_cache做的事情如下:

  • 當(dāng)methodLists遍歷找到方法,找到該方法之后,cache會(huì)以map的方式把method_name作為key,method_imp作為value給存起來(lái)。
  • 下次當(dāng)你在再調(diào)用該方法的時(shí)候,會(huì)先從cache中去找。

還有一個(gè)問題就是發(fā)現(xiàn)類別的方法優(yōu)先級(jí)更好,當(dāng)一個(gè)對(duì)象和類別都有一個(gè)work方法的時(shí)候,發(fā)現(xiàn)類別方法的優(yōu)先級(jí)更高,消息傳遞是怎么執(zhí)行的呢?

  • 就是當(dāng)你遍歷類對(duì)象的methodlist以前,會(huì)把category里面的instanceMethods(實(shí)例方法列表)、classMethods(對(duì)象方法列表)會(huì)插入到methodlist的最前面。
  • 所以當(dāng)遍歷work函數(shù),會(huì)優(yōu)先找到類別的work函數(shù),然后執(zhí)行它的IMP。

runtime消息轉(zhuǎn)發(fā)流程

當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,通過_objc_msgForward嘗試做消息轉(zhuǎn)發(fā)。_objc_msgForward是 IMP類型。
為了展示消息轉(zhuǎn)發(fā)的具體動(dòng)作,可以嘗試向一個(gè)對(duì)象發(fā)送一條錯(cuò)誤的消息,并查看一下_objc_msgForward是如何進(jìn)行轉(zhuǎn)發(fā)的。

當(dāng)上面消息傳遞過程中沒有找到方法,消息就會(huì)進(jìn)行轉(zhuǎn)發(fā),如果還是找不到并且消息轉(zhuǎn)發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報(bào)unrecognized selector錯(cuò)。消息轉(zhuǎn)發(fā)的過程有三次機(jī)會(huì)。

消息轉(zhuǎn)發(fā)的原理如下圖:

runtime.png
  • 第一步,動(dòng)態(tài)添加響應(yīng)方法

    • Objective-C運(yùn)行時(shí)會(huì)調(diào)用 實(shí)例方法調(diào)用+resolveInstanceMethod:
    • 類方法調(diào)用+resolveClassMethod:
- (void)viewDidLoad {
    [super viewDidLoad];
    //執(zhí)行work函數(shù)
    [self performSelector:@selector(work:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(work:)) {//如果是執(zhí)行work函數(shù),就動(dòng)態(tài)解析指定新的IMP
        class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//work函數(shù)
void workMethod(id obj, SEL _cmd) {
    NSLog(@"Working");
}

打印結(jié)果:

Working

表明雖然沒有實(shí)現(xiàn)work:這個(gè)函數(shù),但是我們通過class_addMethod動(dòng)態(tài)添加workMethod函數(shù),并執(zhí)行workMethod這個(gè)函數(shù)的IMP。

如果上面方法返回是NO的話,那就做走下一步。

  • 第二步轉(zhuǎn)發(fā)給響應(yīng)該方法的對(duì)象

如果目標(biāo)對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector:,Runtime 這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給能響應(yīng)該方法的對(duì)象。

下面通過代碼,首先寫聲明一個(gè)能響應(yīng)該方法的對(duì)象Person:

@interface Person: NSObject

- (void)work;

@end

@implementation Person

- (void)work {
    NSLog(@"person working");//Person的foo函數(shù)
}

@end

其次通過forwardingTargetForSelector方法轉(zhuǎn)發(fā)給響應(yīng)該方法的對(duì)象

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
      //執(zhí)行work函數(shù)
    [self performSelector:@selector(work)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,進(jìn)入下一步轉(zhuǎn)發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(work)) {
        return [Person new];//轉(zhuǎn)發(fā)給Person對(duì)象,讓Person對(duì)象接收這個(gè)消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

打印結(jié)果:

person working

如果第二步中沒有相應(yīng)該消息的對(duì)象,就會(huì)走第三步消息轉(zhuǎn)發(fā),就把整個(gè)完整的消息轉(zhuǎn)發(fā)流程走一遍了。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //執(zhí)行foo函數(shù)
    [self performSelector:@selector(foo)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;//返回NO,進(jìn)入下一步轉(zhuǎn)發(fā)
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil;//返回nil,進(jìn)入下一步轉(zhuǎn)發(fā)
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"work"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進(jìn)入forwardInvocation
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;

    Person *p = [Person new];
    if([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }
    else {
        [self doesNotRecognizeSelector:sel];
    }

}
@end

打印結(jié)果

working

從打印結(jié)果來(lái)看,我們實(shí)現(xiàn)了完整的轉(zhuǎn)發(fā)。通過簽名,Runtime生成了一個(gè)對(duì)象anInvocation,發(fā)送給了forwardInvocation,我們?cè)趂orwardInvocation方法里面讓Person對(duì)象去執(zhí)行了work函數(shù)。簽名參數(shù)v@:看蘋果文檔Type Encodings

如果這三步都沒有成功的話,就會(huì)doesNotRecognizeSelector報(bào)異常了。

Runtime實(shí)際應(yīng)用

Runtime簡(jiǎn)直就是做大型框架的利器。它的應(yīng)用場(chǎng)景非常多,下面就介紹一些常見的應(yīng)用場(chǎng)景。

  • 給分類添加屬性(關(guān)聯(lián)對(duì)象)
  • Method Swizzling交換方法
  • KVO原理實(shí)現(xiàn)
  • 消息轉(zhuǎn)發(fā)(熱更新)解決Bug(JSPatch)
  • 實(shí)現(xiàn)NSCoding的自動(dòng)歸檔和自動(dòng)解檔
  • 實(shí)現(xiàn)字典和模型的自動(dòng)轉(zhuǎn)換(MJExtension)

一、給分類添加屬性(關(guān)聯(lián)對(duì)象)

關(guān)聯(lián)對(duì)象Runtime提供了下面幾個(gè)接口:

//關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

//獲取關(guān)聯(lián)的對(duì)象
id objc_getAssociatedObject(id object, const void *key)

//移除關(guān)聯(lián)的對(duì)象
void objc_removeAssociatedObjects(id object)

上面的參數(shù)解釋:

  • id object:被關(guān)聯(lián)的對(duì)象
  • const void *key:關(guān)聯(lián)的key,要求唯一
  • id value:關(guān)聯(lián)的對(duì)象
  • objc_AssociationPolicy policy:內(nèi)存管理的策略

objc_AssociationPolicy是一個(gè)枚舉如下:

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    
    OBJC_ASSOCIATION_ASSIGN = 0,           //weak nonatomic 屬性  
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, //retain\strong nonatomic 屬性  
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  //copy nonatomic屬性                                            
    OBJC_ASSOCIATION_RETAIN = 01401, // retain\strong atomic屬性 
    OBJC_ASSOCIATION_COPY = 01403    // copy atomic屬性     
};

針對(duì)枚舉如下面解釋:

runtime_關(guān)聯(lián)對(duì)象屬性.png

下面就給Person關(guān)聯(lián)一個(gè)name屬性

@interface Person (Name)

@property (nonatomic, copy) NSString *name;

@end

#import "Person+Name.h"
#import <objc/runtime.h>

static NSString *nameKey = @"namekey";
@implementation Person (Name)

- (void)setName:(NSString *)name {
    
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name {
    
    return objc_getAssociatedObject(self, &nameKey);
}

二、方法交換(Method Swizzling)

Method Swizzling也是iOS中AOP(面相切面編程)的一種實(shí)現(xiàn)方式,我們可以利用蘋果這一特性來(lái)實(shí)現(xiàn)AOP編程。

Method Swizzling是發(fā)生在運(yùn)行時(shí),主要用于運(yùn)行時(shí)將兩個(gè)Method進(jìn)行交換,我們可以將Method Swizzle代碼寫到任何地方,但是只有在Method_Swizzling這段Method Swizzle代碼執(zhí)行完畢之后互換才起作用。

Method Swizzling交換時(shí)機(jī):盡可能在+load方法中實(shí)現(xiàn)。原因:

+load方法會(huì)在加載類的時(shí)候就被調(diào)用,也就是ios應(yīng)用啟動(dòng)的時(shí)候,就會(huì)加載所有的類,main函數(shù)之前,就會(huì)調(diào)用每個(gè)類的+load方法。
主要用途:當(dāng)需求需要在調(diào)用系統(tǒng)方法的基礎(chǔ)上需要添加額外的功能,然后要把項(xiàng)目中所有的方法都要實(shí)現(xiàn)都要改一遍的問題上,不需要?jiǎng)釉械南到y(tǒng)方法。并且能使用原有的系統(tǒng)方法功能外,添加一下原有的功能。

例如:當(dāng)系統(tǒng)需要適配ipad圖片的時(shí)候,項(xiàng)目中調(diào)用imageNamed方法的地方非常多。所以每一處都需要進(jìn)行更改肯定非常麻煩?,F(xiàn)在通過Method_Swizzling就可以sw_imageNamed替換imageNamed并且做一些處理。當(dāng)以調(diào)用imageNamed方法時(shí)候?qū)嶋H調(diào)用的是sw_imageNamed。

@implementation UIImage (Category)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(imageNamed);
        SEL swizzledSelector = @selector(sw_imageNamed);
     
        Method originalMethod = class_getInstanceMethod(class,originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class,swizzledSelector);
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        /*
         * NO:嘗試添加需要交換的方法,若添加失敗,則說明已經(jīng)有該方法,直接交換
         * YES:如果添加成功,沒有該方法,則把剛添加的方法替換為我們需要的方法
         */ 
        if (didAddMethod) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}
 /*
  *這里特別注意,必須調(diào)用交換的方法,因?yàn)樯厦嬉呀?jīng)把方法交換了,所以不會(huì)死循環(huán),
  *如果調(diào)用originalMethod,反而會(huì)死循環(huán)
  */ 
- (void)sw_imageNamed {
    UIImage * image;
    //這個(gè)時(shí)候的sw_imageNamed已經(jīng)交換后變成了系統(tǒng)的imageNamed
    if( IS_IPHONE ){
        // iPhone處理
        UIImage * image =  [self sw_imageNamed:name];
        if (image != nil) {
            return image;
        } else {
            return nil;
        }
    } else {
        // ipad處理,_ipad是自己定義。
        UIImage *image = [self sw_imageNamed:[NSString stringWithFormat:@"%@_ipad",name]];
        if (image != nil) {
            return image;
        }else {
            image = [self swizze_imageNamed:name];
            return image;
        }
   }
}
@end

可能有些不明白為啥sw_imageNamed方法里面又調(diào)用[self swizze_imageNamed:name]竟然沒有死循環(huán)。主要是因?yàn)檫@兩個(gè)sw_imageNamed是不一樣意義:

  • load方法表示在加載類的時(shí)候就執(zhí)行了該方法,就已經(jīng)交換完畢,交換的是IMP(就是函數(shù)實(shí)現(xiàn)的內(nèi)存地址指針)。
  • 當(dāng)對(duì)象調(diào)用[self imageNamed:name]調(diào)用的sw_imageNamed的IMP
  • (void)sw_imageNamed {}這個(gè)就是sw_imageNamed方法真正實(shí)現(xiàn)的IMP。
  • 然后在sw_imageNamed的IMP里調(diào)用[self swizze_imageNamed:name],[self swizze_imageNamed:name]這個(gè)就是selector方法名執(zhí)行的是imageNamed的IMP。
  • (void)sw_imageNamed {}與[self swizze_imageNamed:name]這個(gè)兩個(gè)是有區(qū)別的,一個(gè)IMP,一個(gè)Selector。

原理圖如下:

Method_Swizzing.png

Method Swizzing作用就是讓sel2、sel3與IMP2、IMP3交換。

三、熱更新及解決線上bug(JSPatch)

JSPatch 是一個(gè)iOS動(dòng)態(tài)更新框架,只需在項(xiàng)目中引入極小的引擎,就可以使用JavaScript調(diào)用任何Objective-C原生接口,獲得腳本語(yǔ)言的優(yōu)勢(shì):為項(xiàng)目動(dòng)態(tài)添加模塊,或替換項(xiàng)目原生代碼動(dòng)態(tài)修復(fù)bug。

JSPatch基礎(chǔ)原理:

JSPatch 能做到通過JS調(diào)用和改寫OC方法最根本的原因是 Objective-C是動(dòng)態(tài)語(yǔ)言,OC上所有方法的調(diào)用/類的生成都通過 Objective-C Runtime在運(yùn)行時(shí)進(jìn)行,我們可以通過類名/方法名反射得到相應(yīng)的類和方法:

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也可以替換某個(gè)類的方法為新的實(shí)現(xiàn):

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

還可以新注冊(cè)一個(gè)類,為類添加方法:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
class_addMethod(cls, selector, implement, typedesc);
objc_registerClassPair(cls);

理論上你可以在運(yùn)行時(shí)通過類名/方法名調(diào)用到任何 OC 方法,替換任何類的實(shí)現(xiàn)以及新增任意類。

JSPatch的基本原理就是:JS傳遞字符串給OC,OC通過Runtime 接口調(diào)用和替換OC方法。這是最基礎(chǔ)的原理,實(shí)際實(shí)現(xiàn)過程還有很多怪要打。

最后JSPatch被蘋果禁止使用,還能用 JSPatch 嗎?

  • 可以,但請(qǐng)不要自行接入,統(tǒng)一接入 JSPatch 平臺(tái)。

為什么要統(tǒng)一接入 JSPatch 平臺(tái)?

若從蘋果的審核規(guī)則來(lái)看,JSPatch 和 React Native 是一樣的,但蘋果接受 React Native,拒絕 JSPatch,主要是因?yàn)镴SPatch 可以調(diào)用任意原生 API,出于安全和濫用方面的顧慮而拒絕,之前大量 APP 自行接入 JSPatch,導(dǎo)致有幾個(gè)風(fēng)險(xiǎn)點(diǎn):

  • 無(wú)法保證每個(gè) APP 接入方式都是安全的,容易傳輸過程被替換,被黑客中間人攻擊,成為 iOS 上的一個(gè)漏洞。

  • 一些像地圖/推送類 SDK 接入 JSPatch 后覆蓋大量 APP,若這些 SDK 后臺(tái)被攻破,可以對(duì)這些 APP 下發(fā)惡意腳本,造成大面積危害。

  • 開發(fā)者可以方便地不經(jīng)過蘋果審查使用 JSPatch 調(diào)用私有 API。

若大家還是通過一些混淆手段騙過蘋果繼續(xù)各自接入 JSPatch,出于上述三點(diǎn)考慮,蘋果不可能再允許,但若接入 JSPatch 平臺(tái),上述三個(gè)問題可以幫蘋果解決,不再有這類隱患,詳見這里,蘋果才會(huì)考慮允許繼續(xù)使用。

用JSPatch平臺(tái)最新的SDK接入可以通過審核嗎?
可以通過,已有接入并通過審核的 APP。

為什么平臺(tái)SDK能通過審核?

  • 因?yàn)樽隽撕?jiǎn)單的類名修改混淆。

用Github上的代碼接入能通過審核嗎?

  • 不能。前面也說了,不要自行接入JSPatch源碼,不要自行混淆代碼接入

四、NSCoding的自動(dòng)歸檔、解檔

原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行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];
    }
}

字典轉(zhuǎn)模型

首先建立一個(gè)NSObject分類,做個(gè)簡(jiǎn)單的字典轉(zhuǎn)模型

#import <Foundation/Foundation.h>

@interface NSObject (Json)

+ (instancetype)pf_objectWithJson:(NSDictionary *)json;

@end

#import "NSObject+Json.h"
#import <objc/runtime.h>

@implementation NSObject (Json)

+ (instancetype)cs_objectWithJson:(NSDictionary *)json {
    id obj = [[self alloc] init];
    
    unsigned int count;
    Ivar *ivars = class_copyIvarList(self, &count);
    for (int i = 0; i < count; i++) {
        // 取出位置的成員變量
        Ivar ivar = ivars[I];
        NSMutableString *name = [NSMutableString stringWithUTF8String:ivar_getName(ivar)];
        [name deleteCharactersInRange:NSMakeRange(0, 1)];
        
        // 設(shè)值
        id value = json[name];
        if ([name isEqualToString:@"ID"]) {
            value = json[@"id"];
        }
        [obj setValue:value forKey:name];
    }
    free(ivars);
    return obj;
}

@end

字典轉(zhuǎn)模型的模型及調(diào)用:

#import <Foundation/Foundation.h>

@interface Persion : NSObject

@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *name;

@end

// 字典轉(zhuǎn)模型調(diào)用
- (void)jsonToModel {
    // 字典轉(zhuǎn)模型
    NSDictionary *json = @{
                           @"id" : @20,
                           @"age" : @20,
                           @"weight" : @60,
                           @"name" : @"Jack",
                           @"no" : @30
                           };
    Persion *persion = [Persion pf_objectWithJson:json];
    NSLog(@"id = %d, age = %d, weight = %d, name = %@",persion.ID,persion.age,persion.weight,persion.name);
}

Runtime使用場(chǎng)景很多,在這就不一一舉例子了。大家可以看下面的應(yīng)用圖:

runtime應(yīng)用.png

結(jié)尾

Objective-C語(yǔ)言最重要的特點(diǎn)之一就是運(yùn)行時(shí)語(yǔ)言,可見掌握runtime至關(guān)重要。Objective-C是一個(gè)全動(dòng)態(tài)語(yǔ)言,它的一切都是基于Runtime實(shí)現(xiàn)的平時(shí)編寫的OC代碼, 在程序運(yùn)行過程中, 其實(shí)最終都是轉(zhuǎn)成了runtime的C語(yǔ)言代碼, runtime算是OC的幕后工作者。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容