咱們上篇文章OC底層原理之-類的加載過程-上( _objc_init實現原理)講了類的加載流程,咱們大體講了read_image,load_image,unmap_image。上面的文章有些方法咱們沒有提到,這篇文章咱們繼續講類的加載。算法
咱們提到若是是非懶加載類,就會調用realizeClassWithoutSwift方法,下面我來探究下realizeClassWithoutSwift方法。看下整個方法,其中2544-2554行代碼是本身添加的,爲了研究Person類寫的輔助方法。 數組
上面咱們說了,只有非懶加載類纔會調用realizeClassWithoutSwift進行初始化,因此咱們建立Person類,添加+load方法
咱們準備下代碼(加方法屬性是爲了更好的研究類的加載)markdown
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod1;
- (void)instanceMethod2;
- (void)instanceMethod3;
+ (void)sayClassMethod;
@end
@implementation Person
+ (void)load{
}
- (void)instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)sayClassMethod{
NSLog(@"%s",__func__);
}
@end
複製代碼
咱們在2551行打斷點,開始運行代碼 此時調用realizeClassWithoutSwift傳進來的cls爲Person類。
有時候咱們研究本身建立的類會更清楚,添加一些輔助方法去快速找到咱們須要的類,能夠節省很多時間。這是後面研究源碼的思路
。 斷點往下走,第2556行if (cls->isRealized()) return cls;若是類已經類加載過,則直接返回函數
咱們看下251-2575行代碼 咱們講下這個判斷都作了些什麼 咱們看2561行,這個方法就是讀取當前cls的data() 咱們將斷點移到2562行,看下ro都包含什麼
post
咱們發現ro裏有類名,有方法列表,數目是8個,第一個方法名爲instanceMethod3。性能
上面的方法就是咱們從組裝的macho文件中讀到data,按照必定輸數據格式轉化(強轉爲class_ro_t *類型),此時的ro和咱們的cls是沒有關係的
。 繼續往下走2563行的判斷是判斷當前的cls是否爲元類。這裏不是元類,全部會走下面ui
2571行是申請和開闢zalloc,裏面包含rw,此時的rw爲空,咱們看下rw都有什麼 咱們看值都爲空
其中ro_or_rw_ext是ro或者rw_ext,ro是乾淨的內存(clean memory),rw_ext是髒內存(dirty memory)。
atom
2572行是將咱們建立的rw設置爲咱們的ro
,2573行是將class的data從新複製爲rw。
咱們驗證下spa
此時斷點在2572行,此時咱們打印cls 此時咱們發現最後的地址是爲空的,當咱們將斷點移到2578行,咱們在打印
發現最後的地址也爲空。咱們上面的2574行代碼說了,cls的data從新複製了,爲啥還爲空?3d
這是
由於ro爲read only是一塊乾淨的內存地址,那爲何會有一塊乾淨的內存和一塊髒內存呢?這是由於iOS運行時會致使不斷對內存進行增刪改查,會對內存的操做比較嚴重,爲了防止對原始數據的修改,因此把原來的乾淨內存copy一份到rw中,有了rw爲何還要rwe(髒內存),這是由於不是全部的類進行動態的插入,刪除。當咱們添加一個屬性,一個方法會對內存改動很大,會對內存的消耗頗有影響,因此咱們只要對類進行動態處理了,就會生成一個rwe。
下面咱們看下ro的讀取: 上面咱們看到ro的讀取有兩種狀況,class_rw_ext_t存在和不存在。
咱們繼續往下走,來到重要的方法,以下圖所示: 在這裏會調用父類,以及元類讓他們也進行上面的操做,之因此在此處就將父類,元類處理完畢的緣由就是
肯定繼承鏈關係,此時會有遞歸,當cls不存在時,就返回
繼續往下走,來到2604行代碼,此時的isMeta是YES,是由於它確實是元類。 cls->setInstancesRequireRawIsa();此方法就是設置isa 在2642行是將繼承鏈跑完了,繼續往下走,來到2649行 咱們發新此時的cls是個地址,而不是以前的Person了。這是爲啥?這是由於上面
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);方法會取到元類。
咱們來驗證一下。 咱們看到此時的cls確實是元類。繼續往下走:
下圖的方法就是系統幫咱們設置Cxx方法,繼續走,就來到咱們另外一個重點方法
看到這個方法咱們看註釋是跟分類有關,咱們看下methodizeClass()方法
咱們看下methodizeClass方法 一樣寫了輔助代碼,去快速找到咱們想要的類,上面
咱們知道realizeClassWithoutSwift會不斷的遞歸循環,並且會將data()從新複製(等於ro),可是咱們沒有看到對rw,rwe處理
咱們看下這個方法是否是對rw,rwe進行了處理
咱們知道ro裏存在methodlist,咱們在進行方法查找的時候是使用了二分查找,中間對sel進行排序
咱們先看下方法列表順序 先放着無論,咱們繼續往下走
此時的list是存在的,因此進入判斷內,會走prepareMethodLists。prepareMethodLists會對方法進行排序
咱們看下prepareMethodLists方法 咱們在1239行處加斷點,也是爲了快速找到Person類,
之因此在1238行作如此判斷,爲了防止元類形成影響
。
代碼往下走,就回來到下面判斷 這個就會走fixupMethodList方法
就會走到1215行,在1204-1212行咱們將sel名字處理完畢,sort是外界傳的是true,因此此時進入進入1216行,其中1216-1217行代碼就是進行排序(是根據SELAddress),方法不重要,咱們運行到最後,咱們從新打印下方法
這個跟咱們上面打印的方法順序不同,因此
prepareMethodLists是對方法進行序列化了。以前咱們在講方法查找的時候說過,在查找的時候是用的二分法進行查找
回到methodizeClass方法 咱們看到此時的rwe爲NULL,也就是rew沒有賦值,沒有走。這是爲何?
咱們先把這個問題放一下,在非懶加載的時候咱們知道realizeClassWithoutSwift調用時機,那麼非懶加載是何時調用realizeClassWithoutSwift的呢,咱們在main函數寫以下代碼,同時將+load方法刪除,運行代碼: 在realizeClassWithoutSwift方法中打斷點,斷點過來,咱們打堆棧信息,以下
經過上面咱們知道當向Person第一次發送消息時,就會走realizeClassWithoutSwift。由於類有不少代碼,不少方法排序和臨時變量,若是都放在main函數前加載,會致使加載時間很長,若是類歷來沒有被調用,那他不須要提早加載。因此懶加載提升性能。
其實在消息發送的時候有這部分的代碼展現
上面三張圖就是當類進行alloc時,
進行方法查找,若是類沒有被加載,就去加載類。這也就是說在建立類對象,以及方法調用的前提就是類已經被加載完成了。
下面用一張圖來看下懶加載和非懶加載的流程
咱們在main.m函數寫以下代碼
@interface Person (C)
@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, assign) int cate_age;
- (void)cate_instanceMethod1;
- (void)cate_instanceMethod3;
- (void)cate_instanceMethod2;
+ (void)cate_sayClassMethod;
@end
@implementation Person (C)
- (void)cate_instanceMethod1{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod3{
NSLog(@"%s",__func__);
}
- (void)cate_instanceMethod2{
NSLog(@"%s",__func__);
}
+ (void)cate_sayClassMethod{
NSLog(@"%s",__func__);
}
@end
複製代碼
而後用clang生成.cpp文件,看下分類在.cpp是什麼樣的 打開main.cpp文件,咱們看到以下圖所示 發現Person改成_CATEGORY_Person_是被_category_t修飾的,咱們看下_category_t是什麼樣的,全局搜一下
咱們發現_category_t是個結構體,裏面存在名字,cls,對象方法列表,類方法列表,協議,屬性
之因此分類有兩個列表是由於分類是沒有元分類的,分類的方法是在運行時經過attachToClass插入到class的
這個跟咱們被category_t修飾的結構是同樣的,此時的instance_methods被賦值爲_CATEGORY_INSTANCE_METHODS_Person_,咱們全局搜一下 看到這個是對象方法,存在3個,咱們看到有方法名,簽名,地址,這個和method_t結構體同樣。
可是咱們發現咱們的屬性在.cpp不存在set和get方法的,咱們看下屬性的賦值_PROP_LIST_Person_,搜索一下
咱們發現存在屬性可是沒set和get方法,因此分類中沒有實現屬性的set和get屬性,須要咱們用runTime進行屬性關聯
。
咱們發現分類本質就是一個category_t的形式 下面咱們就分析下分類是如何加載到內存中的
下面咱們建立分類,分類寫下以下方法
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
- (void)instanceMethod1;
- (void)instanceMethod2;
- (void)instanceMethod3;
+ (void)sayClassMethod;
@end
@interface Person (A)
- (void)instanceMethod1;
- (void)cateA_1;
- (void)cateA_2;
- (void)cateA_3;
@end
@interface Person (B)
- (void)instanceMethod1;
- (void)cateB_1;
- (void)cateB_2;
- (void)cateB_3;
@end
複製代碼
咱們上面建立了兩個Person的分類,分別是A,B同時寫了幾個方法,其中instanceMethod1是三個共有的,咱們在.m中分別實現+load方法,而後運行代碼 咱們在methodizeClass處打上斷點。(由於咱們上面知道了,若是類被加載,就必定會走realizeClassWithoutSwift方法,進而會調用methodizeClass方法
) 咱們在1468行進行斷點,發現會走到1480行,此時的cls爲Person,咱們看到unattachedCategories這個方法,它的初始化在runtime_init中進行的。 咱們看下attachToClass方法實現
咱們運行的時候發現代碼沒有進入1150-1161行,直接出來了,這是爲何?緣由就在1154行代碼的attachCategories方法。下面咱們具體分析一下
咱們先進行方法總覽
縱覽整個方法,咱們要知道研究的重點在哪,咱們要研究的是分類方法如何加載的,看方法1400行,咱們發現這個方法rwe->methods.attachLists就是方法插入的,這個方法的參數mlists,在1367行,知道研究重點,咱們下面開始研究。
下面在咱們寫的確認當前類爲person類處打斷點,讓代碼運行進來,果斷點 此時咱們發現cats_count,咱們有兩個分類,爲何此處是1呢?緣由是這個attachCategories是循環進來的,每次只有一個,此時咱們看下mlist。
咱們看到這個mlist已經存在方法,第一個是Person(B)的方法,而有4個方法,咱們打印下entry.cat
還記得咱們在對分類進行補充的時候,在.cpp文件看到的分類的name爲Person,而此時是分類的名字B,這就說明在編譯賦值的時候默認賦值是Person,而在運行時會改成分類的名字B。
咱們繼續往下看1369行,此時的mcount值,是不等於ATTACH_BUFSIZ,那麼那就回走1374行,對mlist進行處理,咱們看下怎麼處理的。
總共有64個位置,
[ATTACH_BUFSIZ - ++mcount]這個方法其中ATTACH_BUFSIZ是64也會是讓64減去mcount不斷的+1,獲得的位置等於list,這就是倒序插入。咱們看到第63位是0x0000000100003360,咱們在上面獲取entry.cat的時候它的instanceMethods = 0x0000000100003360。
也應證了倒序插入。
咱們看到mlist是method_list_t類型,是個一維數組,將mlist存到mlists,因此mlists是個二維數組
下面繼續走就會來到1400行 咱們看到此時的rwe是有值的,前面咱們說rwe一直沒有值,何時賦值的呢? 咱們看方法的1348行
點擊方法extAllocIfNeeded,看下實現
爲何此時要初始化rwe呢?
由於後面咱們要向本類裏添加方法、協議,要對原來的clean memory進行處理了
。那麼何時會初始化rwe呢?咱們搜索extAllocIfNeeded咱們發現有這幾種狀況將會調用extAllocIfNeeded初始化rwe。1.分類 2.addMethod 3.class_addProtocol 4._class_addProperty
(不只限於這幾種)
下面咱們看一下extAlloc方法 停在斷點處,咱們打印下rwe
在1283行進行了初始化,此時打印什麼都沒有,由於rwe只是初始化了,並無進行賦值。
繼續向下進行,此時會運行到1294行,此時獲取到List,咱們打印list 咱們看到此時的list爲本類的list,繼續往下走就會來到attachLists。
咱們看下attachLists方法,1399行代碼prepareMethodLists,上面講到的是進行方法排序
。咱們看下attachLists方法實現 咱們看到attachLists添加方法有三種狀況,第一種就是906行,傳進來的addedCount爲1且list不存在,則讓list等於addedLists的第一個元素,此時list是個一維數組,咱們再看else的代碼
上面就是
將新插入的放在lists的最前面,而將舊值放到後面,之因此這樣是由於新加入的價值大於老的,相似於YYCache的LRU算法。
這個也說明一個問題分類和本類有相同方法的時候,優先調用分類方法
。 這是驗證結果。
咱們繼續最後一種狀況就是,就是多個list裏面加入多個list 這個和oldList只有一個的狀況是一致的,
都是將新加入的放到表的最前面
。對上面作個圖更直觀
下面咱們驗證一下
上面的Person原本進來,進行下一步來到908行,在這打印下
咱們發現只有addedLists只有一個方法,但爲何p addedLists[1]有值,緣由是指針是連續的,它的值是地址,但裏面可能沒東西。p addedLists[0]取的是instanceMethod1方法的指針,因此此時是一維賦值。
咱們繼續向下進行 咱們看到此時的list是method_list_t結構。咱們果斷點繼續進行,方法走完,到此原本的方法執行完畢。
也就是在建立初始化rwe時就將本類的方法加載完畢,後面就開始進行分類加載
回到attachCategories方法,來到mlist,咱們打印下 此時就是咱們的分類方法,當通過一系列處理後,又會來到
咱們再次進入attachLists方法,在進入attachLists前,咱們打印些東西
咱們看到attachLists傳入的mlists + ATTACH_BUFSIZ - mcount就是mlists的最後一位地址
。
此時進入的是else方法 代碼運行帶最後,打印
咱們看到list_array_tt含有method_t,而method_t有包含method_list_t,以下圖所示
list_array_tt是個二維數組,裏面包含不少method_t,而method_t是一維數組,包含method_list_t。
當咱們再加一個分類的話是否是就走最上面,咱們放斷點,再次來到Person類,走到attachLists,此時走上面的判斷 此時咱們打印一下
說明
此時的array()第一個地址存放的是分類B的instanceMethod1方法
。 當這個方法執行完的時候,咱們在作相同的打印 這個時候咱們將分類A的方法加入到array()中了。因此attachLists方法將方法加入到array()的最前面。驗證了咱們上面說內容。
經過上面的講解,咱們明白了attachCategories就是準備分類數據,將他插入到本類方法列表裏。
,上面咱們研究的線:map_images->map_images_nolock->_read_images->realizeClassWithoutSwift->methodizeClass->attachToClass->attachCategories->attachLists(將方法加載到class())
。下面咱們看看還有別的地方調用attachCategories沒有,咱們全局搜索一下
咱們看到1紅框就是attachToClass,而紅框二就是load_categories_nolock方法,
那麼這個方法走不走呢?咱們在房裏打印log,運行項目 咱們發現這個方法是執行的,下面咱們來研究下這個方法的執行線。 咱們繼續往上找
又發現一條線:load_image->loadAllCategories->load_categories_nolock->attachCategories
。
上篇文章OC底層原理之-類的加載過程-上( _objc_init實現原理)咱們探究過load_image,知道它是讀取全部類的load方法,並調用的。上面的探索是咱們將類,分類都實現了+load方法。下面咱們來看下類,分類實現或者不實現+load方法,會有什麼樣的狀況。走不走方法的加載,咱們只須要查看attachCategories方法
咱們在attachCategories方法打斷點 下面咱們驗證:
1.其中一個分類存在+load方法,類存在+load方法
咱們看到只加載實現+load的分類,沒有實現的,則不加載
2.分類都存在+load方法,類存在+load方法
加載實現+load的分類,沒有實現+load方法的則不加載
3.分類都不實現+load方法,類存在+load方法 咱們發如今realizeClassWithoutSwift打印的時候,方法(包括分類的)已經都加在進去了,並且沒有進行排序。繼續往下走來到methodizeClass,發現同名方法進行了排序,而非同名的方法未進行排序。咱們在排序方法prepareMethodLists打斷點(講過prepareMethodLists是排序方法),進行打印
咱們看到addedLists是一維數組,後面會調用fixupMethodList方法,來到這個方法
1216行就是經過方法名字的地址進行排序
此時咱們看到
同名方法進行了排序,非同名的方法經過imp,從小到大進行排列的
。同上上面說明fixupMethodList先處理同名,處理完同名就會根據imp從小到大,可是這是同一類的方法,不一樣類的方法不按這個處理
。
若是主類實現,分類沒有實現,那麼分類的方法是從data()裏拿到的,只處理同名方法。
4.主類沒有實現+load方法,分類也沒有實現+load方法 咱們發現readClass後調用realizeClassWithoutSwift說明是在一次方法調用的時候去加載方法,咱們此時在readClass打斷點
看到ro中方法有16個,說明此時已經存在分類和本類方法了,方法也是經過data()拿到的。
5.主類沒有實現+load方法,分類實現+load方法,運行代碼: 咱們發現走了attachCategories方法,可是咱們發現沒有走_getObjc2NonlazyClassList方法,在readClass中咱們發現count爲8
咱們打印下,看下這8個都是什麼方法
咱們
發現這8個都是Person方法,沒有分類方法,分類方法是在load_categories_nolock中調用,也就是咱們說的第二條線上加載的
。
咱們發現當分類實現了,主類沒有實現,會迫使主類成爲一個非懶加載類,提早加載。
上面的文章和這篇都是再說類的加載,內容和多,這篇是上篇的補充。寫到凌晨2點,總算寫的差很少了(拓展內容明天繼續寫)。最後畫了個圖來大體說明整個過程吧
咱們在readClass的時候打斷點,對cls進行打印發現bits爲0x00000000。 可是咱們發現2561行代碼又調用了data()方法,
若是認爲bit是沒有值得,那系統調用data(),就至關於與null.data(),這有什麼意義?因此咱們驗證下bit究竟有沒有值
。 上面能夠看出來
bits是存在值的,之因此x/4gx cls打印的最後地址爲0x0000,那是由於cls此時的內存還未完善,因此纔會是0x00000
。這也說明當內存還未完善的時候,是能夠經過地址指針進行識別的
。
那在何時內存上的bit才存在值呢?咱們繼續往下走 咱們看這個判斷instancesRequireRawIsa,這個條件是初始化isa的必要條件
回到realizeClassWithoutSwift方法,繼續往下走
當執行完setInstanceSize值發生了變化,咱們繼續往下走
咱們發現當執行完setHasCxxDtor方法後,值再次發生變化