共用方式為


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 框架;例如,它不會在該框架中執行任何 catchfinally 子句。

中斷的程序代碼

請思考下列程式碼範例:

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例外狀況通知,NSSetUncaughtExceptionHandlerXamarin.iOS 和 Xamarin.Mac 會使用,此時會嘗試將任何Objective-C例外狀況轉換成 Managed 例外狀況。

案例

案例 1 - 使用 Managed Catch 處理程式攔截 Objective-C 例外狀況

在下列案例中,您可以使用 Managed catch 處理程序攔截Objective-C例外狀況:

  1. 擲 Objective-C 回例外狀況。
  2. 運行時間 Objective-C 會逐步執行堆疊(但不會回溯堆棧),尋找可處理例外狀況的原生 @catch 處理程式。
  3. 運行 Objective-C 時間找不到任何 @catch 處理程式、呼叫 NSGetUncaughtExceptionHandler,並叫用 Xamarin.iOS/Xamarin.Mac 所安裝的處理程式。
  4. 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例外狀況是以另一種方式處理:

  1. 擲 Objective-C 回例外狀況。
  2. 運行時間 Objective-C 會逐步執行堆疊(但不會回溯堆棧),尋找可處理例外狀況的原生 @catch 處理程式。
  3. 運行時間 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.MarshalManagedExceptionRuntime.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除了 之外,這些值與傳遞至 MarshalManagedExceptionMarshalObjectiveCException 事件的值相同ExceptionMode

選項disable大多停用攔截,但我們不會在未新增任何執行額外負荷時攔截例外狀況。 這些例外狀況仍會引發封送處理事件,預設模式是執行平台的預設模式。

限制

我們只會在嘗試攔截Objective-C例外狀況時攔截 P/Invokes 至objc_msgSend函式系列。 這表示 P/Invoke 至另一個 C 函式,然後擲回任何 Objective-C 例外狀況,仍然會遇到舊和未定義的行為(未來可能會改善此行為)。