ios底層-類的加載下 類和分類在不一樣場景下的加載

Xcode 11.6 express

libobjc:781數組

核心方法回顧

類的加載上中羅列了一些和類加載相關的方法以便於這一次的展開分析。主要相關方法以下:緩存

read_images

  • 讀取鏡像文件,進行一些初始化工做,修復一些異常
  • 加載非懶加載類 ->realizeClassWithoutSwift

realizeClassWithoutSwift

  • 調整類的內部結構,讀取ro,建立rw,賦值rw->ro等等
  • 遞歸實現父類和元類
  • 更新父類以及元類和當前類的關係
  • 拷貝ro中的一些標識到rw
  • 條理化類-> methodizeClass

methodizeClass 修復類中的方法

  • 裝載當前cls本身實現的方法和屬性,不包含分類 -> prepareMethodLists
  • ro中的方法列表進行排序
  • 爲根元類動態添加initialize的實現
  • 加載分類數據

prepareMethodLists

  • 對傳入的方法列表進行按照sel地址的排序,確保後續方法查找的數組是有序數組

loadAllCategories

  • 加載全部的macho類型文件中的分類 -> load_categories_nolock

load_categories_nolock

  • 讀取macho文件中的分類列表
  • 加載分類到對應的類中 ->attachCategories/unattachedCategories.addForClass

unattachedCategories.addForClass

  • 當分類對應的類沒有實現時,將分類暫存,當主類實現後再進行加載

attachCategories

  • 建立class_rwe_ex_t結構體 ->class_rw_t::extAlloc()
  • 遍歷分類的列表,優化分類的方法列表(prepareMethodLists),添加分類中的方法、協議、屬性列表到rwe中 -> attachLists

class_rw_t::extAlloc()

  • 開闢class_rwe_ex_t,複製ro中的方法、協議以及屬性等數據到rwe中。

attachLists

  • 將方法/協議/屬性列表做爲單個元素加入到二維數組lists中,具體操做爲開闢新的內存空間,採用頭插法,確保新插入元素位於二維數組的首位。

addForClass

  • 存儲未實現類的分類(此時類爲懶加載,分類爲非懶加載)

分狀況討論類和分類的加載狀況

在上述方法中,realizeClassWithoutSwift爲實現類的必須方法,attachCategories爲添加分類到主類的核心方法,經過在這兩個方法中打下斷點來觀測不一樣狀況下類和分類的加載狀況。markdown

懶加載指的是沒有實現+load方法,不然爲非懶加載app

1. 類lazy+分類lazy

...
@implementation ZHYPerson
- (void)method {}
- (void)instanceMethod {}
@end
...
@implementation ZHYPerson (Category1)
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
複製代碼

觀察斷點發現咱們自定義的懶加載類ZHYPerson的實現是在main函數執行實例化ZHYPerson的時候觸發的。那麼分類是何時加載的呢?咱們先來打印一下當前cls的方法列表。函數

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x00000001000042c8
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:91)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "categoryMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:92)
}
(lldb) p $1.get(2)
(method_t) $3 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson method] at main.m:84)
}
(lldb) p $1.get(3)
(method_t) $4 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003180 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
}
複製代碼

此時ZHYPerson的方法列表中一共有4個方法,他們的順序爲:post

1. [ZHYPerson(Category1) method]
2. [ZHYPerson(Category1) categoryMethod]
3. [ZHYPerson method]
4. [ZHYPerson instanceMethod]
複製代碼

此時就會產生三個疑問:優化

  • realizeClassWithoutSwift的調用時機是在執行咱們本身的代碼ZHYPerson *person = [ZHYPerson alloc]時,和上一節的分析好像不太一致。
  • 經過代碼能夠發現ro並非正常的方法獲得的,而是經過auto ro = (const class_ro_t *)cls->data()獲得的,爲何會這樣呢?
  • 分類是何時加載的?attachCategories並無執行啊?

1. 第一個疑問realizeClassWithoutSwift的調用時機 上圖中按照從下往上有對應的方法調用註釋。由於分類和主類都是懶加載類,所以只有在須要用到的時候纔會實現,並無遵循非懶加載類的那一套流程。ui

2. 第二個疑問ro的獲取方式spa

class_rw_t *data() const {
    return bits.data();
}
複製代碼

根據上面的方法實現得知cls->data()方法原本應該獲得的是class_rw_t,可是系統將rw強轉成了ro。另一點是此時咱們的類尚未實現,所以讀取到的數據應該是直接從macho文件中讀取到的,就是說是編譯期就肯定的數據。那麼惟一能夠解釋的就是咱們的類在編譯時並無class_rw_t這個結構,而是將class_ro_t這個編譯器肯定的結構體指針放在了rw指針的位置。

3. 第三個疑問分類是何時加載的? 通過第二個疑問的解答得出此時的ro是編譯期肯定的內容,同時咱們打印方法列表的時候發現分類中的方法methodcategoryMethod都已經被添加到了方法列表中。這就是說分類已經在編譯期就加載到了咱們的主類當中。

此時咱們過掉斷點,發現attachCategories方法始終沒有執行。

綜上能夠得出以下結論: 懶加載分類在編譯期就加載到了主類當中。

2. 類nonLazy+分類nonLazy

主類和分類都實現了load方法。

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
+ (void)load {}
- (void)method {}
- (void)instanceMethod {}
@end

@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
+ (void)load {}
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
	}
    return 0;
}
複製代碼

觀察斷點發現此時類的實現是在dyld的流程中觸發的。此時咱們的main函數尚未執行。這個結果符合懶加載類的特徵,咱們來打印一下ro中的內容。

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100004390
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}
複製代碼

發現ro中只有兩個主類中的方法,說明當前的這種組合狀況下編譯期只肯定了主類的信息,分類並無加載進來。 過掉斷點: attachCategories執行了。此時的流程也是有dyld觸發的,處於main函數以前,打印ro的內容:

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100004390
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}
複製代碼

ro的內容只是順序發生了變化,這是由於在realizeClassWithoutSwift過程當中對方法進行了排序。接下來打印rwe中的methods:

(lldb) p rwe->methods
(method_array_t) $48 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100707f01
      arrayAndFlag = 4302339841
    }
  }
}
(lldb) p $48.beginLists()
// 二維數組
(method_list_t *const *) $49 = 0x0000000100707f08
(lldb) p *$49
// 讀取數組的第一個元素method_list_t *指針
(method_list_t *const) $50 = 0x00000001000043c8
(lldb) p *$50
(method_list_t) $51 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "categoryMethod"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:94)
    }
  }
}
(lldb) p $51.get(1)
(method_t) $52 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:93)
}
(lldb) p *($49+1)
// 讀取數組的第一個元素method_list_t *指針
(method_list_t *const) $53 = 0x0000000100004390
(lldb) p *$53
(method_list_t) $54 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "instanceMethod"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003170 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
    }
  }
}
(lldb) p $54.get(1)
(method_t) $55 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003160 (objc-debug`-[ZHYPerson method] at main.m:85)
}
複製代碼

咱們知道編譯期類的結構中並無rwe,是在運行時類的結構須要發生變化時纔會生成,生成以後會將ro中對應的數據拷貝到rwe中,同時還負責運行時添加的一些其餘數據。經過上述打印咱們能夠驗證一下幾點:

  • rwe中的methods是一個存儲method_list_t指針的數組,對應的是ro中的方法列表或者分類中的方法列表
  • 分類中的方法列表的位置比主類的方法列表位置靠前,由於插入的時候採起的是頭插法。

3. 類nonLazy+分類Lazy

主類實現load方法,分類不實現

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
+ (void)load {}
- (void)method {}
- (void)instanceMethod {}
@end

@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
- (void)method {}
- (void)categoryMethod{}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
	}
    return 0;
}
複製代碼

類的實現是在dyld流程中執行,在main函數以前,打印ro:

(lldb) p ro.baseMethods()
(method_list_t *) $0 = 0x00000001000042d0
  Fix-it applied, fixed expression was: 
    ro->baseMethods()
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "method"
      types = 0x0000000100003e08 "v16@0:8"
      imp = 0x0000000100003190 (objc-debug`-[ZHYPerson(Category1) method] at main.m:92)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "categoryMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x00000001000031a0 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:93)
}
(lldb) p $1.get(2)
(method_t) $3 = {
  name = "method"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003170 (objc-debug`-[ZHYPerson method] at main.m:85)
}
(lldb) p $1.get(3)
(method_t) $4 = {
  name = "instanceMethod"
  types = 0x0000000100003e08 "v16@0:8"
  imp = 0x0000000100003180 (objc-debug`-[ZHYPerson instanceMethod] at main.m:86)
}
複製代碼

分類的方法在編譯期已經加入到了ro中,因爲主類是非懶加載的緣由,在main函數以前就實現了。

4. 類lazy+分類nonLazy

主類不實現load方法,分類實現load方法。

@interface ZHYPerson:NSObject
@end
@implementation ZHYPerson
- (void)method {}
- (void)instanceMethod {}
@end
​
@interface ZHYPerson (Category1)
@end
@implementation ZHYPerson (Category1)
+ (void)load {}
- (void)method {}
- (void)categoryMethod{}
@end
​
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZHYPerson *person = [ZHYPerson alloc];
    }
    return 0;
}
複製代碼

當主類是懶加載,分類是非懶加載時realizeClassWithoutSwift的調用堆棧如上圖所示,此時觸發時機延遲到了load_images方法中。咱們先來看一下主類中ro的數據:

(lldb) p ro->baseMethods()
(method_list_t *) $0 = 0x0000000100002200
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 2
    first = {
      name = "method"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001650 (objc-debug`-[ZHYPerson method] at main.m:84)
    }
  }
}
(lldb) p $1.get(1)
(method_t) $2 = {
  name = "instanceMethod"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001660 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
}
複製代碼

此時主類的ro的方法列表中只有兩個方法:

  • method
  • instanceMethod

說明當前狀況下分類的數據在編譯期並無合併到主類中。跳過當前斷點來到attachCategories中: 此時分類數據已經加載完畢,咱們來看一下rwe中的數據狀況:

(lldb) p rwe->methods
(method_array_t) $3 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x000000010105c6a1
      arrayAndFlag = 4312123041
    }
  }
}
(lldb) p $3.beginLists()
(method_list_t *const *) $4 = 0x000000010105c6a8
(lldb) p *$4
(method_list_t *const) $5 = 0x0000000100002238
(lldb) p *$5
(method_list_t) $6 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "categoryMethod"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001690 (objc-debug`-[ZHYPerson(Category1) categoryMethod] at main.m:93)
    }
  }
}
(lldb) p $6.get(1)
(method_t) $7 = {
  name = "method"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001680 (objc-debug`-[ZHYPerson(Category1) method] at main.m:92)
}
(lldb) p *($4+1)
(method_list_t *const) $8 = 0x0000000100002200
(lldb) p *$8
(method_list_t) $9 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 2
    first = {
      name = "instanceMethod"
      types = 0x0000000100001ecd "v16@0:8"
      imp = 0x0000000100001660 (objc-debug`-[ZHYPerson instanceMethod] at main.m:85)
    }
  }
}
(lldb) p $8.get(1)
(method_t) $10 = {
  name = "method"
  types = 0x0000000100001ecd "v16@0:8"
  imp = 0x0000000100001650 (objc-debug`-[ZHYPerson method] at main.m:84)
}
複製代碼

能夠看到rwe的方法列表中數組中放着分類的方法列表和主類的方法列表,且分類在前,主類在後,此時的結構和主類分類同爲非懶加載類的結構是同樣的。 主要緣由有兩個:

1. 當前cls是懶加載類,所以在map_images中處理非懶加載類時並無執行cls的實現。

2. 當前cls的分類是非懶加載,所以在load_images中觸發了非懶加載分類的加載,因爲此時主類尚未實現,分類被暫時保存到了unattachedCategories中,在prepare_load_methods中處理非懶加載分類時,觸發了對主類的加載,加載過程當中將unattachedCategories中保存的對應分類添加到了主類中

額外補充

動態添加方法時是怎麼處理的?

動態添加方法通常使用class_addMethod,具體實現以下:

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}
複製代碼

addMethod

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls);
    
    ASSERT(types);
    ASSERT(cls->isRealized());

    method_t *m;
    // 從當前類中查找SEL對應的方法
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
        	// 不能替換的話會返回該方法的IMP
            result = m->imp;
        } else {
        	// 能夠替換的話就替換IMP
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
    // 並無在當前類中找到SEL對應的方法,此時須要查詢rwe是否存在,若是沒有就建立rwe
        auto rwe = cls->data()->extAllocIfNeeded();

        // fixme optimize
        // 生成一個method_list_t,將方法對應的數據放進去
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        // 添加方法列表到rwe的methods中
        rwe->methods.attachLists(&newlist, 1);
        // 清空緩存
        flushCaches(cls);

        result = nil;
    }

    return result;
}
複製代碼

主要流程以下:

1. 在當前類cls中查找是否存在和sel重名的方法

2. 若是找到直接返回對應的imp(replace參數爲NO,說明不能替換,不然直接替換imp)

3. 沒有找到sel對應的method

4. 取出rwe或建立rwe

5. 初始化一個method_list_t數組,將須要添加的方法sel、簽名、實現等賦值給第一個元素

6. 將數組添加到rwe的methods中

7. 清空緩存,此時方法列表已經發生了變化

備註

類懶加載和分類非懶加載的狀況在Xcode12表現有些異樣,多是我寫錯了,此處存疑。

相關文章
相關標籤/搜索