iOS底層原理-Category

weixin_34208283發表於2018-07-25

Category內部實現

//分類的結構:
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;
}

程式一編譯,分類的資訊都會儲存在_category_t這個結構體下,相當於編寫出一個分類,就生成了一個對應的結構體物件,例如有5個分類,到時就會生成對應5個結構體物件,每個結構體變數的變數名和內部儲存的值是不同的,此時並沒有合併到類物件對應的方法中

  • 誤區:由於一個類只存在一個類物件,所以不存在說建立一個分類就對應有一個分類的類物件這麼一個情況,而是分類內部對應的物件方法和類方法都會合併到該類對應的類物件和原類物件中
  • 分類中的方法合併:通過runtime動態將分類的方法合併到類物件、元類物件中

分類的內部實現過程:
1.通過runtime載入某個類的所有category資料
2.把所有category的方法、屬性、協議資料合併到一個大陣列中(其中,後面參與編譯的category資料,會在陣列的前面)
3.將合併後的分類資料(方法、屬性、協議),插入到原來資料的前面

  • 由原始碼可以得出,最後面編譯的分類,在方法列表中位置最靠前
  • 在xcode中Build Phases - Compile Sources中可以手動設定編譯的順序

category(分類)與extension(類擴充套件)的區別:

  • 類擴充套件內容一編譯就已經合併到類中了,分類中是在執行時通過runtime合併到類資訊中

category的實現原理:

  • category編譯之後的底層結構是struct category_t,裡面儲存著分類的物件方法、類方法、屬性、協議資訊
  • 在程式執行的時候,runtime會將category的資料,合併到類資訊中(類物件、元類物件中)

通過檢視原始碼,發現底層將category插入陣列時,呼叫兩個函式memmovememcpy,二者的區別是:
memmove:會先判斷是往左挪還是往右挪(會保證完整性)
memcpy:會一個一個拷貝(從小地址開始)

例如存在下面4塊記憶體空間 1 2 3 4
現在要將1和2,移動到2和3的位置,通過memmove可以實現3 1 2 4
通過memcpy實現即為:1 1 1 4

+load()方法

  • 會在runtime載入類、分類時呼叫
  • 每個類、分類的+load,在程式執行過程中只呼叫一次

從原始碼分析,會先呼叫類的load方法,再呼叫分類的load方法
之所以執行時,類呼叫load方法和分類呼叫load方法不會像其他方法一樣被覆蓋,是因為內部實現中,會直接取出load方法的函式地址直接進行呼叫,而不是通過訊息轉發(objc_msgSend)

如果是通過訊息機制呼叫方法,流程都是通過isa指標,找到對應的類方法/元類方法,在方法列表中按順序查詢
而load方法是直接通過load方法在記憶體中的地址值直接呼叫的

  • 呼叫順序:
    1.先呼叫類的load方法
    2.按照編譯的先後順序呼叫(先編譯,先呼叫)
    3.呼叫子類的load方法之前會先呼叫父類的load方法
    4.呼叫分類的load方法
    5.按照編譯的先後順序呼叫(先編譯,先呼叫)

一道面試題:

Category中有load方法嗎?load方法是什麼時候呼叫的?load方法能繼承嗎?

  • 有load方法
  • load方法在runtime載入類、分類的時候呼叫
  • load方法可以繼承,但是一般情況下不會主動去呼叫load方法,都是系統自動呼叫

+initialize()方法

  • 會在類第一次接收到訊息時呼叫

  • 呼叫順序:
    1.先呼叫父類的+initialize,再呼叫子類的+initialize(前提是父類之前沒有被初始化過,)
    2.先初始化父類,再初始化子類,每個類只會初始化一次

之所以呼叫子類的方法,會呼叫父類的initialize方法,是因為內部主動傳送了2條訊息(一條是給父類傳送initialize訊息,一條是給子類傳送initialize訊息)
注:initialize方法最終是通過objc_msgSend方法呼叫的

+initialize()和+load()的很大區別是,+initialize是通過objc_msgSend進行呼叫的,所以有以下特點:

  • 如果子類沒有實現+initialize,會呼叫父類的+initialize方法(所以父類的+initialize方法可能會被呼叫多次)
  • 如果分類實現了+initialize,就覆蓋了類本身的+initialize呼叫

+load()方法和+initialize()方法總結

1.呼叫方式

  • load是根據函式地址直接呼叫
  • initialize是通過objc_msgSend呼叫

2.呼叫時刻

  • load是runtime載入類、分類的時候呼叫(只會呼叫一次)
  • initialize是類第一次接收到訊息的時候呼叫,每一個類只會initialize一次,但父類的initialize方法可能會被呼叫多次

3.呼叫順序

1)load:

  • 先呼叫類的load 1)先編譯的類優先呼叫load 2)呼叫子類的load之前,會先呼叫父類的load
  • 再呼叫分類的load 1)先編譯的分類,優先呼叫load

2)initialize:

  • 先初始化父類
  • 再初始化子類(可能最終呼叫的是父類的initialize方法)

關聯物件

@property (nonatomic, assign)int age;

在類中宣告屬性,系統會做3件事:
1.生成帶下劃線成員變數
2.setter和getter宣告
3.setter和getter的實現

分類中新增屬性,則只會生成getter和setter的宣告

要想與類一樣,實現一個屬性的讀寫,可供解決方案有:
1.通過全域性變數當中間值賦值,問題是沒次建立一個屬性都是共用一個全域性變數,會導致資料錯亂
2.使用可變字典作為全域性變數,問題:1.存線上程安全的問題2.每次新增屬性比較麻煩

預設情況下,因為分類底層結構的限制,不能新增成員變數到分類中,但可以通過關聯物件來間接實現

//新增關聯物件
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)
  • 關聯策略policy


    2163717-a31b30fc9fa3ccd3.png
    Snip20180703_24.png
  • 關鍵字key
    關聯屬性方法中的key獲取方式:
    1.可以直接通過const void *name = &name方式,即傳入name獲取
    2.static const char name.傳入&name獲取
    3.key處傳入@selector(name);
    4.僅限於getter,直接在引數處傳入_cmd
objc_getAssociatedObject(self, _cmd)

注:
 1)往全域性變數新增一個static,令該全域性變數作用域僅當前檔案
 2)直接將字串寫入,由於字串位於常量區,故所有的值都是相同的
 3)因為每個方法其實都預設傳遞2個引數,第一個是self,第二個是_cmd,故也可以使用一下方式傳遞key值,setter方法不行,因為兩者的@selector是不同的
 4)關聯物件中存入的值既不是存放類物件中,也不是存放在例項物件中

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

  • AssociationsManager
  • AssociationHashMap
  • ObjectAssociationMap
  • ObjcAssociation
2163717-38e89bb52eb8c382.png
Snip20180703_30.png

其中的disguised_ptr_t是經過一個DISGUISE函式(其內部就是獲取object地址進行相應的位運算)

若查詢時發現value已經銷燬或不存在,則系統會報壞記憶體訪問,因為ObjcAssociation內部對value是強引用的

相關文章