Aracılığıyla paylaş


Xamarin.iOS ve Xamarin.Mac'te Özel Durum Hazırlama

Hem yönetilen kod hem Objective-C de çalışma zamanı özel durumları (try/catch/finally yan tümceleri) için desteğe sahiptir.

Ancak, uygulamaları farklıdır; bu da çalışma zamanı kitaplıklarının (Mono çalışma zamanı veya CoreCLR ve Objective-C çalışma zamanı kitaplıkları) özel durumları işlemesi ve ardından başka dillerde yazılmış kodu çalıştırması gerektiğinde sorunlarla karşılaştığı anlamına gelir.

Bu belgede oluşabilecek sorunlar ve olası çözümler açıklanmaktadır.

Ayrıca, farklı senaryoları ve çözümlerini test etmek için kullanılabilecek özel durum hazırlama adlı örnek bir proje içerir.

Sorun

Sorun, bir özel durum oluşturulduğunda ve yığın geri sarma sırasında oluşturulan özel durum türüyle eşleşmeyen bir çerçeveyle karşılaşıldığında oluşur.

Bu sorunun tipik bir örneği, yerel bir API'nin bir Objective-C özel durum oluşturması ve ardından yığın geri sarma işlemi yönetilen bir çerçeveye ulaştığında bu Objective-C özel durumun bir şekilde işlenmesi gerektiğidir.

Eski Xamarin projeleri (pre-.NET) için varsayılan eylem hiçbir şey yapmamaktır. Yukarıdaki örnek için bu, çalışma zamanının yönetilen çerçeveleri bırakmasına Objective-C izin vermek anlamına gelir. Çalışma zamanı yönetilen çerçevelerin Objective-C nasıl geri sarıldığını bilmediğinden, bu eylem sorunludur; örneğin, bu çerçevede herhangi bir catch veya finally yan tümcesi yürütmez.

Bozuk kod

Aşağıdaki kod örneğini inceleyin:

var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero); 

Bu kod yerel kodda bir Objective-C NSInvalidArgumentException oluşturur:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Yığın izlemesi aşağıdakine benzer olacaktır:

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 arası çerçeveler yerel çerçevelerdir ve çalışma zamanındaki Objective-Cyığın geri sarmalayıcı bu çerçeveleri geri alabilir . Özellikle, herhangi bir Objective-C@catch veya @finally yan tümcesini yürütür.

Ancak, Objective-C yığın geri sarmalayıcısı yönetilen çerçeveleri (4-6 arası çerçeveler) düzgün bir şekilde çözemez : Objective-C yığın geri sarmalayıcı yönetilen çerçeveleri geri alır, ancak yönetilen özel durum mantığı (veya 'finally yan tümceleri gibi catch ) yürütmez.

Bu da genellikle bu özel durumları aşağıdaki şekilde yakalamanın mümkün olmadığı anlamına gelir:

try {
    var dict = new NSMutableDictionary ();
    dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
    Console.WriteLine (ex);
} finally {
    Console.WriteLine ("finally");
}

Bunun nedeni, yığın sarmalayıcının Objective-C yönetilen catch yan tümceyi bilmemesi ve yan tümcesinin finally yürütülmemesidir.

Yukarıdaki kod örneği etkili olduğunda, bunun nedeni işlenmeyen Objective-C özel durumlar konusunda bilgilendirilme yöntemine sahip olmasıdır. Bunun nedeniObjective-C, NSSetUncaughtExceptionHandlerXamarin.iOS ve Xamarin.Mac'in kullandığı ve bu noktada özel Objective-C durumları yönetilen özel durumlara dönüştürmeye çalışmasıdır.

Senaryolar

Senaryo 1 - yönetilen catch işleyicisi ile özel durumları yakalama Objective-C

Aşağıdaki senaryoda, yönetilen catch işleyicileri kullanarak özel durumları yakalamak Objective-C mümkündür:

  1. Bir Objective-C özel durum oluşturulur.
  2. Çalışma Objective-C zamanı, özel durumu işleyebilen yerel @catch bir işleyiciyi arayarak yığını gösterir (ancak geri sarmaz).
  3. Çalışma Objective-C zamanı hiçbir @catch işleyici bulmaz, çağırır NSGetUncaughtExceptionHandlerve Xamarin.iOS/Xamarin.Mac tarafından yüklenen işleyiciyi çağırır.
  4. Xamarin.iOS/Xamarin.Mac'in işleyicisi özel durumu yönetilen özel duruma dönüştürür Objective-C ve oluşturur. Objective-C Çalışma zamanı yığını geri almadığından (yalnızca yürüdü), geçerli çerçeve özel durumun oluşturulduğu yerle Objective-C aynıdır.

Mono çalışma zamanı çerçeveleri düzgün bir şekilde nasıl geri Objective-C saracaklarını bilmediğinden burada başka bir sorun oluşur.

Xamarin.iOS'un yakalanmamış Objective-C özel durum geri çağırması çağrıldığında yığın şöyledir:

 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]

Burada tek yönetilen çerçeveler 8-10 arası çerçevelerdir, ancak yönetilen özel durum 0 çerçevesinde oluşturulur. Bu, Mono çalışma zamanının 0-7 arası yerel çerçeveleri geri alması gerektiği anlamına gelir ve bu da yukarıda açıklanan soruna eşdeğer bir soruna neden olur: Mono çalışma zamanı yerel çerçeveleri çözse de, herhangi bir Objective-C@catch veya @finally yan tümcesi yürütmez.

Kod örneği:

-(id) setObject: (id) object forKey: (id) key
{
    @try {
        if (key == nil)
            [NSException raise: @"NSInvalidArgumentException"];
    } @finally {
        NSLog (@"This won't be executed");
    }
}

@finally Yan tümcesi yürütülmeyecek çünkü bu çerçeveyi çözen Mono çalışma zamanı bunu bilmiyor.

Bunun bir çeşitlemesi, yönetilen kodda yönetilen bir özel durum oluşturmak ve ardından ilk yönetilen catch yan tümcesine ulaşmak için yerel çerçeveler arasında geri sarma yapmaktır:

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.");
        }
    }
}

Yönetilen UIApplication:Main yöntem yerel UIApplicationMain yöntemi çağırır ve ardından iOS yönetilen yöntemi çağırmadan önce çok sayıda yerel kod yürütmesi yapar ve yönetilen AppDelegate:FinishedLaunching özel durum oluştuğunda yığında hala çok sayıda yerel çerçeve bulunur:

 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 ve 27-30 çerçeveleri yönetilirken, aradaki tüm çerçeveler yereldir. Mono bu çerçeveler arasında gevşerse, or Objective-C@catch@finally yan tümceleri yürütülemez.

Senaryo 2 - özel durumlar yakalanamıyor Objective-C

Aşağıdaki senaryoda, özel durum başka bir şekilde işlendiği için yönetilen catch işleyicileri Objective-C kullanarak özel durumları yakalamak Objective-C mümkün değildir:

  1. Bir Objective-C özel durum oluşturulur.
  2. Çalışma Objective-C zamanı, özel durumu işleyebilen yerel @catch bir işleyiciyi arayarak yığını gösterir (ancak geri sarmaz).
  3. Çalışma Objective-C zamanı bir @catch işleyici bulur, yığını açar ve işleyiciyi yürütmeye @catch başlar.

Bu senaryo genellikle Xamarin.iOS uygulamalarında bulunur çünkü ana iş parçacığında genellikle aşağıdaki gibi kodlar bulunur:

void UIApplicationMain ()
{
    @try {
        while (true) {
            ExecuteRunLoop ();
        }
    } @catch (NSException *ex) {
        NSLog (@"An unhandled exception occured: %@", exc);
        abort ();
    }
}

Bu, ana iş parçacığında hiçbir zaman işlenmemiş Objective-C bir özel durum olmadığı ve bu nedenle özel durumları yönetilen özel durumlara Objective-C dönüştüren geri çağırmamızın hiçbir zaman çağrılamayacağı anlamına gelir.

Bu, Xamarin.Mac'in desteklediğinden önceki bir macOS sürümünde Xamarin.Mac uygulamalarında hata ayıklama sırasında da yaygındır çünkü hata ayıklayıcıdaki kullanıcı arabirimi nesnelerinin çoğu incelendiğinde yürütme platformunda var olmayan seçicilere karşılık gelen özellikleri getirmeye çalışır (çünkü Xamarin.Mac daha yüksek bir macOS sürümü için destek içerir). Bu tür seçicilerin çağrılması, sonunda işlemin kilitlenmesine neden olan bir NSInvalidArgumentException ("...'ye gönderilen tanınmayan seçici") oluşturur.

Özetlemek gerekirse, işlenmek üzere programlanmamış çalışma zamanının Objective-C veya Mono çalışma zamanının geri sarma çerçevelerini açması kilitlenmeler, bellek sızıntıları ve diğer öngörülemeyen (yanlış) davranış türleri gibi tanımsız davranışlara yol açabilir.

Çözüm

Xamarin.iOS 10 ve Xamarin.Mac 2.10'da, yönetilen-yerel sınırlarda hem yönetilen Objective-C hem de özel durumları yakalama ve bu özel durumu diğer türe dönüştürme desteği ekledik.

Sahte kodda şuna benzer:

[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 için P/Invoke durduruldu ve bunun yerine bu kod çağrılır:

void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
    @try {
        objc_msgSend (obj, sel);
    } @catch (NSException *ex) {
        convert_to_and_throw_managed_exception (ex);
    }
}

Ters durum için de benzer bir işlem yapılır (yönetilen özel durumları özel durumlara Objective-C göre hazırlama).

Yönetilen yerel sınırda özel durumları yakalamak maliyetsiz değildir, bu nedenle eski Xamarin projeleri (pre-.NET) için varsayılan olarak her zaman etkinleştirilmez:

  • Xamarin.iOS/tvOS: Özel durumların Objective-C kesilmesi simülatörde etkinleştirilir.
  • Xamarin.watchOS: Çalışma zamanının Objective-C yönetilen çerçeveleri geri sarmasına izin vermek çöp toplayıcının kafasını karıştıracağından ve yanıt vermemeye veya kilitlenmeye neden olacağı için kesme işlemi her durumda zorlanır.
  • Xamarin.Mac: Hata ayıklama derlemeleri için özel durumların kesilmesi Objective-C etkinleştirildi.

.NET'te, yönetilen özel durumları özel durumlara Objective-C göre sıralama varsayılan olarak her zaman etkindir.

Derleme zamanı bayrakları bölümünde, kesme noktasının varsayılan olarak etkinleştirilmediğinde nasıl etkinleştirileceği açıklanır (veya varsayılan olduğunda kesmeyi devre dışı bırakabilirsiniz).

Ekinlikler

Bir özel durum durdurulduktan sonra tetiklenen iki olay vardır: Runtime.MarshalManagedException ve Runtime.MarshalObjectiveCException.

Her iki olay da, oluşan özgün özel durumu (Exceptionözelliği) içeren bir EventArgs nesneye ve özel durumun nasıl sıralanması gerektiğini tanımlayan bir ExceptionMode özelliğe geçirilir.

özelliği, ExceptionMode işleyicide yapılan herhangi bir özel işlemeye göre davranışı değiştirmek için olay işleyicisinde değiştirilebilir. Bir örnek, belirli bir özel durum oluşursa işlemi durdurmak olabilir.

özelliğinin ExceptionMode değiştirilmesi tek olay için geçerlidir, gelecekte kesilen özel durumları etkilemez.

Yönetilen özel durumlar yerel koda sıralandığında aşağıdaki modlar kullanılabilir:

  • Default: Varsayılan değer platforma göre değişir. Her zaman ThrowObjectiveCException .NET'tedir. Eski Xamarin projeleri için, GC'nin ThrowObjectiveCException işbirliği modunda (watchOS) ve UnwindNativeCode başka bir durumda (iOS / watchOS / macOS) olması gerekir. Varsayılan değer gelecekte değişebilir.
  • UnwindNativeCode: Bu, önceki (tanımlanmamış) davranıştır. Bu, GC'yi işbirliği modunda kullanırken (watchOS'taki tek seçenek budur; bu nedenle watchOS'ta geçerli bir seçenek değildir) veya CoreCLR kullanılırken kullanılamaz, ancak eski Xamarin projelerindeki diğer tüm platformlar için varsayılan seçenektir.
  • ThrowObjectiveCException: Yönetilen özel durumu özel Objective-C duruma dönüştürün ve özel durumu atın Objective-C . Bu, eski Xamarin projelerinde .NET ve watchOS'ta varsayılan değerdir.
  • Abort: İşlemi durdurun.
  • Disable: Özel durum kesmeyi devre dışı bırakır, bu nedenle olay işleyicisinde bu değeri ayarlamak mantıklı değildir, ancak olay tetiklendiğinde devre dışı bırakmak için çok geç olur. Her durumda, ayarlanırsa olarak UnwindNativeCodedavranır.

Yönetilen koda özel durumlar sıralandığında Objective-C aşağıdaki modlar kullanılabilir:

  • Default: Varsayılan değer platforma göre değişir. Her zaman ThrowManagedException .NET'tedir. Eski Xamarin projeleri için, GC'nin ThrowManagedException işbirliği modunda (watchOS) ve UnwindManagedCode başka bir durumda (iOS / tvOS / macOS) olması gerekir. Varsayılan değer gelecekte değişebilir.
  • UnwindManagedCode: Bu, önceki (tanımlanmamış) davranıştır. Bu, GC'yi işbirliği modunda kullanırken (watchOS'taki tek geçerli GC modudur; bu nedenle watchOS'ta geçerli bir seçenek değildir) veya CoreCLR kullanılırken kullanılamaz, ancak eski Xamarin projelerindeki diğer tüm platformlar için varsayılan seçenektir.
  • ThrowManagedException: Özel durumu yönetilen özel duruma dönüştürün Objective-C ve yönetilen özel durumu atın. Bu, eski Xamarin projelerinde .NET ve watchOS'ta varsayılan değerdir.
  • Abort: İşlemi durdurun.
  • Disable: Özel durum kesmeyi devre dışı bırakır, bu nedenle olay işleyicisinde bu değeri ayarlamak mantıklı değildir, ancak olay tetiklendiğinde devre dışı bırakmak için çok geç olur. Her durumda ayarlanırsa işlemi durdurur.

Bu nedenle, her özel durum sıralandığında bunu görebilirsiniz:

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);
};

Derleme Zamanı Bayrakları

Özel durum kesme özelliğinin etkinleştirilip etkinleştirilmediğini belirleyen ve gerçekleşmesi gereken varsayılan eylemi ayarlayan mtouch (Xamarin.iOS uygulamaları için) ve mmp'ye (Xamarin.Mac uygulamaları için) aşağıdaki seçenekleri geçirmek mümkündür:

  • --marshal-managed-exceptions=

    • default
    • unwindnativecode
    • throwobjectivecexception
    • abort
    • disable
  • --marshal-objectivec-exceptions=

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

disabledışında, bu değerler ve MarshalObjectiveCException olaylarına ExceptionMode geçirilen MarshalManagedException değerlerle aynıdır.

Bu disable seçenek çoğunlukla kesmeyi devre dışı bırakır, ancak yürütme ek yükü eklemediği zaman özel durumları kesmeye devam ederiz. Hazırlama olayları yine de bu özel durumlar için oluşturulur ve varsayılan mod, yürütülen platform için varsayılan mod olur.

Sınırlamalar

Özel durumları yakalamaya objc_msgSendObjective-C çalışırken yalnızca işlev ailesine yönelik P/Invoke'ları yakalarız. Bu, daha sonra herhangi Objective-C bir özel durum oluşturan başka bir C işlevine P/Invoke işlevinin eski ve tanımsız davranışlarla çalışmaya devam edeceği anlamına gelir (bu gelecekte geliştirilebilir).