Xamarin.iOS 和 Xamarin.Mac 中的例外狀況封送處理
Managed 程式代碼和 Objective-C 都支援運行時間例外狀況(try/catch/finally 子句)。
不過,其實作不同,這表示運行時間連結庫(Mono 運行時間或 CoreCLR 和 Objective-C 運行時間連結庫)在必須處理例外狀況時發生問題,然後執行以其他語言撰寫的程式代碼。
本文件說明可能發生的問題,以及可能的解決方案。
它也包含範例專案 例外狀況封送處理,可用來測試不同的案例及其解決方案。
問題
擲回例外狀況時發生問題,而且在堆疊回溯期間,遇到框架不符合擲回的例外狀況類型。
此問題的典型範例是當原生 API 擲回 Objective-C 例外狀況時,當堆疊回溯程式到達受控框架時,必須以某種方式處理該 Objective-C 例外狀況。
對於舊版 Xamarin 專案 (pre-.NET),預設動作是不執行任何動作。
針對上述範例,這表示讓 Objective-C 運行時間回溯 Managed 框架。 此動作有問題,因為 Objective-C 運行時間不知道如何回溯 Managed 框架;例如,它不會在該框架中執行任何 catch
或 finally
子句。
中斷的程序代碼
請思考下列程式碼範例:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
此程序代碼會在機器碼中擲回 Objective-C NSInvalidArgumentException:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
堆疊追蹤會像這樣:
0 CoreFoundation __exceptionPreprocess + 194
1 libobjc.A.dylib objc_exception_throw + 52
2 CoreFoundation -[__NSDictionaryM setObject:forKey:] + 1015
3 libobjc.A.dylib objc_msgSend + 102
4 TestApp ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
5 TestApp Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr)
6 TestApp ExceptionMarshaling.Exceptions.ThrowObjectiveCException ()
畫面 0-3 是原生框架,而運行時間中的Objective-C堆疊回溯器可以回溯這些框架。 特別是,它會執行任何 Objective-C@catch
或 @finally
子句。
不過,Objective-C堆棧回溯器無法正確回溯 Managed 框架(畫面 4-6):Objective-C堆棧回溯器會回溯 Managed 框架,但不會執行任何 Managed 例外狀況邏輯(例如 catch
或 'finally 子句)。
這表示通常無法以下列方式攔截這些例外狀況:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
這是因為 Objective-C 堆疊回溯器不知道 Managed catch
子句,而且不會 finally
執行 子句。
當上述程式代碼範例有效時,這是因為Objective-C有一種方法可收到未處理的Objective-C例外狀況通知,NSSetUncaughtExceptionHandler
Xamarin.iOS 和 Xamarin.Mac 會使用,此時會嘗試將任何Objective-C例外狀況轉換成 Managed 例外狀況。
案例
案例 1 - 使用 Managed Catch 處理程式攔截 Objective-C 例外狀況
在下列案例中,您可以使用 Managed catch
處理程序攔截Objective-C例外狀況:
- 擲 Objective-C 回例外狀況。
- 運行時間 Objective-C 會逐步執行堆疊(但不會回溯堆棧),尋找可處理例外狀況的原生
@catch
處理程式。 - 運行 Objective-C 時間找不到任何
@catch
處理程式、呼叫NSGetUncaughtExceptionHandler
,並叫用 Xamarin.iOS/Xamarin.Mac 所安裝的處理程式。 - Xamarin.iOS/Xamarin.Mac 的處理程式會將例外狀況轉換成 Objective-C 受控例外狀況,並擲回它。 由於運行時間 Objective-C 並未回溯堆疊(僅逐步執行堆棧),所以目前的框架與擲回例外狀況的位置 Objective-C 相同。
這裡發生另一個問題,因為Mono運行時間不知道如何正確回溯 Objective-C 畫面。
呼叫 Xamarin.iOS 的未攔截 Objective-C 例外狀況回呼時,堆棧如下所示:
0 libxamarin-debug.dylib exception_handler(exc=name: "NSInvalidArgumentException" - reason: "*** setObjectForKey: key cannot be nil")
1 CoreFoundation __handleUncaughtException + 809
2 libobjc.A.dylib _objc_terminate() + 100
3 libc++abi.dylib std::__terminate(void (*)()) + 14
4 libc++abi.dylib __cxa_throw + 122
5 libobjc.A.dylib objc_exception_throw + 337
6 CoreFoundation -[__NSDictionaryM setObject:forKey:] + 1015
7 libxamarin-debug.dylib xamarin_dyn_objc_msgSend + 102
8 TestApp ObjCRuntime.Messaging.void_objc_msgSend_IntPtr_IntPtr (intptr,intptr,intptr,intptr)
9 TestApp Foundation.NSMutableDictionary.LowlevelSetObject (intptr,intptr) [0x00000]
10 TestApp ExceptionMarshaling.Exceptions.ThrowObjectiveCException () [0x00013]
在這裡,唯一的受管理框架是畫面 8-10,但 Managed 例外狀況會在畫面 0 中擲回。 這表示 Mono 運行時間必須回溯原生框架 0-7,這會導致與上述問題相等的問題:雖然 Mono 運行時間會回溯原生框架,但不會執行任何 Objective-C@catch
或 @finally
子句。
程式碼範例:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
@finally
而且不會執行 子句,因為回溯此框架的Mono運行時間並不知道。
其變化是擲回 Managed 程式代碼中的 Managed 例外狀況,然後透過原生框架回溯以取得第一個 Managed catch
子句:
class AppDelegate : UIApplicationDelegate {
public override bool FinishedLaunching (UIApplication application, NSDictionary launchOptions)
{
throw new Exception ("An exception");
}
static void Main (string [] args)
{
try {
UIApplication.Main (args, null, typeof (AppDelegate));
} catch (Exception ex) {
Console.WriteLine ("Managed exception caught.");
}
}
}
Managed UIApplication:Main
方法會呼叫原生 UIApplicationMain
方法,然後 iOS 會在最終呼叫 Managed 方法之前執行許多原生程式代碼,並在擲回 Managed AppDelegate:FinishedLaunching
例外狀況時,堆棧上仍有許多原生框架:
0: TestApp ExceptionMarshaling.IOS.AppDelegate:FinishedLaunching (UIKit.UIApplication,Foundation.NSDictionary)
1: TestApp (wrapper runtime-invoke) <Module>:runtime_invoke_bool__this___object_object (object,intptr,intptr,intptr)
2: libmonosgen-2.0.dylib mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
3: libmonosgen-2.0.dylib do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
4: libmonosgen-2.0.dylib mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
5: libmonosgen-2.0.dylib mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
6: libxamarin-debug.dylib xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
7: libxamarin-debug.dylib xamarin_arch_trampoline(state=0xbff45ad4)
8: libxamarin-debug.dylib xamarin_i386_common_trampoline
9: UIKit -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:]
10: UIKit -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:]
11: UIKit -[UIApplication _runWithMainScene:transitionContext:completion:]
12: UIKit __84-[UIApplication _handleApplicationActivationWithScene:transitionContext:completion:]_block_invoke.3124
13: UIKit -[UIApplication workspaceDidEndTransaction:]
14: FrontBoardServices __37-[FBSWorkspace clientEndTransaction:]_block_invoke_2
15: FrontBoardServices __40-[FBSWorkspace _performDelegateCallOut:]_block_invoke
16: FrontBoardServices __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__
17: FrontBoardServices -[FBSSerialQueue _performNext]
18: FrontBoardServices -[FBSSerialQueue _performNextFromRunLoopSource]
19: FrontBoardServices FBSSerialQueueRunLoopSourceHandler
20: CoreFoundation __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
21: CoreFoundation __CFRunLoopDoSources0
22: CoreFoundation __CFRunLoopRun
23: CoreFoundation CFRunLoopRunSpecific
24: CoreFoundation CFRunLoopRunInMode
25: UIKit -[UIApplication _run]
26: UIKit UIApplicationMain
27: TestApp (wrapper managed-to-native) UIKit.UIApplication:UIApplicationMain (int,string[],intptr,intptr)
28: TestApp UIKit.UIApplication:Main (string[],intptr,intptr)
29: TestApp UIKit.UIApplication:Main (string[],string,string)
30: TestApp ExceptionMarshaling.IOS.Application:Main (string[])
框架 0-1 和 27-30 會受到管理,而之間的所有畫面都是原生的。
如果Mono會透過這些框架回溯,則不會 Objective-C@catch
執行 或 @finally
子句。
案例 2 - 無法攔截 Objective-C 例外狀況
在下列案例中,無法使用Managed catch
處理程式攔截Objective-C例外狀況,因為Objective-C例外狀況是以另一種方式處理:
- 擲 Objective-C 回例外狀況。
- 運行時間 Objective-C 會逐步執行堆疊(但不會回溯堆棧),尋找可處理例外狀況的原生
@catch
處理程式。 - 運行時間 Objective-C 會
@catch
尋找處理程式、回溯堆疊,並開始執行@catch
處理程式。
此案例通常位於 Xamarin.iOS 應用程式中,因為在主線程上通常會有類似這樣的程式代碼:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
這表示在主線程上,永遠不會真正發生未處理的 Objective-C 例外狀況,因此永遠不會呼叫將例外狀況轉換成 Objective-C Managed 例外狀況的回呼。
在比 Xamarin.Mac 支援的舊版 macOS 上偵錯 Xamarin.Mac 應用程式時,這也很常見,因為檢查調試程式中的大部分 UI 物件會嘗試擷取對應至執行平臺中不存在之選取器的屬性(因為 Xamarin.Mac 包含對更高 macOS 版本的支援)。 呼叫這類選取器會擲回 NSInvalidArgumentException
(“無法辨識的選取器傳送至 ...”),這最終會導致進程當機。
總結來說,讓 Objective-C 運行時間或Mono運行時間回溯框架不經過程序設計,可能會導致未定義的行為,例如當機、記憶體流失,以及其他類型的無法預測(mis)行為。
解決方案
在 Xamarin.iOS 10 和 Xamarin.Mac 2.10 中,我們已新增在任何 Managed 原生界限上攔截 Managed 和 Objective-C 例外狀況的支援,以及將該例外狀況轉換為其他類型。
在虛擬程式碼中,它看起來像這樣:
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
攔截要objc_msgSend的 P/Invoke,並改為呼叫此程式代碼:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
反向案例也做了類似的事情(將 Managed 例外狀況封送處理至 Objective-C 例外狀況)。
攔截 Managed 原生界限上的例外狀況並非無成本,因此對於舊版 Xamarin 專案(pre-.NET),預設不一定會啟用:
- Xamarin.iOS/tvOS:模擬器中已啟用例外狀況攔截 Objective-C 。
- Xamarin.watchOS:在所有情況下都會強制執行攔截,因為讓 Objective-C 運行時間回溯 Managed 框架會混淆垃圾收集行程,並讓它停止回應或當機。
- Xamarin.Mac:針對偵錯組建啟用例外狀況攔截 Objective-C 。
在 .NET 中,預設一律會啟用對例外狀況的Managed 例外 Objective-C 狀況封送處理。
[建置時間旗標] 區段說明如何在預設未啟用攔截時啟用攔截功能(或停用默認攔截時)。
事件
一旦攔截例外狀況,就會引發兩個事件: Runtime.MarshalManagedException
和 Runtime.MarshalObjectiveCException
。
這兩個 EventArgs
事件都會傳遞物件,其中包含擲回的原始例外狀況( Exception
屬性),以及 ExceptionMode
定義如何封送處理例外狀況的屬性。
ExceptionMode
屬性可以在事件處理程式中變更,根據處理程式中完成的任何自定義處理來變更行為。 其中一個範例是,如果發生特定例外狀況,則會中止進程。
變更 ExceptionMode
屬性會套用至單一事件,並不會影響未來攔截的任何例外狀況。
將 Managed 例外狀況封送處理至機器碼時,可以使用下列模式:
Default
:預設會依平臺而有所不同。 它一律ThrowObjectiveCException
在 .NET 中。 對於舊版 Xamarin 專案,如果ThrowObjectiveCException
GC 處於合作模式(watchOS),UnwindNativeCode
否則為 (iOS/ watchOS / macOS)。 預設值未來可能會變更。UnwindNativeCode
:這是先前 (未定義的) 行為。 這在合作模式中使用 GC 時無法使用(這是 watchOS 上唯一的選項;因此,這不是 watchOS 的有效選項),也不適用於使用 CoreCLR,但它是舊版 Xamarin 專案中所有其他平臺的預設選項。ThrowObjectiveCException
:將Managed 例外狀況轉換成 Objective-C 例外狀況,並擲回例外狀況 Objective-C 。 這是 .NET 和舊版 Xamarin 專案中 watchOS 中的預設值。Abort
:中止進程。Disable
:停用例外狀況攔截,因此在事件處理程式中設定此值並無意義,但一旦引發事件,就無法停用此值。 在任何情況下,如果設定,它會以 做為UnwindNativeCode
。
將例外狀況封送處理 Objective-C 至 Managed 程式代碼時,可以使用下列模式:
Default
:預設會依平臺而有所不同。 它一律ThrowManagedException
在 .NET 中。 對於舊版 Xamarin 專案,ThrowManagedException
如果 GC 處於合作模式(watchOS),UnwindManagedCode
否則為 (iOS/ tvOS / macOS)。 預設值未來可能會變更。UnwindManagedCode
:這是先前 (未定義的) 行為。 這在合作模式中使用 GC 時無法使用(這是 watchOS 上唯一有效的 GC 模式;因此這不是 watchOS 的有效選項),也不適用於使用 CoreCLR,但它是舊版 Xamarin 專案中所有其他平台的預設值。ThrowManagedException
:將例外狀況 Objective-C 轉換為Managed 例外狀況,並擲回Managed 例外狀況。 這是 .NET 和舊版 Xamarin 專案中 watchOS 中的預設值。Abort
:中止進程。Disable
:停用例外狀況攔截,因此在事件處理程序中設定此值並無意義,但一旦引發事件,停用它就太晚了。 在任何情況下,如果設定,它會中止進程。
因此,若要查看每次封送處理例外狀況時,您可以執行此動作:
Runtime.MarshalManagedException += (object sender, MarshalManagedExceptionEventArgs args) =>
{
Console.WriteLine ("Marshaling managed exception");
Console.WriteLine (" Exception: {0}", args.Exception);
Console.WriteLine (" Mode: {0}", args.ExceptionMode);
};
Runtime.MarshalObjectiveCException += (object sender, MarshalObjectiveCExceptionEventArgs args) =>
{
Console.WriteLine ("Marshaling Objective-C exception");
Console.WriteLine (" Exception: {0}", args.Exception);
Console.WriteLine (" Mode: {0}", args.ExceptionMode);
};
建置時間旗標
可以將下列選項傳遞至 mtouch (針對 Xamarin.iOS 應用程式) 和 mmp (針對 Xamarin.Mac 應用程式),這會判斷是否已啟用例外狀況攔截,並設定應該發生的默認動作:
--marshal-managed-exceptions=
default
unwindnativecode
throwobjectivecexception
abort
disable
--marshal-objectivec-exceptions=
default
unwindmanagedcode
throwmanagedexception
abort
disable
disable
除了 之外,這些值與傳遞至 MarshalManagedException
和 MarshalObjectiveCException
事件的值相同ExceptionMode
。
選項disable
大多會停用攔截,但我們不會在未新增任何執行額外負荷時攔截例外狀況。 這些例外狀況仍會引發封送處理事件,預設模式是執行平台的預設模式。
限制
我們只會在嘗試攔截Objective-C例外狀況時攔截 P/Invokes 至objc_msgSend
函式系列。 這表示 P/Invoke 至另一個 C 函式,然後擲回任何 Objective-C 例外狀況,仍然會遇到舊和未定義的行為(未來可能會改善此行為)。