1、Category的基本使用
Category的使用場合是什麼?
- 將一個類拆成很多模塊(其實就是解耦,將相關的功能放到一起)
2、Category的實現原理?
- 通過runtime動態將分類的方法合併到類對象、元類對象中
- Category編譯之後的底層結構是 struct_category_t , 裏面存儲着分類的對象方法、類方法、屬性、協議信息
- 在程序運行的時候,runtime會將 Category 的數據,合併到類信息中(類對象、元類對象)
- 轉C++代碼
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MJPerson+Test.m
- 分類結構體
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; // 屬性列表 (分類裏能實現屬性)
};
- Test分類
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Test,
0,
0,
0,
};
- Eat分類
static struct _category_t _OBJC_$_CATEGORY_MJPerson_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"MJPerson",
0, // &OBJC_CLASS_$_MJPerson,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_MJPerson_$_Eat,
0,
0,
0,
};
3、源碼分析1
源碼定義
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);
};
Category的加載處理過程
- 通過Runtime加載某個類的所有Category數據
- 把所有Category的方法、屬性、協議數據,合併到一個大數組中,後面參與編譯的Category數據,會在數組的前面
- 將合併後的分類數據(方法、屬性、協議),插入到類原來數據的前面
源碼解讀順序
objc-os.mm
- _objc_init
- map_images
- map_images_nolock
objc-runtime-new.mm
- _read_images
- remethodizeClass
- attachCategories
- attachLists
- realloc、memmove、memcpy
cls = [MJPerson class]
cats = [category_t(Test), category_t(Eat)]
static void
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
/*
方法數組
[
[method_t, method_t],
[method_t, method_t]
]
*/
method_list_t **mlists = (method_list_t **)
malloc(cats->count * sizeof(*mlists));
/*
屬性數組
[
[property_t, property_t],
[property_t, property_t]
]
*/
property_list_t **proplists = (property_list_t **)
malloc(cats->count * sizeof(*proplists));
/*
協議數組
[
[protocol_t, protocol_t],
[protocol_t, protocol_t]
]
*/
protocol_list_t **protolists = (protocol_list_t **)
malloc(cats->count * sizeof(*protolists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int propcount = 0;
int protocount = 0;
int i = cats->count;
bool fromBundle = NO;
while (i--) {
auto& entry = cats->list[i];
method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= entry.hi->isBundle();
}
property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
proplists[propcount++] = proplist;
}
protocol_list_t *protolist = entry.cat->protocols;
if (protolist) {
protolists[protocount++] = protolist;
}
}
// 得到類對象裏面的數據
auto rw = cls->data();
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
// 將所有分類的對象方法,附加到類對象方法列表中
rw->methods.attachLists(mlists, mcount);
free(mlists);
if (flush_caches && mcount > 0) flushCaches(cls);
// 將所有分類的屬性,附加到類對象屬性列表中
rw->properties.attachLists(proplists, propcount);
free(proplists);
// 將所有分類的協議,附加到類對象協議列表中
rw->protocols.attachLists(protolists, protocount);
free(protolists);
}
5、memmove、memcopy的區別
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
- 他們的作用是一樣的
- 唯一的區別是,當內存發生局部重疊的時候,memmove保證拷貝的結果是正確的,memcpy不保證拷貝的結果的正確
6、load基本使用
Category中有load方法麼?load方法什麼時候調用的?load方法能繼承麼?
- 有load方法
- +load方法會在runtime加載類、分類時調用;
- 每個類、分類的+load,在程序運行過程中只調用一次
- 調用順序
1、先調用類的+load,(按照編譯先後順序,先編譯,先調用),調用子類的+load之前會調用父類的+load
2、再調用分類的+load按照編譯先後順序調用(先編譯,先調用)
7、load調用原理
-load方法調用
test方法和load方法的本質區別?(+load方法爲什麼不會被覆蓋)
- test方法是通過消息機制調用 objc_msgSend([MJPerson class], @selector(test))
- + load方法調用,直接找到內存中的地址,進行方法調用
8、load調用順序
- +load方法會在runtime加載類、分類時調用
- 每個類、分類的+load,在程序運行過程中只調用一次
調用順序
- 1、先調用類的+load
按照編譯先後順序調用(先編譯,先調用)
調用子類的+load之前會先調用父類的+load
- 2、再調用分類的+load
按照編譯的順序調用(先編譯,先調用)
objc4源碼解讀過程:objc-os.mm
- _objc_init
- load_images
- prepare_load_methods
schedule_class_methods
add_class_to_loadable_list
add_category_to_loadable_list
- call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
+ load方法時根據方法地址直接調用,並不是經過objc_msgSend函數調用
- 先調用類方法,後調用分類方法
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) {
call_class_loads();
}
// 2. Call category +loads ONCE
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;
}
如果有100個類,會先調用哪個類的方法(按照數組的先後順序)
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;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, SEL_load);
}
// Destroy the detached list.
if (classes) free(classes);
}
9、initialize的基本使用
initialize 方法(分類會覆蓋類方法,通過消息機制(objc_msgSend)調用)
- initialize方法會在類第一次接收到消息時調用
- 調用順序:
先調用父類的+initialize,再調用子類的+initialize(如果父類已經調用過了,調用子類的時候,父類就不回再調用了)
爲什麼load三個方法都會調用?
- 直接通過函數指針調用,不是通過消息機制(objc_msgSend)調用
10、initialize 注意點
initialize和load的很大區別是,
- +initialize是通過objc_msgSend進行調用的
- +load方法是直接找到內存地址進行調用的
所以initialize有以下特點:
- 如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次)
- 如果分類實現了+initialize,就覆蓋類本身的+initialize調用
// 僞代碼(爲什麼會打印三次的原因,不代表父類初始化了3次)
if (MJStudent沒有初始化) {
if (MJPerson沒有初始化) {
objc_msgSend([MJPerson class], @selector(initialize))
}
objc_msgSend([MJStudent class], @selector(initialize))
}
if (MJTeacher沒有初始化) {
if (MJPerson沒有初始化) {
objc_msgSend([MJPerson class], @selector(initialize))
}
objc_msgSend([MJTeacher class], @selector(initialize))
}
11、面試題
1、load、initialize方法的區別是什麼?它們在category中的調用順序?以及出現繼承時他們之間的調用過程?
區別:
- 調用方式
1、load是根據函數地址直接調用
2、initialize是榮光objc_msgSend調用
- 調用時刻
1、load是runtime加載類、分類的時候調用(只會調用1次)
2、initialize是類第一次接收消息時調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次)
load、initialize的調用順序
- load
1、先調用類的load
先編譯的類,優先調用load
調用子類的load之前,會先調用父類的load
2、再調用分類的load
先編譯的分類,優先調用load
- initialize
先初始化父類
再初始化子類(可能最終調用的是父類的initialize方法)
2、不同Category中存在同一個方法,會執行哪個方法?如果是連個都執行,執行順序是什麼樣的?
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *test = [[NSObject alloc] init];
[test printTest];
}
return 0;
}
@implementation NSObject (Test1)
- (void)printTest {
NSLog(@"-----test1");
}
@end
@implementation NSObject (Test2)
- (void)printTest {
NSLog(@"-----test2");
}
@end
2019-07-04 08:56:36.870570+0800 test[911:13532] -----test2
3、Category和 Class Extension的區別?(分類和 類擴展(就是.m文件中的.h) 的區別)
- Class Extension 在編譯的時候,它的數據就已經包含在類信息中
- Category是在運行時,纔會將數據合併到類信息中
4、Category中有load方法麼?load方法什麼時候調用的?load方法能繼承麼?
- 有load方法
- +load方法會在runtime加載類、分類時調用;
- load方法可以繼承,但是一般情況下不回主動去調用load方法,都是讓系統自動去調用