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

iOS 底層原理 文章匯總

在上一篇文章iOS-底層原理 17:類的加載(上)中,理解了類是如何從Mach-O加載到內(nèi)存中,這次我們來(lái)解釋下分類是如何加載中的,以及分類和類搭配使用的情況

分類的本質(zhì)

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

image

探索分類的本質(zhì),有以下三種方式

  • 【方式一】通過(guò)clang
  • 【方式二】通過(guò)Xcode文檔搜索Category
  • 【方式三】通過(guò)objc源碼搜索 category_t

方式一:通過(guò)clang

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

    • 其中分類的 類型是_category_t
    • 分類的倒數(shù)第二個(gè)0,表示的是沒(méi)有協(xié)議,所以賦值為0
      image
  • 搜索struct _category_t,如下所示

    • 其中有兩個(gè)method_list_t,分別表示實(shí)例方法類方法
      image
  • 搜索_CATEGORY_INSTANCE_METHODS_LGPerson_,找到其底層實(shí)現(xiàn)

    image

    其中有3個(gè)方法,格式為:sel+簽名+地址,是method_t結(jié)構(gòu)體的屬性即key
    image

  • 搜索method_t,其中對(duì)應(yīng)關(guān)系如下

    • name 對(duì)應(yīng) sel
    • type 對(duì)應(yīng) 方法簽名
    • imp 對(duì)應(yīng) 函數(shù)地址
      image

同時(shí),我們發(fā)現(xiàn)了一個(gè)問(wèn)題:查看看_prop_list_t,明明分類中定義了屬性,但是在底層編譯中并沒(méi)有看到屬性,如下圖所示,這是因?yàn)?code>分類中定義的屬性沒(méi)有相應(yīng)的set、get方法,我們可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)設(shè)置(關(guān)于如何設(shè)置關(guān)聯(lián)對(duì)象,我們將在后續(xù)的擴(kuò)展中進(jìn)行說(shuō)明)

image

方式二:通過(guò)Xcode文檔搜索 Category

如果不會(huì)clang,可以通過(guò)Xcode文檔搜索 Category

image

方式三:通過(guò)objc源碼搜索 category_t

還可以通過(guò)objc源碼搜索category_t類型

image

總結(jié)

綜上所述,分類的本質(zhì) 是一個(gè)_category_t類型

  • 有兩個(gè)屬性:name(類的名稱)cls(類對(duì)象)

  • 有兩個(gè) method_list_t類型的方法列表,表示分類中實(shí)現(xiàn)的實(shí)例方法+類方法

  • 一個(gè)protocol_list_t類型的協(xié)議列表,表示分類中實(shí)現(xiàn)的協(xié)議

  • 一個(gè)prop_list_t類型的屬性列表,表示分類中定義的屬性,一般在分類中添加的屬性都是通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)

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

分類的加載

前提:創(chuàng)建LGPerson的兩個(gè)分類:LGA、LGB


image

在上一篇iOS-底層原理 17:類的加載(上)文章中的realizeClassWithoutSwift -> methodizeClass -> attachToClass -> load_categories_nolock -> extAlloc ->attachCategories中提及了rwe的加載,其中分析了分類的data數(shù)據(jù) 時(shí)如何 加載到中的,且分類的加載順序是:LGA -> LGB的順序加載到類中,即越晚加進(jìn)來(lái),越在前面

其中查看methodizeClass的源碼實(shí)現(xiàn),可以發(fā)現(xiàn)類的數(shù)據(jù)分類的數(shù)據(jù)是分開(kāi)處理的,主要是因?yàn)樵?code>編譯階段,就已經(jīng)確定好了方法的歸屬位置(即實(shí)例方法存儲(chǔ)在中,類方法存儲(chǔ)在元類中),而分類是后面才加進(jìn)來(lái)的

image

其中分類需要通過(guò)attatchToClass添加到類,然后才能在外界進(jìn)行使用,在此過(guò)程,我們已經(jīng)知道了分類加載三步驟的后面兩個(gè)步驟,分類的加載主要分為3步:

  • 分類數(shù)據(jù)加載時(shí)機(jī):根據(jù)類和分類是否實(shí)現(xiàn)load方法來(lái)區(qū)分不同的時(shí)機(jī)

  • attachCategories準(zhǔn)備分類數(shù)據(jù)

  • attachLists分類數(shù)據(jù)添加到主類

分類的加載時(shí)機(jī)

下面我們來(lái)探索分類數(shù)據(jù)的加載時(shí)機(jī),以主類LGPerson + 分類LGA、LGB 均實(shí)現(xiàn)+load方法為例

通過(guò)第二步數(shù)據(jù)準(zhǔn)備反推第一步的加載時(shí)機(jī)

  • 通過(guò)上一篇文章我們了解到,在走到attachCategories方法時(shí),必然會(huì)有分類數(shù)據(jù)的加載,可以通過(guò)反推法查看 在什么時(shí)候調(diào)用attachCategories的,通過(guò)查找,有兩個(gè)方法中調(diào)用

    • load_categories_nolock方法中
      image
    • addToClass方法中,這里經(jīng)過(guò)調(diào)試發(fā)現(xiàn),從來(lái)不會(huì)進(jìn)到if流程中,除非加載兩次,一般的類一般只會(huì)加載一次
      image
  • 不加任何斷點(diǎn),運(yùn)行objc代碼,可以得出以下打印日志,通過(guò)日志可以發(fā)現(xiàn)addToClass方法的下一步就是load_categories_nolock方法就是加載分類數(shù)據(jù)

    image

  • 全局搜索load_categories_nolock的調(diào)用,有兩次調(diào)用

    • 一次在loadAllCategories方法中
      image
    • 一次在_read_images方法中
      image
  • 但是經(jīng)過(guò)調(diào)試發(fā)現(xiàn),是不會(huì)走_read_images方法中的if流程的,而是走的loadAllCategories方法中的

    image

  • 全局搜索查看loadAllCategories的調(diào)用,發(fā)現(xiàn)是在load_images時(shí)調(diào)用的

    image

通過(guò)堆棧信息分析

  • attachCategories中加自定義邏輯的斷點(diǎn),bt查看堆棧信息
    image

所以綜上所述,該情況下的分類的數(shù)據(jù)加載時(shí)機(jī)的反推路徑為:attachCategories -> load_categories_nolock -> loadAllCategories -> load_images

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

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


image

我們?cè)賮?lái)看一種情況:主類+分類LGA實(shí)現(xiàn)+load,分類LGB不實(shí)現(xiàn)+load方法

  • 斷點(diǎn)定在attachCategories中加自定義邏輯部分,一步步往下執(zhí)行

    • p entry.cat
    • p *$0
      image
  • 繼續(xù)往下執(zhí)行,會(huì)再次來(lái)到 attachCategories方法中斷住

    • p entry.cat
    • p *$0
      image

總結(jié)只要有一個(gè)分類是非懶加載分類,那么所有的分類都會(huì)被標(biāo)記位非懶加載分類,意思就是加載一次 已經(jīng)開(kāi)辟了rwe,就不會(huì)再次懶加載,重新去處理 LGPerson

分類和類的搭配使用

通過(guò)上面的兩個(gè)例子,我們可以大致將類 和 分類 是否實(shí)現(xiàn)+load的情況分為4種

類+分類
分類實(shí)現(xiàn)+load 分類未實(shí)現(xiàn)+load
類實(shí)現(xiàn)+load 非懶加載類+非懶加載分類 非懶加載類+懶加載分類
類未實(shí)現(xiàn)+load 懶加載類+非懶加載分類 懶加載類+懶加載分類
  • 【情況1】非懶加載類 + 非懶加載分類

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

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

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

非懶加載類 與 非懶加載分類

主類實(shí)現(xiàn)了+load方法,分類同樣實(shí)現(xiàn)了+load方法,在前文分類的加載時(shí)機(jī)時(shí),我們已經(jīng)分析過(guò)這種情況,所以可以直接得出結(jié)論,這種情況下

  • 類的數(shù)據(jù)加載是通過(guò)_getObjc2NonlazyClassList加載,即ro、rw的操作,對(duì)rwe賦值初始化,是在extAlloc方法中

  • 分類的數(shù)據(jù)加載是通過(guò)load_images加載到類中的

其調(diào)用路徑為:

  • map_images -> map_images_nolock -> _read_images -> readClass -> _getObjc2NonlazyClassList -> realizeClassWithoutSwift -> methodizeClass -> attachToClass ,此時(shí)的mlists是一維數(shù)組,然后走到load_images部分

  • load_images --> loadAllCategories -> load_categories_nolock -> load_categories_nolock -> attachCategories -> attachLists,此時(shí)的mlists二維數(shù)組

下面為源碼中調(diào)試的打印日志


image

非懶加載類 與 懶加載分類

主類實(shí)現(xiàn)了+load方法,分類未實(shí)現(xiàn)+load方法

  • 打開(kāi)realizeClassWithoutSwift中的自定義斷點(diǎn),看一下ro

    • 查看kc_ro
      image
    • p kc_ro->baseMethodList


      image
    • p 1.get(0) ~ p1.get(4)
      image
    • p 1.get(5)、 p1.get(10)
      image

      從上面的打印輸出可以看出,方法的順序是 LGB—LGA-LGPerson類,此時(shí)分類已經(jīng) 加載進(jìn)來(lái)了,但是還沒(méi)有排序,說(shuō)明在沒(méi)有進(jìn)行非懶加載時(shí),通過(guò)cls->data讀取Mach-O數(shù)據(jù)時(shí),數(shù)據(jù)就已經(jīng)編譯進(jìn)來(lái)了,不需要運(yùn)行時(shí)添加進(jìn)去
  • 來(lái)到methodizeClass方法中斷點(diǎn)部分

    • p list
    • p $0->get(0)- p $0->get(5)
      image
  • 來(lái)到prepareMethodLists的for循環(huán)部分

    • p addedLists[0]
    • p addedLists[1]
    • p *$1
    • p *$2


      image
  • 來(lái)到fixupMethodList方法中的if (sort) {部分

    • 其中SortBySELAddress的源碼實(shí)現(xiàn)如下:根據(jù)名字的地址進(jìn)行排序
      image
  • 走到mlist->setFixedUp();,在讀取list

    • p mlist
    • p $7->get(0) ~ p $7->get(3)
      image
    • p $7->get(4) ~ p $7->get(6)
      image

      通過(guò)打印發(fā)現(xiàn),僅對(duì)同名方法進(jìn)行了排序,而分類中的其他方法是不需要排序的,其你imp地址是有序的(從小到大) -- fixupMethodList中的排序只針對(duì) name 地址進(jìn)行排序
      image
  • 不加任何斷點(diǎn),運(yùn)行程序,獲取打印日志


    image

總結(jié)非懶加載類 與 懶加載分類的數(shù)據(jù)加載,有如下結(jié)論:

  • 類 和 分類的加載是在read_images就加載數(shù)據(jù)了

  • 其中data數(shù)據(jù)編譯時(shí)期就已經(jīng)完成了

懶加載類 與 懶加載分類

主類和分類均未實(shí)現(xiàn)+load方法

  • 不加任何斷點(diǎn),運(yùn)行程序,獲取打印日志

    image

    其中realizeClassMaybeSwiftMaybeRelock是消息流程中慢速查找中有的函數(shù),即在第一次調(diào)用消息時(shí)才有的函數(shù)

  • readClass斷住,然后讀取kc_ro,即讀取整個(gè)data

    image

    此時(shí)的baseMethodListcount還是16,說(shuō)明也是從data中讀取出來(lái)的,所以不需要經(jīng)過(guò)一層緩慢的load_images加載進(jìn)來(lái)

總結(jié)懶加載類 與 懶加載分類數(shù)據(jù)加載是在消息第一次調(diào)用時(shí)記載

懶加載類 與 非懶加載分類

主類未實(shí)現(xiàn)+load方法, 分類實(shí)現(xiàn)了+load方法

  • 不加任何斷點(diǎn),運(yùn)行程序,獲取打印日志


    image
  • 在打印的日志中沒(méi)有看到load_categories_nolock方法,查看attachCategories -- extAlloc -- attachToClass -- attachCategories,在attachToClass中加斷點(diǎn)

    image

  • readClass方法中斷住,查看kc_ro

    image

    其中baseMethodList的count是8個(gè),打印看看:對(duì)象方法3個(gè)+屬性的setget方法共4個(gè)+1個(gè)cxx方法 ,即 現(xiàn)在只有主類的數(shù)據(jù)
    image

    • 查看kc_ro結(jié)構(gòu)


      image
    • p kc_ro->baseMethodList
    • p 0->get(0) ~ p0->get(3)、p $0->get(7)
      image
  • 為了調(diào)試分類的數(shù)據(jù)加載, 繼續(xù)往下執(zhí)行,bt查看堆棧:load_images -> loadAllCategories -> load_categories_nolock

    image

總結(jié)懶加載類 + 非懶加載分類的數(shù)據(jù)加載,只要分類實(shí)現(xiàn)了load,會(huì)迫使主類提前加載,即 主類 強(qiáng)行轉(zhuǎn)換為 非懶加載類樣式

總結(jié)

類和分類搭配使用,其數(shù)據(jù)的加載時(shí)機(jī)總結(jié)如下:

  • 【情況1】非懶加載類 + 非懶加載分類,其數(shù)據(jù)的加載在load_images方法中,首先對(duì)類進(jìn)行加載,然后把分類的信息貼到類中

  • 【情況2】非懶加載類 + 懶加載分類,其數(shù)據(jù)加載在read_image就加載數(shù)據(jù),數(shù)據(jù)來(lái)自data,data在編譯時(shí)期就已經(jīng)完成,即data中除了類的數(shù)據(jù),還有分類的數(shù)據(jù),與類綁定在一起

  • 【情況3】懶加載類 + 懶加載分類 ,其數(shù)據(jù)加載推遲到 第一次消息時(shí),數(shù)據(jù)同樣來(lái)自data,data在編譯時(shí)期就已經(jīng)完成

  • 【情況4】懶加載類 + 非懶加載分類 ,只要分類實(shí)現(xiàn)了load,會(huì)迫使主類提前加載,即在_read_images中不會(huì)對(duì)類做實(shí)現(xiàn)操作,需要在load_images方法中觸發(fā)類的數(shù)據(jù)加載,即rwe初始化,同時(shí)加載分類數(shù)據(jù)

如下圖所示


image

補(bǔ)充:load_images原理分析

load_images方法的主要作用是加載鏡像文件,其中最重要的有兩個(gè)方法:prepare_load_methods(加載) 和 call_load_methods(調(diào)用)

  • 進(jìn)入load_images源碼實(shí)現(xiàn)

    load_images原理分析-1

  • 進(jìn)入prepare_load_methods源碼

    • 進(jìn)入_getObjc2NonlazyClassList -> schedule_class_load源碼,這里主要是根據(jù)類的繼承鏈遞歸調(diào)用獲取load,直到cls不存在才結(jié)束遞歸,目的是為了確保父類的load優(yōu)先加載

      load_images原理分析-2

    • 進(jìn)入add_class_to_loadable_list,主要是將load方法和cls類名一起加到loadable_classes表中

      load_images原理分析-3

      • 進(jìn)入getLoadMethod,主要是獲取方法的sel為load的方法
        load_images原理分析-4
    • _getObjc2NonlazyCategoryList -> realizeClassWithoutSwift -> add_category_to_loadable_list ,主要是將非懶加載分類的load方法加入表中

      load_images原理分析-5

      • 進(jìn)入add_category_to_loadable_list實(shí)現(xiàn),獲取所有的非懶加載分類中的load方法,將分類名+load加入表loadable_categories
        load_images原理分析-6
  • 進(jìn)入call_load_methods源碼,主要有3部分操作

    • 反復(fù)調(diào)用類的+load,直到不再有

    • 調(diào)用一次分類的+load

    • 如果有類或更多未嘗試的分類,則運(yùn)行更多的+load


      load_images原理分析-7
    • 進(jìn)入call_class_loads,主要是加載類的load方法

      load_images原理分析-8

      其中load方法中有兩個(gè)隱藏參數(shù),第一個(gè)為id 即self,第二個(gè)為sel,即cmd

    • call_category_loads,主要是加載一次分類的load方法

      load_images原理分析-9

綜上所述,load_images方法整體調(diào)用過(guò)程原理圖示如下

  • 調(diào)用過(guò)程圖示


    調(diào)用過(guò)程圖示
  • 原理圖示


    原理圖示

    主要分為兩步

    • 從所有的非懶加載類和分類中的+load分別添加到表
    • 調(diào)用類和分類的+load方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。