本篇主要是對小碼哥底層視頻學習的總結。方便日后復習。
上一篇《iOS底層原理總結 - 探尋KVO的本質》:
http://www.lxweimin.com/p/f5b3199982e6
本篇學習總結:
- 探尋Category本質
- Category底層代碼分析
- load 和 initialize方法底層分析
好了,帶著問題,我們一一開始閱讀吧 ??
一.探尋Category本質
我們還是先來上一段代碼,之后的分析都是基于這段代碼:
MJPerson類
//MJPerson.h文件
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
- (void)run;
@end
//MJPerson.m文件
#import "MJPerson.h"
// class extension (匿名分類\類擴展)
@interface MJPerson()
{
int _abc;
}
@property (nonatomic, assign) int age;
- (void)abc;
@end
@implementation MJPerson
- (void)abc
{
}
- (void)run
{
NSLog(@"MJPerson - run");
}
+ (void)run2
{
}
MJPerson+Test 分類
//MJPerson(Test) .h文件
#import "MJPerson.h"
@interface MJPerson (Test)
- (void)test;
@end
//MJPerson(Test) .m文件
#import "MJPerson+Test.h"
@implementation MJPerson (Test)
- (void)run
{
NSLog(@"MJPerson (Test) - run");
}
- (void)test
{
NSLog(@"test");
}
+ (void)test2
{
}
@end
MJPerson+Eat分類
//MJPerson(Eat).h文件
#import "MJPerson.h"
@interface MJPerson (Eat) <NSCopying, NSCoding>
- (void)eat;
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
@end
//MJPerson(Eat).m文件
#import "MJPerson+Eat.h"
@implementation MJPerson (Eat)
- (void)run
{
NSLog(@"MJPerson (Eat) - run");
}
- (void)eat
{
NSLog(@"eat");
}
- (void)eat1
{
NSLog(@"eat1");
}
+ (void)eat2
{
}
+ (void)eat3
{
}
main.m文件
#import "MJPerson+Eat.h"
#import "MJPerson+Test.h"
#import "MJPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[MJPerson alloc] init];
[person run];
}
return 0;
}
我們之前講過實例對象的isa指針指向類對象,類對象的isa指針指向元類對象,當p調用run方法時,通過實例對象的isa指針找到類對象,然后在類對象中查找對象方法,如果沒有找到,通過類對象的superclass指針找到父類的類對象,接著去尋找run方法。
那么問題來了,當我們調用分類中的run方法時,它也會按照我們前面說的方式去查找run方法嗎?
小碼哥直接給出結論了,先看結論,再看底層代碼實現
分類中的對象方法依然存儲在類對象中,同本類對象在同一個地方,調用步驟也同本類調用對象方法一樣,同理,如果是類方法的話,是存儲在元類對象中的。
二.Category底層代碼分析
我們前面講過了,所有的OC代碼最終都會被編譯成C/C++代碼,我們要想進一步了解底層代碼,需要將文件轉化成C++文件,轉化方式在《iOS底層原理-探尋OC對象本質》中講過了,這里我們直接看.cpp文件。
- _category_t:分類結構體
我們點開MJPerson+Eat.cpp文件,搜索category,我們可以找到 _category_t 結構體,結構如下:
從底層代碼中可以看出,_category_t 結構體包含對象方法列表,類方法列表,協議列表,屬性列表等信息,但是沒有找到成員變量信息,是不是可以初步肯定一下之前的結論:分類中可以添加對象方法,類方法,協議,屬性,不可以添加成員變量,category可以添加屬性,但是不會自動生成成員變量,只能生成setter getter方法,還需要我們手動實現一下方法。
那么我們就一個變量一個變量的分析吧
- _method_list_t:方法列表
然后搜索 _method_list_t,結構體如下:
此時我們發現了兩個名字比較長的變量名,分別是
_OBJC__CATEGORY_INSTANCE_METHODS_MJPerson__Eat 和_OBJC__CATEGORY_CLASS_METHODS_MJPerson__Eat,從名稱上可以推測是類方法和實例方法,下面的代碼賦值跟上面的結構體成員變量一一對應,我們可以看到結構體中存儲了方法占用的內存,方法數量,以及方法列表。并且從上圖中找到分類中我們實現對應的對象方法。
- _protocol_list_t:協議列表信息
繼續搜索 _protocol_list_t,結構體如下:
這里同樣看到了一個名字比較長的變量名:_OBJC_CATEGORY_PROTOCOLS__MJPerson__Ea,下面的代碼賦值跟上面的結構體成員變量一一對應。
- _prop_list_t:屬性列表信息
最后搜索 _prop_list_t,結構體如下:
這里同樣看到了一個名字比較長的變量名:_OBJC__PROP_LIST_MJPerson__Eat,下面的代碼賦值跟上面的結構體成員變量一一對應。
最后我們再搜索一下category 發現了這么一個變量:
**_OBJC__CATEGORY_MJPerson__Eat ** 變量時屬于 _category_t 結構體類型,我們再來看一下 _category_t 結構體信息。
上下兩張圖對比來看,我們發現定義了category_t類型的變量,
MJPerson:賦值給name;
OBJC_CLASS$_MJPreson :賦值給cls指針變量;
對應的列表信息一一賦值給變量。
通過以上分析我們發現,編譯時期,分類被編譯成了catagory_t結構體類型的變量,分類中的對象方法,類方法,屬性,協議等都存放在catagory_t結構體對應的成員變量中。
回到最初的結論,分類中的實例方法是如何放到類對象中去呢,這要從runtime源碼說起,下面是通過查看runtime源碼找到catagory_t存儲的方法,屬性,協議等是如何存儲在類對象中的。
我們先記錄一下看源碼的順序,源碼本來就有點晦澀難懂,我們根據順序去查看
首先我們下載源碼,打開objc-os.mm文件,先從runtime初始函數看起
接著我們來到 & map_images讀取模塊(images這里代表模塊),
點擊 map_images_nolock 函數中找到_read_images函數,
在_read_images函數中我們可以找到重組類信息
從上述代碼中我們可以知道這段代碼是用來查找有沒有分類的。通過_getObjc2CategoryList函數獲取分類列表之后,進行遍歷,獲取其中的方法,協議,屬性等,可以看到最終都調用了remethodizeClass(cls)函數,我們點進去查看一下
通過上述代碼我們發現attachCategories函數接收了類對象cls和分類數組cats,如我們一開始寫的代碼所示,一個類有多少個分類,就會有多少個category_t結構體類型的變量,這些分類信息都都保存在category_list中。我們來到attachCategories函數內部
上述代碼中可以看出,這步驟才是關鍵步驟了
- 1.首先根據傳進來的cats數組分別創建了mlist,proplists,protocols三個二維數組,用于存儲方法每個分類的方法列表,屬性列表,協議列表
- 2.通過while(i--)倒序方式進行遍歷循環,取出每一個分類中的方法數組,屬性數組,協議數組,存進mlist,proplists,protocols三個二維數組中
- 3.取出類對象的class_rw_t,我們在《iOS底層原理總結 - 探尋Class的本質》 中已經講過了,類對象中存儲的方法列表,屬性信息,協議信息,成員變量信息都存儲在class_rw_t中
- 4.將存放所有分類方法的二維數組 mlist 附加到類對象的方法列表中
- 5.將存放所有屬性方法的二維數組 proplists 附加到類對象的屬性列表中
- 6.將存放所有協議方法的二維數組 protocols 附加到類對象的協議列表中
我們看一下attachLists函數內部實現:
array()->lists:類對象原來的方法列表,屬性列表,協議列表。
addedLists:所有分類的方法列表,屬性列表,協議列表。
attachLists函數里面最重要的兩個方法為memmove(內存移動方法)和memcpy(內存拷貝方法)。
我們分別說一下這兩個函數,
下圖是經過memmove函數之后數據在內存中分配情況
1.在原有空間上擴容addedCount大小空間
2.遵循menmove方法移動原則,原有方法開始往后移動
// array()->lists 原來方法、屬性、協議列表數組
// addedCount 分類數組長度
// oldCount * sizeof(array()->lists[0]) 原來數組占據的空間
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
經過memmove方法之后,我們發現,雖然本類的方法,屬性,協議列表會分別后移,但是本類的對應數組的指針依然指向原始位置。
memcpy方法之后,內存變化
// array()->lists 原來方法、屬性、協議列表數組
// addedLists 分類方法、屬性、協議列表數組
// addedCount * sizeof(array()->lists[0]) 原來數組占據的空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
我們發現原來指針并沒有改變,至始至終指向開頭的位置。并且經過memmove和memcpy方法之后,分類的方法,屬性,協議列表被放在了類對象中原本存儲的方法,屬性,協議列表前面。
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法數組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
Preson *p = [[Preson alloc] init];
[p run];
[self printMethodNamesOfClass:[Preson class]];
}
那么為什么要將分類方法的列表追加到本來的對象方法前面呢,這樣做的目的是為了保證分類方法優先調用,我們知道當分類重寫本類的方法時,會覆蓋本類的方法。
其實經過上面的分析我們知道本質上并不是覆蓋,而是優先調用。本類的方法依然在內存中的。我們可以通過打印所有類的所有方法名來查看
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法數組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@ - %@", cls, methodNames);
}
- (void)viewDidLoad {
[super viewDidLoad];
Preson *p = [[Preson alloc] init];
[p run];
[self printMethodNamesOfClass:[Preson class]];
}
通過下圖中打印內容可以發現,調用的是Test2中的run方法,并且Person類中存儲著兩個run方法
三.load 和 initialize
load 方法會在程序啟動,加載類,分類信息的時候調用,只調用一次,調用方法是指針直接調用,一般不主動調用。
先看代碼
再來看一下調用load方法的具體實現
通過源碼我們發現load方法調用順序是優先調用類的load方法,如有繼承關系的類,調用子類的時候會有限調用父類的load方法,之后調用分類的load方法,分類按照編譯順序調用
我們看到load方法中直接拿到load方法的內存地址直接調用方法,不在是通過消息發送機制調用。
代碼驗證如下:
我們添加Student繼承Presen類,并添加Student+Test分類,分別重寫只+load方法,其他什么都不做通過打印發現:
最后用一張圖總結load方法
** initialize** 方法當類第一次接收到消息時,優先調用父類的initialize方法,在調用子類的initialize方法。
之后我們為Preson、Student 、Student+Test 添加initialize方法。
第一次使用類的時候就會調用initialize方法。調用子類的initialize之前,會先保證調用父類的initialize方法。如果之前已經調用過initialize,就不會再調用initialize方法了。當分類重寫initialize方法時會先調用分類的方法。但是load方法并不會被覆蓋,首先我們來看一下initialize的源碼。
最后用一張圖總結:
總結本篇面試題:
- 1.Category的實現原理,或者加載處理過程是什么樣的?
1>編譯時期將所有categor轉化成category_t的結構體變量,并賦值
2>通過runtime加載某個類的所有category數據
3>把所有category的方法,屬性,協議數組,合并到一個大數組中
a)后面參與編譯的category數據,會在數組的前面
4》將合并后的分類數據(方法,屬性,協議),合并到類原來數據的前面
- 2.Category為什么只能加方法不能加屬性?
category可以添加屬性,但是并不會主動生成成員變量及setter/getter方法,因為category_t結構體中并不存在成員變量,通過之前對對象的分析我們知道成員變量是存放在實例對象中,并且編譯的那一刻都已經決定好了,而分類是在運行時才去加載的,那么我們就無法運行時將分類的成員變量添加到實例對象的結構體中,因此category中可以添加屬性,不可添加成員變量。
- 3.load initialize方法的區別是什么?
a.調用方式:
1>load 是根據函數地址直接調用;
2>initialize是通過objc_msgSend方法調用;
b>調用時刻
1>load 是runtime加載類,分類的時候調用(只會調用一次);
2>initialize 是類第一次接收到消息的時候調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次,具體例子是當多個子類都沒有實現initialize方法,卻是第一次給類發送消息)
- 4.load initialize 方法的調用順序?
1.load
1>先調用類的load
a)先編譯的類,優先調用load
b)調用子類的load之前,會先調用父類的load
2>在調用分類的load
a)先編譯的分類,優先調用load
2.initialize
1>先初始化父類
2>在初始化子類(可能最終調用的是父類的initialize方法)
本篇學習先記錄到此,感謝閱讀,如有錯誤,不吝賜教。