Xamarin.iOS'ta Özel Durum Sıralama

Xamarin.iOS, özellikle yerel kodda özel durumlara yanıt vermeye yardımcı olacak yeni olaylar içerir.

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

Ancak, uygulamaları farklıdır; bu da çalışma zamanı kitaplıklarının (Mono çalışma zamanı ve çalışma zamanı kitaplıkları) özel durumları işlemeleri ve ardından başka dillerde yazılmış kodu çalıştırmaları gereken sorunlar olduğu anlamına Objective-C gelir.

Bu belge, ortaya çıkabilir sorunları ve olası çözümleri açıklar.

Ayrıca, farklı senaryoları ve bunların çözümlerinitest etmek için kullanılan örnek bir proje olan Özel Durum Sıralamayı içerir.

Sorun

Sorun, bir özel durum oluştuğunda ve yığın geriye doğru izleme sırasında, oluşan özel durum türüyle eşleşmez bir çerçeveyle karşılaşıldı.

Xamarin.iOS veya Xamarin.Mac için bunun tipik bir örneği, yerel bir API'nin bir özel durum oluşturması ve yığın geriye doğru izleme işlemi yönetilen bir çerçeveye ulaştığında bu özel durumun bir şekilde ele Objective-CObjective-C olmasıdır.

Varsayılan eylem, hiçbir şey yapmamadır. Yukarıdaki örnek için bu, çalışma zamanının yönetilen kareleri geriye Objective-C doğru izlemesine izin verme anlamına gelir. Çalışma zamanı yönetilen karelerin nasıl geriye doğru geriye doğru itiltilcesini bilmiyorsa, örneğin bu çerçevede herhangi bir veya yan Objective-Ccatchfinally tümcesi yürütmeyer, bu soruna neden olur.

Bozuk kod

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

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

Bu, yerel Objective-C kodda bir NSInvalidArgumentException atar:

NSInvalidArgumentException *** setObjectForKey: key cannot be nil

Yığın izlemesi de aşağıdakine benzer:

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ında kareler yerel karelerdir ve çalışma zamanında yığın geriye Objective-C doğru geriye doğru izleme bu kareleri geriye doğru geriye doğru takip edebilirsiniz. Objective-C Özellikle, herhangi bir veya yan Objective-C@catch@finally tümcesini yürütür.

Ancak, yığın geriye doğru izleme, yönetilen kareleri Objective-C (4-6 kareler) düzgün bir şekilde geriye doğru izleme özelliğine sahip değildir, bu durumda kareler geriye doğru olmaz, ancak yönetilen özel durum mantığı yürütülmez. Objective-C

Bu da genellikle bu özel durumları şu şekilde yakalamanın mümkün olmadığını ifade eder:

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 geriye doğru izlemenin yönetilen yan tümcesi hakkında bilgisi olmasıdır Objective-Ccatch ve yan finally tümcesi de yürütülmez.

Yukarıdaki kod örneği etkili olduğunda bunun nedeni, Objective-CNSSetUncaughtExceptionHandler Xamarin.iOS ve Xamarin.Mac'in kullanmakta olduğu işlanmamış özel durumlar hakkında bilgi almak için bir yöntemi olması ve bu noktada herhangi bir özel durumu yönetilen özel durumlara dönüştürmeye Objective-C çalışmasıdır.

Senaryolar

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

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

  1. Bir Objective-C özel durum oluşturur.
  2. Çalışma zamanı yığını gösterir (ancak geriye doğru izlemez), özel durumu Objective-C@catch işleyiciyi arıyor.
  3. Çalışma zamanı hiçbir işleyici bulamaz, çağrısı yapar ve Objective-C@catchNSGetUncaughtExceptionHandler Xamarin.iOS/Xamarin.Mac tarafından yüklenmiş işleyiciyi çağırır.
  4. Xamarin.iOS/Xamarin.Mac'in işleyicisi, özel durumu yönetilen bir özel duruma dönüştürür Objective-C ve oluşturur. Çalışma zamanı yığını geriye doğru izlemez (yalnızca onu takip etti), geçerli çerçeve özel Objective-C durumun Objective-C atılmış olduğu karedir.

Burada başka bir sorun oluşur çünkü Mono çalışma zamanı karelerin nasıl geriye doğru geriye doğru doğru şekilde geriye doğru doğru şekilde geriye Objective-C doğru şekilde gelemeyebilir.

Xamarin.iOS'un yakalanmayan Objective-C özel durum geri çağırması çağrıldığı zaman yığın şöyle olur:

 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 kareler 8-10 kareleridir, ancak yönetilen özel durum 0. karede atılan özel durumdur. Bu, Mono çalışma zamanı 0-7 yerel çerçevelerini geriye doğru izlemesi gerektiğini ve bu da yukarıda tartışılan soruna eşdeğer bir soruna neden olduğu anlamına gelir: Mono çalışma zamanı yerel kareleri geriye doğru izlemesine rağmen, herhangi bir veya yan tümcesi Objective-C@catch@finally yürütmez.

Kod örneği:

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

Bu @finally çerçeveyi geriye doğru takip Mono çalışma zamanı yan tümcesi bunun hakkında bilgi alamayacaktır.

Bunun bir varyasyonu, yönetilen kodda yönetilen bir özel durum atmak ve ardından ilk yönetilen yan tümceye almak için yerel kareler arasında geriye doğru geriye doğru izleme catch 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 yöntem yerel yöntemi çağıracak ve ardından iOS yönetilen yöntemi çağırmadan önce çok fazla yerel kod yürütmesi yapar ve yönetilen özel durum thrown olduğunda yığında hala çok sayıda yerel çerçeve UIApplication:MainUIApplicationMainAppDelegate:FinishedLaunching vardır:

 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 kareler yönetilirken, arasındaki tüm kareler yereldir. Mono bu karelerde geriye doğru geriye doğru ilerlerse, Objective-C@catch hiçbir veya yan @finally tümcesi yürütülmez.

Senaryo 2 - özel durumları Objective-C yakalayamadı

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

  1. Bir Objective-C özel durum oluşturur.
  2. Çalışma zamanı yığını gösterir (ancak geriye doğru izlemez), özel durumu Objective-C@catch işleyiciyi arıyor.
  3. Çalışma Objective-C zamanı bir @catch işleyici bulur, yığını geriye doğru yürütür ve işleyiciyi yürütmeye @catch başlar.

Bu senaryo genellikle Xamarin.iOS uygulamaları içinde bulunur çünkü ana iş parçacığında genellikle aşağıdaki gibi bir kod vardır:

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 gerçekten işlanmamış bir özel durum olduğu anlamına gelir ve bu nedenle özel durumları yönetilen özel durumlara dönüştüren geri Objective-CObjective-C çağırmamız hiçbir zaman çağrılmaz.

Xamarin.Mac'in desteklediğinden önceki bir macOS sürümünde Xamarin.Mac uygulamalarının hata ayıklaması yaparken de oldukça yaygındır çünkü hata ayıklayıcıdaki kullanıcı arabirimi nesnelerinin çoğu incelerken, yürütme platformunda mevcut olmayan seçicilere karşılık gelen özellikleri getirmeye çalışabilirsiniz (çünkü Xamarin.Mac daha yüksek bir macOS sürümü için destek içerir). Bu seçicilerin çağrılarak bir ("tanınmayan seçici ..." içine gönderilir) ve bu da sonunda sürecin NSInvalidArgumentException kilitlenmesine neden olur.

Özetlemek gerekirse, işleyecek şekilde programlanmamış çalışma zamanı veya Mono çalışma zamanı geriye doğru izleme çerçevelerinin olması kilitlenmeler, bellek sızıntıları ve diğer öngörülemeyen (yanlış) davranış türleri gibi tanımsız davranışlara yol Objective-C açabilirsiniz.

## Çözüm

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

Sahte kodda şu şekildedir:

[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);

static void DoSomething (NSObject obj)
{
    objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}

P/Invoke objc_msgSend araya çağrılır ve bunun yerine bu ç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);
    }
}

Ve ters durum için de benzer bir şey yapılır (yönetilen özel durumları özel Objective-C durumlara hazırlar).

Yönetilen yerel sınırda özel durumları yakalamak ücretsiz değildir, bu nedenle her zaman varsayılan olarak etkin değildir:

  • Xamarin.iOS/tvOS: Simülatörde Objective-C özel durumların kesme noktası etkinleştirilir.
  • Xamarin.watchOS: Çalışma zamanının yönetilen kareleri geriye doğru izlemesine izin vermenin atık toplayıcıyı karıştırmasına ve kilitlenmesine neden olduğundan, her durumda kesme Objective-C uygulanır.
  • Xamarin.Mac: Hata ayıklama Objective-C derlemeleri için özel durumların kesme noktası etkinleştirilir.

Derleme zamanı bayrakları bölümünde, varsayılan olarak etkin olmayan kesmenin nasıl etkinleştirilmeleri açıklanmaz.

Ekinlikler

Bir özel durum araya alındıktan sonra iki yeni olay ortaya çıkar: Runtime.MarshalManagedException ve Runtime.MarshalObjectiveCException .

Her iki olay da, atılan özgün özel durumu içeren bir nesnesine (özelliği) ve özel durumun nasıl sıra olacağını tanımlamak EventArgs için bir özel durum ExceptionExceptionMode geçirildi.

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

özelliğinin ExceptionMode değiştirilmesi tek bir olay için geçerlidir; gelecekte araya alınan özel durumları etkilemez.

Aşağıdaki modlar kullanılabilir:

  • Default: Varsayılan değer platforma göre değişir. GC'nin işbirliği modunda (watchOS) ve aksi durumda ThrowObjectiveCExceptionUnwindNativeCode (iOS / watchOS / macOS) ise bu olur. Varsayılan değer gelecekte değişebilir.
  • UnwindNativeCode: Bu, önceki (tanımsız) davranıştır. BU, GC'yi işbirliği modunda kullanırken kullanılamaz (watchOS'ta tek seçenektir; bu nedenle watchOS'ta geçerli bir seçenek değildir) ancak diğer tüm platformlar için varsayılan seçenektir.
  • ThrowObjectiveCException: Yönetilen özel durumu bir özel Objective-C duruma dönüştür ve özel durumu Objective-C oluşturur. Bu, watchOS'ta varsayılan değerdir.
  • Abort: İşlemi iptal edin.
  • Disable: Özel durum kesmeyi devre dışı bırakarak olay işleyicisinde bu değeri ayarlamak mantıklı değildir, ancak olay başlatıldıktan sonra devre dışı bırakmak için çok geç olur. Her durumda, ayarlanırsa olarak UnwindNativeCode davranır.

Özel durumları Objective-C yönetilen koda sıralamak için aşağıdaki modlar kullanılabilir:

  • Default: Varsayılan değer platforma göre değişir. GC'nin işbirliği modunda (watchOS) ve aksi takdirde ThrowManagedExceptionUnwindManagedCode (iOS / tvOS / macOS) ise bu olur. Varsayılan değer gelecekte değişebilir.
  • UnwindManagedCode: Bu, önceki (tanımsız) davranıştır. Bu, GC'yi işbirliği modunda kullanırken kullanılamaz (watchOS'ta tek geçerli GC modudur; bu nedenle watchOS'ta geçerli bir seçenek değildir), ancak diğer tüm platformlar için varsayılan seçenektir.
  • ThrowManagedException: Özel durumu Objective-C yönetilen bir özel duruma dönüştür ve yönetilen özel durum oluşturur. Bu, watchOS'ta varsayılan değerdir.
  • Abort: İşlemi iptal edin.
  • Disable:D özel durum kesme noktası sağlar, bu nedenle olay işleyicisinde bu değeri ayarlamak mantıklı değildir, ancak olay başlatıldıktan sonra devre dışı bırakmak için çok geç olur. Herhangi bir durumda ayarlanırsa, işlemi iptal eder.

Bu nedenle, bir özel durum her sıralandı her durumda görmek için bunu yapabiliriz:

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

Build-Time Bayrakları

Aşağıdaki seçenekleri mtouch (Xamarin.iOS uygulamaları için) ve mmp 'ye (Xamarin.Mac uygulamaları için) geçirilebilir. Bu, özel durum durdurmanın etkin olup olmadığını belirler ve gerçekleşmesi gereken varsayılan eylemi ayarlar:

  • --marshal-managed-exceptions=

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

    • default
    • unwindmanagedcode
    • throwmanagedexception
    • abort
    • disable

dışında, disable bu değerler ve ExceptionMode olaylara geçirilen değerlerle MarshalManagedExceptionMarshalObjectiveCException aynıdır.

Bu disable seçenek çoğunlukla disable devre dışı bıraksa da, yürütme ek yükü eklemeden özel durumları yine de kesmiş oluruz. Sıralama olayları bu özel durumlar için yine de yükseltildi ve varsayılan mod, yürütülen platform için varsayılan moddur.

Sınırlamalar

Özel durumları yakalamaya çalışırken yalnızca işlev ailesine P/Invoke'ları objc_msgSendObjective-C kesmiş oluruz. Bu, başka bir C işlevine P/Invoke işlevinin daha sonra herhangi bir özel durum atarak eski ve tanımsız davranışla karşılaşıp karşılaşmayacakları anlamına gelir (bu durum gelecekte geliştir Objective-C olabilir).