[toc]
## 前言
`Runtime`是近年來面試遇到的一個高頻方向,也是我們平時開發中或多或少接觸的一個領域,那么什么是runtime呢?它又可以用來做什么呢?
**什么是Runtime?平時項目中有用過么?**
- OC是一門`動態性`比較強的編程語言,允許很多操作`推遲`到程序`運行時`再進行
- OC的動態性就是由`Runtime`來支撐和實現的,Runtime是一套`C語言的API`,封裝了很多動 態性相關的函數
- 平時編寫的OC代碼,底層都是轉換成了`Runtime API`進行調用
**具體應用**
- 利用關聯對象(`AssociatedObject`)給分類添加屬性
- 遍歷類的所有成員變量(修改textfield的占位文字顏色、字典轉模型、自動歸檔解檔)
- 交換方法實現(交換系統的方法)
- 利用消息轉發機制解決方法找不到的異常問題
## 詳解isa
我們在研究對象的本質的時候提到過`isa`,當時說的是isa是個`指針`,`存儲的`是個類對象或者元類對象的`地址`。
- 實例對象的isa指向類對象
- 類對象的isa指向元類對象
確實,在arm64架構(真機環境)前,isa單純的就是一個指針,里面存儲著類對象或者元類對象地址,但是arm64架構后,系統對isa指針進行了優化,我們在源碼中可以探其結構:

可以看到,isa是個`isa_t`類型的數據,我們在點進去看一下isa_t是什么數據:

`isa_t`是個`union結構`,里面包含了一個結構體,結構體里面是個宏`ISA_BITFIELD`,我們看看這個宏是什么?

也就是這個結構體里面包含很多東西,但是究竟是什么東西要根據系統來確定。
那么在`arm64`架構下,isa指針的真實結構是:

在我們具體分析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指針做了優化處理,不在單純的存放類對象或者元類對象的內存地址,而是除此之外存儲了更多內容。
## class的具體結構
我們之前在講分類的時候講到了類的大體結構,如下圖所示:

就如我們之前講到的,當我們調用方法的時候是從`bits`中的`methods`中查找方法。
**分類的方法是排在主類方法前面的**,所以調用同名方法是先調用分類的,而且究竟調用哪個分類的方法要取決于`編譯的先后順序`等等:

### rw_t 和ro_t
那么這個`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 (rw:read and write 讀寫)和ro_t (ro:read only 只讀)的關系:
```
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,我們一一來看:
### method_t

- 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的結構:
### cache_t

>`散列表(Hash table,也叫哈希表)`,是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
我們可以看到,`cache_t`里面就三個成員,后兩個代表`長度和數量`,是`int類型`,肯定不是存儲方法的地方,所以方法應該是存儲在`_buckets`這個散列表中。散列存儲的是一個個的`bucket_t的結構體`,那么這個bucket_t又是個什么結構呢?
### bucket_t

所以`cache_t`底部結構是這樣的:

我們看到,`bucket_t`就兩個值,**一個key一個imp**,**key的話就是方法名,也就是SEL,而imp就是Value**,也就是當我們調用一個方法是來到方法緩存中查找,通過比對方法名是不是一致,一致的話就返回對應的imp,也就是`方法地址`,從而可以調用方法,那么這個散列表是怎么查找的呢?難道也是通過遍歷嗎?
### 方法查找
我們通過閱讀源碼來一探究竟:

--------------------------------------------------------

通過上面代碼的閱讀,我們可以知道系統在`cache_t`中查找方法**并不是通過遍歷**,而是通過方法名`SEL&mask得到一個索引`,直接去讀數組索引中的方法,如果該方法的SEL與我們調用的方法名SEL一致,那么就返回這個方法,否則會判斷當前架構,在x86或者i386架構中是向下尋找,在arm64架構中是向上尋找,直到找完為止。
好,既然取值的時候不是遍歷,而是直接讀的索引,那么講方法存儲到緩存中也肯定是通過這種方式了,直接方法名&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是個小于mask的隨機值且散列表存儲空間超過3/4的時候就要擴容,那就會導致散列表中有一部分空間始終被限制。確實,**散列表當分配內存后,每個地方最初都是null的,當某個位置的索引被用到時,對應的位置才會存儲方法**,其余位置仍處于空閑狀態,但是這樣做可以極大提高查找速度(比遍歷快很多),所以這是一種`空間換時間`的方式。

## 方法的傳遞過程
我們現在已經清楚方法的調用順序了,**首先從緩存中找沒有的話再去rw_t中找,那么在沒有的話就去其父類中找**,父類中查找也是如此,先去父類中的cache中查找,沒有的話再去父類的rw_t中找,以此類推。如果查找到基類還沒有呢?難道就直接報`unrecognized selector sent to instance `這個經典錯誤嗎?
其實不是,方法的傳遞主要涉及到三個部分,這也是我們平時用得最多以及面試中經常出現的問題:
我們都知道,當我們調用一個方法是,其實底層是將這個方法轉換成了`objc_msgSend`函數來進行調用,`objc_msgSend`的執行流程可以分為3大階段:
> 消息發送->動態方法解析->消息轉發
這個流程我們是可以從源碼中得到確認,以下是源碼:
```
/***********************************************************************
*_class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
MP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
//這個函數是方法調用流程的函數 即消息發送->動態方法解析->消息轉發
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//先從當前類對象的方法緩存中查看有沒有對應方法
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
//沒有的話再從類對象的方法列表中尋找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
//遍歷所有父類 知道其父類為空
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
//先查找父類的方法緩存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
//再查找父類的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
//消息發送階段沒找到imp 嘗試進行一次動態方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
//跳轉到retry入口 retry入口就在上面,也就是x消息發送過程即找緩存找rw_t
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
//消息發送階段沒找到imp而且執行動態方法解析也沒有幫助 那么就執行方法轉發
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
```
### 消息發送
首先,消息發送,就是我們剛才提到的系統會先去`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的相關內容
首先我們來看一下這段代碼:

我們發現最終的打印結果和我們預期的不一樣,按我們的思路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)
```
- 前言
- WebRTC知識集
- iOS 集成WebRTC各知識點小集
- iOS WebRTC集成時遇到的問題總結
- WebRTC多人音視頻聊天架構及實戰
- iOS端 使用WebRTC實現1對1音視頻實時通話
- iOS 基于WebRTC的點對點音視頻通信 總結篇
- WebRTC Native 源碼導讀 - iOS 相機采集實現分析
- OC 底層原理
- OC runtime 運行時詳解
- GCD dispatch_queue_create 創建隊列
- iOS底層 Runtime深入理解
- iOS底層 RunLoop深入理解
- iOS底層 Block的本質與使用
- iOS內存泄漏
- iOS中isKindOfClass和isMemberOfClass
- 從預編譯的角度理解Swift與Objective-C及混編機制
- 移動支付集成
- iOS 微信支付集成及二次封裝
- iOS 支付寶支付 Alipay集成及二次封裝
- iOS Paypal 貝寶支付集成及二次封裝
- iOS 微信、支付寶、銀聯、Paypal 支付組件封裝
- iOS 微信、支付寶、銀聯支付組件的進一步設計
- iOS 組件化
- iOS 組件化實施過程
- iOS 組件化的二進制化
- 使用pod package打包framework 實現組件的二進制化
- iOS 自制Framework 獲取指定bundle并讀取里面的資源
- .podSpec文件相關知識整理
- 開發并上傳靜態庫到CocoaPods
- pod引用第三方庫的幾種方式
- 如何在.podspec 文件中添加對本地庫的依賴
- lipo 命令合并真機與模擬器生成的framework
- iOS多線程
- NSOperation相關知識點
- 自定義NSOperation
- ios多個網絡請求之間的并行與串行場景的處理
- iOS動畫
- ios animation 動畫學習總結
- CABasicAnimation使用總結
- UITableView cell呈現的動效整理
- CoreAnimation動畫使用詳解
- iOS音視頻開發
- iOS 音視頻開發之AVCaptureMetadataOutput
- iOS操作本地視頻 - 獲取,壓縮,取第一幀
- 使用 GPUImage 實現一個簡單相機
- 直播App架構及思維導圖
- 如何快速的開發一個完整的iOS直播app
- iOS視頻拖動預覽及裁剪
- iOS 直播流程概述
- iOS直播:評論框與粒子系統點贊動畫
- iOS音視頻開發 - 采集
- 基于AVFoundation實現視頻錄制的兩種方式
- Swift知識集
- Swift 的枚舉、結構體和類詳解
- Swift 泛型詳解
- Swift屬性的包裝器@PropertyWrapper
- SwiftHub項目 之網絡層封裝的一點見解
- Moya+RxSwift+HandyJson 實現網絡請求及模型轉換
- Swift開發小記(含面試題)
- RxSwift 入坑手冊 - 基礎概念
- 理解 Swift 中的元類型:.Type 與 .self
- Swift HandyJSON庫中的類型相互轉換的實現
- Swift 中使用嵌套結構體定義一組相關的常量
- Swift Type-Erased(類型擦除)
- Swift中的weak和unowned關鍵字
- Swift 中的錯誤處理
- Swift中的Result 類型的簡單介紹
- Swift Combine 入門導讀
- Swift CustomStringConvertible 協議的使用
- 跨平臺
- Cordova跨平臺方案 iOS工程創建的步驟
- 使用Cordova 打包WebApp為原生應用詳解 (加殼封裝)
- RAC響應式編程
- 快速上手ReactiveCocoa之基礎篇
- RAC ReactiveCocoa 使用小集
- 優雅的 RACCommand
- 三方庫集成及使用
- 融云IM iOS sdk 集成 一篇就夠了
- iOS YYTextView使用筆記
- iOS YYLabel使用筆記
- iOS 蘋果集成登錄及蘋果圖標的制作要求
- iOS 面向切面編程 Aspects 庫的使用
- VKMsgSend庫對oc runtime的封裝
- OC Protocol協議分發器
- iOS 高德地圖實現大頭針展示,分級大頭針,自定制大頭針,在地圖上畫線,線和點共存,路線規劃(駕車路線規劃),路線導航,等一些常見的使用場景
- 工作總結
- 自定義UINavigationBar 適配iOS11, iOS15的問題
- SFSafariViewController 加載的網頁與原生oc之間的交互
- UICollectionView 設置header的二種方法
- UIPanGestureRecognizer進行視圖滑動并處理手勢沖突
- OC與Swift混編 注意事項
- UICollectionView 設置水平滑動后,調整每個Item項的排列方式
- oc 下定義字符串枚舉
- 高性能iOS應用開發中文版讀書筆記
- iOS 圖集滑動到最后時添加“顯示更多”效果的view組件 實現
- CocoaPods 重裝
- WKWebview使用二三事
- IOS電商首頁如何布局
- iOS中的投屏方案
- CGAffineTransform 介紹
- 用Block實現鏈式編程
- iOS 本地化簡明指南
- iOS 檢查及獲取相機、麥克風、相冊、位置等權限
- iOS 手勢UIGestureRecognizer詳解
- ios 編譯時報 Could not build module xxx 的解決方法嘗試
- iOS 常見編譯報錯及解決方案匯總(持續更新)
- AVMakeRectWithAspectRatioInsideRect 的使用
- graphhopper-ios 編譯過程詳解
- 算法
- iOS實現LRU緩存
- 架構
- IOS項目架構
- 其他雜項
- 推薦一個好用的Mac精品軟件下載站
- 如何能成為一位合格的職業經理人
- 零基礎怎么學習視頻剪輯?這篇初剪輯學者指南你一定不要錯過
- 免費SSL證書的制作
- 《一部手機拍全景》匯總課
- Linux下JAVA常用命令大全
- 即時通訊
- 通訊協議與即時通訊雜談
- 簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端
- 基于實踐:一套百萬消息量小規模IM系統技術要點總結
- PaddleOCR 文字識別深度學習
- PaddleOCR mac 安裝指南
- PaddleOCR 標注工具PPOCRLabel的使用
- PaddleOCR 更換模型
- PaddleOCR 自制模型訓練