05 iOS底層原理 - Category本質(zhì)探究

廢話不多說,老規(guī)矩,還是來兩道面試題:

一,Category的實(shí)現(xiàn)原理;
二,Category和Extension的區(qū)別

帶著問題我們對Category一探究竟

在我們平時開發(fā)中,會經(jīng)常用到Category,一般是在什么情況下使用的呢?
也就是說,Category一般會在哪些場合使用?

  1. 我不想繼承父類的方法實(shí)現(xiàn),只是單純的想加一些新的方法實(shí)現(xiàn);
  2. 如果這個類包含了很多方法實(shí)現(xiàn),而這些方法實(shí)現(xiàn)又需要多個人來完成,就可以用到分類。
  3. 想減少單個文件的體積
  4. 聲明一些私有方法

大家有沒有想過,分類中的方法會存放在什么地方,方法在調(diào)用的時候,是先調(diào)用本身類的方法,還是先調(diào)用分類的方法?帶著這個疑惑,咋們一起探究下分類的本質(zhì)吧。

一,Category的簡單調(diào)用

1. Category簡單使用
1>創(chuàng)建一個Person類
// 聲明
@interface Person : NSObject
- (void)play;
@end

// 實(shí)現(xiàn)
@implementation Person
- (void)play {
    NSLog(@"Person - 玩");
}
@end
2> 創(chuàng)建兩個Person的分類Run
// 聲明分類Run
@interface Person (Run)
- (void)run;
+ (void)run2;
@end
// 實(shí)現(xiàn)
@implementation Person (Run)
- (void)run {
    NSLog(@"Person (Run) - run");
}
+ (void)run2 {
    NSLog(@"Person (Run) - run2");
}
@end
3> 調(diào)用
- (void)viewDidLoad {
    [super viewDidLoad];
    // Category 的簡單使用
    Person *person = [[Person alloc]init];
    [person play];
    [person run]; 
    [Person run2];
}

查看打印結(jié)果:

Person - 玩
Person (Run) - run
Person (Run) - run2

通過打印結(jié)果可以看出,分類和類的方法都會調(diào)用,各自不影響。
02 iOS底層原理 - isa和superclass指針探究中講到,

  • 對象方法存儲在類的方法列表中
  • 類方法存儲在元類的類方法列表中

那么,大家想想,分類中的對象方法和類方法都會以什么樣的數(shù)據(jù)格式存儲呢,還有最后會存儲在什么地方的呢?

首先看看,分類信息的存儲數(shù)據(jù)格式:

二,Category信息的存儲格式

為了能將所有的信息涵蓋進(jìn)去,重新創(chuàng)建一個分類Eat,
包含了,對象方法、類方法、協(xié)議、屬性

// 聲明
@interface Person (Eat) <NSCopying, NSCoding>
- (void)eat;
- (void)eat1;
+ (void)eat2;
+ (void)eat3;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end

// 實(shí)現(xiàn)
@implementation Person (Eat)
- (void)eat {
    NSLog(@"吃");
}
- (void)eat1 {
    NSLog(@"吃");
}
+ (void)eat2 {
    NSLog(@"吃2");
}
+ (void)eat3 {
    NSLog(@"吃2");
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    return nil;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder { }
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
    return nil;
}

在終端輸入:

& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m

可以得到Person+Eat.cpp代碼,分析該文件發(fā)現(xiàn)有這么一個結(jié)構(gòu)體

struct _category_t {
         const char *name;
         struct _class_t *cls;
         const struct _method_list_t *instance_methods; // 對象方法列表
         const struct _method_list_t *class_methods; // 類對象方法列表
         const struct _protocol_list_t *protocols; // 協(xié)議列表
         const struct _prop_list_t *properties; // 屬性列表
     };

我總結(jié)了一張示意圖,這個比較清晰:

image.png

總結(jié):
Category編譯之后的底層數(shù)據(jù)結(jié)構(gòu)是一個 struct category_t,里面存儲著本身類的類名、類、對象方法列表、類方法列表、屬性列表、協(xié)議列表

我們知道了Category的數(shù)據(jù)結(jié)構(gòu),那么這個數(shù)據(jù)最后會存儲到什么地方法呢?下面我們從runtime源碼分析Category的本質(zhì)。

三,runtime源碼分析Category

在看源碼之前,先做一個代碼測試:

1. 給分類Run中加一個對象方法play
@interface Person (Run)
- (void)run;
+ (void)run2;
// 新加入的
- (void)play;
@end

@implementation Person (Run)
- (void)run {
    NSLog(@"Person (Run) - run");
}
+ (void)run2 {
    NSLog(@"Person (Run) - run2");
}
// 新加方法實(shí)現(xiàn)
- (void)play {
    NSLog(@"Person (Run) - 玩");
}
@end

調(diào)用代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    // Category 的簡單使用
    Person *person = [[Person alloc]init];
    [person play];
    [person run]; 
    [Person run2];
}

再次運(yùn)行程序,查看打印結(jié)果:

Person (Run) - 玩
Person (Run) - run
Person (Run) - run2

// 第一次的打印結(jié)果
Person - 玩
Person (Run) - run
Person (Run) - run2

我們會發(fā)現(xiàn),第一條的打印結(jié)果是不一樣的,說明分類中出現(xiàn)與本身類重名的方法,在調(diào)用時,只會調(diào)用分類的方法,而本身類的方法就不會調(diào)用了。這是為啥呢?

帶著這個疑問,我們正式研究源碼。

2. 研究runtime源碼

源碼下載

1> 源碼分析查找流程:
image.png
2>Category實(shí)現(xiàn)原理示意圖
image.png

簡單說明下上圖要表達(dá)的意思:

  • 在編譯階段,分類Run和Eat會以struct category_t的形式存儲,并按照從上往下的順序編譯(這個順序會影響到后面調(diào)用的順序);
  • 在運(yùn)行階段,會將后編譯的信息放到大數(shù)組前面,先編譯的放到大數(shù)組后面;
  • 最終,這個大數(shù)組就會插到本身類和元類的對應(yīng)列表的第一個元素中去,也就是說,Category的信息最終還是會和本身類和元類的信息存儲到一起。

現(xiàn)在就可以解釋:分類中出現(xiàn)與本身類重名的方法,在調(diào)用時,只會調(diào)用分類的方法,而本身類的方法就不會調(diào)用了。
原因:分類中的信息根據(jù)runtime源碼得知,分類中的信息最終會存儲在本身類和元類的對應(yīng)數(shù)據(jù)列表中去,并且是插入到第0個元素,所以在調(diào)用時,會先調(diào)用分類的信息,并且調(diào)用后,本身類和元類的信息就會被覆蓋(沒有銷毀),從而不會再被調(diào)用了。

3> Category實(shí)現(xiàn)原理核心源碼分析
image.png
4> Category原理實(shí)現(xiàn)總結(jié)
  • 通過runtime加載某個類的所有Category數(shù)據(jù);
  • 將所有Category數(shù)據(jù)中的方法、屬性、協(xié)議數(shù)據(jù)列表,合并到一個大數(shù)組里面去;

注意:后編譯的Category會存放在這個大數(shù)組的最前面,編譯順序如圖所示:

image.png

四,回答文章開頭的面試題

一,Category的實(shí)現(xiàn)原理
答:Category編譯之后的底層數(shù)據(jù)結(jié)構(gòu)是個 struct catory_t,里面存放著本身類的類名、類、對象方法列表、類方法列表、屬性列表、協(xié)議列表等信息;
在程序運(yùn)行的時候,runtime會將Category數(shù)據(jù),合并到本身類和元類中相關(guān)數(shù)據(jù)列表中去(插入到第0個元素);
Category中的方法,是按照后編譯先執(zhí)行的順序調(diào)用的,如果分類和本身類出現(xiàn)重名的方法,先找到先調(diào)用,之后就不會再繼續(xù)查找了。
二,Category和Extension的區(qū)別
Category是在程序運(yùn)行的時候,將數(shù)據(jù)合并到本身類和元類中的,
Extension在編譯的時候,它的信息就包含在了本身類中了。

順便看下Extension,
給Person類添加一個Extension

// 類的擴(kuò)展
/*
 編譯的時候,就會合并到.h里面去,編譯之后就會存在于類對象中
 作用:就是將原來公開的成員變量、屬性、方法,放到了.m文件中去,成為私有的
 */
@interface Person()
@property (nonatomic, assign) int age;
- (void)abc;
@end

@implementation Person
// 擴(kuò)展的方法實(shí)現(xiàn)
- (void)abc {
    NSLog(@"實(shí)現(xiàn)類擴(kuò)展的方法");
}

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

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

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