想要理解volatile為什么能確??梢娦裕鸵壤斫釰ava中的內(nèi)存模型是什么樣的。
“只有客戶發(fā)展了,才有我們的生存與發(fā)展!”這是創(chuàng)新互聯(lián)建站的服務(wù)宗旨!把網(wǎng)站當作互聯(lián)網(wǎng)產(chǎn)品,產(chǎn)品思維更注重全局思維、需求分析和迭代思維,在網(wǎng)站建設(shè)中就是為了建設(shè)一個不僅審美在線,而且實用性極高的網(wǎng)站。創(chuàng)新互聯(lián)對成都網(wǎng)站設(shè)計、成都做網(wǎng)站、網(wǎng)站制作、網(wǎng)站開發(fā)、網(wǎng)頁設(shè)計、網(wǎng)站優(yōu)化、網(wǎng)絡(luò)推廣、探索永無止境。
Java內(nèi)存模型規(guī)定了 所有的變量都存儲在主內(nèi)存中 。 每條線程中還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了被該線程所使用到的變量(這些變量是從主內(nèi)存中拷貝而來) 。 線程對變量的所有操作(讀取,賦值)都必須在工作內(nèi)存中進行。不同線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成 。
基于此種內(nèi)存模型,便產(chǎn)生了多線程編程中的數(shù)據(jù)“臟讀”等問題。
舉個簡單的例子:在java中,執(zhí)行下面這個語句:
i = 10;
執(zhí)行線程必須先在自己的工作線程中對變量i所在的緩存行進行賦值操作,然后再寫入主存當中。而不是直接將數(shù)值10寫入主存當中。
比如同時有2個線程執(zhí)行這段代碼,假如初始時i的值為10,那么我們希望兩個線程執(zhí)行完之后i的值變?yōu)?2。但是事實會是這樣嗎?
可能存在下面一種情況:初始時,兩個線程分別讀取i的值存入各自所在的工作內(nèi)存當中,然后線程1進行加1操作,然后把i的最新值11寫入到內(nèi)存。此時線程2的工作內(nèi)存當中i的值還是10,進行加1操作之后,i的值為11,然后線程2把i的值寫入內(nèi)存。
最終結(jié)果i的值是11,而不是12。這就是著名的緩存一致性問題。通常稱這種被多個線程訪問的變量為共享變量。
那么如何確保共享變量在多線程訪問時能夠正確輸出結(jié)果呢?
在解決這個問題之前,我們要先了解并發(fā)編程的三大概念: 原子性,有序性,可見性 。
1.定義
原子性:即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
2.實例
一個很經(jīng)典的例子就是銀行賬戶轉(zhuǎn)賬問題:
比如從賬戶A向賬戶B轉(zhuǎn)1000元,那么必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。
試想一下,如果這2個操作不具備原子性,會造成什么樣的后果。假如從賬戶A減去1000元之后,操作突然中止。這樣就會導致賬戶A雖然減去了1000元,但是賬戶B沒有收到這個轉(zhuǎn)過來的1000元。
所以這2個操作必須要具備原子性才能保證不出現(xiàn)一些意外的問題。
同樣地反映到并發(fā)編程中會出現(xiàn)什么結(jié)果呢?
舉個最簡單的例子,大家想一下假如為一個32位的變量賦值過程不具備原子性的話,會發(fā)生什么后果?
假若一個線程執(zhí)行到這個語句時,我暫且假設(shè)為一個32位的變量賦值包括兩個過程:為低16位賦值,為高16位賦值。
那么就可能發(fā)生一種情況:當將低16位數(shù)值寫入之后,突然被中斷,而此時又有一個線程去讀取i的值,那么讀取到的就是錯誤的數(shù)據(jù)。
3.Java中的原子性
在Java中, 對基本數(shù)據(jù)類型的變量的讀取和賦值操作是原子性操作 ,即這些操作是不可被中斷的,要么執(zhí)行,要么不執(zhí)行。
上面一句話雖然看起來簡單,但是理解起來并不是那么容易??聪旅嬉粋€例子i:
請分析以下哪些操作是原子性操作:
x = 10; //語句1
y = x; //語句2
x++; //語句3
x = x + 1; //語句4
咋一看,可能會說上面的4個語句中的操作都是原子性操作。其實只有語句1是原子性操作,其他三個語句都不是原子性操作。
語句1是直接將數(shù)值10賦值給x,也就是說線程執(zhí)行這個語句的會直接將數(shù)值10寫入到工作內(nèi)存中。
語句2實際上包含2個操作,它先要去讀取x的值,再將x的值寫入工作內(nèi)存 ,雖然讀取x的值以及 將x的值寫入工作內(nèi)存 這2個操作都是原子性操作,但是合起來就不是原子性操作了。
同樣的, x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值 。
所以上面4個語句只有語句1的操作具備原子性。
也就是說, 只有簡單的讀取、賦值(而且必須是將數(shù)字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。
從上面可以看出,Java內(nèi)存模型只保證了基本讀取和賦值是原子性操作, 如果要實現(xiàn)更大范圍操作的原子性,可以通過synchronized和Lock來實現(xiàn)。由于synchronized和Lock能夠保證任一時刻只有一個線程執(zhí)行該代碼塊,那么自然就不存在原子性問題了,從而保證了原子性。
1.定義
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
2.實例
舉個簡單的例子,看下面這段代碼:
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
由上面的分析可知,當線程1執(zhí)行 i =10這句時,會先把i的初始值加載到工作內(nèi)存中,然后賦值為10,那么在線程1的工作內(nèi)存當中i的值變?yōu)?0了,卻沒有立即寫入到主存當中。
此時線程2執(zhí)行 j = i,它會先去主存讀取i的值并加載到線程2的工作內(nèi)存當中,注意此時內(nèi)存當中i的值還是0,那么就會使得j的值為0,而不是10.
這就是可見性問題,線程1對變量i修改了之后,線程2沒有立即看到線程1修改的值。
3.Java中的可見性
對于可見性,Java提供了volatile關(guān)鍵字來保證可見性。
當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內(nèi)存中讀取新值。
而普通的共享變量不能保證可見性, 因為普通共享變量被修改之后,什么時候被寫入主存是不確定的,當其他線程去讀取時,此時內(nèi)存中可能還是原來的舊值,因此無法保證可見性。
另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且 在釋放鎖之前會將對變量的修改刷新到主存當中 。因此可以保證可見性。
1.定義
有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
2.實例
舉個簡單的例子,看下面這段代碼:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個int型變量,定義了一個boolean類型變量,然后分別對兩個變量進行賦值操作。從代碼順序上看,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時候會保證語句1一定會在語句2前面執(zhí)行嗎?不一定,為什么呢?這里可能會發(fā)生指令重排序(Instruction Reorder)。
下面解釋一下什么是指令重排序, 一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優(yōu)化,它不保證程序中各個語句的執(zhí)行先后順序同代碼中的順序一致,但是它會保證程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果是一致的。
比如上面的代碼中,語句1和語句2誰先執(zhí)行對最終的程序結(jié)果并沒有影響,那么就有可能在執(zhí)行過程中,語句2先執(zhí)行而語句1后執(zhí)行。
但是要注意,雖然處理器會對指令進行重排序,但是它會保證程序最終結(jié)果會和代碼順序執(zhí)行結(jié)果相同,那么它靠什么保證的呢?再看下面一個例子:
int a = 10; //語句1
int r = 2; //語句2
a = a + 3; //語句3
r = a*a; //語句4
這段代碼有4個語句,那么可能的一個執(zhí)行順序是:
那么可不可能是這個執(zhí)行順序呢: 語句2 語句1 語句4 語句3
不可能,因為處理器在進行重排序時是會考慮指令之間的數(shù)據(jù)依賴性,如果一個指令I(lǐng)nstruction 2必須用到Instruction 1的結(jié)果,那么處理器會保證Instruction 1會在Instruction 2之前執(zhí)行。
雖然重排序不會影響單個線程內(nèi)程序執(zhí)行的結(jié)果,但是多線程呢?下面看一個例子:
上面代碼中,由于語句1和語句2沒有數(shù)據(jù)依賴性,因此可能會被重排序。假如發(fā)生了重排序,在線程1執(zhí)行過程中先執(zhí)行語句2,而此是線程2會以為初始化工作已經(jīng)完成,那么就會跳出while循環(huán),去執(zhí)行doSomethingwithconfig(context)方法,而此時context并沒有被初始化,就會導致程序出錯。
從上面可以看出, 指令重排序不會影響單個線程的執(zhí)行,但是會影響到線程并發(fā)執(zhí)行的正確性。
也就是說, 要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
3.Java中的有序性
在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。
在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
另外,Java內(nèi)存模型具備一些先天的“有序性”, 即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執(zhí)行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
下面就來具體介紹下happens-before原則(先行發(fā)生原則):
①程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
②鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作
③volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作
④傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
⑤線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作
⑥線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
⑦線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行
⑧對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始
這8條規(guī)則中,前4條規(guī)則是比較重要的,后4條規(guī)則都是顯而易見的。
下面我們來解釋一下前4條規(guī)則:
對于程序次序規(guī)則來說,就是一段程序代碼的執(zhí)行 在單個線程中看起來是有序的 。注意,雖然這條規(guī)則中提到“書寫在前面的操作先行發(fā)生于書寫在后面的操作”,這個應(yīng)該是程序看起來執(zhí)行的順序是按照代碼順序執(zhí)行的, 但是虛擬機可能會對程序代碼進行指令重排序 。雖然進行重排序,但是最終執(zhí)行的結(jié)果是與程序順序執(zhí)行的結(jié)果一致的,它只會對不存在數(shù)據(jù)依賴性的指令進行重排序。因此, 在單個線程中,程序執(zhí)行看起來是有序執(zhí)行的 ,這一點要注意理解。事實上, 這個規(guī)則是用來保證程序在單線程中執(zhí)行結(jié)果的正確性,但無法保證程序在多線程中執(zhí)行的正確性。
第二條規(guī)則也比較容易理解,也就是說無論在單線程中還是多線程中, 同一個鎖如果處于被鎖定的狀態(tài),那么必須先對鎖進行了釋放操作,后面才能繼續(xù)進行l(wèi)ock操作。
第三條規(guī)則是一條比較重要的規(guī)則。直觀地解釋就是, 如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發(fā)生于讀操作。
第四條規(guī)則實際上就是體現(xiàn)happens-before原則 具備傳遞性 。
1.volatile保證可見性
一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
1)保證了 不同線程對這個變量進行操作時的可見性 ,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2) 禁止進行指令重排序。
先看一段代碼,假如線程1先執(zhí)行,線程2后執(zhí)行:
這段代碼是很典型的一段代碼,很多人在中斷線程時可能都會采用這種標記辦法。但是事實上,這段代碼會完全運行正確么?即一定會將線程中斷么?不一定,也許在大多數(shù)時候,這個代碼能夠把線程中斷,但是也有可能會導致無法中斷線程(雖然這個可能性很小,但是只要一旦發(fā)生這種情況就會造成死循環(huán)了)。
下面解釋一下這段代碼為何有可能導致無法中斷線程。在前面已經(jīng)解釋過,每個線程在運行過程中都有自己的工作內(nèi)存,那么線程1在運行的時候,會將stop變量的值拷貝一份放在自己的工作內(nèi)存當中。
那么當線程2更改了stop變量的值之后,但是還沒來得及寫入主存當中,線程2轉(zhuǎn)去做其他事情了,那么線程1由于不知道線程2對stop變量的更改,因此還會一直循環(huán)下去。
但是用volatile修飾之后就變得不一樣了:
第一:使用volatile關(guān)鍵字會 強制將修改的值立即寫入主存 ;
第二:使用volatile關(guān)鍵字的話,當線程2進行修改時, 會導致線程1的工作內(nèi)存中緩存變量stop的緩存行無效 (反映到硬件層的話,就是CPU的L1或者L2緩存中對應(yīng)的緩存行無效);
第三:由于線程1的工作內(nèi)存中緩存變量stop的緩存行無效,所以 線程1再次讀取變量stop的值時會去主存讀取 。
那么在線程2修改stop值時(當然這里包括2個操作,修改線程2工作內(nèi)存中的值,然后將修改后的值寫入內(nèi)存),會使得線程1的工作內(nèi)存中緩存變量stop的緩存行無效,然后線程1讀取時,發(fā)現(xiàn)自己的緩存行無效,它會等待緩存行對應(yīng)的主存地址被更新之后,然后去對應(yīng)的主存讀取最新的值。
那么線程1讀取到的就是最新的正確的值。
2.volatile不能確保原子性
下面看一個例子:
大家想一下這段程序的輸出結(jié)果是多少?也許有些朋友認為是10000。但是事實上運行它會發(fā)現(xiàn)每次運行結(jié)果都不一致,都是一個小于10000的數(shù)字。
可能有的朋友就會有疑問,不對啊,上面是對變量inc進行自增操作,由于volatile保證了可見性,那么在每個線程中對inc自增完之后,在其他線程中都能看到修改后的值啊,所以有10個線程分別進行了1000次操作,那么最終inc的值應(yīng)該是1000*10=10000。
這里面就有一個誤區(qū)了, volatile關(guān)鍵字能保證可見性沒有錯,但是上面的程序錯在沒能保證原子性。 可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性。
在前面已經(jīng)提到過, 自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內(nèi)存 。那么就是說自增操作的三個子操作可能會分割開執(zhí)行,就有可能導致下面這種情況出現(xiàn):
假如某個時刻變量inc的值為10,
線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然后線程1被阻塞了 ;
然后線程2對變量進行自增操作,線程2也去讀取變量inc的原始值, 由于線程1只是對變量inc進行讀取操作,而沒有對變量進行修改操作,所以不會導致線程2的工作內(nèi)存中緩存變量inc的緩存行無效,也不會導致主存中的值刷新, 所以線程2會直接去主存讀取inc的值,發(fā)現(xiàn)inc的值時10,然后進行加1操作,并把11寫入工作內(nèi)存,最后寫入主存。
然后線程1接著進行加1操作,由于已經(jīng)讀取了inc的值,注意此時在線程1的工作內(nèi)存中inc的值仍然為10,所以線程1對inc進行加1操作后inc的值為11,然后將11寫入工作內(nèi)存,最后寫入主存。
那么兩個線程分別進行了一次自增操作后,inc只增加了1。
根源就在這里,自增操作不是原子性操作,而且volatile也無法保證對變量的任何操作都是原子性的。
解決方案:可以通過synchronized或lock,進行加鎖,來保證操作的原子性。也可以通過AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些 原子操作類 ,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數(shù)),減法操作(減一個數(shù))進行了封裝,保證這些操作是原子性操作。 atomic是利用CAS來實現(xiàn)原子性操作的(Compare And Swap) ,CAS實際上是 利用處理器提供的CMPXCHG指令實現(xiàn)的,而處理器執(zhí)行CMPXCHG指令是一個原子性操作。
3.volatile保證有序性
在前面提到volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。
volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當程序執(zhí)行到volatile變量的讀操作或者寫操作時, 在其前面的操作的更改肯定全部已經(jīng)進行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行 ;
2)在進行指令優(yōu)化時, 不能將在對volatile變量的讀操作或者寫操作的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。
可能上面說的比較繞,舉個簡單的例子:
由于 flag變量為volatile變量 ,那么在進行指令重排序的過程的時候, 不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
并且volatile關(guān)鍵字能保證, 執(zhí)行到語句3時,語句1和語句2必定是執(zhí)行完畢了的,且語句1和語句2的執(zhí)行結(jié)果對語句3、語句4、語句5是可見的。
那么我們回到前面舉的一個例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
前面舉這個例子的時候,提到有可能語句2會在語句1之前執(zhí)行,那么久可能導致context還沒被初始化,而線程2中就使用未初始化的context去進行操作,導致程序出錯。
這里如果用volatile關(guān)鍵字對inited變量進行修飾,就不會出現(xiàn)這種問題了, 因為當執(zhí)行到語句2時,必定能保證context已經(jīng)初始化完畢。
1.可見性
處理器為了提高處理速度,不直接和內(nèi)存進行通訊,而是將系統(tǒng)內(nèi)存的數(shù)據(jù)獨到內(nèi)部緩存后再進行操作,但操作完后不知什么時候會寫到內(nèi)存。
2.有序性
Lock前綴指令實際上相當于一個內(nèi)存屏障(也成內(nèi)存柵欄),它確保 指令重排序時不會把其后面的指令排到內(nèi)存屏障之前的位置,也不會把前面的指令排到內(nèi)存屏障的后面; 即在執(zhí)行到內(nèi)存屏障這句指令時,在它前面的操作已經(jīng)全部完成。
synchronized關(guān)鍵字是防止多個線程同時執(zhí)行一段代碼,那么就會很影響程序執(zhí)行效率,而volatile關(guān)鍵字在某些情況下性能要優(yōu)于synchronized,但是要注意volatile關(guān)鍵字是無法替代synchronized關(guān)鍵字的,因為volatile關(guān)鍵字無法保證操作的原子性。通常來說,使用volatile必須具備以下2個條件:
1)對變量的寫操作不依賴于當前值
2)該變量沒有包含在具有其他變量的不變式中
下面列舉幾個Java中使用volatile的幾個場景。
①.狀態(tài)標記量
volatile boolean flag = false;
//線程1
while(!flag){
doSomething();
}
//線程2
public void setFlag() {
flag = true;
}
根據(jù)狀態(tài)標記,終止線程。
②.單例模式中的double check
為什么要使用volatile 修飾instance?
主要在于instance = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。
自己是從事了七年開發(fā)的Android工程師,不少人私下問我,2019年Android進階該怎么學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發(fā),卻又不知道怎么進階學習的朋友。【 包括高級UI、性能優(yōu)化、架構(gòu)師課程、NDK、Kotlin、混合式開發(fā)(ReactNative+Weex)、Flutter等架構(gòu)技術(shù)資料 】,希望能幫助到您面試前的復(fù)習且找到一個好的工作,也節(jié)省大家在網(wǎng)上搜索資料的時間來學習。
自己寫flutter也有段時間了,這次來聊聊flutter開發(fā)App和原生iOS開發(fā)App各有什么優(yōu)缺點.
不廢話,直奔主題????
面試資料分享:資料內(nèi)容包括逆向安防、算法、架構(gòu)設(shè)計、Swift、多線程,網(wǎng)絡(luò)進階,還有底層、音視頻、Flutter等等
文/陳爐軍
整理/LiveVideoStack
大家好,我是阿里巴巴閑魚事業(yè)部的陳爐軍,本次分享的主題是Flutter浪潮下的音視頻研發(fā)探索,主要內(nèi)容是針對閑魚APP在當下流行的跨平臺框架Flutter的大規(guī)模實踐,介紹其在音視頻領(lǐng)域碰到的一些困難以及解決方案。
分享內(nèi)容主要分為四個方面,首先會對Flutter有一個簡單介紹以及選擇Flutter作為跨平臺框架的原因,其次會介紹Flutter中與音視頻關(guān)系非常大的外接紋理概念,以及對它做出的一些優(yōu)化。之后會對閑魚在音視頻實踐過程中碰到的一些Flutter問題提出了一些解決方案——TPM音視頻框架。最后是閑魚Flutter多媒體開源組件的介紹。
Flutter
Flutter是一個跨平臺框架,以往的做法是將音頻、視頻和網(wǎng)絡(luò)這些模塊都下沉到C++層或者ARM層,在其上封裝成一個音視頻的SDK,供UI層的PC、iOS和Android調(diào)用。
而Flutter做為一個UI層的跨平臺框架,顧名思義就是在UI層也實現(xiàn)了一個跨平臺開發(fā)??梢灶A(yù)想的是未Flutter發(fā)展的好的話,會逐漸變?yōu)橐粋€從底層到UI層的一個全鏈路的跨平臺開發(fā),技術(shù)人員分別負責SDK和UI層的開發(fā)。
在Flutter之前已經(jīng)有很多跨平臺UI解決方案,那為什么選擇Flutter呢?
我們主要考慮性能和跨平臺的能力。
以往的跨平臺方案比如Weex,ReactNative,Cordova等等因為架構(gòu)的原因無法滿足性能要求,尤其是在音視頻這種性能要求幾乎苛刻的場景。
而諸如Xamarin等,雖然性能可以和原生App一致,但是大部分邏輯還是需要分平臺實現(xiàn)。
我們可以看一下,為什么Flutter可以實現(xiàn)高性能:
原生的native組件渲染以IOS為例,蘋果的UIKit通過調(diào)用平臺自己的繪制框架QuaztCore來實現(xiàn)UI的繪制,圖形繪制也是調(diào)用底層的API,比如OpenGL、Metal等。
而Flutter也是和原生API邏輯一致,也是通過調(diào)用底層的繪制框架層SKIA實現(xiàn)UI層。這樣相當于Flutter他自己實現(xiàn)了一套UI框架,提供了一種性能超越原生API的跨平臺可能性。
但是我們說一個框架最終性能怎樣,其實取決于設(shè)計者和開發(fā)者。至于現(xiàn)在到底是一個什么狀況:
在閑魚的實踐中,我們發(fā)現(xiàn)在正常的開發(fā)沒有特意的去優(yōu)化UI代碼的情況下,在一些低端機上,F(xiàn)lutter界面的流暢性是比Native界面要好的。
雖然現(xiàn)在閑魚某些場景下會有卡頓閃退等情況,但是這是一個新事物發(fā)展過程中的必然問題,我們相信未來性能肯定不會成為限制Flutter發(fā)展的瓶頸的。
在閑魚實踐Flutter的過程中,混合棧和音視頻是其中比較難解決的兩個問題,混合棧是指一個APP在Flutter過程中不可能一口氣將所有業(yè)務(wù)全部重寫為Flutter,所以這是一個逐步迭代的過程,這期間原生native界面與Flutter界面共存的狀態(tài)就稱之為混合棧。閑魚在混合棧上也有一些比較好的輸出,例如FlutterBoost。
外接紋理
在講音視頻之前需要簡要介紹一下外接紋理的概念,我們將它稱之為是Flutter和Frame之間的橋梁。
Flutter渲染一幀屏幕數(shù)據(jù)首先要做的是,GPU發(fā)出的VC信號在Flutter的UI線程,通過AOT編譯的機器碼結(jié)合當前Dart Runtime,生成Layer Tree UI樹,Layer Tree上每一個葉子節(jié)點都代表了當前屏幕上所需要渲染的每一個元素,包含了這些元素渲染所需要的內(nèi)容。將Layer Tree拋給GPU線程,在GPU線程內(nèi)調(diào)用Skia去完成整個UI的渲染過程。Layer Tree中有PictureLayer和TextureLayer兩個比較重要的節(jié)點。PictureLayer主要負責屏幕圖片的渲染,F(xiàn)lutter內(nèi)部實現(xiàn)了一套圖片解碼邏輯,在IO線程將圖片讀取或者從網(wǎng)絡(luò)上拉取之后,通過解碼能夠在IO線程上加載出紋理,交給GPU線程將圖片渲染到屏幕上。但是由于音視頻場景下系統(tǒng)API太過繁多,業(yè)務(wù)場景過于復(fù)雜。Flutter沒有一套邏輯去實現(xiàn)跨平臺的音視頻組件,所以說Flutter提出了一種讓第三方開發(fā)者來實現(xiàn)音視頻組件的方式,而這些音視頻組件的視頻渲染出口,就是TextureLayer。
在整個Layer Tree渲染的過程中,TextureLayer的數(shù)據(jù)紋理需要由外部第三方開發(fā)者來指定,可以把視頻數(shù)據(jù)和播放器數(shù)據(jù)送到TextureLayer里,由Flutter將這些數(shù)據(jù)渲染出來。
TextureLayer渲染過程:首先判斷Layer是否已經(jīng)初始化,如果沒有就創(chuàng)建一個Texture,然后將Texture Attach到一個SufaceTexture上。
這個SufaceTexture是音視頻的native代碼可以獲取到的對象,通過這個對象創(chuàng)建的Suface,我們可以將視頻數(shù)據(jù)、攝像頭數(shù)據(jù)解碼放到Suface中,然后Flutter端通過監(jiān)聽SufaceTexture的數(shù)據(jù)更新就可以順利把剛才創(chuàng)建的數(shù)據(jù)更新到它的紋理中,然后再將紋理交給SKIA渲染到屏幕上。
然而我們?nèi)绻枰肍lutter實現(xiàn)美顏,濾鏡,人臉貼圖等等功能,就需要將視頻數(shù)據(jù)讀取出來,更新到紋理中,再將GPU紋理經(jīng)過美顏濾鏡處理后生成一個處理后的紋理。按Flutter提供的現(xiàn)有能力,必須先將紋理中的數(shù)據(jù)從GPU讀出到CPU中,生成Bitmap后再寫入Surface中,這樣在Flutter中才能順利的更新到視頻數(shù)據(jù),這樣做對系統(tǒng)性能的消耗很大。
通過對Flutter渲染過程分析,我們知道Flutter底層需要渲染的數(shù)據(jù)就是GPU紋理,而我們經(jīng)過美顏濾鏡處理完成以后的結(jié)果也是GPU紋理,如果可以將它直接交給Flutter渲染,那就可以避免GPU-CPU-GPU這樣的無用循環(huán)。這樣的方法是可行的,但是需要一個條件,就是OpenGL上下文共享。
OpenGL
在說上下文之前,得提到一個和上線文息息相關(guān)的概念:線程。
Flutter引擎啟動后會啟動四個線程:
第一個線程是UI線程,這是Flutter自己定義的UI線程,主要負責GPU發(fā)出的VSync信號時候用當前Dart編譯的機器碼和當前運行環(huán)境創(chuàng)建出Layer Tree。
還有就是IO線程和GPU線程。和大部分OpenGL處理解決方案中一樣,F(xiàn)lutter也采取一個線程責資源加載,一部分負責資源渲染這種思路。
兩個線程之間紋理共享有兩種方式。一種是EGLImage(IOS是 CVOpenGLESTextureCache)。一種是OpenGL Share Context。Flutter通過Share Context來實現(xiàn)紋理共享,將IO線程的Context和GPU線程的Context進行Share,放到同一個Share Group下面,這樣兩個線程下資源是互相可見可以共享的。
Platform線程是主線程,F(xiàn)lutter中有一個很奇怪的設(shè)定,GPU線程和主線程共用一個Context。并且在主線程也有很多OpenGL 操作。
這樣的設(shè)計會給音視頻開發(fā)帶來很多問題,后面會詳細說。
音視頻端美顏處理完成的OpenGL紋理能夠讓Flutter直接使用的條件就是Flutter的上下文需要和平臺音視頻相關(guān)的OpenGL上下文處在一個Share Group下面。
由于Flutter主線程的Context就是GPU的Context,所以在音視頻端主線程中有一些OpenGL操作的話,很有可能使Flutter整個OpenGL被破壞掉。所以需要將所有的OpenGL操作都限制在子線程中。
通過上述這兩個條件的處理,我們就可以在沒有增加GPU消耗的前提下實現(xiàn)美顏和濾鏡等等功能。
TPM
在經(jīng)過demo驗證之后,我們將這個方案應(yīng)用到閑魚音視頻組件中,但改造過程中發(fā)現(xiàn)了一些問題。
上圖是攝像頭采集數(shù)據(jù)轉(zhuǎn)換為紋理的一段代碼,其中有兩個操作:首先是切進程,將后面的OpenGL操作都切到cameraQueue中。然后是設(shè)置一次上下文。然后這種限制條件或者說是潛規(guī)則往往在開發(fā)過程中容易被忽略的。而這個條件一旦忽略后果就是出現(xiàn)一些莫名其妙的詭異問題極難排查。因此我們就希望能抽象出一套框架,由框架本身實現(xiàn)線程的切換、上下文和模塊生命周期等的管理,開發(fā)者接入框架以后只需要安心實現(xiàn)自己的算法,而不需要關(guān)心這些潛規(guī)則還有其他一些重復(fù)的邏輯操作。
在引入Flutter之前閑魚的音視頻架構(gòu)與大部分音視頻邏輯一樣采用分層架構(gòu):
1:底層是一些獨立模塊
2:SDK層是對底層模塊的封裝
3:最上層是UI層。
引入Flutter之后,通過分析各個模塊的使用場景,我們可以得出一個假設(shè)或者說是抽象:音視頻應(yīng)用在終端上可以歸納為視頻幀解碼之后視頻數(shù)據(jù)幀在各個模塊之間流動的過程,基于這種假設(shè)去做Flutter音視頻框架的抽象。
咸魚Flutter多媒體開源組件
整個Flutter音視頻框架抽象分為管線和數(shù)據(jù)的抽象、模塊的抽象、線程統(tǒng)一管理和上下文同一管理四部分。
管線,其實就是視頻幀流動的管道。數(shù)據(jù),音視頻中涉及到的數(shù)據(jù)包括紋理、Bit Map以及時間戳等。結(jié)合現(xiàn)有的應(yīng)用場景我們定義了管線流通數(shù)據(jù)以Texture為主數(shù)據(jù),同時可以選擇性的添加Bit Map等作為輔助數(shù)據(jù)。這樣的數(shù)據(jù)定義方式,避免重復(fù)的創(chuàng)建和銷毀紋理帶來的性能開銷以及多線程訪問紋理帶來的一些問題。也滿足一些特殊模塊對特殊數(shù)據(jù)的需求。同時也設(shè)計了紋理池來管理管線中的紋理數(shù)據(jù)。
模塊:如果把管線和數(shù)據(jù)比喻成血管和血液,那框架音視頻的場景就可以比喻成器官,我們根據(jù)模塊所在管線的位置抽象出采集、處理和輸出三個基類。這三個基類里實現(xiàn)了剛才說的線程切換,上下文切換,格式轉(zhuǎn)換等等共同邏輯,各個功能模塊通過集成自這些基類,可以避免很多重復(fù)勞動。
線程:每一個模塊初始化的時候,初始化函數(shù)就會去線程管理的模塊去獲取自己的線程,線程管理模塊可以決定給初始化函數(shù)分配新的線程或者已經(jīng)分配過其他模塊的線程。
這樣有三個好處:
一是可以根據(jù)需要去決定一個線程可以掛載多少模塊,做到線程間的負載均衡。第二,多線程并發(fā)式能夠保證模塊內(nèi)的OpenGL操作是在當前線程內(nèi)而不會跑到主線程去,徹底避免Flutter的OpenGL 環(huán)境被破壞。第三,多線程并行可以充分利用CPU多核架構(gòu),提升處理速度。
從Flutter端修改Flutter引擎將Context取出后,根據(jù)Context創(chuàng)建上下文的統(tǒng)一管理模塊,每一個模塊在初始化的時候會獲取它的線程,獲取之后會調(diào)用上下文管理模塊獲取自己的上下文。這樣可以保證每一個模塊的上下文都是與Flutter的上下文進行Share的,每個模塊之間資源都是共享可見的,F(xiàn)lutter和音視頻native之間也是互相共享可見的。
基于上述框架如果要實現(xiàn)一個簡單的場景,比如畫面實時預(yù)覽和濾鏡處理功能,
1:需要選擇功能模塊,功能模塊包括攝像頭模塊、濾鏡處理模塊和Flutter畫面渲染模塊,
2:需要配置模塊參數(shù),比如采集分辨率、濾鏡參數(shù)和前后攝像頭設(shè)置等,
3:在創(chuàng)建視頻管線后使用已配置的參數(shù)創(chuàng)建模塊
4:最后管線搭載模塊,開啟管線就可以實現(xiàn)這樣簡單的功能。
上圖為整個功能實現(xiàn)的代碼和結(jié)構(gòu)圖。
結(jié)合上述音視頻框架,閑魚實現(xiàn)了Flutter多媒體開源組件。
組要包含四個基本組件分別是:
1:視頻圖像拍攝組件
2:播放器組件
3:視頻圖像編輯組件
4:相冊選擇組件
現(xiàn)在這些組件正在走內(nèi)部開源流程。預(yù)計9月份,相冊和播放器會實現(xiàn)開源。
后續(xù)展望和規(guī)劃
1:實現(xiàn)開頭所說的從底層SDK到UI的全鏈路的跨端開發(fā)。目前底層框架層和模塊層都是各個平臺各自實現(xiàn),反而是Flutter的UI端進行了跨平臺的統(tǒng)一,所以后續(xù)會將底層也按照音視頻常用做法把邏輯下沉到C++層,盡可能的實現(xiàn)全鏈路跨平臺。
2:第二部分內(nèi)容為開源共建,閑魚開源的內(nèi)容不僅包括拍攝、編輯組件,還包括了很多底層模塊,希望有開發(fā)者在基于Flutter開發(fā)音視頻應(yīng)用時可以充分利用閑魚開源出的音視頻模塊能力,搭建APP框架,開發(fā)者只要去負責實現(xiàn)特殊需求模塊就可以,盡可能的減少重復(fù)勞動。
想要進入互聯(lián)網(wǎng)行業(yè),首先得搞清楚互聯(lián)網(wǎng)行業(yè)中有哪些崗位,這些崗位是做什么的,再去選擇一個崗位、一個方向,有目的的學習和發(fā)展。
一、互聯(lián)網(wǎng)中的崗位。
一般來說公司越大崗位越完善,我們選擇騰訊官網(wǎng)的招聘頁,去看看大型互聯(lián)網(wǎng)公司都有哪些崗位。
圖片來源:騰訊官網(wǎng)
從圖片可以得出,除開一個公司的基礎(chǔ)職能崗位(行政、財務(wù)、法務(wù)等),互聯(lián)網(wǎng)崗位大的方向可以分為技術(shù)、設(shè)計、產(chǎn)品、運營。
1、技術(shù):
技術(shù)崗中包含前端、開發(fā)、運維、質(zhì)量保證、數(shù)據(jù)、算法、地圖(GIS)等。
2、設(shè)計:
設(shè)計崗有交互設(shè)計、視覺設(shè)計、用戶體驗與研究。
3、產(chǎn)品:
產(chǎn)品針對不同業(yè)務(wù)方向,對產(chǎn)品也會有所區(qū)分。
圖片來源:阿里巴巴官網(wǎng) 招聘頁
4、運營:
運營也和產(chǎn)品一樣,公司的業(yè)務(wù)不同,運營的方向、內(nèi)容、方式也會不同。
知道了這些崗位,那這些崗位在具體的工作是什么呢?一個項目的從無到有,前期的工作就不細述了,項目正式開始大概的流程:
(1)項目負責人:制定項目計劃。
(2)需求/產(chǎn)品:據(jù)用戶訴求,分析產(chǎn)品規(guī)劃,輸出需求文檔?;蛘呤怯脩舴治觯a(chǎn)品定位,輸出產(chǎn)品需求。
(3)需求/產(chǎn)品:依據(jù)初步的原型圖和需求文檔,對開發(fā)、測試澄清需求。或者是產(chǎn)品依據(jù)需求輸出線框圖。
(4)設(shè)計:根據(jù)需求,交互設(shè)計師設(shè)計原型圖,輸出交互稿;UI設(shè)計師輸出UI稿。交互和UI評審。
(5)開發(fā):依據(jù)迭代要求和開發(fā)計劃,開發(fā)對應(yīng)功能模塊。
(6)測試:輸出測試方案,依據(jù)測試計劃和開發(fā)給出的轉(zhuǎn)測范圍,測試對應(yīng)功能。
(7)運維:迭代開發(fā)完成后,項目交付,進入運維期。
依據(jù)以上流程,需求(產(chǎn)品崗)需要參與項目前期產(chǎn)品定位、需求整理的工作,同時在整個項目周期需要依據(jù)項目計劃,給項目組人員澄清需求。依據(jù)測試反饋、用戶反饋、活動周期去調(diào)整產(chǎn)品需求,需求(產(chǎn)品崗)孕育產(chǎn)品。
在阿里的招聘頁中,設(shè)計崗包含交互設(shè)計、視覺設(shè)計以及用戶體驗與研究。設(shè)計依據(jù)產(chǎn)品提出的需求去做產(chǎn)品的交互設(shè)計、視覺設(shè)計,讓產(chǎn)品生動形象。
開發(fā)人員依據(jù)需求和設(shè)計,賦予產(chǎn)品真正的生命,讓產(chǎn)品活起來。測試是保證產(chǎn)品質(zhì)量,讓產(chǎn)品更完美。一個成熟的產(chǎn)品上線后,運維人員去維護產(chǎn)品的正常使用,運營人員提高產(chǎn)品的曝光,吸引用戶。
二、簡單介紹了互聯(lián)網(wǎng)中的崗位,那什么崗位適合零基礎(chǔ)的人學習然后快速入行呢?
由于市場需求量大,薪資待遇高,所以越來越多的人想要加入互聯(lián)網(wǎng)行業(yè),針對零基礎(chǔ)想要轉(zhuǎn)行的小伙伴到底選擇哪個方向去學習才能快速入行呢?
這個答案肯定不統(tǒng)一,因為每個人的興趣、性格、行業(yè)背景不同,選擇、適合的方向也不同。我們?nèi)チ牧拿總€方向需要的能力與發(fā)展,再結(jié)合你自己的情況選擇一個方向深入發(fā)展。
1、技術(shù):
技術(shù)崗一般分為開發(fā)、測試、運維,開發(fā)針對不同的語言,有不同的方向,如java、C++、Python等。不同的職責分為前端開發(fā)、后臺、手機應(yīng)用等,不同的業(yè)務(wù)方向分為區(qū)塊鏈、人工智能、C端、B端、移動端等。
測試又依據(jù)不同的測試內(nèi)容,分為功能測試、性能測試、安全測試、自動化測試等。
針對這些方向,如果你是零基礎(chǔ)而且對編碼很感興趣,可以嘗試前端開發(fā)、java開發(fā)、測試,原因有以下幾點:
1、前端開發(fā)零基礎(chǔ)好入門,上手快可以立馬看到學習效果,可以大大提高學習興趣。但是并不是說前端沒有技術(shù)含量,我們不僅需要學習前端基礎(chǔ),還需要學習vue.js、react.js、react-native和Flutter等主流框架,并擴展three.js、typescript等等技術(shù),深挖、剖析框架原理。甚至了解后端的知識,在工作中才能減少溝通成本。
2、Java作為一門面向?qū)ο缶幊陶Z言,是全球主流的編程語言之一。Java技術(shù)具有卓越的通用性、簡單性、安全性、高效性、健壯性、多線程、動態(tài)性、平臺獨立與移植性等特點,可以用于編寫Web 應(yīng)用程序、桌面應(yīng)用程序、分布式系統(tǒng)和嵌入式系統(tǒng)應(yīng)用程序等。相關(guān)調(diào)查顯示,在各種編程語言中,Java使用者比例很高,達40%以上!
3、為了保證軟件在出廠時的"健康狀態(tài)",幾乎所有的IT企業(yè)在軟件產(chǎn)品發(fā)布前都需要大量的質(zhì)量控制工作。你可能會說,為什么要對編碼感興趣才建議去學測試,測試不就是點點點嗎?你錯了,點點點的工作已經(jīng)不能滿足企業(yè)對測試的需求了,現(xiàn)在市場上更傾向于有開發(fā)能力的測試。比如在測試工作中,我們可能會需要寫測試腳本、測試工具,所以這些都需要測試工程師具備一定的編碼能力。所以如果對測試感興趣一定也要學習編碼,具備一定的編碼能力哦。
2、設(shè)計:
互聯(lián)網(wǎng)中的設(shè)計分為視覺設(shè)計和交互設(shè)計,視覺設(shè)計又會依據(jù)公司業(yè)務(wù)、項目分為Web網(wǎng)頁設(shè)計、電商設(shè)計、移動端設(shè)計、運營插畫設(shè)計等。
交互設(shè)計是努力去創(chuàng)造和建立的是人與產(chǎn)品及服務(wù)之間有意義的關(guān)系,而視覺設(shè)計主要是讓產(chǎn)品富有靈魂,生動有美感。由于大多數(shù)不了解互聯(lián)網(wǎng)的人,聽得最多的崗位大概是開發(fā)和測試,那我們就一起來了解一下UI設(shè)計。
UI(User Interface),中文名“用戶界面”。Ps:(百度解釋)UI是指對軟件的人機交互、操作邏輯、界面美觀的整體設(shè)計。
通俗來說,大家生活中的手機和電腦上使用的各種App、網(wǎng)頁軟件等產(chǎn)品的原型設(shè)計都來自于UI。
如果你覺得開發(fā)編碼太累,測試太枯燥,可以嘗試了解一下UI設(shè)計。也許你會問沒有繪畫基礎(chǔ)也可以學習UI設(shè)計嗎?
答案是肯定是可以的,市面上大多數(shù)的UI設(shè)計并非科班專業(yè)出生,通過不斷的學習和積累,也可以具備UI設(shè)計的相關(guān)專業(yè)技能。
阿里巴巴資深總監(jiān)楊光曾表示:無論是魯班,還是未來升級的人工智能都不可能取代設(shè)計師,機器人只是幫助設(shè)計師解決重復(fù)性的工作,重塑整個設(shè)計生態(tài),而真正的“設(shè)計師”,反而會越來越值錢。
綜上所述,想要不被社會淘汰,最好讓人無法取代。入行互聯(lián)網(wǎng),選擇UI設(shè)計讓你既有技術(shù)又有發(fā)展。如果對UI設(shè)計感興趣,選擇UI讓興趣和夢想齊飛。
3、產(chǎn)品:
當然也有很多小伙伴對產(chǎn)品經(jīng)理這個職位很感興趣,難道是因為經(jīng)理這個頭銜聽起來很拉風。那我們又來了解互聯(lián)網(wǎng)行業(yè)中的產(chǎn)品經(jīng)理。
人人都是產(chǎn)品經(jīng)理,但是真的每個人都能做產(chǎn)品經(jīng)理嗎?產(chǎn)品經(jīng)理會伴隨一個產(chǎn)品走完全部的生命周期,他需要和開發(fā)、設(shè)計、測試、運營等團隊,及上下游緊密合作,對項目進行風險把控和資源協(xié)調(diào),推進達成產(chǎn)品目標。
雖然看似產(chǎn)品經(jīng)理不需要技術(shù),但是沒有技術(shù)背景你如何和技術(shù)人員溝通,如何把控進度與風險,所以產(chǎn)品經(jīng)理特別考驗一個人的綜合素質(zhì),就不建議零基礎(chǔ)的小伙伴通過產(chǎn)品經(jīng)理入行互聯(lián)網(wǎng)了。你可以通過技術(shù)入行,再轉(zhuǎn)到產(chǎn)品崗,有了技術(shù)背景,對你的產(chǎn)品經(jīng)理的職業(yè)發(fā)展有很大幫助哦。
4、運營:
運營就是對運營過程的計劃、組織、實施和控制,是與產(chǎn)品生產(chǎn)和服務(wù)創(chuàng)造密切相關(guān)的各項管理工作的總稱。而互聯(lián)網(wǎng)運營就是要利用一切資源與策略去吸引用戶,增加用戶粘性。
大多數(shù)的人會說,運營崗位門檻低,沒有太多技術(shù)含量。對于運營來說,因為沒有固定的概念和標準的工作定義,不同的產(chǎn)品、不同的平臺所采取的方式方法不一樣,所以運營工作靈活,方式多變,需要順應(yīng)變化。由于回答中有很多關(guān)于運營崗位的回答,在這里我就不再對運營崗位進行詳細說明了。
最后我想說,互聯(lián)網(wǎng)行業(yè)中的崗位很多,根據(jù)自己的興趣和背景去選擇合適的方向,但是零基礎(chǔ)找到工作肯定不現(xiàn)實。因為企業(yè)不是學校,不會傳授技術(shù)給零經(jīng)驗、零基礎(chǔ)的人,所以你一定得具備勝任崗位的能力,才有可能入行互聯(lián)網(wǎng)。零基礎(chǔ)、零經(jīng)驗的小伙伴,大多數(shù)都會以初級人員的要求進入行業(yè),所以想要入行,你至少得具備一定的技能和前提條件。
在操作系統(tǒng)中,線程是操作系統(tǒng)調(diào)度的最小單元,同時線程又是一種受限的系統(tǒng)資源,即線程不可能無限制地產(chǎn)生,并且 線程的創(chuàng)建和銷毀都會有相應(yīng)的開銷。 當系統(tǒng)中存在大量的線程時,系統(tǒng)會通過會時間片輪轉(zhuǎn)的方式調(diào)度每個線程,因此線程不可能做到絕對的并行。
如果在一個進程中頻繁地創(chuàng)建和銷毀線程,顯然不是高效的做法。正確的做法是采用線程池,一個線程池中會緩存一定數(shù)量的線程,通過線程池就可以避免因為頻繁創(chuàng)建和銷毀線程所帶來的系統(tǒng)開銷。
AsyncTask是一個抽象類,它是由Android封裝的一個輕量級異步類(輕量體現(xiàn)在使用方便、代碼簡潔),它可以在線程池中執(zhí)行后臺任務(wù),然后把執(zhí)行的進度和最終結(jié)果傳遞給主線程并在主線程中更新UI。
AsyncTask的內(nèi)部封裝了 兩個線程池 (SerialExecutor和THREAD_POOL_EXECUTOR)和 一個Handler (InternalHandler)。
其中 SerialExecutor線程池用于任務(wù)的排隊,讓需要執(zhí)行的多個耗時任務(wù),按順序排列 , THREAD_POOL_EXECUTOR線程池才真正地執(zhí)行任務(wù) , InternalHandler用于從工作線程切換到主線程 。
1.AsyncTask的泛型參數(shù)
AsyncTask是一個抽象泛型類。
其中,三個泛型類型參數(shù)的含義如下:
Params: 開始異步任務(wù)執(zhí)行時傳入的參數(shù)類型;
Progress: 異步任務(wù)執(zhí)行過程中,返回下載進度值的類型;
Result: 異步任務(wù)執(zhí)行完成后,返回的結(jié)果類型;
如果AsyncTask確定不需要傳遞具體參數(shù),那么這三個泛型參數(shù)可以用Void來代替。
有了這三個參數(shù)類型之后,也就控制了這個AsyncTask子類各個階段的返回類型,如果有不同業(yè)務(wù),我們就需要再另寫一個AsyncTask的子類進行處理。
2.AsyncTask的核心方法
onPreExecute()
這個方法會在 后臺任務(wù)開始執(zhí)行之間調(diào)用,在主線程執(zhí)行。 用于進行一些界面上的初始化操作,比如顯示一個進度條對話框等。
doInBackground(Params...)
這個方法中的所有代碼都會 在子線程中運行,我們應(yīng)該在這里去處理所有的耗時任務(wù)。
任務(wù)一旦完成就可以通過return語句來將任務(wù)的執(zhí)行結(jié)果進行返回,如果AsyncTask的第三個泛型參數(shù)指定的是Void,就可以不返回任務(wù)執(zhí)行結(jié)果。 注意,在這個方法中是不可以進行UI操作的,如果需要更新UI元素,比如說反饋當前任務(wù)的執(zhí)行進度,可以調(diào)用publishProgress(Progress...)方法來完成。
onProgressUpdate(Progress...)
當在后臺任務(wù)中調(diào)用了publishProgress(Progress...)方法后,這個方法就很快會被調(diào)用,方法中攜帶的參數(shù)就是在后臺任務(wù)中傳遞過來的。 在這個方法中可以對UI進行操作,在主線程中進行,利用參數(shù)中的數(shù)值就可以對界面元素進行相應(yīng)的更新。
onPostExecute(Result)
當doInBackground(Params...)執(zhí)行完畢并通過return語句進行返回時,這個方法就很快會被調(diào)用。返回的數(shù)據(jù)會作為參數(shù)傳遞到此方法中, 可以利用返回的數(shù)據(jù)來進行一些UI操作,在主線程中進行,比如說提醒任務(wù)執(zhí)行的結(jié)果,以及關(guān)閉掉進度條對話框等。
上面幾個方法的調(diào)用順序:
onPreExecute() -- doInBackground() -- publishProgress() -- onProgressUpdate() -- onPostExecute()
如果不需要執(zhí)行更新進度則為onPreExecute() -- doInBackground() -- onPostExecute(),
除了上面四個方法,AsyncTask還提供了onCancelled()方法, 它同樣在主線程中執(zhí)行,當異步任務(wù)取消時,onCancelled()會被調(diào)用,這個時候onPostExecute()則不會被調(diào)用 ,但是要注意的是, AsyncTask中的cancel()方法并不是真正去取消任務(wù),只是設(shè)置這個任務(wù)為取消狀態(tài),我們需要在doInBackground()判斷終止任務(wù)。就好比想要終止一個線程,調(diào)用interrupt()方法,只是進行標記為中斷,需要在線程內(nèi)部進行標記判斷然后中斷線程。
3.AsyncTask的簡單使用
這里我們模擬了一個下載任務(wù),在doInBackground()方法中去執(zhí)行具體的下載邏輯,在onProgressUpdate()方法中顯示當前的下載進度,在onPostExecute()方法中來提示任務(wù)的執(zhí)行結(jié)果。如果想要啟動這個任務(wù),只需要簡單地調(diào)用以下代碼即可:
4.使用AsyncTask的注意事項
①異步任務(wù)的實例必須在UI線程中創(chuàng)建,即AsyncTask對象必須在UI線程中創(chuàng)建。
②execute(Params... params)方法必須在UI線程中調(diào)用。
③不要手動調(diào)用onPreExecute(),doInBackground(Params... params),onProgressUpdate(Progress... values),onPostExecute(Result result)這幾個方法。
④不能在doInBackground(Params... params)中更改UI組件的信息。
⑤一個任務(wù)實例只能執(zhí)行一次,如果執(zhí)行第二次將會拋出異常。
先從初始化一個AsyncTask時,調(diào)用的構(gòu)造函數(shù)開始分析。
這段代碼雖然看起來有點長,但實際上并沒有任何具體的邏輯會得到執(zhí)行,只是初始化了兩個變量,mWorker和mFuture,并在初始化mFuture的時候?qū)Worker作為參數(shù)傳入。mWorker是一個Callable對象,mFuture是一個FutureTask對象,這兩個變量會暫時保存在內(nèi)存中,稍后才會用到它們。 FutureTask實現(xiàn)了Runnable接口,關(guān)于這部分內(nèi)容可以看這篇文章。
mWorker中的call()方法執(zhí)行了耗時操作,即result = doInBackground(mParams);,然后把執(zhí)行得到的結(jié)果通過postResult(result);,傳遞給內(nèi)部的Handler跳轉(zhuǎn)到主線程中。在這里這是實例化了兩個變量,并沒有開啟執(zhí)行任務(wù)。
那么mFuture對象是怎么加載到線程池中,進行執(zhí)行的呢?
接著如果想要啟動某一個任務(wù),就需要調(diào)用該任務(wù)的execute()方法,因此現(xiàn)在我們來看一看execute()方法的源碼,如下所示:
調(diào)用了executeOnExecutor()方法,具體執(zhí)行邏輯在這個方法里面:
可以 看出,先執(zhí)行了onPreExecute()方法,然后具體執(zhí)行耗時任務(wù)是在exec.execute(mFuture),把構(gòu)造函數(shù)中實例化的mFuture傳遞進去了。
exec具體是什么?
從上面可以看出具體是sDefaultExecutor,再追溯看到是SerialExecutor類,具體源碼如下:
終于追溯到了調(diào)用了SerialExecutor 類的execute方法。SerialExecutor 是個靜態(tài)內(nèi)部類,是所有實例化的AsyncTask對象公有的,SerialExecutor 內(nèi)部維持了一個隊列,通過鎖使得該隊列保證AsyncTask中的任務(wù)是串行執(zhí)行的,即多個任務(wù)需要一個個加到該隊列中,然后執(zhí)行完隊列頭部的再執(zhí)行下一個,以此類推。
在這個方法中,有兩個主要步驟。
①向隊列中加入一個新的任務(wù),即之前實例化后的mFuture對象。
②調(diào)用 scheduleNext()方法,調(diào)用THREAD_POOL_EXECUTOR執(zhí)行隊列頭部的任務(wù)。
由此可見SerialExecutor 類僅僅為了保持任務(wù)執(zhí)行是串行的,實際執(zhí)行交給了THREAD_POOL_EXECUTOR。
THREAD_POOL_EXECUTOR又是什么?
實際是個線程池,開啟了一定數(shù)量的核心線程和工作線程。然后調(diào)用線程池的execute()方法。執(zhí)行具體的耗時任務(wù),即開頭構(gòu)造函數(shù)中mWorker中call()方法的內(nèi)容。先執(zhí)行完doInBackground()方法,又執(zhí)行postResult()方法,下面看該方法的具體內(nèi)容:
該方法向Handler對象發(fā)送了一個消息,下面具體看AsyncTask中實例化的Hanlder對象的源碼:
在InternalHandler 中,如果收到的消息是MESSAGE_POST_RESULT,即執(zhí)行完了doInBackground()方法并傳遞結(jié)果,那么就調(diào)用finish()方法。
如果任務(wù)已經(jīng)取消了,回調(diào)onCancelled()方法,否則回調(diào) onPostExecute()方法。
如果收到的消息是MESSAGE_POST_PROGRESS,回調(diào)onProgressUpdate()方法,更新進度。
InternalHandler是一個靜態(tài)類,為了能夠?qū)?zhí)行環(huán)境切換到主線程,因此這個類必須在主線程中進行加載。所以變相要求AsyncTask的類必須在主線程中進行加載。
到此為止,從任務(wù)執(zhí)行的開始到結(jié)束都從源碼分析完了。
AsyncTask的串行和并行
從上述源碼分析中分析得到,默認情況下AsyncTask的執(zhí)行效果是串行的,因為有了SerialExecutor類來維持保證隊列的串行。如果想使用并行執(zhí)行任務(wù),那么可以直接跳過SerialExecutor類,使用executeOnExecutor()來執(zhí)行任務(wù)。
四、AsyncTask使用不當?shù)暮蠊?/p>
1.)生命周期
AsyncTask不與任何組件綁定生命周期,所以在Activity/或者Fragment中創(chuàng)建執(zhí)行AsyncTask時,最好在Activity/Fragment的onDestory()調(diào)用 cancel(boolean);
2.)內(nèi)存泄漏
3.) 結(jié)果丟失
屏幕旋轉(zhuǎn)或Activity在后臺被系統(tǒng)殺掉等情況會導致Activity的重新創(chuàng)建,之前運行的AsyncTask(非靜態(tài)的內(nèi)部類)會持有一個之前Activity的引用,這個引用已經(jīng)無效,這時調(diào)用onPostExecute()再去更新界面將不再生效。
自己是從事了七年開發(fā)的Android工程師,不少人私下問我,2019年Android進階該怎么學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發(fā),卻又不知道怎么進階學習的朋友?!?包括高級UI、性能優(yōu)化、架構(gòu)師課程、NDK、Kotlin、混合式開發(fā)(ReactNative+Weex)、Flutter等架構(gòu)技術(shù)資料 】,希望能幫助到您面試前的復(fù)習且找到一個好的工作,也節(jié)省大家在網(wǎng)上搜索資料的時間來學習。
Uniapp目前比較成熟,而且用的是Vue語法,學習成本比較低,而且行業(yè)里面用的也比較廣泛,而Flutter的話,學習成本略高,因為要學習新的語言,還有就是目前生態(tài)不是特別完備,等他再發(fā)展發(fā)展吧。黑馬程序員官網(wǎng)有成套免費視頻哦,有什么不懂的可以直接過去學習。您的采納是對我成長的鞭策