Category為什麼不能新增屬性
分類中能不能定義例項變數,為什麼?
答案:不能。類的記憶體佈局在編譯時期就已經確定了,category是執行時才載入的早已經確定了記憶體佈局所以無法新增例項變數,如果新增例項變數就會破壞category的內部佈局。
繼續追問:
1:為什麼說category是在執行時載入的?
2:不能新增例項變數,那為什麼能新增屬性?
先來看看在runtime中的結構體的樣子
在分類轉化為c++檔案中可以看出_category_t結構體中,存放著類名,物件方法列表,類方法列表,協議列表,以及屬性列表。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CategoryName.m
1:category小括號裡寫的名字
2:要擴充套件的類物件,編譯期間這個值是不會有的,在app被runtime載入時才會根據name對應到類物件
3:這個category所有的-方法
4:這個category所有的+方法
5:這個category實現的protocol,比較不常用在category裡面實現協議,但是確實支援的
6:這個category所有的property,這也是category裡面可以定義屬性的原因,不過這個property不會@synthesize例項變數,一般有需求新增例項變數屬性時會採用objc_setAssociatedObject和objc_getAssociatedObject方法繫結方法繫結,不過這種方法生成的與一個普通的例項變數完全是兩碼事。
這裡已經可以回答第二個問題了。
再看catagory如何新增進runtime
其實在main函式之前,將runtime通過dyld動態載入進來的時候生效的。怎麼驗證,再來看runtime原始碼:
先從objc_init開始,其中大量出現的image並不是圖片,而是一個二進位制檔案(可執行檔案或 so 檔案),裡面是被編譯過的符號、程式碼等,所以 ImageLoader 作用是將這些檔案載入進記憶體,且每一個檔案對應一個ImageLoader例項來負責載入。
void _objc_init(void) { // fixme defer initialization until an objc-using image is found? environ_init(); tls_init(); lock_init(); exception_init(); // Register for unmap first, in case some +load unmaps something _dyld_register_func_for_remove_image(&unmap_image); dyld_register_image_state_change_handler(dyld_image_state_bound, 1/*batch*/, &map_images); dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images); }
在load_images之後呼叫_read_images方法初始化map後的image,這裡面幹了很多的事情,像load所有的類、協議和category。
再仔細看category的初始化:
/ Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
class_t *cls = remapClass(cat->cls);
// 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 (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}
__objc_catlist,就是上面category存放的資料段。
以上程式碼做的事:
1把category的例項方法、協議以及屬性新增到類上。
2把category的類方法和協議新增到類的metaclass上。
具體怎麼做,主要是兩個方法addUnattachedCategoryForClass和remethodizeClass。
addUnattachedCategoryForClass實現對映,remethodizeClass去做具體操作。
再往下看category的各種列表是怎麼最終新增到類上的。
點開attachCategoryMethods方法可以看到它將所有category的例項方法列表拼成了一個大的例項方法列表,再通過attachMethodLists去加到方法列表裡
static void
attachCategoryMethods(class_t *cls, category_list *cats,
BOOL *inoutVtablesAffected)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
BOOL isMeta = isMetaClass(cls);
method_list_t **mlists = (method_list_t **)
_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
_free_internal(mlists);
}
結論:
1)category的方法並沒有“完全替換掉”原來類已經有的方法,而是把擴充套件的方法放入到方法列表的前頭,舉個栗子(原來的方法列表<a,b,c,>,擴充套件的方法是<1,2,3>,會變成<1,2,3,a,b,c>。)
2)為什麼平常所說的category的方法會“覆蓋”掉原來類的同名方法,就是因為執行時在查詢方法的時候是順著方法列表的順序查詢的,而且只要一找到對應名字的方法,就會結束查詢。
另外的一些疑問
問:Category中有load方法嗎?load方法是什麼時候呼叫的?load 方法能繼承嗎?
答:Category中有load方法,load方法在程式啟動裝載類資訊的時候就會呼叫。load方法可以繼承。呼叫子類的load方法之前,會先呼叫父類的load方法
問:load、initialize的區別,以及它們在category重寫的時候的呼叫的次序。
答:區別在於呼叫方式和呼叫時刻
呼叫方式:load是根據函式地址直接呼叫,initialize是通過objc_msgSend呼叫
呼叫時刻:load是runtime載入類、分類的時候呼叫(只會呼叫1次),initialize是類第一次接收到訊息的時候呼叫,每一個類只會initialize一次(父類的initialize方法可能會被呼叫多次)
呼叫順序:先呼叫類的load方法,先編譯那個類,就先呼叫load。在呼叫load之前會先呼叫父類的load方法。分類中load方法不會覆蓋本類的load方法,先編譯的分類優先呼叫load方法。initialize先初始化父類,之後再初始化子類。如果子類沒有實現+initialize,會呼叫父類的+initialize(所以父類的+initialize可能會被呼叫多次),如果分類實現了+initialize,就覆蓋類本身的+initialize呼叫。
反觀擴充套件(extension),作用是為一個已知的類新增一些私有的資訊,必須有這個類的原始碼,才能擴充套件,它是在編譯器生效的,所以能直接為類新增屬性或者例項變數。
protocol新增屬性會怎麼?
protocol是一系列的協議,要求代理去實現,自己並沒有實現,方法或屬性都是這樣,只是做了宣告要求代理去實現。所以新增的屬性也只是宣告其代理有實現這個屬性,自身並沒有實現其getter、setter以及ivar。
我們來試一下:
未實現警告
當我們在協議中定義一個必須實現(@ required修飾)的屬性以後,如果實現類沒有對這個屬性做任何實現那麼XCode中實現類中就會發出警告
方式一:在.m實現類中新增@synthesize speed;
@protocol FlyDelegate <NSObject>
@required
@property(nonatomic, assign) NSUInteger speed;
@end
@interface TestProtocolProperty : NSObject<FlyDelegate>
@end
@implementation TestProtocolProperty
@synthesize speed;
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
@end
方式二:在.m實現檔案中新增合成speed屬性的成員變數_speed和對應的getter和setter方法
@protocol FlyDelegate <NSObject>
@required
@property(nonatomic, assign) NSUInteger speed;
@end
@interface TestProtocolProperty : NSObject<FlyDelegate> {
NSUInteger _speed;
}
@end
@implementation TestProtocolProperty
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
- (void)setSpeed:(NSUInteger)speed {
_speed = speed;
}
- (NSUInteger)speed {
return _speed;
}
@end
結論:OC語言的協議裡面是支援定義屬性的,而在協議中定義屬性其實和在其中定義方法一樣只是定義了getter和setter方法,並沒有具體實現,所以當這個協議屬性修飾符為@ required時,如果不實現編譯器就會報出警告,最簡單的方式就是加上屬性同步語句@synthesize propertyName;
思考:屬性和方法其實都是一個事物的特性,協議正是描述某類行為和特性的一種規範,基於這個事實,所以在協議中定義屬性是很符合道理的。之所以在iOS開發中很少看到有人這麼使用過是因為,iOS開發中協議通常是被用作代理模式而存在的,並且如果在協議中定義了是屬性,就必須在實現類中新增對屬性自動同步或者手動新增屬性實現程式碼