1. 程式人生 > 實用技巧 >iOS-底層原理 18:類的載入(下)

iOS-底層原理 18:類的載入(下)

iOS 底層原理 文章彙總

在上一篇文章iOS-底層原理 17:類的載入(上)中,理解了類是如何從Mach-O載入到記憶體中,這次我們來解釋下分類是如何載入中的,以及分類和類搭配使用的情況

分類的本質

前提:在main中定義LGperson的分類LG
image

探索分類的本質,有以下三種方式

  • 【方式一】通過clang
  • 【方式二】通過Xcode文件搜尋Category
  • 【方式三】通過objc原始碼搜尋 category_t

方式一:通過clang

  • 【方式一】clang -rewrite-objc main.m -o main.cpp 檢視底層編譯,即 main.cpp,

    • 其中分類的 型別是_category_t
    • 分類的倒數第二個0,表示的是沒有協議,所以賦值為0
      image
  • 搜尋struct _category_t,如下所示

    • 其中有兩個method_list_t,分別表示例項方法類方法
      image
  • 搜尋_CATEGORY_INSTANCE_METHODS_LGPerson_,找到其底層實現
    image
    其中有3個方法,格式為:sel+簽名+地址,是method_t結構體的屬性即key
    image

  • 搜尋method_t,其中對應關係如下

    • name 對應 sel
    • type 對應 方法簽名
    • imp 對應 函式地址
      image

同時,我們發現了一個問題:檢視看_prop_list_t,明明分類中定義了屬性,但是在底層編譯中並沒有看到屬性,如下圖所示,這是因為分類中定義的屬性沒有相應的set、get方法

,我們可以通過關聯物件來設定(關於如何設定關聯物件,我們將在後續的擴充套件中進行說明)
image

方式二:通過Xcode文件搜尋 Category

如果不會clang,可以通過Xcode文件搜尋 Category
image

方式三:通過objc原始碼搜尋 category_t

還可以通過objc原始碼搜尋category_t型別
image

總結

綜上所述,分類的本質 是一個_category_t型別

  • 有兩個屬性:name(類的名稱)cls(類物件)

  • 有兩個 method_list_t型別的方法列表,表示分類中實現的例項方法+類方法

  • 一個protocol_list_t型別的協議列表,表示分類中實現的協議

  • 一個prop_list_t

    型別的屬性列表,表示分類中定義的屬性,一般在分類中新增的屬性都是通過關聯物件來實現

  • 需要注意的是,分類中的屬性是沒有set、get方法

分類的載入

前提:建立LGPerson的兩個分類:LGA、LGB
image

在上一篇iOS-底層原理 17:類的載入(上)文章中的realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的載入,其中分析了分類的data資料 時如何 載入到中的,且分類的載入順序是:LGA -> LGB的順序載入到類中,即越晚加進來,越在前面

其中檢視methodizeClass的原始碼實現,可以發現類的資料分類的資料是分開處理的,主要是因為在編譯階段,就已經確定好了方法的歸屬位置(即例項方法儲存在中,類方法儲存在元類中),而分類是後面才加進來的
image

其中分類需要通過attatchToClass新增到類,然後才能在外界進行使用,在此過程,我們已經知道了分類載入三步驟的後面兩個步驟,分類的載入主要分為3步:

  • 分類資料載入時機:根據類和分類是否實現load方法來區分不同的時機

  • attachCategories準備分類資料

  • attachLists分類資料新增到主類

分類的載入時機

下面我們來探索分類資料的載入時機,以主類LGPerson + 分類LGA、LGB 均實現+load方法為例

通過第二步資料準備反推第一步的載入時機

  • 通過上一篇文章我們瞭解到,在走到attachCategories方法時,必然會有分類資料的載入,可以通過反推法檢視 在什麼時候呼叫attachCategories的,通過查詢,有兩個方法中呼叫

    • load_categories_nolock方法中
      image
    • addToClass方法中,這裡經過除錯發現,從來不會進到if流程中,除非載入兩次,一般的類一般只會載入一次
      image
  • 不加任何斷點,執行objc程式碼,可以得出以下列印日誌,通過日誌可以發現addToClass方法的下一步就是load_categories_nolock方法就是載入分類資料
    image

  • 全域性搜尋load_categories_nolock的呼叫,有兩次呼叫

    • 一次在loadAllCategories方法中
      image
    • 一次在_read_images方法中
      image
  • 但是經過除錯發現,是不會走_read_images方法中的if流程的,而是走的loadAllCategories方法中的
    image

  • 全域性搜尋檢視loadAllCategories的呼叫,發現是在load_images時呼叫的
    image

通過堆疊資訊分析

  • attachCategories中加自定義邏輯的斷點,bt檢視堆疊資訊
    image

所以綜上所述,該情況下的分類的資料載入時機的反推路徑為:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images

而我們的分類載入正常的流程的路徑為:realizeClassWithoutSwift -> methodizeClass -> attachToClass ->attachCategories

其中正向和反向的流程如下圖所示:
image

我們再來看一種情況:主類+分類LGA實現+load,分類LGB不實現+load方法

  • 斷點定在attachCategories中加自定義邏輯部分,一步步往下執行

    • p entry.cat
    • p *$0
      image
  • 繼續往下執行,會再次來到 attachCategories方法中斷住

    • p entry.cat
    • p *$0
      image

總結只要有一個分類是非懶載入分類,那麼所有的分類都會被標記位非懶載入分類,意思就是載入一次 已經開闢了rwe,就不會再次懶載入,重新去處理 LGPerson

分類和類的搭配使用

通過上面的兩個例子,我們可以大致將類 和 分類 是否實現+load的情況分為4種

類+分類
分類實現+load分類未實現+load
類實現+load非懶載入類+非懶載入分類非懶載入類+懶載入分類
類未實現+load懶載入類+非懶載入分類懶載入類+懶載入分類
  • 【情況1】非懶載入類 + 非懶載入分類

  • 【情況2】非懶載入類 + 懶載入分類

  • 【情況3】懶載入類 + 懶載入分類

  • 【情況4】懶載入類 + 非懶載入分類

非懶載入類 與 非懶載入分類

主類實現了+load方法,分類同樣實現了+load方法,在前文分類的載入時機時,我們已經分析過這種情況,所以可以直接得出結論,這種情況下

  • 類的資料載入是通過_getObjc2NonlazyClassList載入,即ro、rw的操作,對rwe賦值初始化,是在extAlloc方法中

  • 分類的資料載入是通過load_images載入到類中的

其呼叫路徑為:

  • map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,此時的mlists是一維陣列,然後走到load_images部分

  • load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此時的mlists二維陣列

下面為原始碼中除錯的列印日誌
image

非懶載入類 與 懶載入分類

主類實現了+load方法,分類未實現+load方法

  • 開啟realizeClassWithoutSwift中的自定義斷點,看一下ro

    • 檢視kc_ro
      image
    • p kc_ro->baseMethodList
      image
    • p $1.get(0) ~ p $1.get(4)
      image
    • p $1.get(5)、 p $1.get(10)
      image
      從上面的列印輸出可以看出,方法的順序是 LGB—LGA-LGPerson類,此時分類已經 載入進來了,但是還沒有排序,說明在沒有進行非懶載入時,通過cls->data讀取Mach-O資料時,資料就已經編譯進來了,不需要執行時新增進去
  • 來到methodizeClass方法中斷點部分

    • p list
    • p $0->get(0)- p $0->get(5)
      image
  • 來到prepareMethodLists的for迴圈部分

    • p addedLists[0]
    • p addedLists[1]
    • p *$1
    • p *$2
      image
  • 來到fixupMethodList方法中的if (sort) {部分

    • 其中SortBySELAddress的原始碼實現如下:根據名字的地址進行排序
      image
  • 走到mlist->setFixedUp();,在讀取list

    • p mlist
    • p $7->get(0) ~ p $7->get(3)
      image
    • p $7->get(4) ~ p $7->get(6)
      image
      通過打印發現,僅對同名方法進行了排序,而分類中的其他方法是不需要排序的,其你imp地址是有序的(從小到大) – fixupMethodList中的排序只針對 name 地址進行排序
      image
  • 不加任何斷點,執行程式,獲取列印日誌
    image

總結非懶載入類 與 懶載入分類的資料載入,有如下結論:

  • 類 和 分類的載入是在read_images就載入資料了

  • 其中data資料編譯時期就已經完成了

懶載入類 與 懶載入分類

主類和分類均未實現+load方法

  • 不加任何斷點,執行程式,獲取列印日誌
    image
    其中realizeClassMaybeSwiftMaybeRelock是訊息流程中慢速查詢中有的函式,即在第一次呼叫訊息時才有的函式

  • readClass斷住,然後讀取kc_ro,即讀取整個data
    image
    此時的baseMethodListcount還是16,說明也是從data中讀取出來的,所以不需要經過一層緩慢的load_images載入進來

總結懶載入類 與 懶載入分類資料載入是在訊息第一次呼叫時記載

懶載入類 與 非懶載入分類

主類未實現+load方法, 分類實現了+load方法

  • 不加任何斷點,執行程式,獲取列印日誌
    image

  • 在列印的日誌中沒有看到load_categories_nolock方法,檢視attachCategories -- extAlloc -- attachToClass -- attachCategories,在attachToClass中加斷點
    image

  • readClass方法中斷住,檢視kc_ro
    image
    其中baseMethodList的count是8個,列印看看:物件方法3個+屬性的setget方法共4個+1個cxx方法 ,即 現在只有主類的資料
    image

    • 檢視kc_ro結構
      image
    • p kc_ro->baseMethodList
    • p $0->get(0) ~ p $0->get(3)、p $0->get(7)
      image
  • 為了除錯分類的資料載入, 繼續往下執行,bt檢視堆疊:load_images -> loadAllCategories -> load_categories_nolock
    image

總結懶載入類 + 非懶載入分類的資料載入,只要分類實現了load,會迫使主類提前載入,即 主類 強行轉換為 非懶載入類樣式

總結

類和分類搭配使用,其資料的載入時機總結如下:

  • 【情況1】非懶載入類 + 非懶載入分類,其資料的載入在load_images方法中,首先對類進行載入,然後把分類的資訊貼到類中

  • 【情況2】非懶載入類 + 懶載入分類,其資料載入在read_image就載入資料,資料來自data,data在編譯時期就已經完成,即data中除了類的資料,還有分類的資料,與類繫結在一起

  • 【情況3】懶載入類 + 懶載入分類 ,其資料載入推遲到 第一次訊息時,資料同樣來自data,data在編譯時期就已經完成

  • 【情況4】懶載入類 + 非懶載入分類 ,只要分類實現了load,會迫使主類提前載入,即在_read_images中不會對類做實現操作,需要在load_images方法中觸發類的資料載入,即rwe初始化,同時載入分類資料

如下圖所示
image