Runtime是近年來面試遇到的一個高頻方向,也是咱們平時開發中或多或少接觸的一個領域,那麼什麼是runtime呢?它又能夠用來作什麼呢?html
什麼是Runtime?平時項目中有用過麼?
OC是一門動態性比較強的編程語言,容許不少操做推遲到程序運行時再進行
OC的動態性就是由Runtime來支撐和實現的,Runtime是一套C語言的API,封裝了不少動 態性相關的函數
平時編寫的OC代碼,底層都是轉換成了Runtime API進行調用
具體應用
利用關聯對象(AssociatedObject)給分類添加屬性
遍歷類的全部成員變量(修改textfield的佔位文字顏色、字典轉模型、自動歸檔解檔)
交換方法實現(交換系統的方法)
利用消息轉發機制解決方法找不到的異常問題
咱們在研究對象的本質的時候提到過isa,當時說的是isa是個指針,存儲的是個類對象或者元類對象的地址,實例對象的isa指向類對象,類對象的isa指向元類對象。確實,在arm64架構(真機環境)前,isa單純的就是一個指針,裏面存儲着類對象或者元類對象地址,可是arm64架構後,系統對isa指針進行了優化,咱們在源碼中能夠探其結構:ios
能夠看到,isa是個isa_t類型的數據,咱們在點進去看一下isa_t是什麼數據:c++
isa_t是個union結構,裏面包含了一個結構體,結構體裏面是個宏ISA_BITFIELD,咱們看看這個宏是什麼?面試
也就是這個機構體裏面包含不少東西,可是到底是什麼東西要根據系統來肯定。編程
那麼在arm64架構下,isa指針的真實結構是:api
在咱們具體分析isa內部各個參數分別表明什麼以前,咱們須要弄清楚這個union是什麼呢?咱們看着這個union和結構體的結構很像,這二者的區別以下↓↓數組
union:共用體,顧名思義,就是多個成員共用一塊內存。在編譯時會選取成員中長度最長的來聲明。 共用體內存=MAX(各變量) struct:結構體,每一個成員都是獨立的一塊內存。 結構的內存=sizeof(各變量之和)+內存對齊
也就是說,union共用體內全部的變量,都用同一塊內存,而struct結構體內的變量是各個變量有各個變量本身的內存,舉例說明:緩存
咱們分別定義了一個共用體test1和一個結構體test2,裏面都各自有八個char變量,打印出來各自佔用內存咱們發現共用體只佔用了1個內存,而結構體佔用了8個內存,安全
其實結構體佔用8個內存很好理解,8個char變量,每一個char佔用一個,因此是8;而union共用體爲何只佔用一個呢?這是由於他們共享同一個內存存儲東西,他們的內存結構是這樣的:數據結構
咱們看到te就一個內存空間,也就是全部的公用體成員公用一個空間,而且同一時間只能存儲其中一個成員變量的值,這一點咱們能夠打斷點或打印進行確認:
咱們發現,第一次打印的時候,bdf這些值都是1的打印出來都是0,這是由於當te.g = '0',執行完後,這個內存存儲的是g的值0,因此訪問的時候打印結果都是0。第二次打印同理,te.h執行完內存中存儲的是1,再訪問這塊內存那麼獲得的結果都會是1。因此咱們從這也能夠看出,union共用體就是系統分配一個內存供裏面的成員共同使用,某一時間只能存儲其中某一個變量的值,這樣作相比結構體而言能夠很大程度的節省內存空間。
既然咱們已經知道isa_t使用共用體的緣由是爲了最大限度節省內存空間,那麼各個成員後面的數字表明什麼呢?這就涉及到了位域.
咱們看到union共用體爲了節省空間是不斷的進行值覆蓋操做,也就是新值覆蓋舊值,結合位域的話能夠更大限度的節約內存空間還不用覆蓋舊值。咱們都知道一個字節是8個bit位,因此位域的做用就是將字節這個內存單位縮小爲bit位來存儲東西。咱們把上面這個union共用體加上位域:
上面這段代碼的意思就是,abcdefgh這八個char變量再也不是不停地覆蓋舊值操做了,而是將一個字節分紅8個bit位,每一個變量一個bit位,按照順序從右到左一次排列。
咱們都知道char變量佔用一個字節,一個字節有8個bit位,也就是char變量有8位,那麼te和te2的內存結構以下所示:
這個結構咱們也能夠經過打印來驗證:te佔用一個字節位置,內存地址對應的值是0xaa,轉換成二進制正好是10101010,也就是a~h存儲的值。
咱們能夠看到,如今是將一個字節中的8個bit位分別讓給8個char變量存儲數據,因此這些char變量存儲的數據不是0就是1,能夠看出來這種方式很是省內存空間,將一個字節分紅8個bit位存儲東西,物盡其用。因此咱們根據isa_t結構體中的所佔用bit位加起來=64能夠得知isa指針佔用8個字節空間。
雖然位域極大限度的節省了內存空間,可是如今面臨着一個問題,那就是如何給這些變量賦值或者取值呢?普通結構體中由於每一個變量都有本身的內存地址,因此直接根據地址讀取值便可, 可是union共用體中是你們共用同一個內存地址,只是分佈在不一樣的bit位上,因此是沒有辦法經過內存地址讀取值的,那麼這就用到了位運算符,咱們須要知道如下幾個概念:
&:按位與,同真爲真,其他爲假
|:按位或,有真則真,全假則假
<<:左移,表示左移動一位 (默認是00000001 那麼1<<1 則變成了00000010 1<<2就是00000100)
~:按位取反
掩碼 : 通常把用來進行按位與(&)運算來取出相應的值的值稱之爲掩碼(Mask)。如 #define TallMask 0b00000100 :TallMask就是用來取出右邊第三個bit位數據的掩碼
好,那麼咱們來看下這些運算符是怎麼能夠作到取值賦值的呢?好比說咱們上面的te共用體內有8個char,要是咱們想出去char b的值怎麼取呢?這就用到了&:
按位與上1<<1 就能夠取出b位的值了,b是1那麼結果就是1,b是0那麼結果就是0;
同理,當咱們爲f設置值的時候,也是相似的操做,就是在改變f的值的同時不影響其餘值,這裏咱們要看賦的值是0仍是1,不一樣值操做不一樣:
因此,這就是共同體中取值賦值的操做流程,那麼咱們接下來回到isa指針這個結構體中,看一下它裏面的各個成員以及怎麼取賦值的↓↓
/*nonpointer 0,表明普通的指針,存儲着Class、Meta-Class對象的內存地址 1,表明優化過,使用位域存儲更多的信息 */ uintptr_t nonpointer : 1; \ /*has_assoc:是否有設置過關聯對象,若是沒有,釋放時會更快*/ uintptr_t has_assoc : 1; \ /*是否有C++的析構函數(.cxx_destruct),若是沒有,釋放時會更快*/ uintptr_t has_cxx_dtor : 1; \ /*存儲着Class、Meta-Class對象的內存地址信息*/ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \ /*用於在調試時分辨對象是否未完成初始化*/ uintptr_t magic : 6; \ /*是否有被弱引用指向過,若是沒有,釋放時會更快*/ uintptr_t weakly_referenced : 1; \ /*對象是否正在釋放*/ uintptr_t deallocating : 1; \ /*裏面存儲的值是引用計數器減1*/ uintptr_t has_sidetable_rc : 1; \ /* 引用計數器是否過大沒法存儲在isa中 若是爲1,那麼引用計數會存儲在一個叫SideTable的類的屬性中 */ uintptr_t extra_rc : 19;
咱們看到,isa指針確實作了很大的優化,一樣是佔用8個字節,優化後的共用體不只存放這類對象或元類對象地址,還存放了不少額外屬性,接下來咱們對這個結構進行驗證:須要注意的是由於是arm64架構 因此這個驗證須要是ios項目且須要運行在真機上 這樣纔會得出準確的結果
首先,咱們來驗證這個shiftcls是否就是類對象內存地址。
咱們定義了一個dog對象,咱們打印它的isa是0x000001a102a48de1
從上面的分析咱們得知,要取出shiftcls的值須要isa的值&ISA_MASK(這個isa_mask在源碼中有定義),得出$1 = 0x000001a102a48de0
而$1的地址值正是咱們上面打印出來Dog類對象的地址值,因此這也驗證了isa_t的結構。
咱們還能夠來看一下其餘一些成員,好比說是否被弱指針指向過?咱們先將上面沒有被__weak指向過的數據保存一下,其中紅色框中的就是這個屬性,0表示沒有被指向過
而後咱們修改代碼,添加弱指針指向dog:__weak Dog *weaKDog = dog;
注意:只要設置過關聯對象或者弱引用引用過對象,has_assoc或
weakly_referenced
的值就會變成1,不論以後是否將關聯對象置爲nil或斷開弱引用。
發現確實由0變成了1,因此能夠驗證isa_t的結構,這個實驗要確保程序運行在真機才能出現這個結果。因此arm64後確實對isa指針作了優化處理,不在單純的存放類對象或者元類對象的內存地址,而是除此以外存儲了更多內容。
咱們以前在講分類的時候講到了類的大致結構,以下圖所示:
就如咱們以前講到的,當咱們調用方法的時候是從bits中的methods中查找方法,分類的方法是排在主類方法前面的,因此調用同名方法是先調用分類的,並且究竟調用哪一個分類的方法要取決於編譯的前後順序等等:
那麼這個rw_t中的methods和ro_t中的methods有什麼不同呢?
首先,ro_t中methods,是只包含原始類的方法,不包括分類的,而rw_t中的methods即包含原始類的也包含分類的;
其次,ro_t中的methods只能讀取不能修改,而rw_t中的methods既能夠讀取也能夠修改,因此咱們從此在動態添加方法修改方法的時候是在rw_t中的methods去操做的;
而後,ro_t中的methods是個一維數組,裏面存放着method_t(對方法/函數的封裝,即一個method_t表明一個方法或函數),而rw_t中的methods是個二維數組,裏面存放着各個分類和原始類的數組,分類和原始類的數組中存放着method_t。即:
咱們也能夠在源碼中找到rw_t和ro_t的關係,
static Class realizeClass(Class cls) { runtimeLock.assertLocked(); const class_ro_t *ro; class_rw_t *rw; Class supercls; Class metacls; bool isMeta; if (!cls) return nil; if (cls->isRealized()) return cls; assert(cls == remapClass(cls)); // 最開始cls->data是指向ro的 ro = (const class_ro_t *)cls->data(); if (ro->flags & RO_FUTURE) { // rw已經初始化而且分配內存空間 rw = cls->data(); // cls->data指向rw ro = cls->data()->ro; // cls->data()->ro指向ro 即rw中的ro指向ro cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE); } else { // 若是rw並不存在,則爲rw分配空間 rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 分配空間 rw->ro = ro;// rw->ro從新指向ro rw->flags = RW_REALIZED|RW_REALIZING; // 將rw傳入setData函數,等於cls->data()從新指向rw cls->setData(rw); } }
首先,cls->data(即bits)是指向存儲類初始化信息的ro_t的,而後在運行過程當中建立了class_rw_t,等
rw_t分配好內存空間後,開始將cls->data指向了rw_t並將rw_t中的ro指向了存儲初始化信息的ro_t。
那麼ro_t和rw_t中存儲的這個method_t是個什麼結構呢?咱們閱讀源碼發現結構以下,咱們發現有三個成員:name、types、imp,咱們一一來看:
name,表示方法的名稱,通常叫作選擇器,能夠經過@selector()
和sel_registerName()
得到。
/* 好比test方法,它的SEL就是@selector(test);或者sel_registerName("test");須要注意的一點就是不一樣類中的同名方法,它們的方法選擇器是相同的,好比A、B兩個類中都有test方法,那麼這兩個test方法的名稱都是@selector(test);或者sel_registerName("test"); */
types,表示方法的編碼,即返回值、參數的類型,經過字符串拼接的方式將返回值和參數拼接成一個字符串,來表明函數返回值及參數。
/* 好比ViewDidload方法,咱們都知道它的返回值是void,參數轉爲底層語言後是self和_cmd,即一個id類型和一個方法選擇器,那麼encode後就是v16@0:8(它所表示的意思是:返回值是void類型,參數一共佔用16個字節,第一個參數是@類型,內存空間從0開始,第二個參數是:類型,內存空間從8開始),固然這裏的數字能夠不寫,簡寫成V@: */
關於更多encode規則,能夠查看下面這個表:
固然除了本身手寫外,iOS提供了@encode
的指令,能夠將具體的類型轉化成字符串編碼。
NSLog(@"%s",@encode(int)); NSLog(@"%s",@encode(float)); NSLog(@"%s",@encode(id)); NSLog(@"%s",@encode(SEL)); // 打印內容 Runtime-test[25275:9144176] i Runtime-test[25275:9144176] f Runtime-test[25275:9144176] @ Runtime-test[25275:9144176] :
imp,表示指向函數的指針(函數地址),即方法的具體實現,咱們調用的方法實際上最後都是經過這個imp去進行最終操做的。
咱們在分析清楚方法列表和方法的結構後,咱們再來看一下方法的調用是怎麼一個流程呢?是直接去方法列表裏面遍歷查找對應的方法嗎?
其實否則,咱們在分析類的結構的時候,除了bits(指向類的具體信息,包括rw_t、ro_t等等一些內容)外,還有一個方法緩存:cache,用來緩存曾經調用過的方法
因此係統查找對應方法不是經過遍歷rw_t這個二維數組來尋找方法的,這樣作太慢,效率過低,系統是先從方法緩存中找有沒有對應的方法,有的話就直接調用緩存裏的方法,根據imp去調用方法,沒有的話,就再去方法數組中遍歷查找,找到後調用並保存到方法緩存裏,流程以下:
那麼方法是怎麼緩存到cache中的呢?系統又是怎麼查找緩存中的方法的呢?咱們經過源碼來看一下cache的結構:
散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。
咱們能夠看到,cache_t裏面就三個成員,後兩個表明長度和數量,是int類型,確定不是存儲方法的地方,因此方法應該是存儲在_buckets這個散列表中。散列存儲的是一個個的bucket_t的結構體,那麼這個bucket_t又是個什麼結構呢?
因此cache_t底部結構是這樣的:
咱們看到,bucket_t就兩個值,一個key一個imp,key的話就是方法名,也就是SEL,而imp就是Value,也就是當咱們調用一個方法是來到方法緩存中查找,經過比對方法名是否是一致,一致的話就返回對應的imp,也就是方法地址,從而能夠調用方法,那麼這個散列表是怎麼查找的呢?難道也是經過遍歷嗎?
咱們經過閱讀源碼來一探究竟:
經過上面代碼的閱讀,咱們能夠知道系統在cache_t中查找方法並非經過遍歷,而是經過方法名SEL&mask獲得一個索引,直接去讀數組索引中的方法,若是該方法的SEL與咱們調用的方法名SEL一直,那麼就返回這個方法,不然就一直向下尋找直到找完爲止。
好,既然取值的時候不是遍歷,而是直接讀的索引,那麼講方法存儲到緩存中也確定是經過這種方式了,直接方法名&mask拿到索引,而後將_key和_imp存儲到對應的索引上,這一點咱們經過源碼也能夠確認:
咱們看到不管是存仍是讀,都是調用了find函數,查看SEL&mask對應的索引的方法,不合適的話再向下尋找直到找到合適的位置。
那麼這裏有兩個疑問,爲何SEL&mask會出現不是該方法名(讀)或者不爲空(寫)的狀況呢?散列表擴容後方法還在嗎?
首先,SEL&mask這個問題,是由於不一樣的方法名&mask可能出現同一個結果,好比test方法的SEL是011,run方法的SEL是010,mask是010,那麼不管是test的SEL&mask仍是run的SEL&mask 記過都是010,若是你們都存在這個索引裏面是會出問題的,因此爲了解決這個索引重複的問題須要先作判斷,即拿到索引後先判斷這個索引對應的值是否是你想要的,是的話你拿走用,不是的話向下繼續找,方法緩存也是一樣的道理。咱們先調用test方法,緩存到010索引,再調用run方法,發現010位置不爲空了,那就判斷010下面的索引是否爲空,爲空的話就將run方法緩存到這個位置。
關於散列表擴容後,緩存方法在不在的問題,經過源碼就能夠知道,舊散列表已經釋放掉了,因此是不存在的,再次調用的時候就得從新去rw_t中遍歷找方法而後從新緩存到散列表中,好比下面這個例子:
更正更正更正
咱們前面講到當SEL&mask出來一個索引起現被佔用或者不是我想要的時候,系統是向索引下一位再次尋找,這個地方失誤了,不是向下是向上尋找,這個地方看源碼的時候忽略了條件,在x86或者i386架構中是向下尋找,在arm64架構中是向上尋找:(由於上面圖片資源都已經刪掉了就沒有再更改,這裏須要注意一下)
到如今咱們清楚了,那就是散列表中並非按照索引依次排序或者遍歷索引依次讀取,那麼就會出現個問題,由於SEL&mask是個小於mask的隨機值且散列表存儲空間超過3/4的時候就要擴容,那就會致使散列表中有一部分空間始終被限制。確實,散列表當分配內存後,每一個地方最初都是null的,當某個位置的索引被用到時,對應的位置纔會存儲方法,其他位置仍處於空閒狀態,可是這樣作能夠極大提升查找速度(比遍歷快不少),因此這是一種空間換時間的方式。
咱們如今已經清楚方法的調用順序了,實現從緩存中找沒有的話再去rw_t中找,那麼在沒有的話就去其父類中找,父類中查找也是如此,先去父類中的cache中查找,沒有的話再去父類的rw_t中找,以此類推。若是查找到基類尚未呢?難道就直接報unrecognized selector sent to instance 這個經典錯誤嗎?
其實不是,方法的傳遞主要涉及到三個部分,這也是咱們平時用得最多以及面試中常常出現的問題:
咱們都知道,當咱們調用一個方法是,其實底層是將這個方法轉換成了objc_msgSend函數來進行調用,objc_msgSend的執行流程能夠分爲3大階段:
消息發送->動態方法解析->消息轉發
這個流程咱們是能夠從源碼中獲得確認,如下是源碼:
1 /*********************************************************************** 2 * _class_lookupMethodAndLoadCache. 3 * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). 4 * This lookup avoids optimistic cache scan because the dispatcher 5 * already tried that. 6 **********************************************************************/ 7 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) 8 { 9 return lookUpImpOrForward(cls, sel, obj, 10 YES/*initialize*/, NO/*cache*/, YES/*resolver*/); 11 } 12 13 14 /*********************************************************************** 15 * lookUpImpOrForward. 16 * The standard IMP lookup. 17 * initialize==NO tries to avoid +initialize (but sometimes fails) 18 * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere) 19 * Most callers should use initialize==YES and cache==YES. 20 * inst is an instance of cls or a subclass thereof, or nil if none is known. 21 * If cls is an un-initialized metaclass then a non-nil inst is faster. 22 * May return _objc_msgForward_impcache. IMPs destined for external use 23 * must be converted to _objc_msgForward or _objc_msgForward_stret. 24 * If you don't want forwarding at all, use lookUpImpOrNil() instead. 25 **********************************************************************/ 26 //這個函數是方法調用流程的函數 即消息發送->動態方法解析->消息轉發 27 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 28 bool initialize, bool cache, bool resolver) 29 { 30 IMP imp = nil; 31 bool triedResolver = NO; 32 33 runtimeLock.assertUnlocked(); 34 35 // Optimistic cache lookup 36 if (cache) { 37 imp = cache_getImp(cls, sel); 38 if (imp) return imp; 39 } 40 41 // runtimeLock is held during isRealized and isInitialized checking 42 // to prevent races against concurrent realization. 43 44 // runtimeLock is held during method search to make 45 // method-lookup + cache-fill atomic with respect to method addition. 46 // Otherwise, a category could be added but ignored indefinitely because 47 // the cache was re-filled with the old value after the cache flush on 48 // behalf of the category. 49 50 runtimeLock.lock(); 51 checkIsKnownClass(cls); 52 53 if (!cls->isRealized()) { 54 realizeClass(cls); 55 } 56 57 if (initialize && !cls->isInitialized()) { 58 runtimeLock.unlock(); 59 _class_initialize (_class_getNonMetaClass(cls, inst)); 60 runtimeLock.lock(); 61 // If sel == initialize, _class_initialize will send +initialize and 62 // then the messenger will send +initialize again after this 63 // procedure finishes. Of course, if this is not being called 64 // from the messenger then it won't happen. 2778172 65 } 66 67 68 retry: 69 runtimeLock.assertLocked(); 70 71 // Try this class's cache. 72 //先從當前類對象的方法緩存中查看有沒有對應方法 73 imp = cache_getImp(cls, sel); 74 if (imp) goto done; 75 76 // Try this class's method lists. 77 //沒有的話再從類對象的方法列表中尋找 78 { 79 Method meth = getMethodNoSuper_nolock(cls, sel); 80 if (meth) { 81 log_and_fill_cache(cls, meth->imp, sel, inst, cls); 82 imp = meth->imp; 83 goto done; 84 } 85 } 86 87 // Try superclass caches and method lists. 88 { 89 unsigned attempts = unreasonableClassCount(); 90 //遍歷全部父類 知道其父類爲空 91 for (Class curClass = cls->superclass; 92 curClass != nil; 93 curClass = curClass->superclass) 94 { 95 // Halt if there is a cycle in the superclass chain. 96 if (--attempts == 0) { 97 _objc_fatal("Memory corruption in class list."); 98 } 99 100 // Superclass cache. 101 //先查找父類的方法緩存 102 imp = cache_getImp(curClass, sel); 103 if (imp) { 104 if (imp != (IMP)_objc_msgForward_impcache) { 105 // Found the method in a superclass. Cache it in this class. 106 log_and_fill_cache(cls, imp, sel, inst, curClass); 107 goto done; 108 } 109 else { 110 // Found a forward:: entry in a superclass. 111 // Stop searching, but don't cache yet; call method 112 // resolver for this class first. 113 break; 114 } 115 } 116 117 // Superclass method list. 118 //再查找父類的方法列表 119 Method meth = getMethodNoSuper_nolock(curClass, sel); 120 if (meth) { 121 log_and_fill_cache(cls, meth->imp, sel, inst, curClass); 122 imp = meth->imp; 123 goto done; 124 } 125 } 126 } 127 128 // No implementation found. Try method resolver once. 129 //消息發送階段沒找到imp 嘗試進行一次動態方法解析 130 if (resolver && !triedResolver) { 131 runtimeLock.unlock(); 132 _class_resolveMethod(cls, sel, inst); 133 runtimeLock.lock(); 134 // Don't cache the result; we don't hold the lock so it may have 135 // changed already. Re-do the search from scratch instead. 136 triedResolver = YES; 137 //跳轉到retry入口 retry入口就在上面,也就是x消息發送過程即找緩存找rw_t 138 goto retry; 139 } 140 141 // No implementation found, and method resolver didn't help. 142 // Use forwarding. 143 //消息發送階段沒找到imp並且執行動態方法解析也沒有幫助 那麼就執行方法轉發 144 imp = (IMP)_objc_msgForward_impcache; 145 cache_fill(cls, sel, imp, inst); 146 147 done: 148 runtimeLock.unlock(); 149 150 return imp; 151 }
首先,消息發送,就是咱們剛纔提到的系統會先去cache_t中查找,有的話調用,沒有的話去類對象的rw_t中查找,有的話調用並緩存到cache_t中,沒有的話根據supperclass指針去父類中查找。父類查找也是如此,先去父類的cache_t中查找,有的話進行調用並添加到本身的cache_t中而不是父類的cache_t中,沒有的話再去父類的rw_t中查找,有的話調用並緩存到本身的cache_t中,沒有的話以此類推。流程以下:
當消息發送找到最後一個父類尚未找到對應的方法時,就會來到動態方法解析。動態解析,就是意味着開發者能夠在這裏動態的往rw_t中添加方法實現,這樣的話系統再次遍歷rw_t就會找到對應的方法進行調用了。
動態方法解析的流程示意圖以下:
主要涉及到了兩個方法:
+resolveInstanceMethod://添加對象方法 也就是-開頭的方法 +resolveClassMethod://添加類方法 也就是+開頭的方法
咱們在實際項目中進行驗證:
動態添加類方法也是如此,只不過是添加到元類對象中(此時run方法已經改爲了個類方法)
並且咱們也發現,動態添加方法的話其實無非就是找到方法實現,添加到類對象或元類對象中,至於這個方法實現是什麼形式都沒有關係,好比說咱們再給對象方法添加方法實現時,這個實現方法能夠是個類方法,一樣給類方法動態添加方法實現時也能夠是對象方法。也就是說系統根本沒有區分類方法和對象方法,只要把imp添加到元類對象的rw_t中就是類方法,添加到類對象中就是對象方法。
當咱們在消息發送和動態消息解析階段都沒有找到對應的imp的時候,系統回來到最後一個消息轉發階段。所謂消息轉發,就是你這個消息處理不了後能夠找其餘人或者其餘方法來代替,消息轉發的流程示意圖以下:
即分爲兩步,第一步是看能不能找其餘人代你處理這方法,能夠的話直接調用這我的的這個方法,這一步不行的話就來到第二部,這個方法沒有的話有沒有能夠替代的方法,有的話就執行替代方法。咱們經過代碼來驗證:
咱們調用dog的run方法是,由於dog自己沒有實現這個方法,因此不能處理。正好cat實現了這個方法,因此咱們就將這個方法轉發給cat處理:
咱們發現,確實調用了小貓run方法,可是隻轉發方法執行者太侷限了,要求接收方法對象必須實現了一樣的方法才行,不然仍是沒法處理,因此實用性不強。這時候,咱們能夠經過methodSignatureForSelector來進行更大限度的轉發。
須要注意的是要想來到methodSignatureForSelector這一步須要將forwardingTargetForSelector返回nil(即默認狀態)不然系統找到目標執行者後就不會再往下轉發了。
開發者能夠在forwardInvocation:方法中自定義任何邏輯。
////爲方法從新轉發一個目標執行 //- (id)forwardingTargetForSelector:(SEL)aSelector{ // if (aSelector == @selector(run)) { // //dog的run方法沒有實現 因此咱們將此方法轉發到cat對象上去實現 也就是至關於將[dog run]轉換成[cat run] // return [[Cat alloc] init]; // } // return [super forwardingTargetForSelector:aSelector]; //} //方法簽名 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if (aSelector == @selector(run)) { //注意:這裏返回的是咱們要轉發的方法的簽名 好比咱們如今是轉發run方法 那就是返回的就是run方法的簽名 //1.可使用methodSignatureForSelector:方法從實例中請求實例方法簽名,或者從類中請求類方法簽名。 //2.也可使用instanceMethodSignatureForSelector:方法從一個類中獲取實例方法簽名 //這裏使用self的話會進入死循環 因此不可使用 若是其餘方法中有同名方法能夠將self換成其餘類 // return [self methodSignatureForSelector:aSelector]; // return [NSMethodSignature instanceMethodSignatureForSelector:aSelector]; //3.直接輸入字符串 return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } //當返回方法簽名後 就會轉發到這個方法 因此咱們能夠在這裏作想要實現的功能 可操做空間很大 //這個anInvocation裏面有轉發方法的信息,好比方法調用者/SEL/types/參數等等信息 - (void)forwardInvocation:(NSInvocation *)anInvocation{ //這樣寫不安全 能夠致使cat被過早釋放掉引起懷內存訪問 // anInvocation.target = [[Cat alloc] init]; Cat *ca = [[Cat alloc] init]; //指定target anInvocation.target = ca; //對anInvocation作出修改後要執行invoke方法保存修改 [anInvocation invoke]; //或者乾脆一行代碼搞定 [anInvocation invokeWithTarget:[[Cat alloc] init]]; //上面這段代碼至關於- (id)forwardingTargetForSelector:(SEL)aSelector{}中的操做 //固然 轉發到這裏的話可操做性更大 也能夠什麼都不寫 至關於轉發到的這個方法是個空方法 也不會報方法找不到的錯誤 //也能夠在這裏將報錯信息提交給後臺統計 好比說某個方法找不到提交給後臺 方便線上錯誤收集 //...不少用處 }
固然咱們也能夠訪問修改anInvocation的參數,好比如今run有個age參數,
// 參數順序:receiver、selector、other arguments int age; //索引爲2的參數已經放到了&age的內存中,咱們能夠經過age來訪問 [anInvocation getArgument:&age atIndex:2]; NSLog(@"%d", age + 10);
咱們發現,消息轉發有兩種狀況,一種是forwardingTargetForSelector,一種是methodSignatureForSelector+forwardInvocation:
其實,第一種也稱快速轉發,特色就是簡單方便,缺點就是能作的事情有限,只能轉發消息調用者;第二種也稱標準轉發,缺點就是寫起來麻煩點,須要寫方法簽名等信息,可是好處就是能夠很大成都的自定義方法的轉發,能夠在找不到方法imp的時候作任何邏輯。
固然,咱們上面的例子都是經過對象方法來演示消息轉發的,類方法一樣存在消息轉發,只不過對應的方法都是類方法,也就是-變+
因此,以上關於消息傳遞過程能夠用下面這個流程圖進一步總結:
關於源碼閱讀指南:
首先咱們來看一下這段代碼:
咱們發現最終的打印結果和咱們預期的不同,按咱們的思路Super就是指的的Dog的父類Animal,Animal調用class方法應該返回Animal 可是結果卻不是這樣,這是爲何?首先咱們先將這段代碼轉換成c++底層代碼來一探究竟:
static instancetype _I_Dog_init(Dog * self, SEL _cmd) { self = ((Dog *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("init")); if (self) { // NSLog(@"%@",[self class]); NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))); //NSLog(@"%@",[self superclass]); NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_1,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass"))); //NSLog(@"%@",[super class]); NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_2,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("class"))); //NSLog(@"%@",[super superclass]); NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_3,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("superclass"))); } return self; }
將上述代碼簡化後獲得下面的結果:
咱們發現,當self調用class方法時,是執行的objc_msdSend(self,@selector(class))函數,消息的接收者是當前所在類的實例對象(Dog) , 這個時候就會去self所在類 Dog去查找class方法 , 若是當前類Dog沒有class方法會向其父類Animal類找 class 方法, 若是Animal類也沒有找到class方法,最終會找到最頂級父類NSObject的class方法, 最終找到NSObject的class方法 ,並調用了object_getClass(self) ,因爲消息接收者是 self 當前類實例對象, 因此最終 [self class]輸出Dog(class方法是返回方法調用者的類型,superclass方法是返回方法調用者的父類)
[self superclass] 也是同理,找到superclass方法,而後返回調用者的父類,即Animal;
可是當咱們調用super的class方法時,底層不是轉換成objc_msdSend而是變成了objc_msgSendSuper函數。這個函數有兩個參數,第一個參數是個結構體,結構體中有兩個成員:方法調用者和調用者的父類,第二個參數就是方法名,也就是class方法的SEL。
[super class] -> objc_msgSendSuper( //第一個參數:結構體 {self,//方法調用者 class_getSuperclass(objc_getClass("Dog"))//當前類的父類 }, //第二個參數:方法名 sel_registerName("class")));
因此,咱們看到[self class]和[super class],他們轉換成的底層實現都不一致。objc_msgSendSuper函數的做用是告訴方法調用者去其父類中查找該方法,也就是相比objc_msdSend函數而言少了去本身類中查找方法這一步,而是直接去父類中找class方法,可是方法調用者仍是沒變,都是Dog。class方法和superclass它們都是返回方法調用者的類型或父類,因此[super class]和[super superclass]仍是返回的Dog的類型和父類,因此打印結果是Dog和Animal,與[self class]和[self superclass]結果一致。
因此,總結起來就是,super方法底層會轉換爲objc_msgSendSuper函數的調用,這個函數的做用是告訴方法調用者去父類中查找方法。
最後,咱們再來總結一下runtime中常見的API:
動態建立一個類(參數:父類,類名,額外的內存空間) Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) 註冊一個類(要在類註冊以前添加成員變量) void objc_registerClassPair(Class cls) 銷燬一個類 void objc_disposeClassPair(Class cls) 獲取isa指向的Class Class object_getClass(id obj) 設置isa指向的Class Class object_setClass(id obj, Class cls) 判斷一個OC對象是否爲Class BOOL object_isClass(id obj) 判斷一個Class是否爲元類 BOOL class_isMetaClass(Class cls) 獲取父類 Class class_getSuperclass(Class cls) 獲取一個實例變量信息 Ivar class_getInstanceVariable(Class cls, const char *name) 拷貝實例變量列表(最後須要調用free釋放) Ivar *class_copyIvarList(Class cls, unsigned int *outCount) 設置和獲取成員變量的值 void object_setIvar(id obj, Ivar ivar, id value) id object_getIvar(id obj, Ivar ivar) 動態添加成員變量(已經註冊的類是不能動態添加成員變量的) BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types) 獲取成員變量的相關信息 const char *ivar_getName(Ivar v) const char *ivar_getTypeEncoding(Ivar v) 獲取一個屬性 objc_property_t class_getProperty(Class cls, const char *name) 拷貝屬性列表(最後須要調用free釋放) objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) 動態添加屬性 BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) 動態替換屬性 void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) 獲取屬性的一些信息 const char *property_getName(objc_property_t property) const char *property_getAttributes(objc_property_t property) 得到一個實例方法、類方法 Method class_getInstanceMethod(Class cls, SEL name) Method class_getClassMethod(Class cls, SEL name) 方法實現相關操做 IMP class_getMethodImplementation(Class cls, SEL name) IMP method_setImplementation(Method m, IMP imp) void method_exchangeImplementations(Method m1, Method m2) 拷貝方法列表(最後須要調用free釋放) Method *class_copyMethodList(Class cls, unsigned int *outCount) 動態添加方法 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 動態替換方法 IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) 獲取方法的相關信息(帶有copy的須要調用free去釋放) SEL method_getName(Method m) IMP method_getImplementation(Method m) const char *method_getTypeEncoding(Method m) unsigned int method_getNumberOfArguments(Method m) char *method_copyReturnType(Method m) char *method_copyArgumentType(Method m, unsigned int index) 選擇器相關 const char *sel_getName(SEL sel) SEL sel_registerName(const char *str) 用block做爲方法實現 IMP imp_implementationWithBlock(id block) id imp_getBlock(IMP anImp) BOOL imp_removeBlock(IMP anImp)
這些api中有些咱們用的比較少,有的比較經常使用,好比咱們在修改UITextField的佔位文字顏色的話,能夠經過獲取UITextField的成員列表,發現其中佔位文字的顯示實際上是個placeholderLabel,因此咱們直接能夠經過kvo修改這個label的顏色:
還有好比常常用的字典轉模型框架MJExtension中,也是經過runtime函數遍歷全部的屬性或者成員變量,而後利用KVO去設值等等。
另外須要注意的一點是,咱們通常將動態添加方法、方法交換等等這些運行時操做放在load方法裏面實現,咱們在以前講解分類的時候提到了:
當類被引用進項目的時候就會執行load函數(在main函數開始執行以前),與這個類是否被用到無關,每一個類的load函數只會自動調用一次.也就是load函數是系統自動加載的,load方法會在runtime加載類、分類時調用。
方法交換通常用在將系統的某個方法交換成咱們本身寫的方法從而實現相應功能,這裏也有兩點須要注意:
①避免死循環:
方法交換交換的只是兩個方法的實現,也就是imp的交換,因此原理以下:
用代碼來演示,好比咱們如今要攔截全部按鈕的點擊事件,在作出點擊相應以前打印出相關信息,UIButton繼承自UIControl,button的addTarget: action: forControlEvents:方法底層也是調用了UIControl的sendAction:to:forEvent:方法,因此咱們須要將sendAction:to:forEvent:來進行方法交換,交換成咱們本身的方法:
#import "UIControl+Extension.h" #import <objc/runtime.h> @implementation UIControl (Extension) + (void)load { //這裏最好加上一個dispatch_once 雖然load方法原則上只會調用一次,可是萬一開發者手動再調用一次的話,那麼兩個方法交換了兩次就至關於沒交換 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method method2 = class_getInstanceMethod(self, @selector(my_sendAction:to:forEvent:)); method_exchangeImplementations(method1, method2); }); } //咱們本身實現的方法 - (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action)); // 調用系統原來的實現 //調用sendAction:會出現死循環 由於sendAction:方法的實現是my_sendAction: //[self sendAction:action to:target forEvent:event]; //因此須要調用my_sendAction:方法來實現系統原來的實現 由於my_sendAction:方法實現就是系統的sendAction:方法實現 [self my_sendAction:action to:target forEvent:event]; }
②須要注意類簇,確保交換的是正確的類
好比咱們在使用NSMutableArray添加數據的時候,若是添加nil會出錯,因此咱們要將系統的這個方法交換成咱們本身的方法從而能夠進行判斷,咱們也知道
addObject:方法底層是調用的insertObject:atIndex:方法,因此:
#import "NSMutableArray+Extension.h" #import <objc/runtime.h> @implementation NSMutableArray (Extension) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method method1 = class_getInstanceMethod(self, @selector(insertObject:atIndex:)); Method method2 = class_getInstanceMethod(self, @selector(my_insertObject:atIndex:)); method_exchangeImplementations(method1, method2); }); } - (void)my_insertObject:(id)anObject atIndex:(NSUInteger)index { if (anObject == nil) return; [self my_insertObject:anObject atIndex:index]; }
可是咱們發現,這樣作仍是不行,根本進不去咱們本身的my_insertObject:方法就會出錯:
//reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
這是由於咱們交換的類不對,在出錯信息咱們能夠看到這個insertObject:atIndex:是存在__NSArrayM中的,因此咱們應該交換__NSArrayM的方法而不是NSMutableArray的方法:
#import "NSMutableArray+Extension.h" #import <objc/runtime.h> @implementation NSMutableArray (Extension) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //這裏要是交換方法的真實類 Class cls = NSClassFromString(@"__NSArrayM"); Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:)); Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:)); method_exchangeImplementations(method1, method2); }); } - (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index { if (anObject == nil) return; [self mj_insertObject:anObject atIndex:index]; }
幾種常見類的真實類型:
method swizzling(俗稱黑魔法),簡單說就是進行方法交換,能夠經過如下幾種方式實現:
weak使咱們開發中常見的修飾符
,它是一種「非擁有關係」的指針(即弱引用)。經過weak修飾的指針變量,都不會改變被引用對象的引用計數,最主要的做用是爲了防止引用循環(retained cycle)
,常常用於block
和delegate。
weak、assign以及unsafe_unretained都是弱引用,這三者有什麼區別嗎?
①在一個對象被釋放後,weak會自動將指針指向nil,而assign和unsafe_unretained則不會。在iOS中,向nil發送消息時不會致使崩潰的,因此assign和unsafe_unretained會致使野指針的錯誤unrecognized selector sent to instance。
② weak 只能夠修飾對象。若是修飾基本數據類型,編譯器會報錯-「Property with ‘weak’ attribute must be of object type」。
assign 可修飾對象,和基本數據類型。unsafe_unretained也是只可修飾對象,因此用assign修飾對象和unsafe_unretained修飾對象實際上是同樣的。
weak不管是用做property修飾符仍是用來修飾一個變量的聲明其做用是同樣的,就是不增長新對象的引用計數,被釋放時也不會減小新對象的引用計數,同時在新對象被銷燬時,weak修飾的屬性或變量均會被設置爲nil,這樣能夠防止野指針錯誤,那麼runtime如何將weak修飾的變量的對象在銷燬時自動置爲nil?weak底層實現原理是什麼?
//用做property修飾符 @property(weak,nonatomic) NSObject *weakObj; //修飾變量的聲明 NSObject *obj = [[NSObject alloc]init]; __weak typeof(obj)weakObj = obj;
runtime對註冊的類會進行佈局,對於weak修飾的對象會放入一個hash表中。用weak指向的對象內存地址做爲key,當此對象的引用計數爲0的時候會dealloc,假如weak指向的對象內存地址是a,那麼就會以a爲鍵在這個weak表中搜索,找到全部以a爲鍵的weak對象,從而設置爲nil。
好比咱們上面__weak typeof(obj)weakObj = obj;這個例子:
當爲weakObj這一weak類型的對象賦值時,編譯器會根據obj的地址爲key去查找weak哈希表,這個表能夠理解成一個數組,將weakObj對象的地址(&weakObj)加入到數組中。當obj引用計數爲0時,會執行dealloc函數,在執行該函數時,編譯器會以obj變量的地址去查找weak哈希表的值,並將數組裏全部 weak對象所有賦值爲nil。
也就是,系統會建立一個全局的weak表(實際上是一個hash(哈希)表),Key是所指對象的地址,Value是weak指針的地址數組
NSObject *obj = [[NSObject alloc]init]; __weak typeof(obj)weakObj = obj; __weak typeof(obj)weakTest = obj; PeopleClass *perple = [[PeopleClass alloc]init]; __weak typeof(perple)weakTeacher = perple; AnimalClass *animal = [[AnimalClass alloc]init]; __weak typeof(animal)weakCat = animal; __weak typeof(animal)weakDog= animal; __weak typeof(animal)weakPig = animal;
接下來咱們在源碼中去查看weak的實現:
property中使用weak修飾
@property (nonatomic,weak) NSObject *referent; // 底層實現函數入口 id objc_storeWeak(id *location, id newObj) { return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object *)newObj); }
使用__weak修飾對象:
__weak NSObject *referent // 底層實現函數入口 id objc_initWeak(id *location, id newObj) { if (!newObj) { *location = nil; return nil; } return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating> (location, (objc_object*)newObj); }
不管是使用weak仍是__weak底層都是調用storeWeak
這個函數,區別在於模板的第一個參數HaveOld,這個參數用來表示這個弱指針是否有值。
Runtime維護了一個weak表,用於存儲指向某個對象的全部weak指針。weak表實際上是一個hash(哈希)表,Key是所指對象的地址,Value是weak指針的地址(這個地址的值是所指對象的地址)數組。
weak 的實現原理能夠歸納一下三步:
一、初始化時:runtime會調用objc_initWeak函數,初始化一個新的weak指針指向對象的地址。【property中使用weak修飾不存在這一步】
二、添加引用時:objc_initWeak函數會調用 objc_storeWeak() 函數, objc_storeWeak() 的做用是更新指針指向,建立對應的弱引用表。
三、釋放時,調用clearDeallocating函數。clearDeallocating函數首先根據對象地址獲取全部weak指針地址的數組,而後遍歷這個數組把其中的數據設爲nil,最後把這個entry從weak表中刪除,最後清理對象的記錄。
接下來用僞代碼說明一下具體流程:
//weak對象建立 static id storeWeak(id *location, objc_object *newObj){ //首先,咱們能夠拿到兩個值:一個是弱指針指向的對象obj,一個是弱指針對象weakObj if(全局weak表中存在一個key爲&obj的鍵值對){ 1、取出&obj這個key對應的value,這個value是個數組array; 2、將&weakObj插入到這個數組中 [weak_entry_insert(weak_table, &new_entry);] }else{//--若是weak表中沒有一個key爲&obj的鍵值對 那麼說明這個對象歷來沒有被弱指針對象指向過 因此就須要在weak表中建立一個新的鍵值對存儲了 1、建立這樣一個鍵值對 NSArry *value = @[&weakObj]; NSDictory *dic = @{&obj:value}; 2、查看weak剩餘存儲空間還多很少 if(weak表使用空間不足3/4) 將鍵值對dic插入到weak表中 }else{//若是剩餘空間不如1/4了 那麼就進行擴容 /* Grow if at least 3/4 full. if (weak_table->num_entries >= old_size * 3 / 4) { weak_resize(weak_table, old_size ? old_size*2 : 64); } */ 將weak表的存儲空間擴展其兩倍大; 將舊錶中的數據經過for循環 所有copy到新表中 將舊錶內存空間釋放 將將鍵值對dic插入到weak表中 } } //這個weak表很像咱們以前講方法緩存中的那個cache_t,其實都同樣,weak表中插入數據也不是按照索引去插入的,而是由&obj&mask獲得一個索引,若是這個索引有數據的話那就向下再找空間存儲 size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask); size_t index = begin; size_t hash_displacement = 0; while (weak_entries[index].referent != nil) { index = (index+1) & weak_table->mask; if (index == begin) bad_weak_table(weak_entries); hash_displacement++; }
//weak的釋放 weak_clear_no_lock(weak_table_t *weak_table, id referent_id){ 1.從weak表中取出這個以&obj爲key的鍵值對 2.取出鍵值對的value 存放弱指針的數組,weakAry for(int i = 0,i<weakAry.count,i++){ 將weakAry中的指針指向的內容所有置爲nil } 3.從weak表中刪除這個鍵值對 weak_entry_remove(weak_table, entry); }
詳細介紹及源碼解析: