Flutter?Zone異常處理方法及基本原理
1. 認(rèn)識(shí)Zone
Zone像一個(gè)沙盒,是我們代碼執(zhí)行的一個(gè)環(huán)境。
我們的main
函數(shù)默認(rèn)就運(yùn)行在Root Zone
當(dāng)中。
子Zone的構(gòu)造有點(diǎn)像Linux中的進(jìn)程,它支持從當(dāng)前的Zone中Fork出一個(gè)子Zone:
Zone myZone = Zone.current.fork(...)
對(duì)于Zone而言,它有兩個(gè)構(gòu)造函數(shù):
- ZoneSpecification
- ZoneValues
ZoneSpecification:其實(shí)是Zone內(nèi)部代碼行為的一個(gè)提取,我們可以通過(guò)它來(lái)為Zone設(shè)置一些監(jiān)聽(tīng)。
ZoneValues:Zone的變量,私有變量。
類(lèi)似Linux 通過(guò)Fork創(chuàng)建的 myZone默認(rèn)也具有源Zone的ZoneSpecification和ZoneValues。
1.1 ZoneValues
和Linux類(lèi)似地,當(dāng)Zone做Fork的時(shí)候,會(huì)將父Zone所持有的ZoneSpecification、ZoneValues會(huì)繼承下來(lái),可以直接使用。并且是支持追加的,secondZone在firstZone的基礎(chǔ)之上,又追加了extra_values
屬性,不會(huì)因?yàn)閟econdZone的ZoneValues就導(dǎo)致name屬性被替換掉。
Zone firstZone = Zone.current .fork(specification: zoneSpecification, zoneValues: {"name": "bob"}); Zone secondZone = firstZone.fork(zoneValues: {"extra_values": 12345}); secondZone.run(() { print(secondZone["name"]); // bob print(secondZone["extra_values"]); // 12345 }
我們可以使用Zone.current
,訪(fǎng)問(wèn)當(dāng)前的代碼執(zhí)行在哪一個(gè)Zone
當(dāng)中,默認(rèn)情況下,代碼執(zhí)行在Root Zone
當(dāng)中,后續(xù)會(huì)根據(jù)需求分化出多個(gè)Zone
,也可以使用Zone.root
訪(fǎng)問(wèn)到RootZone的實(shí)例。
1.2 ZoneSpecification
和ZoneValues不同,ZoneValues支持追加不同的屬性,而ZoneSpecification
只支持重寫(xiě),并且RootZone
已經(jīng)預(yù)設(shè)好了一系列的Zone中運(yùn)行的規(guī)則,一旦我們重寫(xiě)了ZoneSpecification
的一些方法回調(diào),之前的一些功能可能會(huì)消失。
這種基于配置對(duì)象
的擴(kuò)展方法和基于繼承
的子類(lèi)的重寫(xiě)是不一樣的,該方法具有更強(qiáng)的擴(kuò)展性,但是在類(lèi)似于特性保留的機(jī)制上就明顯不如繼承來(lái)的方便,一旦重寫(xiě)某個(gè)方法,該方法原有的特性需要重新實(shí)現(xiàn)一遍,否則原有的功能會(huì)消失。
如果你只重寫(xiě)了其中的一個(gè)方法,那么其他方法不會(huì)被覆蓋,依然采用默認(rèn)配置。
ZoneSpecification的構(gòu)造方法中,包含非常多的參數(shù),其中絕大多數(shù)都是以回Callback形式出現(xiàn),首先來(lái)看看run
系列的方法:
RunHandler? run, RunUnaryHandler? runUnary, RunBinaryHandler? runBinary,
其實(shí)這三個(gè)方法的區(qū)別在于參數(shù),我們看看RunHandler
、RunUnaryHandler
和RunBinaryHandler
的具體定義:
typedef RunHandler = R Function<R>( Zone self, ZoneDelegate parent, Zone zone, R Function() f);? typedef RunUnaryHandler = R Function<R, T>( Zone self, ZoneDelegate parent, Zone zone, R Function(T arg) f, T arg);? typedef RunBinaryHandler = R Function<R, T1, T2>(Zone self, ZoneDelegate parent, Zone zone, R Function(T1 arg1, T2 arg2) f, T1 arg1, T2 arg2);
不難發(fā)現(xiàn),三者除了固定的:self
、parent
、zone
之外,區(qū)別就在于
UnaryHandler
和BinaryHandler
提供了分別提供了一個(gè)參數(shù)、兩個(gè)參數(shù)的選項(xiàng)。這個(gè)參數(shù)的作用是提供給另外一個(gè)參數(shù):f,類(lèi)型是一個(gè)Function
,顯然它是我們調(diào)用Zone.run
方法傳進(jìn)來(lái)的body
參數(shù),以RunHandler
為例,我們對(duì)run
做出如下的定義:
Zone secondZone = firstZone.fork( zoneValues: {"extra_values": 12345}, specification: ZoneSpecification( run: <int>(self, parent, zone, f) { int output = f(); return output; }, ));
我們?cè)谕獠空{(diào)用secondZone.run(()=>...)
時(shí),就可以在run方法的開(kāi)始、結(jié)尾做一些其他的事情了:
secondZone.run(body);// 執(zhí)行 run: <int>(self, parent, zone, f) { // 1. print("before"); int output = f();// 這里的f就是body,它是可執(zhí)行的 print("after"); return output; // 2. },
直覺(jué)告訴我,1/2之間的代碼應(yīng)該是在Second Zone
中執(zhí)行的,但是打印一下Zone.root
,我們發(fā)現(xiàn)實(shí)際上是在Root Zone
中執(zhí)行的,二者的HashCode相同。
// 在body內(nèi)部打印的 body internal Zone:195048515 // Root Zone:195048515 // first Zone:700091970 second Zone:707932504
大致上去跟了一下代碼,發(fā)現(xiàn)默認(rèn)的run方法的實(shí)現(xiàn),被我們新編寫(xiě)的run
參數(shù)覆蓋掉了,所以會(huì)導(dǎo)致本該在secondZone中執(zhí)行的body結(jié)果在Root Zone
中執(zhí)行。然后再run
參數(shù)的注釋里,發(fā)現(xiàn)了這么一段話(huà):
Since the root zone is the only zone that can modify the value of [current], custom zones intercepting run should always delegate to their parent zone. They may take actions before and after the call.
大致上的意思是:
因?yàn)镽oot Zone是唯一能夠修改Zone.current
參數(shù)的Zone,所以自定義的Zone攔截run方法必須總是將方法交給它們的父Zone去代為處理。而run自己可以在run調(diào)用之前或者之后采取一些行動(dòng)。
也就是說(shuō),我們不能直接return f();
,而要把f()
委托給parent
來(lái)執(zhí)行,像這樣:
secondZone.run(body);// 執(zhí)行 ? run: <int>(self, parent, zone, f) { // 1.這里執(zhí)行在Root Zone print("before"); Function output = parent.run(self, () { // 這里執(zhí)行在second Zone return f(); }); print("after"); return output; // 2. },
委托之后,由Root Zone
去做統(tǒng)一的調(diào)度、Zone的切換。這樣,我們?cè)偃ゴ蛴∫幌聢?zhí)行的Zone,發(fā)現(xiàn)正常了,secondZone.run
方法(其實(shí)是被ZoneSpecification中的run指定的方法)的Zone仍然是Root Zone
,而我們傳遞過(guò)去的任務(wù)被執(zhí)行在了self
之中,也就是SecondZone
當(dāng)中,符合我們的預(yù)期:
current zone:692810917
body internal Zone:558922284
Root Zone:692810917
firstZone Zone:380051056
second Zone:558922284
額外地,可以牽出ZoneDelegate
是做什么的,它允許子Zone,訪(fǎng)問(wèn)父Zone的一些方法,與此同時(shí)保留自己額外的一些行為:綠框表示額外的行為,當(dāng)Zone A
調(diào)用Zone B
的run時(shí),它通常執(zhí)行在調(diào)用者的Zone當(dāng)中,也就是ZoneA。
1.3 通過(guò)runZoned快速創(chuàng)建Zone
Dart提供了runZoned方法,支持Zone的快速創(chuàng)建:
R runZoned<R>(R body(), {Map<Object?, Object?>? zoneValues, ZoneSpecification? zoneSpecification, @Deprecated("Use runZonedGuarded instead") Function? onError}) {
其中body、zoneValues、zoneSpecification都是老熟人了,關(guān)鍵在于它對(duì)于run方法的處理:
/// Runs [body] in a new zone based on [zoneValues] and [specification]. R _runZoned<R>(R body(), Map<Object?, Object?>? zoneValues, ZoneSpecification? specification) => Zone.current .fork(specification: specification, zoneValues: zoneValues) .run<R>(body);
如果我們不顯式地傳遞一個(gè)ZoneSpecififation
進(jìn)來(lái),fork
時(shí)傳進(jìn)去的是null,自然不會(huì)導(dǎo)致Specification被我們重寫(xiě),因此代碼能按照Dart默認(rèn)的實(shí)現(xiàn)方式,運(yùn)行在一個(gè)新的、Fork出來(lái)的Zone當(dāng)中(至少能看出不是Root Zone):
runZoned(() { print("body internal Zone:" + Zone.current.hashCode.toString()); print("Root Zone:" + Zone.root.hashCode.toString()); }); ? // 打印結(jié)果 body internal Zone:253994638 Root Zone:1004225004
但是如果你像之前手動(dòng)fork一樣,指定它的ZoneSpecification,又不把f委托給上層Zone處理,那么就會(huì):
body internal Zone:44766141 Root Zone:44766141
2. 異步基本原理和異常捕獲
默認(rèn)大家已經(jīng)知道什么事單線(xiàn)程模型,以及Future的執(zhí)行機(jī)制了,Dart的單線(xiàn)程模型和事件循環(huán)機(jī)制。
來(lái)看看這段簡(jiǎn)單的代碼:
void asyncFunction() { print('1'); Future((){ print('2'); }).then((e) { print('3'); }); print('4'); }
大家都知道,這段代碼的輸出的順序是:1423,它的大致流程是:
print 1 創(chuàng)建一個(gè)Future,并扔到Event Queue末尾 print 4 // 從Event Queue中取出,并執(zhí)行下一個(gè)消息...... 執(zhí)行Future構(gòu)造函數(shù)中的方法:-> print 2 print 2執(zhí)行完成,即Future完成,回調(diào)它的then: -> print 3
我們?yōu)樗由蟖wait和async,并稍作改造,寫(xiě)成async、await的同步形式,同時(shí)刪掉4
void asyncFunction() async { print('1'); await Future(() { print('2'); }); print('3'); print('4'); }
它的輸出是:1234,他所做的是:
print 1; 創(chuàng)建一個(gè)Future@1,并扔到Event Queue末尾; // 從Event Queue中取出,并執(zhí)行下一個(gè)消息...... 取出Future@1,立刻執(zhí)行它構(gòu)造中的方法: -> print 2; 并將之后的代碼打包,重新放到Event Queue的末尾(這里一般會(huì)等待IO完成,之后就會(huì)去執(zhí)行和這個(gè)回調(diào)) 執(zhí)行完成之后,執(zhí)行之后的代碼: print 3; print 4;
今天我們不是討論Async和Await的,就不再展開(kāi)。
但是大家可以比較一下這兩次調(diào)用,發(fā)現(xiàn)第二種和第一種相比,第二種調(diào)用的代碼是會(huì) “回來(lái)” 繼續(xù)執(zhí)行的,而第一種的Future創(chuàng)建不搭配await/async的就好比脫韁的野馬,這種代碼我們并不關(guān)心它的結(jié)果,自然也不要求代碼在此await,執(zhí)行起來(lái)就無(wú)法控制,但在Dart中我們也無(wú)法通過(guò)try/catch
捕獲異常。
關(guān)鍵點(diǎn)在于:async + await是會(huì)回到異步阻塞的代碼處(await處)執(zhí)行的。既然回來(lái)了,那么try/catch
自然而然是能夠繼續(xù)監(jiān)聽(tīng)是否有異常拋出的。
而第一種的Future,即使我們?cè)谕饷姘狭?code>try/catch,而Future的代碼卻是在未來(lái)的某個(gè)時(shí)間內(nèi),在Event Queue的末尾的某個(gè)位置解包執(zhí)行的,上下文和try/catch
所在的代碼并沒(méi)什么關(guān)聯(lián),自然不能攔截到異常。我們可以從Stack Trace中看看這兩種代碼拋出異常時(shí)的執(zhí)行棧:
左側(cè)是一種方法的執(zhí)行棧,throwExceptionFunction()
項(xiàng)相關(guān)的棧幀已經(jīng)消失了,異常自然沒(méi)有辦法通過(guò)throwExceptionFunction()
中的try/catch
進(jìn)行捕獲。
問(wèn)題就出在這了, 對(duì)于這種錯(cuò)誤我們是否有辦法去捕獲呢?
答案仍然還是是今天的主題 : Zone。
3. HandleUncaughtErrorHandler
雖然異步代碼的執(zhí)行,可能會(huì)橫跨多個(gè)Event,讓代碼前后的上下文失去聯(lián)系,導(dǎo)致異常無(wú)法被正常捕獲,但是它仍然在一個(gè)Zone之內(nèi)。
就像仙劍奇?zhèn)b傳三中,李逍遙對(duì)景天說(shuō)“邪劍仙(Exception)雖身處六界(Event)之外卻是在道(Zone)之內(nèi)”。
Zone提供了一些特殊的編程接口,讓我們能夠?qū)Ξ?dāng)前這個(gè)Zone沙盒內(nèi)的未捕獲的異常進(jìn)行集中處理。
它就是HandleUncaughtErrorHandler
。作為ZoneSpecification
的一個(gè)參數(shù),它支持將Zone當(dāng)中未被處理的錯(cuò)誤統(tǒng)一歸到這里進(jìn)行處理(Dart和Java不一樣,Dart的異常本身通常不會(huì)導(dǎo)致程序的退出),因此,常使用HandleUncaughtErrorHandler來(lái)做異常的統(tǒng)計(jì)、上報(bào)等等。
另外,因?yàn)镈art執(zhí)行環(huán)境的單線(xiàn)程 + 事件隊(duì)列機(jī)制本身,Dart的try/catch
對(duì)于異步代碼是無(wú)法處理的,如下的代碼異常會(huì)穿透(或者說(shuō)根本不經(jīng)過(guò))try/catch后拋出,會(huì)在控制臺(tái)中留下紅色的報(bào)錯(cuò)。
// Zone.run(()=>throwExceptionFunctino()); void throwExceptionFunction() { try { Future.delayed(const Duration(seconds: 1)) .then((e) => throw("This is an Exception")); } catch (e) { print("an Exception has been Captured: ${e.toString()}"); } }
顯然,異步的異常并沒(méi)有被捕獲:
Unhandled exception: This is an Exception #0 throwExceptionFunction.<anonymous closure> (file:///Users/rEd/IdeaProjects/dartProjs/zone/bin/zone.dart:140:22) #1 _rootRunUnary (dart:async/zone.dart:1434:47) #2 _CustomZone.runUnary (dart:async/zone.dart:1335:19) <asynchronous suspension>
但是我們改成這樣呢?
void throwExceptionFunction() async{ try { await Future.delayed(const Duration(seconds: 1)) .then((e) => throw ("This is an Exception")); } catch (e) { print("an Exception has been Captured: ${e.toString()}"); } }
我們對(duì)異步的方法throwExceptionFunction()
加了await/async
關(guān)鍵字。我們會(huì)發(fā)現(xiàn)異常,又能被捕獲了:
an Exception has been Captured: This is an Exception Process finished with exit code 0
其實(shí)上文已經(jīng)提到了是異步時(shí)Dart代碼上下文切換的原因,這里也不做過(guò)多的贅述了,我們像這樣,將我們的App包裹在一個(gè)額外的Zone里面,并在它的HandleUncaughtErrorHandler
相關(guān)方法做如下定義:
void main() { runZoned(() => runApp(const MyExceptionApp()), zoneSpecification: ZoneSpecification( // print: (self, parent, zone, line) {}, handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { // 同樣將print代理給上層Zone,這樣就可以在上層捕獲到這些異常了。 parent.print(self," ### \n $stackTrace \n ### "); })); }
隨便找個(gè)地方拋出個(gè)異常:
floatingActionButton: FloatingActionButton( onPressed: () => Future((){ throw ("ERROR!"); }), ),
我們可以發(fā)現(xiàn),異常在此處被HandleUncaughtErrorHandler
集中捕獲了。
或者我們也可以使用runZoned
自帶的回調(diào)來(lái)處理,而不是去自己重寫(xiě)ZoneSpecification
:
// runZonedGuarded替換runZoned runZonedGuarded(() => runApp(const MyExceptionApp()), (Object error, StackTrace stack) { print('stack: $stack'); });
不過(guò),我們?nèi)ニ鼉?nèi)部看看,其實(shí)它還是HandleUncaughtErrorHandler
實(shí)現(xiàn)的。
注意:如果重寫(xiě)了ZoneSpecification的run相關(guān)的方法,可能會(huì)導(dǎo)致當(dāng)前的Zone無(wú)法捕獲到異常,就像1.中所說(shuō)的那樣,基于配置類(lèi)的重寫(xiě)將原有特性覆蓋掉了,導(dǎo)致當(dāng)前代碼并不一定在我們直覺(jué)認(rèn)為的Zone中執(zhí)行。
這需要編寫(xiě)者自己去解決這個(gè)問(wèn)題,所以,如果沒(méi)有特殊的需求,一般不給Zone傳遞ZoneSpecification選項(xiàng),如果要傳遞,需要去實(shí)現(xiàn)它,以保證相關(guān)的功能特性可用。
以上就是Flutter Zone異常處理方法及基本原理的詳細(xì)內(nèi)容,更多關(guān)于Flutter Zone異常處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android貝塞爾曲線(xiàn)實(shí)現(xiàn)消息拖拽消失
這篇文章主要為大家詳細(xì)介紹了Android貝塞爾曲線(xiàn)實(shí)現(xiàn)消息拖拽消失,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01Android實(shí)現(xiàn)動(dòng)態(tài)自動(dòng)匹配輸入內(nèi)容
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)動(dòng)態(tài)自動(dòng)匹配輸入內(nèi)容,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Android實(shí)現(xiàn)照片墻效果的實(shí)例代碼
Android實(shí)現(xiàn)照片墻效果的設(shè)計(jì)思路其實(shí)也非常簡(jiǎn)單,用一個(gè)GridView控件當(dāng)作“墻”,然后隨著GridView的滾動(dòng)將一張張照片貼在“墻”上,這些照片可以是手機(jī)本地中存儲(chǔ)的,也可以是從網(wǎng)上下載的2018-05-05Android實(shí)現(xiàn)動(dòng)畫(huà)效果的自定義下拉菜單功能
這篇文章主要介紹了Android實(shí)現(xiàn)動(dòng)畫(huà)效果的自定義下拉菜單功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02Android 驗(yàn)證碼功能實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 驗(yàn)證碼功能實(shí)現(xiàn)代碼的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08Android實(shí)現(xiàn)帶附件的郵件發(fā)送功能
這篇文章主要介紹了Android實(shí)現(xiàn)帶附件的郵件發(fā)送功能的相關(guān)資料,android發(fā)送郵件有兩種方式,本文重點(diǎn)介紹基于JMail實(shí)現(xiàn)郵件發(fā)送功能,感興趣的小伙伴們可以參考一下2016-01-01Android實(shí)現(xiàn)點(diǎn)擊Button產(chǎn)生水波紋效果
這篇文章主要介紹了Android實(shí)現(xiàn)點(diǎn)擊Button產(chǎn)生水波紋效果,需要的朋友可以參考下2016-01-01Flutter應(yīng)用程序?qū)崿F(xiàn)隱私屏幕示例解析
這篇文章主要為大家介紹了Flutter應(yīng)用程序?qū)崿F(xiàn)隱私屏幕示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09