面試驅動技術 - Category 相關考點

小蠢驢打程式碼發表於2019-02-28

面試驅動技術合集(初中級iOS開發),關注倉庫,及時獲取更新 Interview-series

面試驅動技術 - Category 相關考點

Category 相關的問題一般初中級問的比較多,一般最深的就問到關聯物件,本文把比較常見的 Category 的問題都羅列解決了一下,如果還有其他常見的 Category 的試題歡迎補充~


I. Category

Category相關面試題

  • Category實現原理?
  • 實際開發中,你用Category做了哪些事?
  • Category能否新增成員變數,如果可以,如何新增?
  • load 、initialize方法的區別是什麼,他們在category中的呼叫順序?以及出現繼承時他們之間的呼叫過程?
  • Category 和 Class Extension的區別是什麼?
  • 為什麼分類會“覆蓋”宿主類的方法?

1.Category的特點

  • 執行時決議
    • 通過 runtime 動態將分類的方法合併到類物件、元類物件中
    • 例項方法合併到類物件中,類方法合併到元類物件中
  • 可以為系統類新增分類

2.分類中可以新增哪些內容

  • 例項方法
  • 類方法
  • 協議
  • 屬性

分類中原理解析

使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MNPerson+Test.m 函式,生產一個cpp檔案,窺探其底層結構(編譯狀態)

struct _category_t {
    //宿主類名稱 - 這裡的MNPerson
    const char *name;
	
    //宿主類物件,裡面有isa
    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;
};

//_class_t 結構
struct _class_t {
	struct _class_t *isa;
	struct _class_t *superclass;
	void *cache;
	void *vtable;
	struct _class_ro_t *ro;
};
複製程式碼
  • 每個分類都是獨立的
  • 每個分類的結構都一致,都是category_t

函式轉換

@implementation MNPerson (Test)

- (void)test{
    NSLog(@"test - rua~");
}

@end
複製程式碼

面試驅動技術 - Category 相關考點

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    
    /* 二維陣列( **mlists => 兩顆星星,一個)
     [
        [method_t,],
        [method_t,method_t],
        [method_t,method_t,method_t],
     ]
     
     */
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    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);
}
複製程式碼
void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    
    if (hasArray()) {
        // many lists -> many lists
        uint32_t oldCount = array()->count;
        uint32_t newCount = oldCount + addedCount;
        
        //realloc - 重新分配記憶體 - 擴容了
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        
        //memmove,記憶體挪動
        //array()->lists 原來的方法列表
        memmove(array()->lists + addedCount,
                array()->lists,
                oldCount * sizeof(array()->lists[0]));
        
        //memcpy - 將分類的方法列表 copy 到原來的方法列表中
        memcpy(array()->lists,
               addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
    ...
}
複製程式碼

畫圖分析就是

面試驅動技術 - Category 相關考點

面試驅動技術 - Category 相關考點

面試驅動技術 - Category 相關考點

面試驅動技術 - Category 相關考點

面試驅動技術 - Category 相關考點

3.實際開發中,你用Category做了哪些事?

  • 宣告私有方法
  • 分解體積龐大的類檔案
    • Framework的私有方法公開
  • 。。。

4.Category實現原理?

  • Category編譯之後,底層結構是category_t,裡面儲存著分類的各種資訊,包括 物件方法、類方法、屬性、協議資訊
  • 分類的在編譯後,方法並不會直接新增到類資訊中,而是要在程式執行的時候,通過 runtime, 講Category的資料,

5.為什麼分類會“覆蓋”宿主類的方法?

  • 其實不是真正的“覆蓋”,宿主類的同名方法還是存在
  • 分類將附加到類物件的方法列表中,整合的時候,分類的方法優先放到前面
  • OC的函式呼叫底層走的是msg_send() 函式,它做的是方法查詢,因為分類的方法優先放在前面,所以通過選擇器查詢到分類的方法之後直接呼叫,宿主類的方法看上去就像被“覆蓋”而沒有生效

6.Category 和 Class Extension的區別是什麼?

Class Extension(擴充套件)

  • 宣告私有屬性
  • 宣告私有方法
  • 宣告私有成員變數
  • 編譯時決議,Category 執行時決議
  • 不能為系統類新增擴充套件
  • 只能以宣告的形式存在,多數情況下,寄生於宿主類的.m檔案中


II. load 、initialize

load實現原理

  • 類第一次載入進記憶體的時候,會呼叫 + load 方法,無需匯入,無需使用
  • 每個類、分類的 + load 在程式執行過程中只會執行一次
  • + load 走的不是訊息傳送的 objc_msgSend 呼叫,而是找到 + load 函式的地址,直接呼叫
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) {
            //先載入宿主類的load方法(按照編譯順序,呼叫load方法)
            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;
}
複製程式碼
static void schedule_class_load(Class cls)
{
    if (!cls) return;
    assert(cls->isRealized());  // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    // 遞迴呼叫,先將父類新增到load方法列表中,再將自己加進去
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED); 
}
複製程式碼


呼叫順序

  1. 先呼叫宿主類的+ load 函式
    • 按照編譯先後順序呼叫(先編譯,先呼叫)
    • 呼叫子類的+load之前會先呼叫父類的+load
  2. 再呼叫分類的的+ load 函式
    • 按照編譯先後順序呼叫(先編譯,先呼叫)

實驗證明:宿主類先呼叫,分類再呼叫

2019-02-27 17:28:00.519862+0800 load-Initialize-Demo[91107:2281575] MNPerson + load
2019-02-27 17:28:00.520032+0800 load-Initialize-Demo[91107:2281575] MNPerson (Play) + load
2019-02-27 17:28:00.520047+0800 load-Initialize-Demo[91107:2281575] MNPerson (Eat) + load
複製程式碼

面試驅動技術 - Category 相關考點


2019-02-27 17:39:10.354050+0800 load-Initialize-Demo[91308:2303030] MNDog + load (宿主類1)
2019-02-27 17:39:10.354237+0800 load-Initialize-Demo[91308:2303030] MNPerson + load (宿主類2)
2019-02-27 17:39:10.354252+0800 load-Initialize-Demo[91308:2303030] MNDog (Rua) + load (分類1)
2019-02-27 17:39:10.354263+0800 load-Initialize-Demo[91308:2303030] MNPerson (Play) + load(分類2)
2019-02-27 17:39:10.354274+0800 load-Initialize-Demo[91308:2303030] MNPerson (Eat) + load(分類3)
2019-02-27 17:39:10.354285+0800 load-Initialize-Demo[91308:2303030] MNDog (Run) + load(分類4)
複製程式碼

Initialize實現原理

  • 類第一次接收到訊息的時候,會呼叫該方法,需匯入,並使用
  • + Initialize 走的是訊息傳送的 objc_msgSend 呼叫

Initialize題目出現

/*父類*/
@interface MNPerson : NSObject

@end

@implementation MNPerson

+ (void)initialize{
    NSLog(@"MNPerson + initialize");
}

@end

/*子類1*/
@interface MNTeacher : MNPerson

@end

@implementation MNTeacher

@end

/*子類2*/
@interface MNStudent : MNPerson

@end

@implementation MNStudent

@end


---------------------------------------------
問題出現:以下會輸出什麼結果
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        [MNTeacher alloc];
        [MNStudent alloc];
    }
    return 0;
}

複製程式碼


結果如下:

2019-02-27 17:57:33.305655+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.305950+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
2019-02-27 17:57:33.306476+0800 load-Initialize-Demo[91661:2331296] MNPerson + initialize
複製程式碼

exo me? 為啥列印三次呢

面試驅動技術 - Category 相關考點

原理分析:

  1. initialize 在類第一次接收訊息的時候會呼叫,OC裡面的 [ xxx ] 呼叫都可以看成 objc_msgSend,所以這時候,[MNTeacher alloc] 其實內部會呼叫 [MNTeacher initialize]
  2. initialize 呼叫的時候,要先實現自己父類的 initialize 方法,第一次呼叫的時候,MNPerson 沒被使用過,所以未被初始化,要先呼叫一下父類的 [MNPerson initialize],輸出第一個MNPerson + initialize
  3. MNPerson 呼叫了 initialize 之後,輪到MNTeacher 類自己了,由於他內部沒有實現 initialize方法,所以呼叫父類的initialize, 輸出第二個MNPerson + initialize
  4. 然後輪到[MNStudent alloc],內部也是呼叫 [MNStudent initialize], 然後判斷得知 父類MNPerson類呼叫過initialize了,因此呼叫自身的就夠了,由於他和MNTeacher 一樣,也沒實現initialize 方法,所以同理呼叫父類的[MNPerson initialize],輸出第3個MNPerson + initialize

initialize 與 load 的區別

  • load 是類第一次載入的時候呼叫,initialize 是類第一次接收到訊息的時候呼叫,每個類只會initialize一次(父類的initialize方法可能被呼叫多次)
  • load 和 initialize,載入or呼叫的時候,都會先呼叫父類對應的 load or initialize 方法,再呼叫自己本身的;
  • load 和 initialize 都是系統自動呼叫的話,都只會呼叫一次
  • 呼叫方式也不一樣,load 是根據函式地址直接呼叫,initialize 是通過objc_msgSend
  • 呼叫時刻,load是runtime載入類、分類的時候呼叫(只會呼叫一次)
  • 呼叫順序:
    • load:
      • 先呼叫類的load
        • 先編譯的類,優先呼叫load
        • 呼叫子類的load之前,會先呼叫父類的load
      • 在呼叫分類的load
    • initialize:
      • 先初始化父列
      • 再初始化子類(可能最終呼叫的是父類的初始化方法)
/*父類*/
@interface MNPerson : NSObject

@end

@implementation MNPerson

+ (void)initialize{
    NSLog(@"MNPerson + initialize");
}

+ (void)load{
    NSLog(@"MNPerson + load");
}

/*子類1*/
@interface MNTeacher : MNPerson

@end

@implementation MNTeacher

+ (void)load{
    NSLog(@"MNTeacher + load");
}

/*子類2*/
@interface MNStudent : MNPerson

@end

@implementation MNStudent

+ (void)load{
    NSLog(@"MNStudent + load");
}


------------------------------------
問題出現:以下會輸出什麼結果?

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        [MNTeacher load];
    }
    return 0;
}

複製程式碼

答案出現!!!

2019-02-27 18:17:12.034392+0800 load-Initialize-Demo[92064:2370496] MNPerson + load
2019-02-27 18:17:12.034555+0800 load-Initialize-Demo[92064:2370496] MNStudent + load
2019-02-27 18:17:12.034569+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
2019-02-27 18:17:12.034627+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034645+0800 load-Initialize-Demo[92064:2370496] MNPerson + initialize
2019-02-27 18:17:12.034658+0800 load-Initialize-Demo[92064:2370496] MNTeacher + load
複製程式碼

exo me again!怎麼這麼多!連load 也有了?

面試驅動技術 - Category 相關考點

解釋:

  1. 前三個load不多bb了吧,程式一執行,runtime直接將全部的類載入到記憶體中,肯定最先輸出;
  2. 第一個 MNPerson + initialize,因為是MNTeacher的呼叫,所以會先讓父類MNPerson 呼叫一次initialize,輸出第一個 MNPerson + initialize
  3. 第二個 MNPerson + initialize, MNTeacher 自身呼叫,由於他自己沒有實現 initialize, 呼叫父類的initialize, 輸出第二個 MNPerson + initialize
  4. 最後一個MNTeacher + load可能其實有點奇怪,不是說 load只會載入一次嗎,而且他還不走 objc_msgSend 嗎,怎麼還能呼叫這個方法?
    • 因為!當類第一次載入進記憶體的時候,呼叫的 load 方法是系統調的,這時候不走 objc_msgSend
    • 但是,你現在是[MNTeacher load]啊,這個就是objc_msgSend(MNTeacher,@selector(MNTeacher)),這就跑到MNTeacher + load裡了!
    • 只是一般沒人手動呼叫load 函式,但是,還是可以呼叫的!

III. 關聯物件AssociatedObject

Category能否新增成員變數,如果可以,如何新增?

這道題實際上考的就是關聯物件

如果是普通類宣告生命屬性的話

@interface MNPerson : NSObject

@property (nonatomic, copy)NSString *property;

@end
複製程式碼

上述程式碼系統內部會自動三件事:

  1. 幫我們生成一個生成變數_property
  2. 生成一個 get 方法 - (NSString *)property
  3. 生成一個 set 方法 - (void)setProperty:(NSString *)property
@implementation MNPerson{
    NSString *_property;
}

- (void)setProperty:(NSString *)property{
    _property = property;
}

- (NSString *)property{
    return _property;
}

@end
複製程式碼

分類也是可以新增屬性的 - 類結構裡面,有個properties 列表,裡面就是 存放屬性的;

分類裡面,生成屬性,只會生成方法的宣告,不會生成成員變數 && 方法實現!

面試驅動技術 - Category 相關考點

人工智障翻譯:例項變數不能放在分類中

所以:

不能直接給category 新增成員變數,但是可以間接實現分類有成員變數的效果(效果上感覺像成員變數)

@interface MNPerson (Test)

@property (nonatomic, assign) NSInteger age;

@end

@implementation MNPerson (Test)

@end
複製程式碼

面試驅動技術 - Category 相關考點

person.age = 10等價於 [person setAge:10],所以證明了,給分類宣告屬性之後,並沒有新增其對應的實現!


關聯物件

objc_setAssociatedObject Api

objc_setAssociatedObject(    <#id  _Nonnull object#>, (物件)
                             <#const void * _Nonnull key#>,(key)
                             <#id  _Nullable value#>,(關聯的值)
                             <#objc_AssociationPolicy policy#>)(關聯策略)
複製程式碼

關聯策略,等價於屬性宣告

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,          
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  
    OBJC_ASSOCIATION_RETAIN = 01401,      
    OBJC_ASSOCIATION_COPY = 01403         
};
複製程式碼

面試驅動技術 - Category 相關考點

比如這裡的age屬性,預設宣告是@property (nonatomic, assign) NSInteger age;,就是 assign,所以這裡選擇OBJC_ASSOCIATION_ASSIGN


取值

objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)
複製程式碼

面試題 - 以下程式碼輸出的結果是啥

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        MNPerson *person = [[MNPerson alloc]init];

        {
            MNPerson *test = [[MNPerson alloc]init];
            objc_setAssociatedObject(person,
                                     @"test",
                                     test,
                                     OBJC_ASSOCIATION_ASSIGN);
        }
        
        NSLog(@"%@",objc_getAssociatedObject(person, @"test"));
    }
    return 0;
}

複製程式碼

面試驅動技術 - Category 相關考點

原因,關聯的物件是person,關聯的value是 test,test變數 出了他們的{} 作用域之後,就會銷燬; 此時通過key 找到 對應的物件,訪問物件內部的value,因為test變數已經銷燬了,所以程式崩潰了,這也說明了 => 內部 test 對 value是強引用!

關聯物件的本質

在分類中,因為類的例項變數的佈局已經固定,使用 @property 已經無法向固定的佈局中新增新的例項變數(這樣做可能會覆蓋子類的例項變數),所以我們需要使用關聯物件以及兩個方法來模擬構成屬性的三個要素。

引用自 關聯物件 AssociatedObject 完全解析


關聯物件的原理

實現關聯物件技術的核心物件有

  • AssociationsManager
  • AssociationsHashMap
  • ObjectAssociationMap
  • ObjcAssociation
class AssociationsManager {
    static spinlock_t _lock;//自旋鎖,保證執行緒安全
    static AssociationsHashMap *_map;
}
複製程式碼
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap> 
複製程式碼
class ObjectAssociationMap : public std::map<void *, ObjcAssociation>
複製程式碼
class ObjcAssociation {
    uintptr_t _policy;
    id _value;
}
複製程式碼

以關聯物件程式碼為例:

  objc_setAssociatedObject(obj, @selector(key), @"hello world", OBJC_ASSOCIATION_COPY_NONATOMIC);
複製程式碼

面試驅動技術 - Category 相關考點

  • 關聯物件並不是儲存在被關聯物件本身的記憶體中的
  • 關聯物件,儲存在全域性的一個統一的AssociationsManager
  • 關聯物件其實就是 ObjcAssociation 物件,關聯的 value 就放在 ObjcAssociation
  • 關聯物件由 AssociationsManager 管理並在 AssociationsHashMap 儲存
  • 物件的指標以及其對應 ObjectAssociationMap 以鍵值對的形式儲存在 AssociationsHashMap
  • ObjectAssociationMap 則是用於儲存關聯物件的資料結構
  • 每一個物件都有一個標記位 has_assoc 指示物件是否含有關聯物件
  • 儲存在全域性的一個統一的AssociationsManager 內部有一持有一個_lock,他其實是一個spinlock_t(自旋鎖),用來保證AssociationsHashMap操作的時候,是執行緒安全的

Category 相關的問題一般初中級問的比較多,一般最深的就問到關聯物件,上面的問題以及解答已經把比較常見的 Category 的問題都羅列解決了一下,如果還有其他常見的 Category 的試題歡迎補充~

傳言的網際網路寒冬貌似真的來臨了,在這種環境下,無法得知公司是否不裁員,還是讓自己?起來!19年的 銅三鐵四 從明天就要開始拉開帷幕了,也希望近期找工作的iOS們能找到一份滿意的工作,看下寒冬下,iOS開發是不是叕沒人要了~



本文基於 MJ老師 的基礎知識之上,結合了包括 draveness 在內的一系列大神的文章總結的,如果不當之處,歡迎討論~


友情演出:小馬哥MJ

參考資料:

關聯物件 AssociatedObject 完全解析

associated-objects

相關文章