問題
-
Category
的實現(xiàn)原理,以及Category
為什么只能加方法不能加屬性。 -
Category
和Extension
的區(qū)別是什么? -
Category
中有load
方法嗎?load
方法是什么時候調(diào)用的?load
方法能繼承嗎? -
load
、initialize
的區(qū)別,以及它們在Category
重寫的時候的調(diào)用的次序。 -
Category
能否添加成員變量?如果可以,如何給Category
添加成員變量?
1. Category
的使用
使用下面的這一段簡單代碼來分析:
// Preson類
// Preson.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
int _age;
}
- (void)run;
@end
// Preson.m
#import "Preson.h"
@implementation Preson
- (void)run
{
NSLog(@"Person - run");
}
@end
// Preson擴(kuò)展1
// Preson+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)abc;
@property (assign, nonatomic) int age;
- (void)setAge:(int)age;
- (int)age;
@end
// Preson+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}
+ (void)abc
{
}
- (void)setAge:(int)age
{
}
- (int)age
{
return 10;
}
@end
// Preson分類2
// Preson+Test2.h
#import "Preson.h"
@interface Preson (Test2)
@end
// Preson+Test2.m
#import "Preson+Test2.h"
@implementation Preson (Test2)
- (void)run
{
NSLog(@"Person (Test2) - run");
}
@end
我們之前講到過實例對象的isa
指針指向類對象,類對象的isa
指針指向元類對象,當(dāng)p
調(diào)用run
方法時,通過實例對象的isa
指針找到類對象,然后在類對象中查找對象方法,如果沒有找到,就通過類對象的super_class
指針找到父類對象,接著去尋找run
方法。
那么當(dāng)調(diào)用分類的方法時,步驟是否和調(diào)用對象方法一樣呢?
分類中的對象方法依然是存儲在類對象中的,同本類對象方法在同一個地方,調(diào)用步驟也同調(diào)用對象方法一樣。如果是類方法的話,也同樣是存儲在元類對象中。
那么分類方法是如何存儲在類對象中的,我們來通過源碼看一下分類的底層結(jié)構(gòu)。
2. 分類的底層結(jié)構(gòu)
擴(kuò)展的方法不是在編譯時期合并至原來的類,而是在運(yùn)行時合并的。
我們將OC
編譯的文件生成底層的C++
實現(xiàn)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Preson+Test.m
2.1 分類結(jié)構(gòu)體
在分類轉(zhuǎn)化為C++
文件中可以找到_category_t
結(jié)構(gòu)體中,存放著類名,對象方法列表,類方法列表,協(xié)議列表,以及屬性列表。
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; // 屬性列表
};
2.2 分類結(jié)構(gòu)體的成員列表
2.2.1 分類的實例方法結(jié)構(gòu)體
存放實例方法_method_list_t
類型的結(jié)構(gòu)體,如下所示
static struct /*_method_list_t*/ {
unsigned int entsize; // 方法占用的內(nèi)存
unsigned int method_count; // 方法數(shù)量
struct _objc_method method_list[3]; // 方法列表
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_Test_test},
{(struct objc_selector *)"setAge:", "v20@0:8i16", (void *)_I_Person_Test_setAge_},
{(struct objc_selector *)"age", "i16@0:8", (void *)_I_Person_Test_age}}
};
上面中我們發(fā)現(xiàn)這個結(jié)構(gòu)體 _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test
從名稱可以看出是INSTANCE_METHODS
對象方法,并且一一對應(yīng)為上面結(jié)構(gòu)體內(nèi)賦值。我們可以看到結(jié)構(gòu)體中存儲了方法占用的內(nèi)存,方法數(shù)量,以及方法列表。并且從上圖中找到分類中我們添加的對象方法,test
, setAge
, age
三個方法。
2.2.2 分類的類方法結(jié)構(gòu)體
存放類方法_method_list_t
類型的類方法結(jié)構(gòu)體,如下所示
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"abc", "v16@0:8", (void *)_C_Person_Test_abc}}
};
同上面實例方法列表一樣,這個我們可以看出是類方法列表結(jié)構(gòu)體 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test
,同對象方法結(jié)構(gòu)體相同,同樣可以看到我們實現(xiàn)的類方法abc
。
2.2.3 分類的協(xié)議方法結(jié)構(gòu)體
存放協(xié)議列表結(jié)構(gòu)體_protocol_list_t
,假如我們實現(xiàn)了NSCopying
協(xié)議
static const char *_OBJC_PROTOCOL_METHOD_TYPES_NSCopying [] __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"@24@0:8^{_NSZone=}16"
};
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"copyWithZone:", "@24@0:8^{_NSZone=}16", 0}}
};
struct _protocol_t _OBJC_PROTOCOL_NSCopying __attribute__ ((used)) = {
0,
"NSCopying",
0,
(const struct method_list_t *)&_OBJC_PROTOCOL_INSTANCE_METHODS_NSCopying,
0,
0,
0,
0,
sizeof(_protocol_t),
0,
(const char **)&_OBJC_PROTOCOL_METHOD_TYPES_NSCopying
};
struct _protocol_t *_OBJC_LABEL_PROTOCOL_$_NSCopying = &_OBJC_PROTOCOL_NSCopying;
static struct /*_protocol_list_t*/ {
long protocol_count; // 協(xié)議數(shù)量
struct _protocol_t *super_protocols[1]; // 存儲協(xié)議方法
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
1,
&_OBJC_PROTOCOL_NSCopying
};
通過上述源碼可以看到先將協(xié)議方法通過_method_list_t
結(jié)構(gòu)體存儲
之后通過_protocol_t
結(jié)構(gòu)體存儲在_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test
中同_protocol_list_t
結(jié)構(gòu)體一一對應(yīng),分別為protocol_count
協(xié)議數(shù)量以及存儲了協(xié)議方法的_protocol_t
結(jié)構(gòu)體。
2.2.4 分類的屬性列表
最后我們可以看到屬性列表結(jié)構(gòu)體_prop_list_t
static struct /*_prop_list_t*/ {
unsigned int entsize; // 占用空間
unsigned int count_of_properties; // 屬性數(shù)量
struct _prop_t prop_list[1]; // 屬性列表
} _OBJC_$_PROP_LIST_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"age","Ti,N"}} // age屬性
};
屬性列表結(jié)構(gòu)體_OBJC_$_PROP_LIST_Person_$_Test
同_prop_list_t
結(jié)構(gòu)體對應(yīng),存儲屬性的占用空間,屬性屬性數(shù)量,以及屬性列表,從上圖中可以看到我們自己添加的age
屬性。
2.2.5 分類_category_t
結(jié)構(gòu)體總結(jié)
最后我們可以看到定義了_OBJC_$_CATEGORY_Person_$_Test
結(jié)構(gòu)體,并且將我們上面著重分析的結(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;
const struct _prop_list_t *properties;
};
//************************ 上下一一對應(yīng) ******************************************
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_Person;
static struct _category_t _OBJC_$_CATEGORY_Person_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Test,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Test,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Test,
};
static void OBJC_CATEGORY_SETUP_$_Person_$_Test(void ) {
_OBJC_$_CATEGORY_Person_$_Test.cls = &OBJC_CLASS_$_Person;
}
并且我們看到定義原類_class_t
類型的OBJC_CLASS_$_Preson
結(jié)構(gòu)體
最后將分類_OBJC_$_CATEGORY_Person_$_Test
的cls
指針指向原類OBJC_CLASS_$_Preson
結(jié)構(gòu)體地址。我們這里可以看出,cls
指針指向的應(yīng)該是原類的類對象的地址。
3. 源碼分析
通過查看分類的源碼我們可以找到底層分類category_t
結(jié)構(gòu)體。
objc源碼路徑:runtime/objc-runtime-new.h
struct category_t {
const char *name; // 類名
classref_t cls;
struct method_list_t *instanceMethods; // 對象方法
struct method_list_t *classMethods; // 類方法
struct protocol_list_t *protocols; // 協(xié)議
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
的方式,對象方法,類方法,協(xié)議,和屬性都可以找到對應(yīng)的存儲方式。并且我們發(fā)現(xiàn)分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量的。分類中添加的屬性并不會幫助我們自動生成成員變量,只會生成get set
方法的聲明,需要我們自己去實現(xiàn)。
通過源發(fā)現(xiàn),分類的方法,協(xié)議,屬性等好像確實是存放在categroy
結(jié)構(gòu)體里面的,那么他又是如何合并存儲到原類的類對象中的?
3.1 分類是如何存儲方法,屬性,協(xié)議的
通過我們runtime
的初始化函數(shù)_objc_init
來探尋答案。
objc源碼路徑:runtime/objc-os.mm
入口函數(shù):_objc_init
:
/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// images指是鏡像、模塊
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
我們來到&map_images
讀取模塊(images
這里代表模塊)
↓↓↓
來到map_images_nolock
函數(shù)
↓↓↓
來到_read_images
函數(shù)
最終在_read_images
函數(shù)中找到分類相關(guān)代碼:
objc源碼路徑:runtime/objc-runtime-new.mm
// Discover categories.
for (EACH_HEADER) {
// 獲取到分類列表
category_t **catlist = _getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
// 遍歷,獲取其中的方法,協(xié)議,屬性
// 內(nèi)部調(diào)用 remethodizeClass
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
// 重新整合分類里面新添加的東西
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
從上述代碼中我們可以知道這段代碼是用來查找有沒有分類的。通過_getObjc2CategoryList
函數(shù)獲取到分類列表之后,進(jìn)行遍歷,獲取其中的方法,協(xié)議,屬性等。可以看到最終都調(diào)用了remethodizeClass(cls)
函數(shù)。我們來到remethodizeClass(cls)
函數(shù)內(nèi)部查看:
↓↓↓
remethodizeClass(cls)
函數(shù):
/***********************************************************************
* remethodizeClass
* Attach outstanding categories to an existing class.
* Fixes up cls's method list, protocol list, and property list.
* Updates method caches for cls and its subclasses.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void remethodizeClass(Class cls)
{
category_list *cats;
bool isMeta;
runtimeLock.assertLocked();
isMeta = cls->isMetaClass();
// Re-methodizing: check for more categories
if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
if (PrintConnecting) {
_objc_inform("CLASS: attaching categories to class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// 開始附加分類的相關(guān)信息至原類
attachCategories(cls, cats, true /*flush caches*/);
free(cats);
}
}
通過上述代碼我們發(fā)現(xiàn)attachCategories
函數(shù)接收了類對象cls
和分類數(shù)組cats
,如我們一開始寫的代碼所示,一個類可以有多個分類。之前我們說到分類信息存儲在category_t
結(jié)構(gòu)體中,那么多個分類則保存在category_list
中。
↓↓↓
我們來到attachCategories()
函數(shù)內(nèi)部:
/** 接受的參數(shù)
Class cls: 原類的類對象
category_list *cats: 所有的分類列表
*/
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
// 0. 根據(jù)每個分類中方法列表、屬性列表、協(xié)議列表分類存儲
// 方法數(shù)組
/** 是一個二維數(shù)組
[
[// 第一個分類的方法
method_list_t,
method_list_t
],
[// 第二個分類的方法
method_list_t,
method_list_t
]
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
// 屬性數(shù)組
/** 是一個二維數(shù)組
[
[// 第一個分類的屬性
property_list_t,
property_list_t
],
[// 第二個分類的屬性
property_list_t,
property_list_t
]
]
*/
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
// 協(xié)議數(shù)組
/** 是一個二維數(shù)組
[
[// 第一個分類的協(xié)議
protocol_list_t,
protocol_list_t
],
[// 第二個分類的協(xié)議
protocol_list_t,
protocol_list_t
]
]
*/
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// 1. 遍歷每一個分類
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
// 取出一個分類
auto& entry = cats->list[i];
// 1.1 將所有分類中的所有方法合并存入mlist數(shù)組中
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
// 1.2 將所有分類中的所有屬性合并存入proplist數(shù)組中
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
// 1.3 將所有分類中的所有協(xié)議合并存入protolist數(shù)組中
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 2 調(diào)用attachLists()方法,賦值合并至原來的類對象
// 獲取原來的類對象rw:即 class_rw_t結(jié)構(gòu)體,是class結(jié)構(gòu)體中用來存儲類對象中的對象方法、屬性、協(xié)議的結(jié)構(gòu)體
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 2.1 將所有分類的類對象方法mlists數(shù)組,合并附加到類對象的方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
// 2.2 將所有分類的屬性proplists數(shù)組,合并附加到類對象的屬性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 2.3 將所有分類的協(xié)議protolists數(shù)組,合并附加到類對象的屬性列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
上述源碼中可以看出,首先根據(jù)方法列表,屬性列表,協(xié)議列表,malloc
分配內(nèi)存,根據(jù)多少個分類以及每一塊方法需要多少內(nèi)存來分配相應(yīng)的內(nèi)存地址。
之后將每一個分類的所有的方法、屬性、協(xié)議分別放入對應(yīng)mlist
、proplists
、protolosts
數(shù)組中,這三個數(shù)組放著所有分類的所有方法,屬性和協(xié)議。
之后通過類對象的data()
方法,拿到類對象的class_rw_t
結(jié)構(gòu)體rw
,在class
結(jié)構(gòu)中我們介紹過,class_rw_t
中存放著類對象的方法,屬性和協(xié)議等數(shù)據(jù),rw
結(jié)構(gòu)體通過類對象的data
方法獲取,所以rw
里面存放這類對象里面的數(shù)據(jù)。
之后分別通過rw
調(diào)用方法列表、屬性列表、協(xié)議列表的attachList
函數(shù),將所有的分類的方法、屬性、協(xié)議列表數(shù)組傳進(jìn)去。
我們大致可以猜想到在attachList
方法內(nèi)部將分類和本類相應(yīng)的對象方法,屬性,和協(xié)議進(jìn)行了合并。
↓↓↓
我們來到attachLists
函數(shù)內(nèi)部:
objc源碼路徑:runtime/objc-runtime-new.h
/** 接收參數(shù)
addedLists:二維數(shù)組
addedCount: 二位數(shù)組數(shù)量
*/
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 原來的列表數(shù)組
uint32_t oldCount = array()->count;
// 1. 重新申請內(nèi)存
// 新的的列表數(shù)量:原來的數(shù)量 + 新添加的分類的數(shù)量
uint32_t newCount = oldCount + addedCount;
// 將原來的數(shù)組進(jìn)行擴(kuò)容,來存放新添加
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
// 數(shù)組數(shù)量為新的值
array()->count = newCount;
// 2. 移動原來的方法列表
// array()->lists 是原來的方法列表位置
// oldCount * sizeof(array()->lists[0] 是需要移動的字節(jié)數(shù)
// array()->lists + addedCount新的位置
// 內(nèi)存移動,將原來的方法列表移動到新的位置,相當(dāng)于前面空出了位置
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
// 3. 內(nèi)存復(fù)制
// array()->lists 是原來的列表位置
// addedLists是所有的分類的所有列表
// addedCount * sizeof(array()->lists[0])是需要的字節(jié)數(shù)
// 內(nèi)存復(fù)制:將所有的分類復(fù)制到原來的位置,在上面一步已經(jīng)提前空出了位置。
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));;
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
由上面的源碼中可以看出,新添加的分類的其實是相當(dāng)于移動到了原來存儲的列表之前的,所以原類和分類重名的時候會優(yōu)先調(diào)用分類的方法。
另外,當(dāng)多個分類有重名的方法的時候由源碼的attachCategories()
函數(shù)可以知道,其實是有優(yōu)先調(diào)用最后參與編譯的分類的方法:
...
// 這兒是i--
while (i--) {
// 取出一個分類
auto& entry = cats->list[i];
...
}
...
3.2 Extension
類擴(kuò)展
類擴(kuò)展其實就是我們平常寫的@interface
// 類擴(kuò)展
@interface AppDelegate ()
// 可以擴(kuò)展一些私有的成員變量、屬性、方法
@end
// 實現(xiàn)
@implementation AppDelegate
@end
和分類categroy
不同的是:類擴(kuò)展的信息是在編譯的時候已經(jīng)合并在了類對象中,而分類是在運(yùn)行時合并至原來中的。
3.3 memmove
和memcpy
上面的源代碼中有兩個重要的數(shù)組:
-
array()->lists
: 原類對象原來的方法列表,屬性列表,協(xié)議列表。 -
addedLists
:傳入所有分類的方法列表,屬性列表,協(xié)議列表。
attachLists
函數(shù)中最重要的兩個方法為memmove
內(nèi)存移動和memcpy
內(nèi)存拷貝。我們先來分別看一下這兩個函數(shù):
// memmove :內(nèi)存移動。
/* __dst : 移動內(nèi)存的目的地
* __src : 被移動的內(nèi)存首地址
* __len : 被移動的內(nèi)存長度
* 將__src的內(nèi)存移動__len塊內(nèi)存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :內(nèi)存拷貝。
/* __dst : 拷貝內(nèi)存的拷貝目的地
* __src : 被拷貝的內(nèi)存首地址
* __n : 被移動的內(nèi)存長度
* 將__src的內(nèi)存移動__n塊內(nèi)存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面我們圖示經(jīng)過memmove
和memcpy
方法過后的內(nèi)存變化。
3.3.1 內(nèi)存移動memmove
經(jīng)過memmove
方法之后,內(nèi)存變化為:
// array()->lists 原來方法、屬性、協(xié)議列表數(shù)組
// addedCount 分類數(shù)組長度
// oldCount * sizeof(array()->lists[0]) 原來數(shù)組占據(jù)的空間
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
經(jīng)過memmove
方法之后,我們發(fā)現(xiàn),雖然本類的方法,屬性,協(xié)議列表會分別后移,但是本類的對應(yīng)數(shù)組的指針依然指向原始位置。
3.3.2 內(nèi)存復(fù)制memcpy
// array()->lists 原來方法、屬性、協(xié)議列表數(shù)組
// addedLists 分類方法、屬性、協(xié)議列表數(shù)組
// addedCount * sizeof(array()->lists[0]) 原來數(shù)組占據(jù)的空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
我們發(fā)現(xiàn)原來指針并沒有改變,至始至終指向開頭的位置。并且經(jīng)過memmove
和memcpy
方法之后,分類的方法,屬性,協(xié)議列表被放在了類對象中原本存儲的方法,屬性,協(xié)議列表前面。
那么為什么要將分類方法的列表追加到本來的對象方法前面呢,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫本類的方法時,會覆蓋本類的方法。
其實經(jīng)過上面的分析我們知道本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用。本類的方法依然在內(nèi)存中的。我們可以通過打印所有類的所有方法名來查看
2020-01-14 11:47:02.458927+0800 分類Category的本質(zhì)[84870:4069262] Person (Test2) - run
2020-01-14 11:47:02.459080+0800 分類Category的本質(zhì)[84870:4069262] Person類: age, run, run, setAge:, test,
調(diào)用的是Test2
中的run
方法,并且Person
類中存儲著兩個run
方法。
3.4 總結(jié)
Category
的實現(xiàn)原理,以及Category
為什么只能加方法不能加屬性?
分類的實現(xiàn)原理是將Category
中的方法,屬性,協(xié)議數(shù)據(jù)放在category_t
結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對象的方法列表中。
Category
可以添加屬性,但是并不會自動生成成員變量及set get
方法。因為category_t
結(jié)構(gòu)體中并不存在成員變量。
通過之前對對象的分析我們知道成員變量是存放在實例對象中的,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運(yùn)行時才去加載的。那么我們就無法再程序運(yùn)行時將分類的成員變量中添加到實例對象的結(jié)構(gòu)體中。因此分類中不可以添加成員變量。
Category
和Extension
的區(qū)別是什么?
和分類Categroy
不同的是:類擴(kuò)展的信息是在編譯的時候已經(jīng)合并在了類對象中,而分類是在運(yùn)行時合并至原類中的。
4. load
和initialize
4.1 load
方法
4.4.1 基本使用
load
方法是runtime在加載類和分類的時候會調(diào)用,是在程序入口調(diào)用函數(shù)main
之前調(diào)用,而且只會調(diào)用一次。
通過代碼驗證一下調(diào)用本類的load
方法調(diào)用。
我們添加Student
繼承Presen
類,并添加Student+Test
分類,分別重寫+load
方法。
2020-01-14 14:07:53.689561+0800 分類Category的本質(zhì)[92689:4179307] Person - load
2020-01-14 14:07:53.690079+0800 分類Category的本質(zhì)[92689:4179307] Student - load
2020-01-14 14:07:53.690142+0800 分類Category的本質(zhì)[92689:4179307] Student (Test) - load
通過驗證我們發(fā)現(xiàn)不僅調(diào)用了分類的load
方法,而且調(diào)用了原類的load
方法,這和上面我們驗證的優(yōu)先調(diào)用分類的方法的邏輯相沖突,到底為什么會調(diào)用原類的方法,我們通過底層的源碼一探究竟。
4.4.2 調(diào)用原理
同樣的我們從runtime
的入口_objc_init
函數(shù)的load_images
函數(shù)尋找答案:
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
static_init();
lock_init();
exception_init();
// images指是鏡像、模塊
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
}
↓↓↓
最終來到了call_load_methods
函數(shù)
objc源碼路徑:runtime/objc-loadmethod.mm
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
// 先調(diào)用本類的load方法
call_class_loads();
}
// 2. Call category +loads ONCE
// 再調(diào)用分類的load方法
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
↓↓↓
先調(diào)用原類的load
方法:
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 得到load方法的函數(shù)地址
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
// 直接調(diào)用原類的load方法
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
↓↓↓
再調(diào)用分類的load
方法:
static bool call_category_loads(void)
{
int i, shift;
bool new_categories_added = NO;
// Detach current loadable list.
struct loadable_category *cats = loadable_categories;
int used = loadable_categories_used;
int allocated = loadable_categories_allocated;
loadable_categories = nil;
loadable_categories_allocated = 0;
loadable_categories_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Category cat = cats[i].cat;
// 獲取分類的load方法地址
load_method_t load_method = (load_method_t)cats[i].method;
Class cls;
if (!cat) continue;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
if (PrintLoading) {
_objc_inform("LOAD: +[%s(%s) load]\n",
cls->nameForLogging(),
_category_getName(cat));
}
// 調(diào)用分類的load方法
(*load_method)(cls, @selector(load));
cats[i].cat = nil;
}
}
...
...
...
}
和runtime
的消息發(fā)送機(jī)制不同的是,消息發(fā)送機(jī)制是需要通過isa
指針去逐層尋找,而load
方法是不需要通過消息發(fā)送的,而是直接通過函數(shù)的地址來調(diào)用的
struct loadable_class {
Class cls; // may be nil
IMP method; // 函數(shù)實現(xiàn)地址,指向的是原類的load方法
};
struct loadable_category {
Category cat; // may be nil
IMP method; // 函數(shù)的實現(xiàn)地址,指向的是分類的load方法
};
4.4.3 調(diào)用順序
即使是再復(fù)雜繼承關(guān)系,原類、分類、子類的load
方法都會被調(diào)用,并且是按照一定的順序調(diào)用的。
通過上面的源碼可以看到在調(diào)用原類和分類的load
方法的時候,都是分別通過一個數(shù)組loadable_classes
、loadable_categories
進(jìn)行for
循環(huán)去遍歷的,所以知道數(shù)組的順序,就可以知道方法的調(diào)用順序
我們回到之前的入口函數(shù)load_images
,發(fā)現(xiàn)在調(diào)用call_load_methods()
函數(shù)之前調(diào)用了prepare_load_methods()
方法:
void
load_images(const char *path __unused, const struct mach_header *mh)
{
// Return without taking locks if there are no +load methods here.
if (!hasLoadMethods((const headerType *)mh)) return;
recursive_mutex_locker_t lock(loadMethodLock);
// Discover load methods
{
mutex_locker_t lock2(runtimeLock);
// 之前調(diào)用
prepare_load_methods((const headerType *)mh);
}
// Call +load methods (without runtimeLock - re-entrant)
call_load_methods();
}
↓↓↓
void prepare_load_methods(const headerType *mhdr)
{
size_t count, i;
runtimeLock.assertLocked();
// 1. 先遍歷所有的原類的的load方法
// 這個順序是有編譯順序決定的,可以手動設(shè)置順序
classref_t const *classlist =
_getObjc2NonlazyClassList(mhdr, &count);
for (i = 0; i < count; i++) {
// 1.1 先將父類的load方法進(jìn)行添加
// 1.2 再將子類的load方法進(jìn)行添加
schedule_class_load(remapClass(classlist[i]));
}
// 2. 再遍歷分類的load方法
// 這個順序也是有編譯順序決定的,可以手動設(shè)置編譯順序
category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
for (i = 0; i < count; i++) {
category_t *cat = categorylist[i];
Class cls = remapClass(cat->cls);
if (!cls) continue; // category for ignored weak-linked class
if (cls->isSwiftStable()) {
_objc_fatal("Swift class extensions and categories on Swift "
"classes are not allowed to have +load methods");
}
realizeClassWithoutSwift(cls, nil);
ASSERT(cls->ISA()->isRealized());
add_category_to_loadable_list(cat);
}
}
↓↓↓
原類的load
方法添加到loadable_classes
數(shù)組的順序:
// 1. 會先添加父類的load方法
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// 先通過遞歸調(diào)用
// 將父類的load方法添加到loadable_classes數(shù)組
schedule_class_load(cls->superclass);
// 再將子類的cls添加到loadable_classes數(shù)組的
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
// 2. 添加到loadable_classes數(shù)組
void add_class_to_loadable_list(Class cls)
{
IMP method;
loadMethodLock.assertLocked();
method = cls->getLoadMethod();
if (!method) return; // Don't bother if cls has no +load method
if (PrintLoading) {
_objc_inform("LOAD: class '%s' scheduled for +load",
cls->nameForLogging());
}
if (loadable_classes_used == loadable_classes_allocated) {
loadable_classes_allocated = loadable_classes_allocated*2 + 16;
loadable_classes = (struct loadable_class *)
realloc(loadable_classes,
loadable_classes_allocated *
sizeof(struct loadable_class));
}
loadable_classes[loadable_classes_used].cls = cls;
loadable_classes[loadable_classes_used].method = method;
loadable_classes_used++;
}
分類的load
方法添加到loadable_categories
數(shù)組的順序:
// 直接添加到loadable_categories數(shù)組
void add_category_to_loadable_list(Category cat)
{
IMP method;
loadMethodLock.assertLocked();
method = _category_getLoadMethod(cat);
// Don't bother if cat has no +load method
if (!method) return;
if (PrintLoading) {
_objc_inform("LOAD: category '%s(%s)' scheduled for +load",
_category_getClassName(cat), _category_getName(cat));
}
if (loadable_categories_used == loadable_categories_allocated) {
loadable_categories_allocated = loadable_categories_allocated*2 + 16;
loadable_categories = (struct loadable_category *)
realloc(loadable_categories,
loadable_categories_allocated *
sizeof(struct loadable_category));
}
loadable_categories[loadable_categories_used].cat = cat;
loadable_categories[loadable_categories_used].method = method;
loadable_categories_used++;
}
通過以上源碼的邏輯處理,我們發(fā)現(xiàn)數(shù)組的添加順序?qū)е铝?code>load方法的調(diào)用順序:先將元類添加到數(shù)組,同時會先去將父類添加到數(shù)組,再講子類添加到數(shù)組,最后將分類添加到數(shù)組。所以在調(diào)用load
方法的時候也會是按照類的添加順序來調(diào)用。
4.4.4 總結(jié)
-
先調(diào)用所有原類的
laod
方法- 按照編譯順序調(diào)用(可以手動設(shè)置編譯順序)
- 調(diào)用子類的
load
之前會先調(diào)用父類的load
方法
-
再調(diào)用分類的
laod
方法- 按照編譯順序調(diào)用(可以手動設(shè)置編譯順序)
Category
中有load
方法嗎?load
方法是什么時候調(diào)用的?load
方法能繼承嗎?
Category
中有load
方法,load
方法在程序加載了類和分類的時候就會調(diào)用,在main
函數(shù)之前調(diào)用。load
方法可以繼承。調(diào)用子類的load
方法之前,會先調(diào)用父類的load
方法。一般我們不會手動去調(diào)用load
方法,而是讓系統(tǒng)去調(diào)用。
如果非要手動調(diào)用load
方法,那么就會按照消息發(fā)送機(jī)制通過isa
指針來尋找方法。
4.2 initialize
方法
我們?yōu)?code>Preson、Person+Test
、Student
、Student+Test
添加initialize
方法。
4.2.1 基本使用
initialize
類第一次接收到消息時,就會調(diào)用,相當(dāng)于第一次使用類的時候就會調(diào)用initialize
方法。
initialize
方法的調(diào)用是通過消息發(fā)送機(jī)制調(diào)用的,不像load
方法是直接通過函數(shù)指針去調(diào)用。
再來驗證一下調(diào)用順序:
2020-04-28 21:22:18.874260+0800 分類Category的本質(zhì)[90053:17829138] Person (Test) initialize
2020-04-28 21:22:18.874726+0800 分類Category的本質(zhì)[90053:17829138] Student (Test) initialize
調(diào)用子類的initialize
之前,會先保證調(diào)用父類的initialize
方法。如果之前已經(jīng)調(diào)用過initialize
,就不會再調(diào)用initialize
方法了。
當(dāng)分類重寫initialize
方法時會先調(diào)用分類的方法不再調(diào)用原類的方法。
initialize
是通過消息發(fā)送機(jī)制調(diào)用的,消息發(fā)送機(jī)制通過isa
指針找到對應(yīng)的方法與實現(xiàn),因此優(yōu)先找到分類方法中的實現(xiàn),會優(yōu)先調(diào)用分類方法中的實現(xiàn)。
另外還有一點需要注意:如果子類沒有實現(xiàn)initialize
方法,那么會調(diào)用父類的initialize
方法,這一點我們會通過源碼去驗證。
4.2.2 源碼分析
在底層源碼里面通過函數(shù)class_getInstanceMethod
和函數(shù)class_getClassMethod
來找到實例方法和類方法
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
Method meth;
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
}
// 搜索方法
lookUpImpOrForward(nil, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER);
meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache);
if (meth == (Method)1) {
// Cache contains forward:: . Stop searching.
return nil;
} else if (meth) {
return meth;
}
return _class_getMethod(cls, sel);
}
↓↓↓
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
Class curClass;
IMP methodPC = nil;
Method meth;
bool triedResolver = NO;
methodListLock.assertUnlocked();
// Optimistic cache lookup
if (behavior & LOOKUP_CACHE) {
methodPC = _cache_getImp(cls, sel);
if (methodPC) goto out_nolock;
}
// Check for freed class
if (cls == _class_getFreedObjectClass())
return (IMP) _freedHandler;
// 檢查是否已經(jīng)調(diào)用了+initialize方法,如果沒有調(diào)用過initialize方法
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
// 調(diào)用initialize方法
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
// If sel == initialize, initializeNonMetaClass will send +initialize
// and then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
...
...
...
}
↓↓↓
void initializeNonMetaClass(Class cls)
{
ASSERT(!cls->isMetaClass());
Class supercls;
bool reallyInitialize = NO;
// 在調(diào)用initialize方法之前,需要先判斷是否調(diào)用了父類的initialize方法
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
...
...
...
#if __OBJC2__
@try
#endif
{
// 開始調(diào)用initialize方法
callInitialize(cls);
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
objc_thread_self(), cls->nameForLogging());
}
}
#if __OBJC2__
@catch (...) {
if (PrintInitializing) {
_objc_inform("INITIALIZE: thread %p: +[%s initialize] "
"threw an exception",
objc_thread_self(), cls->nameForLogging());
}
@throw;
}
@finally
#endif
{
// Done initializing.
lockAndFinishInitializing(cls, supercls);
}
return;
}
else if (cls->isInitializing()) {
...
...
...
}
else if (cls->isInitialized()) {
return;
}
else {
// We shouldn't be here.
_objc_fatal("thread-safe class init in objc runtime is buggy!");
}
}
↓↓↓
void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
asm("");
}
最終來到了消息發(fā)送objc_msgSend
函數(shù),給類對象發(fā)送initialize
消息,消息發(fā)送機(jī)制通過isa
指針找到對應(yīng)的方法與實現(xiàn)。
上面我們說到的如果子類沒有實現(xiàn)initialize
方法,那么當(dāng)我們有多個類繼承了父類的時候,父類的initialize
方法有可能會調(diào)用多次:
[[Student alloc] init];
[[Student1 alloc] init];
Student
和Student1
都繼承自Person
,只實現(xiàn)了兩個子類的initialize
方法,沒有實現(xiàn)父類的,那么當(dāng)?shù)谝淮握{(diào)用兩個子類的時候會輸出如下:
2020-04-28 22:20:48.690528+0800 分類Category的本質(zhì)[92558:17870282] Person initialize
2020-04-28 22:20:48.690715+0800 分類Category的本質(zhì)[92558:17870282] Person initialize
2020-04-28 22:20:48.690859+0800 分類Category的本質(zhì)[92558:17870282] Person initialize
我們發(fā)現(xiàn)父類的initialize
方法調(diào)用了3次,我們通過上面的源碼我們已經(jīng)知道了只有當(dāng)類在第一次接收消息的時候才會被調(diào)用,也就是說每個類只會initialize
一次,但是父類的``initialize```為什么會被多次調(diào)用呢?
我們通過底層的邏輯將上面的代碼轉(zhuǎn)化為偽代碼:
// 1. 調(diào)用子類Student之前先判斷父類的initialize
if (Student沒有調(diào)用initialize) {
if (Student的父類Person沒有調(diào)用initialize) {
// 1.1 調(diào)用父類Person的initialize
objc_msgSend)(Person類, SEL_initialize)
}
}
// 1.2 調(diào)用子類Student的initialize
objc_msgSend)(Student類, SEL_initialize)
// 2. 調(diào)用子類Student1之前先判斷父類的initialize
if (Student1沒有調(diào)用initialize) {
// **在此,父類已經(jīng)initialize了,所以不再執(zhí)行**
if (Student1的父類Person沒有調(diào)用initialize) {
// 2.1 調(diào)用父類Person的initialize
objc_msgSend)(Person類, SEL_initialize)
}
}
// 2.2 調(diào)用子類Student1的initialize
objc_msgSend)(Student1類, SEL_initialize)
所以其實相當(dāng)于最后發(fā)送了3次消息:
objc_msgSend)(Person類, SEL_initialize)
objc_msgSend)(Student類, SEL_initialize)
objc_msgSend)(Student1類, SEL_initialize)
消息發(fā)送機(jī)制是通過isa
指針尋找到各自的元類對象中的類方法initialize
的實現(xiàn),但是兩個子類都沒有實現(xiàn)initialize
方法,所以會通過super_class
指針找到父類的實現(xiàn),所有最后才會來到父類的initialize
方法。
注意:雖然父類被多次調(diào)用initialize
方法,但是父類也是只初始化了一次。
4.3 總結(jié)
load
、initialize
的區(qū)別,以及它們在category
重寫的時候的調(diào)用的次序。
區(qū)別在于調(diào)用方式和調(diào)用時刻
調(diào)用方式:load
是根據(jù)函數(shù)地址直接調(diào)用,initialize
是通過objc_msgSend
消息發(fā)送調(diào)用
調(diào)用時刻:load
是runtime
加載類、分類的時候調(diào)用(只會調(diào)用1次),initialize
是類第一次接收到消息的時候調(diào)用,每一個類只會initialize
一次(父類的initialize
方法可能會被調(diào)用多次)
調(diào)用順序:先調(diào)用本類的load
方法,先編譯那個類,就先調(diào)用load
。在調(diào)用load
之前會先調(diào)用父類的load
方法。分類中load
方法不會覆蓋本類的load
方法,先編譯的分類優(yōu)先調(diào)用load
方法。initialize
先初始化父類,之后再初始化子類。如果子類沒有實現(xiàn)+initialize
,會調(diào)用父類的+initialize
(所以父類的+initialize
可能會被調(diào)用多次),如果分類實現(xiàn)了+initialize
,就覆蓋類本身的+initialize
調(diào)用。