OC 語法底層基礎

OC 語法底層基礎

分類(實現機制,原理等)

分類都做了哪些事情?

  • 聲明一些私有方法(對外不暴露)
  • 分解體積龐大的類文件(根據不同功能組織到不同的分類中,方便多人共同開發一個類)
  • 把Framework的私有方法公開
  • 模擬多繼承(可以模擬多繼承的還有protocol、NSProxy)

特點

  • 運行時決議 (只有在運行時纔會通過runtime添加到宿主類中)
  • 可以爲系統類添加分類
  • 分類可以訪問原來類的成員變量
  • 分類如果和原來類有同名方法,優先調用分類的(分類(最後參與編譯)—>原來類->父類)

分類中都可以添加哪些內容?

  • 實例方法
  • 類方法
  • 協議
  • 屬性(實際上只聲明瞭getter和setter方法,並未在分類中添加實例變量(可以通過關聯對象添加實例變量))

Category的底層結構

typedef struct category_t *Category;
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; //實例屬性列表

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

分類加載調用棧:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-n8gyviLP-1584405995454)(media/15457312839940/15457936552352.jpg)]
images (指的是鏡像而不是圖片)

源碼解讀順序

  • objc-os.mm

    • _objc_init
    • map_images
    • map_images_nolock
  • objc-runtime-new.mm

    • _read_images
    • remethodizeClass
    • attachCategories
    • attchLists
    • realloc、memmove 、memcpy
  • 分類添加的方法可以"覆蓋(不是覆蓋只是分類的方法比原類的方法靠前)"原類方法

  • 同名分類方法誰能生效取決於編譯順序

  • 名字相同的分類會引起編譯報錯

實現原理

  • Category 編譯之後的底層結構是 struct category_t,裏面存儲着分類的對象方法、類方法、屬性、協議等信息
  • 在程序運行時,Runtime 會將 Category 的數據,合併到類信息中

Category 的加載處理過程

  • 通過 Runtime 加載某個類的所有的 Category 數據
  • 把所有 Category 的方法、屬性、協議數據,合併到一個大數組中,後面把參與編譯的Category 數據,會在數組的前面(會優先調用)
  • 將合併後的分類數據(方法、屬性、協議),插入到類原來數據的前面(memmove、 memcpy)
  • 分類最後參與編譯,分類的方法列表會追加到原來類的方法列表中,並且是在前面,所以調用類的方法時去方法列表中找的時候先找到前面分類的實現,類似被覆蓋的效果( memmove memcpy,先把原來的方法往後移動,再把分類的方法列表拷貝到原來的位置)

常見面試題

  • Category 和 Class Extension 的區別是什麼?

    • Class Extension 在編譯的時候(編譯時決議),他的數據就已經包含在類信息中
    • Category 是在運行時(運行時決議),纔會將數據合併到類信息中
    • 擴展只以聲明的形式存在,多數情況下寄生於宿主類的.m文件中;
    • 不能爲系統類添加擴展,分類可以爲系統類添加擴展
  • Category 中有 load 方法嗎? load 方法是什麼時候調用的? load 方法能繼承嗎?

    • 有load 方法
    • load 方法在 Runtime 加載類、分類的時候調用
    • load 方法可以繼承,但是一般情況下不會主動去調用 load 方法,都是讓系統自動調用
  • laod initialize 方法的區別是什麼? 他們在 Category 中的調用順序? 以及出現繼承時他們之間的調用過程?

    • 區別:
      • 1、調用方式的區別:
        • load 是根據函數地址直接調用
        • initialize 是通過 objc_msgSend 調用
      • 2、調用時刻的區別:
        • load 是 runtime 加載類、分類的時候調用(只會調用1次)
        • initialize 是類第一次接受到消息的時候調用,每一個類只會 initialize 一次 (父類的initialize 可能會被調用多次)
    • 調用順序:
      • load
        • 先調用類的 load
          • 先編譯的類,優先調用load
          • 調用子類的load 之前,會先調用父類的load
        • 再調用分類的load
          • 先調用的分類,優先調用load
      • initialize
        • 先初始化父類
        • 再初始化子類 (可能最終調用的是父類的initialize 方法)
  • Category 能否添加成員變量?如果可以,如何給Category 添加成員變量?

    • 不能直接給 Category 添加成員變量,但是可以間接實現 Category 有成員變量的效果

load 方法

  • +load 方法會在 runtime 加載類 、分類 時調用

  • 每個類、分類的 +load 在程序運行過程中只調用一次

  • 調用順序

    • 先調用類的 +load
      • 按照編譯先後順序調用(先編譯,先調用)
      • 調用子類的 +load 之前會先調用父類的 +load 方法
    • 再調用分類的 +load
      • 按照編譯先後順序調用(先編譯,先調用)
  • objc4源碼解讀過程:objc-os.mm

     _objc_init
     load_images
     prepare_load_methods
         schedule_class_load
         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函數調用

initialize 方法

  • +initialize 方法會在 類 第一次接收到消息時調用

  • 調用順序:

    • 先調用父類的 +initialize , 再調用子類的 +initialize(沒有用到子類(沒有繼承關係)就只調用類的 +initialize。分類調用 +initialize 與編譯順序有關係)
    • 先初始化父類,再初始化子類,每個類只會初始化1次
  • +initialize 和 + load 區別是:

    • +initialize 是通過 objc_msgSend 進行調用的
      • 如果子類沒有實現+initialize ,會調用父類的 +initialize(所以父類的+initialize 可能會被調用多次)
      • 如果分類實現了 +initialize 就會覆蓋本身類的 +initialize 調用
    • +load方法是根據方法地址直接調用,並不是經過objc_msgSend函數調用
  • objc4源碼解讀過程

    objc-msg-arm64.s
        objc_msgSend
    objc-runtime-new.mm
        class_getInstanceMethod
        lookUpImpOrNil
        lookUpImpOrForward
         _class_initialize
         callInitialize
         objc_msgSend(cls, SEL_initialize)
    

關聯對象

類和分類添加屬性的區別

添加屬性會自動生成成員變量,getter和setter 方法聲明 和 對應的實現
分類 添加屬性會自動生成成員變量,getter和setter 方法聲明,但是不會生成實現

{
    int _age;
}
- (void)setAge:(int)age;
-(int)age;

//@property (nonatomic,assign) int age;
/*
 以上代碼
 1、會自動生成成員變量
 2、生成 setter 和 getter 方法的聲明
 3、生成 setter 和 getter 方法的實現
 */
 - (void)setAge:(int)age {
    _age = age;
}
-(int)age {
    return _age;
}

分類添加“成員變量”

關聯對象並不是把成員變量添加到實例對象的屬性列表中,而是自己維護的一個 hashMap

//添加關聯對象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
//獲取關聯對象                        
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)  
//移除所有的關聯對象  
void objc_removeAssociatedObjects(id _Nonnull object)                    
@implementation Person (Test)

/* key 的方式可以寫成多種形式
 static void *MyKey = &MyKey;
 objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
 objc_getAssociatedObject(obj, MyKey)
 
 static char MyKey;
 objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
 objc_getAssociatedObject(obj, &MyKey)
 
 使用屬性名作爲key
 objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 objc_getAssociatedObject(obj, @"property");
 
 使用get方法的@selecor作爲key (常用的方式)
 objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
 objc_getAssociatedObject(obj, @selector(getter))
 */
- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}
//常用以下方式
- (void)setWeight:(int)weight {
    objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_ASSIGN);
}
- (int)weight {
    //_cmd = @selector(weight)
    return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end

關聯對象的實現

  • 實現關聯對象技術的核心對象有
    • AssociationsManager
    • AssociationsHashMap
    • ObjectAssociationMap
    • ObjcAssociation
源碼解讀:
class AssociationsManager {
    static AssociationsHashMap *_map;
    ......
};

//key : disguised_ptr_t       value : ObjectAssociationMap *
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator>

// key : ObjcAssociation   value : ObjectPointerLess 
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator>

class ObjcAssociation {
        uintptr_t _policy;
        id _value;
        ......
}
  • 關聯對象的原理
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yhnJKWcU-1584405995456)(media/15457312839940/15638762809616.jpg)]

AssociationsManager 是一個全局的管理者,內部是一個字典(AssociationsHashMap) ,key 是傳進去的object對象,value 也是一字典(ObjectAssociationMap ),key 是第三個參數 key ,value 是一個 ObjcAssociation ,裏面存放的是 policy 和 value

擴展(Extension 和分類的區別)

一般用擴展做什麼?

  • 聲明私有屬性
  • 聲明私有方法(便於閱讀,沒有太大意義)
  • 聲明私有成員變量

分類和擴展的區別

  • 擴展是編譯時決議,而分類是運行時決議;
  • 擴展只以聲明的形式存在,多數情況下寄生於宿主類的.m文件中;
  • 不能爲系統類添加擴展,分類可以爲系統類添加擴展

代理

介紹代理

  • 準確來說是一種設計模式(代理模式
  • iOS中以@protocol 形式體現
  • 傳遞方式一對一

工作流程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-iQpEWr0F-1584405995457)(media/15457312839940/15815242180051.jpg)]
協議中可以定義方法和屬性

使用注意

  • 一般聲明爲 weak 以規避循環引用

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FnFKaUzo-1584405995460)(media/15457312839940/15458089417658.jpg)]

通知實現機制( NSNotification 原理)

特點

  • 使用觀察者模式來實現的用於跨層傳遞信息的機制;
  • 傳遞方法是一對多;
  • 通知是松耦合的,通知方不需要知道被通知方的任何情況,而 delegate 不行
  • 通知的效率比 delegate 略低

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HMsIe6FB-1584405995462)(media/15457312839940/15841812461183.jpg)]

如何實現通知機制?

NSNotificationCenter 內部會維護一個map表(字典) ,key 是 notificationName value 是 一個數組列表
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uCCIVU3D-1584405995464)(media/15457312839940/15458092549569.jpg)]

KVO (實現機制)

什麼是KVO ?KVO 的實現機制是什麼?

  • KVO 是 OC 對觀察者設計模式的一種實現;

  • 使用了isa混寫(isa-swizzling)來實現的;(其實就是在調用了addObserver:之後系統動態創建一個派生類(NSKVONotifying_XXX),並把isa 指針指向了派生類)
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gss2VyP1-1584405995465)(media/15457312839940/15458095894570.jpg)]
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WE1zJyGN-1584405995466)(media/15457312839940/15458099584582.jpg)]
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-oD8tGHiD-1584405995468)(media/15457312839940/15458099689254.jpg)]

  • 通過KVC 設置的value 能否生效?爲什麼?
    能夠生效;
    考察的KVC的實現機制,KVC 調用的時候會調用setter方法,而 setter 方法被派生類重寫了,就會調用派生類的方法,這樣就會觸發KVO 。

  • 通過成員變量直接賦值value 能否生效?(手動KVO)
    不能夠生效;
    通過添加 willChangeValueForKey:didChangeValueForKey:使其生效。

觸發KVO 的方式:

  • 使用setter 方法改變值 KVO 纔會生效。
  • 使用setValue:forKey: 改變值KVO 纔會生效
  • 成員變量直接修改需要手動添加KVO 纔會生效

KVC

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GyWStZ7I-1584405995475)(media/15457312839940/15458120083176.jpg)]
KVC就是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態地訪問和修改對象的屬性。而不是在編譯時確定,這也是iOS開發中的黑魔法之一。很多高級的iOS開發技巧都是基於KVC實現的

  • 通過鍵值編碼是否有違背面向對象的編程思想?
    知道一個類或者實例內部私有成員名稱的情況下可以通過KVC 進行設置和取值的,是破壞了面向對象編程思想的。

KVC 原理

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uXEWHm3N-1584405995476)(media/15457312839940/15458122258873.jpg)]

訪問器方法是否存在流程:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hvcrrAcC-1584405995477)(media/15457312839940/15458122868788.jpg)]

實例變量說明:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WYZcfyk3-1584405995478)(media/15457312839940/15458123529484.jpg)]

setValue:forKey: 調用流程:

setKey _setKey
accessInstanceVariablesDirectly 返回YES
_key _isKey key isKey

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-U3e4MR4I-1584405995479)(media/15457312839940/15458124088385.jpg)]

  • 程序優先調用set:屬性值方法,代碼通過setter方法完成設置。注意,這裏的是指成員變量名,首字母大小寫要符合KVC的命名規則,下同
  • 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUndefinedKey:方法,不過一般開發者不會這麼做。所以KVC機制會搜索該類裏面有沒有名爲的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什麼樣的訪問修飾符,只在存在以命名的變量,KVC都可以對該成員變量賦值。
  • 如果該類即沒有set:方法,也沒有_成員變量,KVC機制會搜索_is的成員變量。
  • 和上面一樣,如果該類即沒有set:方法,也沒有_和_is成員變量,KVC機制再會繼續搜索和is的成員變量。再給它們賦值。
  • 如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
  • 即如果沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員並進行賦值操作。
  • 如果開發者想讓這個類禁用KVC,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set:屬性名時,會直接用setValue:forUndefinedKey:方法。

valueForKey 調用流程:

getKey key isKey _key
accessInstanceVariablesDirectly 返回yes
_key _isKey key isKey

  • 首先按get,,is的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
  • 如果上面的getter沒有找到,KVC則會查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外兩個方法中的一個被找到,那麼就會返回一個可以響應NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬於NSArray的方法,就會以countOf,objectInAtIndex或AtIndexes這幾個方法組合的形式調用。還有一個可選的get:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
  • 如果上面的方法沒有找到,那麼會同時查找countOf,enumeratorOf,memberOf格式的方法。如果這三個方法都找到,那麼就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf,enumeratorOf,memberOf組合的形式調用。
  • 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行爲),那麼和先前的設值一樣,會按_,_is,,is的順序搜索成員變量名,這裏不推薦這麼做,因爲這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那麼會直接調用valueForUndefinedKey:方法,默認是拋出異常。

屬性關鍵字 (weak/assign/copy/strong)

  • 讀寫權限
  • 原子性
  • 引用計數

讀寫權限

  • readonly
  • readwrite(默認)

原子性

  • atomic (默認)
    賦值和獲取是線程安全的(成員屬性的直接賦值和獲取) 不代表操作和訪問,比如說對一個數組的賦值和獲取是線程安全的,但是對數組進行操作比如添加和移除是不保證線程安全的。
  • nonatomic

引用計數

  • retain(MRC)/strong(ARC)

  • assign/unsafe_unretained(MRC ARC幾乎不行)

  • weak/copy

  • assgin 和 weak 區別:

    assgin:

    • 修飾基本數據類型,比如int Bool 等。
    • 修飾對象類型時,不改變其引用計數。
    • 會產生懸垂指針。(assgin 修飾的對象被釋放後,assgin指針仍然指向原來對象的地址,如果繼續訪問的話就會導致異常)

    weak:

    • 不改變被修飾對象的引用計數
    • 所指對象在被釋放之後會自動置爲nil

    相同點:都不改變引用計數。

    • 爲什麼weak 修飾的對象被釋放之後會自動置爲nil ?<內存管理章節>
  • copy(深拷貝淺拷貝 重點)

    • 淺拷貝是對內存地址的複製,讓目標對象指針和源對象指向同一片內存空間。
      • 會增加引用計數
      • 並未發生新的內存分配
    • 讓目標對象指針和源對象指針指向兩片內容相同(不是同一塊內存)的內存空間。
      • 不會增加引用計數
      • 產生了新的內存分配
    • 區分
      • 看是否有新的內存分配
      • 看是否增加引用計數

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8BYPcfVa-1584405995481)(media/15457312839940/15840938160407.jpg)]

可變對象copy 和 mutableCopy 都是深拷貝。
不可變對象的copy 是淺拷貝,mutableCopy 是深拷貝。
copy 方法返回的都是不可變對象

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-gwD8QIuz-1584405995481)(media/15457312839940/15458149050100.jpg)]

MRC下如何重寫retain 修飾變量的setter 方法?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aIX2m0tn-1584405995483)(media/15457312839940/15458150305498.jpg)]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章