原子類源碼
㈠ Netty 源碼解析 ——— ChannelConfig 和 Attribute
嗯,本文與其說是ChannelConfig、Attribute源碼解析,不如說是對ChannelConfig以及Attribute結構層次的分析。因為這才是它們在Netty中使用到的重要之處。
在 Netty 源碼解析 ——— 服務端啟動流程 (下) 中說過,當我們在構建NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象賦值給NioServerSocketChannel的成員變數config。
而這一個NioServerSocketChannelConfig是當前NioServerSocketChannel配置屬性的集合。NioServerSocketChannelConfig主要用於對NioServerSocketChannel相關配置的設置(如,網路的相關參數配置),比如,配置Channel是否為非阻塞、配置連接超時時間等等。
NioServerSocketChannelConfig其實是一個ChannelConfig實例。ChannelConfig表示為一個Channel相關的配置屬性的集合。所以NioServerSocketChannelConfig就是針對於NioServerSocketChannel的配置屬性的集合。
ChannelConfig是Channel所需的公共配置屬性的集合,如,setAllocator(設置用於channel分配buffer的分配器)。而不同類型的網路傳輸對應的Channel有它們自己特有的配置,因此可以通過擴展ChannelConfig來補充特有的配置,如,ServerSocketChannelConfig是針對基於TCP連接的服務端ServerSocketChannel相關配置屬性的集合,它補充了針對TCP服務端所需的特有配置的設置setBacklog、setReuseAddress、setReceiveBufferSize。
DefaultChannelConfig作為ChannelConfig的默認實現,對ChannelConfig中的配置提供了默認值。
接下來,我們來看一個設置ChannelConfig的流程:
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
我們可以在啟動服務端前通過ServerBootstrap來進行相關配置的設置,該選項配置會在Channel初始化時被獲取並設置到Channel中,最終會調用底層ServerSocket.setReuseAddress方法來完成配置的設置。
ServerBootstrap的init()方法:
首先對option和value進行校驗,其實就是進行非空校驗。
然後判斷對應的是哪個常量屬性,並進行相應屬性的設置。如果傳進來的ChannelOption不是已經設定好的常量屬性,則會列印一條警告級別的日誌,告知這是未知的channel option。
Netty提供ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。
這里,我們可以看到有個ChannelOption類,它允許以類型安全的方式去配置一個ChannelConfig。支持哪一種ChannelOption取決於ChannelConfig的實際的實現並且也可能取決於它所屬的傳輸層的本質。
可見ChannelOption是一個Consant擴展類,Consant是Netty提供的一個單例類,它能安全去通過』==』來進行比較操作。通過ConstantPool進行管理和創建。
常量由一個id和name組成。id:表示分配給常量的唯一數字;name:表示常量的名字。
如上所說,Constant是由ConstantPool來進行管理和創建的,那麼ConstantPool又是個什麼樣的類了?
首先從constants中get這個name對應的常量,如果不存在則調用newConstant()來構建這個常量tempConstant,然後在調用constants.putIfAbsent方法來實現「如果該name沒有存在對應的常量,則插入,否則返回該name所對應的常量。(這整個的過程都是原子性的)」,因此我們是根據putIfAbsent方法的返回來判斷該name對應的常量是否已經存在於constants中的。如果返回為null,則說明當前創建的tempConstant就為name所對應的常量;否則,將putIfAbsent返回的name已經對應的常量值返回。(注意,因為ConcurrentHashMap不會允許value為null的情況,所以我們可以根據putIfAbsent返回為null則代表該name在此之前並未有對應的常量值)
正如我們前面所說的,這個ConstantPool<ChannelOption<Object>> pool(即,ChannelOption常量池)是ChannelOption的一個私有靜態成員屬性,用於管理和創建ChannelOption。
這些定義好的ChannelOption常量都已經存儲數到ChannelOption的常量池(ConstantPool)中了。
注意,ChannelOption本身並不維護選項值的信息,它只是維護選項名字本身。比如,「public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");」👈這只是維護了「SO_RCVBUF」這個選項名字的信息,同時泛型表示選擇值類型,即「SO_RCVBUF」選項值為Integer。
好了,到目前為止,我們對Netty的ChannelOption的設置以及底層的實現已經分析完了,簡單的來說:Netty在初始化Channel時會構建一個ChannelConfig對象,而ChannelConfig是Channel配置屬性的集合。比如,Netty在初始化NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象,並將其賦值給NioServerSocketChannel的成員變數config,而這個config(NioServerSocketChannelConfig)維護了NioServerSocketChannel的所有配置屬性。比如,NioServerSocketChannelConfig提供了setConnectTimeoutMillis方法來設置NioServerSocketChannel連接超時的時間。
同時,程序可以通過ServerBootstrap或Boostrap的option(ChannelOption<T> option, T value)方法來實現配置的設置。這里,我們通過ChannelOption來實現配置的設置,ChannelOption中已經將常用的配置項預定義為了常量供我們直接使用,同時ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。
一個attribute允許存儲一個值的引用。它可以被自動的更新並且是線程安全的。
其實Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。
我們可以通過程序ServerBootstrap或Boostrap的attr方法來設置一個Channel的屬性,如:
serverBootstrap.attr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString());
當Netty底層初始化Channel的時候,就會將我們設置的attribute給設置到Channel中:
如上面所說,Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。
而AttributeKey也是Constant的一個擴展,因此也有一個ConstantPool來管理和創建,這和ChannelOption是類似的。
Channel類本身繼承了AttributeMap類,而AttributeMap它持有多個Attribute,這些Attribute可以通過AttributeKey來訪問的。所以,才可以通過channel.attr(key).set(value)的方式將屬性設置到channel中了(即,這里的attr方法實際上是AttributeMap介面中的方法)。
AttributeKey、Attribute、AttributeMap間的關系:
AttributeMap相對於一個map,AttributeKey相當於map的key,Attribute是一個持有key(AttributeKey)和value的對象。因此在map中我們可以通過AttributeKey key獲取Attribute,從而獲取Attribute中的value(即,屬性值)。
Q:ChannelHandlerContext和Channel都提供了attr方法,那麼它們設置的屬性作用域有什麼不同了?
A:在Netty 4.1版本之前,它們兩設置的屬性作用域確實存在著不同,但從Netty 4.1版本開始,它們兩設置的屬性的作用域已經完全相同了。
若文章有任何錯誤,望大家不吝指教:)
聖思園《精通並發與Netty》
㈡ 新手求教linux下的原子操作該怎麼寫
linux中關於原子操作
2016年08月02日
- 一.整型原子操作定義於#include<asm/atomic.h>分為 定義,獲取,加減,測試,返回。void atomic_set(atomic_t *v,int i); //設置原子變數v的值為iatomic_t v = ATOMIC_INIT(0); //定義原子變數v,並初始化為0;atomic_read(atomic_t* v); //返回原子變數v的值;void atomic_add(int i, atomic_t* v); //原子變數v增加i;void atomic_sub(int i, atomic_t* v); void atomic_inc(atomic_t* v); //原子變數增加1;void atomic_dec(atomic_t* v);int atomic_inc_and_test(atomic_t* v); //先自增1,然後測試其值是否為0,若為0,則返回true,否則返回false;int atomic_dec_and_test(atomic_t* v); int atomic_sub_and_test(int i, atomic_t* v); //先減i,然後測試其值是否為0,若為0,則返回true,否則返回false;注意:只有自加,沒有加操作int atomic_add_return(int i, atomic_t* v); //v的值加i後返回新的值;int atomic_sub_return(int i, atomic_t* v); int atomic_inc_return(atomic_t* v); //v的值自增1後返回新的值;int atomic_dec_return(atomic_t* v);二.位原子操作定義於#include<asm/bitops.h>分為 設置,清除,改變,測試void set_bit(int nr, volatile void* addr); //設置地址addr的第nr位,所謂設置位,就是把位寫為1;void clear_bit(int nr, volatile void* addr); //清除地址addr的第nr位,所謂清除位,就是把位寫為0;void change_bit(int nr, volatile void* addr); //把地址addr的第nr位反轉;int test_bit(int nr, volatile void* addr); //返回地址addr的第nr位;int test_and_set_bit(int nr, volatile void* addr);//測試並設置位;若addr的第nr位非0,則返回true; 若addr的第nr位為0,則返回false;int test_and_clear_bit(int nr, volatile void* addr);//測試並清除位;int test_and_change_bit(int nr, volatile void* addr);//測試並反轉位;上述操作等同於先執行test_bit(nr,voidaddr)然後在執行xxx_bit(nr,voidaddr)
- 舉個簡單例子:為了實現設備只能被一個進程打開,從而避免競態的出現static atomic_t scull_available = ATOMIC_INIT(1);//init atomic在scull_open 函數和scull_close函數中:int scull_open(struct inode *inode, struct file *filp){ struct scull_dev *dev; // device information dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev;// for other methods if(!atomic_dec_and_test(&scull_available)){ atomic_inc(&scull_available); return -EBUSY; } return 0; // success}int scull_release(struct inode *inode, struct file *filp){ atomic_inc(&scull_available); return 0;}
#if__LINUX_ARM_ARCH__>=6
......(通過ldrex/strex指令的匯編實現)
#else/*ARM_ARCH_6*/
#ifdef CONFIG_SMP
#errorSMPnotsupportedonpre-ARMv6 CPUs
#endif
......(通過關閉CPU中斷的C語言實現)
#endif/*__LINUX_ARM_ARCH__*/
......
#ifndef CONFIG_GENERIC_ATOMIC64
......(通過ldrexd/strexd指令的匯編實現的64bit原子變數的訪問)
#else/*!CONFIG_GENERIC_ATOMIC64*/
#include<asm-generic/atomic64.h>
#endif
#include<asm-generic/atomic-long.h>
/*
*ARMv6 UP 和 SMP 安全原子操作。 我們是用獨占載入和
*獨占存儲來保證這些操作的原子性。我們可能會通過循環
*來保證成功更新變數。
*/
static inline void atomic_add(inti,atomic_t*v)
{
unsigned long tmp;
intresult;
__asm__ __volatile__("@ atomic_add "
"1: ldrex %0, [%3] "
" add %0, %0, %4 "
" strex %1, %0, [%3] "
" teq %1, #0 "
" bne 1b"
:"=&r"(result),"=&r"(tmp),"+Qo"(v->counter)
:"r"(&v->counter),"Ir"(i)
:"cc");
}
A:將該物理地址標記為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
B:標記此物理地址為CPU1獨占訪問,並清除CPU1對其他任何物理地址的任何獨占訪問標記。
C:再次標記此物理地址為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
D:已被標記為CPU0獨占訪問,進行存儲並清除獨占訪問標記,並返回0(操作成功)。
E:沒有標記為CPU1獨占訪問,不會進行存儲,並返回1(操作失敗)。
F:沒有標記為CPU0獨占訪問,不會進行存儲,並返回1(操作失敗)。
原子操作:就是在執行某一操作時不被打斷。
linux原子操作問題來源於中斷、進程的搶占以及多核smp系統中程序的並發執行。
對於臨界區的操作可以加鎖來保證原子性,對於全局變數或靜態變數操作則需要依賴於硬體平台的原子變數操作。
因此原子操作有兩類:一類是各種臨界區的鎖,一類是操作原子變數的函數。
對於arm來說,單條匯編指令都是原子的,多核smp也是,因為有匯流排仲裁所以cpu可以單獨佔用匯流排直到指令結束,多核系統中的原子操作通常使用內存柵障(memory barrier)來實現,即一個CPU核在執行原子操作時,其他CPU核必須停止對內存操作或者不對指定的內存進行操作,這樣才能避免數據競爭問題。但是對於load update store這個過程可能被中斷、搶占,所以arm指令集有增加了ldrex/strex這樣的實現load update store的原子指令。
但是linux種對於c/c++程序(一條c編譯成多條匯編),由於上述提到的原因不能保證原子性,因此linux提供了一套函數來操作全局變數或靜態變數。
假設原子變數的底層實現是由一個匯編指令實現的,這個原子性必然有保障。但是如果原子變數的實現是由多條指令組合而成的,那麼對於SMP和中斷的介入會不會有什麼影響呢?我在看ARM的原子變數操作實現的時候,發現其是由多條匯編指令(ldrex/strex)實現的。在參考了別的書籍和資料後,發現大部分書中對這兩條指令的描訴都是說他們是支持在SMP系統中實現多核共享內存的互斥訪問。但在UP系統中使用,如果ldrex/strex和之間發生了中斷,並在中斷中也用ldrex/strex操作了同一個原子變數會不會有問題呢?就這個問題,我認真看了一下內核的ARM原子變數源碼和ARM官方對於ldrex/strex的功能解釋,總結如下:
一、ARM構架的原子變數實現結構
對於ARM構架的原子變數實現源碼位於:arch/arm/include/asm/atomic.h
其主要的實現代碼分為ARMv6以上(含v6)構架的實現和ARMv6版本以下的實現。
該文件的主要結構如下:
這樣的安排是依據ARM核心指令集版本的實現來做的:
(1)在ARMv6以上(含v6)構架有了多核的CPU,為了在多核之間同步數據和控制並發,ARM在內存訪問上增加了獨占監測(Exclusive monitors)機制(一種簡單的狀態機),並增加了相關的ldrex/strex指令。請先閱讀以下參考資料(關鍵在於理解local monitor和Global monitor):
1.2.2.Exclusive monitors
4.2.12.LDREX和STREX
(2)對於ARMv6以前的構架不可能有多核CPU,所以對於變數的原子訪問只需要關閉本CPU中斷即可保證原子性。
對於(2),非常好理解。
但是(1)情況,我還是要通過源碼的分析才認同這種代碼,以下我僅僅分析最具有代表性的atomic_add源碼,其他的API原理都一樣。如果讀者還不熟悉C內嵌匯編的格式,請參考《ARM GCC內嵌匯編手冊》
二、內核對於ARM構架的atomic_add源碼分析
源碼分析:
注意:根據內聯匯編的語法,result、tmp、&v->counter對應的數據都放在了寄存器中操作。如果出現上下文切換,切換機制會做寄存器上下文保護。
(1)ldrex %0, [%3]
意思是將&v->counter指向的數據放入result中,並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是將result保存到&v->counter指向的內存中,此時Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4)teq %1, #0
測試strex是否成功(tmp == 0??)
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。以下通過可能的情況分析ldrex/strex指令機制。(請閱讀時參考4.2.12.LDREX和STREX)
1、UP系統或SMP系統中變數為非CPU間共享訪問的情況
此情況下,僅有一個CPU可能訪問變數,此時僅有Local monitor需要關注。
假設CPU執行到(2)的時候,來了一個中斷,並在中斷里使用ldrex/strex操作了同一個原子變數。則情況如下圖所示:
雖然對於人來說,這種情況比較BT。但是在飛速運行的CPU來說,BT的事情隨時都可能發生。
當然還有其他許多復雜的可能,也可以通過ldrex/strex指令的機制分析出來。從上面列舉的分析中,我們可以看出:ldrex/strex可以保證在任何情況下(包括被中斷)的訪問原子性。所以內核中ARM構架中的原子操作是可以信任的。
㈢ 「okhttp3 4.9.3 版本簡單解析」
關於okhttp3的解析網上已經有非常多優秀的博文了,每每看完都覺得醍醐灌頂,豁然開朗。但等不了幾天再回頭看,還是跟當初一樣陌生,究其根本原因,我們不過是在享受著別人的成果跟著別人的思路雲閱讀源碼了一遍。okhttp從早期的Java版本到Kotlin版本一直不斷優化升級,實現細節上也作出了調整。重讀源碼加上自身的思考能深刻地理解okhttp的實現原理。
從execute()開始,發現其實是一個介面中的方法(Call),這個很好理解根據官方的解釋,Call其實是一個待執行的請求,並且這個請求所要的參數已經被准備好;當然既然是請求,那麼它是可以被取消的。其次代表單個請求與響應流,因此不能夠被再次執行。
Call介面具體的代碼實現,重點關注同步執行方法execute()與非同步請求enqueue():
Call作為介面,那麼具體的實現細節則需要看它的實現類,RealCall作為Call的實現類,先找到對execute()的重寫。
代碼很少,逐步分析,首先對同步請求進行了檢查-判斷請求是否已經被執行過了;而這里使用的是並發包下的原子類CAS樂觀鎖,這里使用CAS比較演算法目的也是為提升效率。其次是超時時間的判斷,這個比較簡單。在看callStart()的具體實現。上代碼:
看名稱猜測應該是事件監聽之類的,可能是包括一些信息的記錄與列印。回到RealCall類中,看看這個eventListener的作用到底是什麼:
internal val eventListener: EventListener = client.eventListenerFactory.create(this)
繼續向下可以知道這個EventListener是一個抽象類,而項目中其唯一實現類為LoggingEventListener,猜測還是有依據的,繼續往下看:
實現類LoggingEventListener中對此方法的具體實現:
總結這個callStart()的作用,當同步請求或者非同步請求被加到隊列時,callStart()會被立即執行(在沒有達到線程限制的情況下)記錄請求開始的時間與請求的一些信息。如下:
代賣第四段#4 client.dispatcher.executed(this),看樣子是在這里開啟執行的,可實際真是如此嘛?回到OkHttpClient類中,看看這個分發器dispatcher到底是什麼。
具體實現類Dispatcher代碼(保留重要代碼):
根據注釋的信息可以知道,Dispatcher是處理非同步請求的執行的策略,當然開發可以實現自己的策略。
知道了Dispatcher的作用,再回到client.dispatcher.executed(this),也即:
結合execute()與Dispatcher分析
到這里請求其實還沒有真正的執行,只是在做一些前期的工作,回到Call介面中看看對方法同步請求方法execute()的說明:同步請求可以立即執行,阻塞直到返回正確的結果,或者報錯結束。
到#4步執行後,return () //#5這個方法才是請求一步步推進的核心。也是okhttp網路請求責任鏈的核心模塊。
分析()方法之前有必要看看OkHttpClient的構造參數,使用的Builder模式,參數很多,可配置化的東西很多,精簡一下主要關注幾個參數:
到這里有個疑問,這個添加自定義攔截器與添加自定義網路攔截器有什麼區別呢?方法看上去是差不多的,查看官方的說明可以發現一些細節。文檔中解釋了Application Interceptor與Network Interceptors的細微差別。先回到RealCall中查看()是如何對攔截器結合組裝的:
看#1與#2分別對應添加的自定義攔截器與自定義網路攔截器的位置,自定義攔截器是攔截器鏈的鏈頭,而自定義網路攔截器在ConnectInterceptor攔截器與CallServerInterceptor攔截器之間被添加。總結一下:
Don』t need to worry about intermediate responses like redirects and retries.
Are always invoked once, even if the HTTP response is served from the cache.
Observe the application』s original intent. Unconcerned with OkHttp-injected headers like If-None-Match.
Permitted to short-circuit and not call Chain.proceed().
Permitted to retry and make multiple calls to Chain.proceed().
Can adjust Call timeouts using withConnectTimeout, withReadTimeout, withWriteTimeout.
Able to operate on intermediate responses like redirects and retries.
Not invoked for cached responses that short-circuit the network.
Observe the data just as it will be transmitted over the network.
Access to the Connection that carries the request.
綜上可以得出整個鏈的順序結構,如果都包含自定義攔截器與自定義網路攔截器,則為自定義攔截器->RetryAndFollowUpInterceptor->BridgeInterceptor->CacheInterceptor->ConnectInterceptor->自定義網路攔截器->CallServerInterceptor;那麼鏈是如何按照順序依次執行的呢?okhttp在這里設計比較精妙,在構造RealInterceptorChain對象時帶入index信息,這個index記錄的就是單個攔截器鏈的位置信息,而RealInterceptorChain.proceed(request: Request)通過index++自增一步步執行責任鏈一直到鏈尾。
簡單的分析推進過程:
1.RealInterceptorChain的構造參數中攜帶了index的信息,index++自增通過proceed方法不斷執行。
2.攔截器統一實現Interceptor介面,介面中fun proceed(request: Request): Response保證了鏈式鏈接。當然攔截器的順序是按照一定的規則排列的,逐個分析。
1.重試攔截器規定默認的重試次數為20次
2.以response = realChain.proceed(request)為分界點,包括其他的攔截器,在責任鏈傳遞之前所做的工作都是前序工作,然後將request下發到下一個攔截器。
3.response = realChain.proceed(request)後的代碼邏輯為後續工作,即拿到上個攔截器的response結果,有點遞歸的意思,按照責任鏈的執行一直到最後一個攔截器獲得的結果依次上拋每個攔截器處理這個response完成一些後序工作。
4.當然並不是每個請求都會走完整個鏈,如CacheInterceptor當開啟了緩存(存在緩存)拿到了緩存的response那麼之後的攔截器就不會在繼續傳遞。
1.橋接攔截器主要對請求的Hader的信息的補充,包括內容長度等。
2.傳遞請求到下一個鏈,等待返回的response信息。
3.後序的操作包括Cookie、gzip壓縮信息,User-Agent等信息的補充。
1.緩存攔截器默認沒有被開啟,需要在調用時指定緩存的目錄,內部基於DiskLruCache實現了磁碟緩存。
2.當緩存開啟,且命中緩存,那麼鏈的調用不會再繼續向下傳遞(此時已經拿到了response)直接進行後序的操作。
3.如果未命中,則會繼續傳遞到下一個鏈也即是ConnectInterceptor。
1.建立與目標的伺服器的TCP或者TCP-TLS的鏈接。
2.與之前的攔截器不同,前面的攔截器的前序操作基於調用方法realChain.proceed()之前,但是ConnectInterceptor 沒有後序操作,下發到下一個攔截器 。
1.實質上是請求與I/O操作,將請求的數據寫入到Socket中。
2.從Socket讀取響應的數據 TCP/TCP-TLS對應的埠 ,對於I/O操作基於的是okio,而okhttp的高效請求同樣離不開okio的支持。
3.拿到數據reponse返回到之前包含有後序操作的攔截器,但ConnectInterceptor除外,ConnectInterceptor是沒有後續操作的。
整個攔截器流程圖如下:
1.排除極端的情況,System.exit()或者其他, finally 塊必然執行,不論發生異常與否,也不論在 finally 之前是否有return。
2.不管在 try 塊中是否包含 return, finally 塊總是在 return 之前執行。
3.如果 finally 塊中有 return ,那麼 try 塊和 catch 塊中的 return 就沒有執行機會了。
Tip:第二條的結論很重要,回到execute()方法,dispatcher.finished(this)在結果response結果返回之前執行,看finished()具體實現。
1.#1方法一,calls.remove(call)返回為 true ,也即是這個同步請求被從runningSyncCalls中移除釋放;所以idleCallback為空。
2.#3很顯然asyncCall的結果為空,沒有非同步請求,在看#4具體實現,runningSyncCalls的size為1。則isRunning的結果為 true 。idleCallback.run()不會被執行,並且idleCallback其實也是為空。
1.從 OkHttpClient().newCall(request).execute() 開啟同步請求任務。
2.得到的 RealCall 對象作為 Call 的唯一實現類,其中同步方法 execute() 是阻塞的,調用到會立即執行 阻塞 到有結果返回,或者發生錯誤 error 被打斷阻塞。
3. RealCall 中同步 execute() 請求方法被執行,而此時 OkHttpClient 實例中的非同步任務分發器 Dispatcher 會將請求的實例 RealCall 添加到雙端隊列 runningSyncCalls 中去。
4.通過 RealCall 中的方法 () 開啟請求攔截器的責任鏈,將請求逐一下發,通過持有 index 並自增操作,其次除 ConnectInterceptor 與鏈尾 CallServerInterceptor 其餘默認攔截器均有以 chain.proceed(request) 為分界點的前序與後序操作,拿到 response 後依次處理後序操作。
5.最終返回結果 response 之前,對進行中的同步任務做了移除隊列的操作也即 finally 中 client.dispatcher.finished(this) 方法,最終得到的結果 response 返回到客戶端,至此整個 同步請求 流程就結束了。
Github
Square
㈣ Apple 源碼用到的一些數據結構
本篇英文名叫 CWC:Kitchen Tools That Cook Loves ,翻譯過來的意思是 蘋果源碼中出現的一些數據結構 ,不斷積累更新。
CWC : Cooking With Cook ,翻譯過來的中文意思就是 作為一個長期熱愛蘋果的蘋果開發者,我們要陪著水果公司一起積累和成長。
目前: entsize_list_tt 、 list_array_tt 、 cache_t's buckets ...
entsize_list_tt 其實就是一個通用的容器,可以獲取 內部的迭代器,用於遍歷內部存儲的元素
出現場景:
三者的聲明頭如下:
entsize_list_t 定義源碼,省略大部分方法:
這個類用來表示一個空、單數組、或者多數組。它和 list 的帆桐區別就是 多了一個多維數組的封裝。
出現場景:
ro 中沒有,只有三個單 List。
三者的聲明頭如下:
list_array_tt 源碼部分如下:
cache_t 的結構體定義:
buckets 的內部是一個連續的存儲空間,存儲是一個散列表。
開辟聲明的函數調用的是 calloc
當 msgSend 的時候,就會調用 fillCache 進行方法的緩存,存儲的涉及 cls sel 和 imp
bucket_t 的結構體很有意思,arm64 和 i386 的兩個值的順序是反著的。
arm64 的時候是 :
armv7* , i386 和 x86_64 的時候是:
源碼注釋:
初始的 capacity 是 4。
源碼中 cache_t::insert(cls, sel, imp, reveiver) 方法調用的時候,判斷擴容。
fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)
也就是說當大於四分之三的時候,就會進行擴容操作,每次 double 擴容
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
當然不是無限制的擴容,有一個最大容量的限制:
MAX_CACHE_SIZE = 1 << 16
這個類型應該是執行最多次的,看一些文章說一秒鍾iOS中執行幾百萬次
explicit_atomic用來給catchT緩存方法用,核心是原子性和線程安全。
weak弱引用的散列表
擴展: non-fragile structs 是什麼?OC 1.0 (iOS自始至終都是2.0起的,Mac最開始是1.0)桐轎缺譯器生成了一個 ivar 布局,顯示了在類中從哪可局辯以訪問 ivars ,對 ivar 的訪問就可以通過 對象地址 + ivar偏移位元組 的方法。蘋果更新了NSObject類,例如增加一些屬性,這個又是靜態庫,發布新版本的系統,這個時候布局就出錯了,就不得不重新編譯子類來恢復兼容性。(那如果是在線上運行的app,升級系統後就沒辦法運行了)
使用 Non Fragile ivars 時,程序進行檢測來調整類中新增的 ivar 的偏移量。 這樣就可以通過 對象地址 + 基類大小 + ivar偏移位元組 的方法來計算出 ivar 相應的地址,並訪問到相應的 ivar。(即使升級iOS系統,之前的app也能正常運行)
擴展再擴展: 為什麼OC類不能動態添加成員變數? runtime函數中,確實有一個class_addIvar()函數用於給類添加成員變數,但是文檔中特別說明: This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported. 這個函數只能在「構建一個類的過程中」調用。一旦完成類定義,就不能再添加成員變數了。經過編譯的類在程序啟動後就被runtime載入,沒有機會調用addIvar。程序在運行時動態構建的類需要在調用objc_registerClassPair之後才可以被使用,同樣沒有機會再添加成員變數。
理論上說,我還是認為可以添加,只是為什麼一定不可以,就不得而知了。