探索底層原理,積累從點滴做起。大家好,我是Mars。
往期回顧
iOS底層原理探索—OC對象的本質
iOS底層原理探索—class的本質
iOS底層原理探索—KVO的本質
iOS底層原理探索— KVC的本質
今天帶領大家探索iOS之Category的本質。
Category
首先我們聲明一個Person
類
//Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
{
int _age;
}
- (void)run;
@end
//Person.m
#import "Person.h"
@implementation Person
- (void)run
{
NSLog(@"Person:run");
}
@end
我們之前在iOS底層原理探索—OC對象的本質中講到:實例對象的isa
指針指向類對象,類對象的isa
指針指向元類對象。我們創建一個Person
對象p
,當p
調用run
方法時,通過實例對象的isa指針找到類對象,然后在類對象中查找對象方法,如果沒有找到,就通過類對象的superclass
指針找到父類對象,接著去尋找run
方法。
那么當我們調用分類的方法時,是否跟上面的調用順序一樣呢?下面我們創建分類來驗證一下:
創建Person
的分類:
在New File
的iOS
文件下選擇Objective-C File
File Type
選擇Category
,Class
父類選擇Person
//Person+test.h
#import "Person.h"
@interface Person (Test)
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end
//Person+test.m
#import "Person+Test.h"
@implementation Person (Test)
- (void)test
{
}
+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
return 18;
}
- (void)run
{
NSLog(@"Person+test:run");
}
@end
以上我們就完成創建了Person
的Test
分類。
在此先告訴大家結論:分類中的對象方法是存儲在類對象中的,和類對象方法在同一個地方,調用步驟也和調用對象方法一樣。如果是類方法的話,同樣也是存儲在元類對象中
。
這一點大致可以從分類的底層結構中看出來:
分類的底層結構
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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
從分類的源碼中可以看出Categroy
在底層是以categroy _t
的結構存在,里面包括對象方法
,類方法
,協議
,和屬性
。注意分類結構體中是不存在成員變量
的,因此分類中是不允許添加成員變量。分類中添加的屬性并不會幫助我們自動生成成員變量,只會生成set
、get
方法的聲明,需要我們自己去實現。
至此我們可以得出結論:
分類的實現原理是將分類中的方法,屬性,協議信息放在
category_t
結構體中,然后將結構體內的方法列表拷貝到類對象的方法列表中。分類中可以添加屬性,但是并不會自動生成
成員變量
及set
、get
方法。因為底層的category_t
結構體中并不存在成員變量。通過之前對對象的分析我們知道成員變量是存放在實例對象中的,并且編譯的那一刻就已經決定好了。而分類是在運行時才去加載的,那么我們就無法在程序運行時將分類的成員變量中添加到實例對象的結構體中。因此分類中不可以添加成員變量
由于上述結論的驗證是依據底層源碼,過程比較枯燥,也不能保證大家閱讀一次就能弄清楚整個流程,所以將結論提前告知。不愿意閱讀源碼的讀者也可以忽略以下內容,掌握上面的結論即可。
首先把Person+Test.m
文件通過命令行轉化為c++
文件,查看底層編譯過程。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Test.m
然后將生成的.cpp
文件拖拽至Xcode中查看。在.cpp
文件中搜索category_t
,通過搜索結果我們可以看到,_category_t
結構體中,存放著類名
,對象方法列表
,類方法列表
,協議列表
,以及屬性列表
:
在.cpp
文件中繼續往下看,我們可以看到_method_list_t *instance_methods
結構體的內容:
通過結構體名稱_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test
可以看出是INSTANCE_METHODS
--對象方法。我們可以看到結構體中存儲了方法占用的內存,方法數量以及方法列表。并且從上圖中可以看到在分類中我們實現的test
,setAge
, age
和run
四個方法。
同樣,我們繼續往下閱讀,查看看到_method_list_t *class_methods
結構體的內容:
同樣通過結構體名稱 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test
可以看出是CLASS_METHODS
--類方法。同樣可以看到我們實現的abc
類方法。
繼續往下查看時,我們可以看到屬性列表結構體_prop_list_t
:
屬性列表結構體_OBJC_$_PROP_LIST_Person_$_Test
即_prop_list_t
結構體,里面存儲了屬性的占用空間
,屬性數量
以及屬性列表
,從上圖中可以看到我們聲明的age
屬性。
同時我們發現,.cpp
文件中沒有protocol_list_t *protocols
協議信息列表結構體的相關信息。這是由于我們創建分類是并沒有遵守任何協議,自認分類里面也就沒有任何協議相關的信息。我們返回分類Person+Test
,使其遵守NSCopying
協議,再通過命令行將分類的.m
文件編譯成.cpp
文件后查看:
通過上圖可以看到分類底層先將協議方法通過_method_list_t
結構體存儲,之后通過_protocol_t
結構體存儲在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test
中,分別為protocol_count
--協議數量以及存儲協議方法
的_protocol_t
結構體。
在.cpp
文件末尾處,我們看到系統定義了_category_t
類型的_OBJC_$_CATEGORY_Person_$_Test
結構體:
將_OBJC_$_CATEGORY_Person_$_Test
結構體跟上文提到的catrgory_t
結構體對照:
不難看出,上下兩圖中兩個結構體內容一一對應,并且我們在紅框標注的方法中看到,定義的_class_t
類型的OBJC_CLASS_$_Person
結構體,最后將_OBJC_$_CATEGORY_Person_$_Test
的cls
指針指向OBJC_CLASS_$_Person
結構體地址。我們可以得出結論,cls
指針指向的應該是分類的主類類對象
的地址。
通過以上分析我們發現,分類確實是將我們定義的對象方法
,類方法
,屬性
等都存放在catagory_t
結構體中。那么catagory_t
結構體又如何讓將這些信息存儲到類對象中呢?我們通過分析runtime
的源碼來進一步了解。
runtime源碼
我們通過opensource網站下載最新的源碼來進一步分析。
首先來到runtime初始化函數
接著我們來到&map_images
讀取模塊,來到map_images_nolock
函數中找到_read_images
函數,在_read_images
函數中我們找到分類相關代碼:
從上述代碼中for
循環中的判斷我們可以知道這段代碼是用來檢查有沒有分類的。通過_getObjc2CategoryList
函數獲取到分類列表之后,進行遍歷,獲取其中的方法,協議,屬性等。可以看到最終都調用了remethodizeClass(cls)
函數。我們來到remethodizeClass(cls)
函數內部查看:
通過上述代碼我們發現attachCategories
函數接收了類對象cls
和分類數組cats
,當然一個類可以有多個分類,分類信息存儲在category_t
結構體中,那么多個分類則保存在category_list
中。
我們來到attachCategories
函數內部:
上述源碼中可以看出,首先根據方法列表
,屬性列表
,協議列表
通過malloc
分配內存,根據多少個分類以及每一塊方法需要多少內存來分配相應的內存地址。之后從分類數組里面往三個數組里面存放分類數組里面存放的分類方法
,屬性
以及協議
放入對應mlist
、proplists
、protolosts
數組中,這三個數組放著所有分類的方法
,屬性
和協議
。
之后通過類對象的data()
方法,拿到類對象的class_rw_t
結構體rw
,在class
結構中我們介紹過,class_rw_t
中存放著類對象的方法
,屬性
和協議
等數據,rw結構體
通過類對象的data
方法獲取,所以rw
里面存放這類對象里面的數據。
之后分別通過rw
調用方法列表
、屬性列表
、協議列表
的attachList
函數,將所有的分類的方法
、屬性
、協議列表
數組傳進去,我們大致可以猜想到在attachList
方法內部將分類和本類相應的對象方法
,屬性
和協議
進行了合并。
我們來看一下attachLists函數內部查看:
上述源代碼中有兩個重要的數組
array()->lists: 類對象原來的方法列表,屬性列表,協議列表。
addedLists:傳入所有分類的方法列表,屬性列表,協議列表。
attachLists
函數中最重要的兩個方法為memmove
內存移動和memcpy
內存拷貝。我們先來分別看一下這兩個函數
// memmove :內存移動。
/* __dst : 移動內存的目的地
* __src : 被移動的內存首地址
* __len : 被移動的內存長度
* 將__src的內存移動__len塊內存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :內存拷貝。
/* __dst : 拷貝內存的拷貝目的地
* __src : 被拷貝的內存首地址
* __n : 被移動的內存長度
* 將__src的內存移動__n塊內存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面我們圖示經過memmove
和memcpy
方法過后的內存變化:
首先未經過內存移動和拷貝時:
經過memmove
方法之后,內存變化為:
// 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);
}
我們在控制器中引入Person
類,在控制器的viewDidLoad
方法中創建Person
對象,并且調用run
方法和上面的打印所有類的所有方法名的方法:
- (void)viewDidLoad {
[super viewDidLoad];
Preson *p = [[Preson alloc] init];
[p run];
[self printMethodNamesOfClass:[Preson class]];
}
通過打印臺打印內容可以發現,調用的是分類中的run
方法,并且Person
類中存儲著兩個run
方法。
關于Category的底層原理探索我們告一段落,如有疑問,歡迎在評論區留言。
更多技術知識請關注公眾號
iOS進階
iOS進階.jpg