Android multiple back stacks導航 談談android中多棧導航的幾種實現. 什麼是multiple stacks 當用戶在app里切換頁面時, 會需要向後回退到上一個頁面, 頁面歷史被保存在一個棧里. 在Android里我們經常說"back stack". 有時候在app ...
原文: https://mikeash.com/pyblog/objc_msgsends-new-prototype.html
總結 :
objc_msgSend 類型申明改變的原因: 讓錯誤在編譯的時候發生,而不是等到運行時。
為什麼有 運行時錯誤 : ABI 的錯配,調用方的ABI (對參數的傳遞) 和 接受方ABI( 對參加的處理)錯配了
為什麼 錯配: 我傳的是float ,你把我當double 了,浮點 轉成 雙精度浮點,這可不是和short 轉 int 單單高位補幾個0就可以了
為什麼float 專 double 了: C 語言 經常搞這種騷操作,畢竟處理數據的時候,用更高的精度有好處。
怎麼阻止 float 轉 double : 可以把函數中的參數類型申明成float ,而不是模棱兩可
等等函數參數 怎麼會模棱兩可: C 語言 變參函數瞭解一下 ,也就是void functionA(int a,int b, ...) 這種點點點函數,不幸原來的objc_msgSend 就是長這個樣子的
翻譯筆記:
objc_msgSend 變了
objc_msgSend 和 objc_msgSendSuper 的類型申明改了,那他們實際上接受什麼參數,以及它實際上返回什麼?
objc_msgSend 不得不用彙編
objc_msgSend 是用彙編實現的,不只是為了快,只用 C 是實現不了objc_msgSend的功能是的(pencilCool: 因為不能去擾亂傳參的寄存器呀 )
objc_msgSend 做的基礎工作
objc_msgSend的fast path 做了幾件關鍵的事情:
- 載入對象的類
- 在該類的方法緩存中查找selector 。
- 跳轉到在緩存中找到的方法實現。
objc_msgSend 不幹擾函數傳參的寄存器
因為objc_msgSend直接跳到了方法實現,而沒有進行函數調用,所以一旦它的工作完成,它就有效地消失了。
這個實現很謹慎,沒有干擾任何可以用來向函數傳遞參數的寄存器。
調用者在調用objc_msgSend時,就像直接調用方法實現一樣,以同樣的方式傳遞所有的參數。
一旦objc_msgSend查找到實現並跳轉到它,這些參數仍然是實現所期望的位置。當實現返回時,它直接返回給調用者,而返回值是由標準機制提供的。
這就回答了上面的問題:objc_msgSend的原型是它最終調用的方法實現的原型。
舊的原型在某些情況下可以工作(當ABI匹配時),而在其他情況下卻奇怪地失敗(當ABI不匹配時)。
新的原型永遠不會工作,除非你先把它投到適當的類型。只要你把它投到正確的類型上,它就總是能工作。因此,新的做事方式鼓勵正確做事,並使做錯事變得更難。
最小的原型
儘管objc_msgSend的原型取決於將被調用的方法實現,但有兩件事在所有的方法實現中是共同的:第一個參數總是id self,第二個參數總是SEL _cmd。objc_msgSend需要這兩個信息來執行它的方法調度工作,所以它們必須總是在同一個地方,以便它能夠找到它們。
我們可以為objc_msgSend寫一個近似的泛化原型來表示這一點。
objc_msgSend(id self, SEL _cmd, ???)
其中,??的意思是我們不知道,它取決於將被調用的特定方法實現。當然,C語言沒有辦法表示這樣的通配符。
對於返回值,我們可以嘗試挑選一些常見的東西。因為Objective-C是關於對象的,所以假設返回值是id是有意義的。
id objc_msgSend(id self, SEL _cmd, ???)
這不僅涵蓋了返回值是一個對象的情況,也涵蓋了返回值是無效的情況,以及其他一些返回值是不同類型但沒有被使用的情況。
那麼參數呢?C語言實際上有一種方法來表示任意類型的任意數量的參數,其形式是可變參數函數原型。在參數列表的末尾有一個省略號,意味著後面有一個任意類型的可變數量的值。
id objc_msgSend(id self, SEL _cmd, ...)
這正是最近改變之前的原型。
ABI錯配
運行時的相關問題是調用地點的ABI是否與方法實現的ABI匹配。也就是說,接收者是否會從相同的位置,以調用者傳遞的相同格式檢索參數?如果調用者將一個參數放入 $rdx,那麼實現者就需要從 $rdx中獲取該參數,否則就會出現混亂。
最小的原型可能能夠表達傳遞任意數量的任意類型的概念,但是為了讓它在運行時真正發揮作用,它需要使用與方法實現相同的ABI。該實現幾乎肯定使用了不同的原型,並且通常有固定數量的參數。
不能保證一個變數函數的ABI與一個有固定參數數的函數的ABI相匹配。在一些平臺上,它們幾乎完全匹配。在其他平臺上,它們完全不匹配。
英特爾ABI
讓我們看一個具體的例子。macOS使用x86-64的標準System V ABI。ABI中有大量的細節,但我們將專註於基礎知識。
參數是在寄存器中傳遞的。整數參數在寄存器rdi、rsi、rdx、rcx、r8和r9中傳遞,按順序排列。浮點參數在SSE寄存器xmm0到xmm7中傳遞。當調用一個變數函數時,寄存器 al 被設置為用於傳遞參數的SSE寄存器的數量。整數返回值被放在rax和rdx中,浮點返回值被放在xmm0和xmm1中。
變參函數的ABI幾乎與普通函數的ABI相同。唯一的例外是傳遞al中使用的SSE寄存器的數量。然而,當使用變參函數 ABI調用普通函數時,這一點是無害的,因為普通函數會忽略al的內容。
C語言把事情搞得有點亂。C語言規定,當作為變數參數傳遞時,某些類型會被提升為更廣泛的類型。小於int的整數(如char和short)被提升為int,而float被提升為double。如果你的方法簽名中包括這些類型之一,那麼如果調用者使用變數原型,就不可能將參數傳遞為該確切的類型。
對於整數來說,這實際上並不重要。整數被存儲在相應的寄存器的底層位,無論哪種方式,其位數最終都在同一個地方。然而,這對浮點數來說是災難性的。將一個較小的整數轉換為int只需要用額外的位來填充它。將float轉換為double涉及到將數值完全轉換為一個不同的結構。float中的位與double中的相應位並不一致。如果你試圖使用一個變數原型來調用一個接受浮動參數的非變數函數,該函數將收到垃圾。
To illustrate this problem, here's a quick example:
// Use the old variadic prototype for objc_msgSend.
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface Foo : NSObject @end
@implementation Foo
- (void)log: (float)x {
printf("%f\n", x);
}
@end
int main(int argc, char **argv) {
id obj = [Foo new];
[obj log: (float)M_PI];
objc_msgSend(obj, @selector(log:), (float)M_PI);
}
It produces this output:
3.141593
3370280550400.000000
正如你所看到的,當寫成消息發送時,這個值是正確的,但是當通過顯式調用objc_msgSend時,這個值被完全扭曲了。
這個問題可以通過轉換objc_msgSend的簽名來解決。回顧一下,objc_msgSend的實際原型是最終將被調用的方法的原型,所以使用它的正確方法是將其轉換為相應的函數指針類型。這個調用可以正常工作。
((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);
ARM64 ABI
讓我們看看另一個相關的例子。iOS使用了ARM64的標準ABI的一個變體。
整數參數在寄存器x0到x7中傳遞。浮點參數在v0到v7中傳遞。 其他參數在堆棧中傳遞。返回值被放置在作為參數傳遞的同一個或多個寄存器中。
這隻適用於普通參數。變數參數從不在寄存器中傳遞。它們總是在堆棧中傳遞,即使是在有參數寄存器的情況下。
沒有必要仔細分析這在實踐中是如何實現的。ABI是完全不匹配的,一個方法在被調用的時候,如果沒有cast objc_msgSend,那麼它的參數中就會收到垃圾。
新的原型
新的原型簡短而貼心。
void objc_msgSend(void)。
這一點都不正確。然而,舊的原型也不正確。這個原型的錯誤更明顯,這是件好事。舊的原型使你可以很容易地使用它而不去鑄造它,並且經常工作,以至於你很容易就會認為一切都很好。當你遇到有問題的情況時,這些錯誤就非常不清楚了。
這個原型甚至不允許你傳遞self和_cmd這兩個必要參數。你可以在完全沒有參數的情況下調用它,但它會立即崩潰,這應該是很明顯的出錯原因。如果你試圖使用它而不進行轉換,編譯器會抱怨,這比奇怪的破參數值要好得多。
因為它仍然有一個函數類型,你仍然可以把它投到一個適當類型的函數指針上,然後用這種方式調用它。只要你把類型弄對了,這就能正確工作。