Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Spravovaný kód i Objective-C podporují výjimky modulu runtime (klauzule try/catch/finally).
Jejich implementace se ale liší, což znamená, že knihovny modulu runtime (moduly runtime MonoVM/CoreCLR a knihovny modulu runtime Objective-C) mají problémy, když narazí na výjimky z jiných modulů runtime.
Tento článek vysvětluje problémy, ke kterým může dojít, a možná řešení.
Obsahuje také ukázkový projekt, zařazování výjimek, které lze použít k testování různých scénářů a jejich řešení.
Problém
K problému dochází, když je vyvolána výjimka a při odvíjení zásobníku je narazeno na rámec, který neodpovídá typu vyvolané výjimky.
Typickým příkladem tohoto problému je, že nativní rozhraní API vyvolá výjimku Objective-C a pak musí být nějak zpracována výjimka Objective-C, když proces odvíjení zásobníku dosáhne spravovaného rámce.
V minulosti (pre-.NET) byla výchozí akce nic dělat. U výše uvedené ukázky by to znamenalo, že Objective-C modul runtime uvolní spravované rámce. Tato akce je problematická, protože modul runtime Objective-C neví, jak uvolnit spravované rámce; Například se nespustí žádné spravované catch klauzule ani finally klauzule, což vede k působivému obtížnému hledání chyb.
Poškozený kód
Vezměme si následující příklad kódu:
var dict = new NSMutableDictionary ();
dict.LowlevelSetObject (IntPtr.Zero, IntPtr.Zero);
Tento kód vyvolá výjimku Objective-C NSInvalidArgumentException v nativním kódu:
NSInvalidArgumentException *** setObjectForKey: key cannot be nil
Trasování zásobníku bude vypadat přibližně takto:
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 ()
Rámce 0–3 jsou nativní rámce a v modulu runtime Objective-C může tyto rámce uvolnit. Konkrétně se spustí všechny Objective-C @catch nebo @finally klauzule.
Objective-C odvíječ zásobníku však nedokáže správně uvolnit spravované rámce (rámce 4–6): Objective-C odvíječ zásobníku uvolní spravované rámce, ale nespustí žádnou logiku spravovaných výjimek (například catch nebo finally klauzule).
To znamená, že tyto výjimky obvykle není možné zachytit následujícím způsobem:
try {
var dict = new NSMutableDictionary ();
dict.LowLevelSetObject (IntPtr.Zero, IntPtr.Zero);
} catch (Exception ex) {
Console.WriteLine (ex);
} finally {
Console.WriteLine ("finally");
}
Důvodem je to, že rozbalovač zásobníku Objective-C neví o spravované catch klauzuli, a ani klauzule finally nebude spuštěna.
Pokud je výše uvedený vzorový kód účinný, je to proto, že Objective-C má metodu upozorňování na neošetřené výjimky Objective-C, NSSetUncaughtExceptionHandlerkteré sady .NET SDK používají, a v tomto okamžiku se pokusí převést všechny Objective-C výjimky na spravované výjimky.
Scénáře
Scénář 1 – zachycení Objective-C výjimek pomocí spravovaného zpracovatele výjimek
V následujícím scénáři je možné zachytit výjimky Objective-C pomocí spravovaných catch obslužných rutin:
- Výjimka typu Objective-C je vyvolána.
- Modul runtime Objective-C prochází zásobník (ale nerozbalí ho), hledá nativní
@catchobslužný mechanismus, který dokáže zpracovat výjimku. - Modul runtime Objective-C nenašel žádné obslužné rutiny, volá
NSGetUncaughtExceptionHandlera vyvolá obslužnou rutinu nainstalovanou v rámci .NET SDK. - Obslužný modul sad SDK .NET převede výjimku Objective-C na spravovanou výjimku a vyvolá ji. Vzhledem k tomu, že modul runtime Objective-C nerozbalil zásobník (jen ho prošel), aktuální stav je stejný jako v okamžiku, kdy došlo k vyvolání výjimky Objective-C.
K dalšímu problému dochází tady, protože modul runtime Mono neví, jak správně uvolnit Objective-C rámců.
Když se volá nezachycená sada .NET SDK Objective-C zpětné volání výjimky, zásobník bude vypadat takto:
0 TestApp 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 TestApp 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]
V této části jsou jedinými spravovanými snímky rámce 8–10, ale spravovaná výjimka se vyvolá v rámci 0. To znamená, že modul runtime Mono musí uvolnit nativní rámce 0–7, což způsobí problém, který odpovídá výše uvedenému problému: přestože modul runtime Mono uvolní nativní rámce, neprovede žádné Objective-C @catch ani @finally klauzule.
Příklad kódu:
-(id) setObject: (id) object forKey: (id) key
{
@try {
if (key == nil)
[NSException raise: @"NSInvalidArgumentException"];
} @finally {
NSLog (@"This won't be executed");
}
}
@finally A klauzule se nespustí, protože modul runtime Mono, který tento rámec uvolní, o něm neví.
Variantou je vyvolání spravované výjimky ve spravovaném kódu a následné odvíjení nativních rámců, aby se dostala k první spravované catch klauzuli:
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.");
}
}
}
Spravovaná UIApplication:Main metoda bude volat nativní UIApplicationMain metodu a pak iOS provede spoustu nativního spuštění kódu, než nakonec zavolá spravovanou AppDelegate:FinishedLaunching metodu, s stále mnoha nativními snímky v zásobníku při vyvolání spravované výjimky:
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: TestApp mono_jit_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
3: TestApp do_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>, error=<unavailable>)
4: TestApp mono_runtime_invoke [inlined] mono_runtime_invoke_checked(method=<unavailable>, obj=<unavailable>, params=<unavailable>, error=0xbff45758)
5: TestApp mono_runtime_invoke(method=<unavailable>, obj=<unavailable>, params=<unavailable>, exc=<unavailable>)
6: TestApp xamarin_invoke_trampoline(type=<unavailable>, self=<unavailable>, sel="application:didFinishLaunchingWithOptions:", iterator=<unavailable>), context=<unavailable>)
7: TestApp xamarin_arch_trampoline(state=0xbff45ad4)
8: TestApp 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[])
Rámce 0–1 a 27–30 jsou spravovány, zatímco všechny rámce mezi nimi jsou nativní.
Pokud se Mono odvijí přes tyto rámce, nebudou provedeny žádné Objective-C @catch ani @finally klauzule.
Důležité
Pouze modul runtime MonoVM podporuje rozbalování nativních záznamů během zpracování spravovaných výjimek. Modul runtime CoreCLR pouze přeruší proces při výskytu této situace (modul runtime CoreCLR se používá pro aplikace pro macOS a také při povolení NativeAOT na libovolné platformě).
Scénář 2 – nelze zachytit výjimky Objective-C
V následujícím scénáři není možné zachytit Objective-C výjimky pomocí spravovaných catch obslužných rutin, protože Objective-C výjimka byla zpracována jiným způsobem:
- Výjimka Objective-C je vyvolána.
- Modul runtime Objective-C prochází ten zásobník (ale nerozvine ho) a hledá nativní
@catchobslužný mechanismus, který dokáže zpracovat výjimku. - Modul runtime Objective-C najde obslužnou rutinu
@catch, rozbalí zásobník a spustí obslužnou rutinu@catch.
Tento scénář se běžně vyskytuje v .NET pro aplikace pro iOS, protože v hlavním vlákně je obvykle kód podobný tomuto:
void UIApplicationMain ()
{
@try {
while (true) {
ExecuteRunLoop ();
}
} @catch (NSException *ex) {
NSLog (@"An unhandled exception occured: %@", exc);
abort ();
}
}
To znamená, že v hlavním vlákně se nikdy neošetřená výjimka Objective-C, a proto zpětné volání, které převádí Objective-C výjimky na spravované výjimky, se nikdy nevolá.
To je také běžné při ladění aplikací pro macOS ve starší verzi macOS než nejnovější, protože kontrola většiny objektů uživatelského rozhraní v ladicím programu se pokusí načíst vlastnosti, které odpovídají selektorům, které neexistují na spuštěné platformě. Volání takových selektorů vyvolá výjimku NSInvalidArgumentException (Nerozpoznaný selektor odeslaný do ...), což nakonec způsobí chybové ukončení procesu.
Chcete-li shrnout, pokud se runtime Objective-C nebo Mono pokusí zpracovat volání, která nejsou naprogramována k jejich řešení, může to vést k nedefinovanému chování, jako jsou pády aplikací, paměťové úniky a jiné typy nepředvídatelného chování.
Návod
U aplikací macOS a Mac Catalyst (ale ne iOS nebo tvOS) je možné, aby smyčka uživatelského rozhraní nezachytá všechna výjimky nastavením NSApplicationCrashOnExceptions vlastnosti aplikace na true:
var defs = new NSDictionary ((NSString) "NSApplicationCrashOnExceptions", NSNumber.FromBoolean (true));
NSUserDefaults.StandardUserDefaults.RegisterDefaults (defs);
Všimněte si však, že tato vlastnost není zdokumentována společností Apple, takže chování se může v budoucnu změnit.
Řešení
Podporujeme zachytávání spravovaných i Objective-C výjimek na libovolné hranici nativní pro správu a pro převod této výjimky na druhý typ.
V pseudokódu vypadá přibližně takto:
class MyClass {
[DllImport (Constants.ObjectiveCLibrary)]
static extern void objc_msgSend (IntPtr handle, IntPtr selector);
static void DoSomething (NSObject obj)
{
objc_msgSend (obj.Handle, Selector.GetHandle ("doSomething"));
}
}
Volání nespravovaného kódu objc_msgSend se zachytí a místo toho se volá tento kód:
void
xamarin_dyn_objc_msgSend (id obj, SEL sel)
{
@try {
objc_msgSend (obj, sel);
} @catch (NSException *ex) {
convert_to_and_throw_managed_exception (ex);
}
}
A něco podobného se provádí pro reverzní případ (zařazování spravovaných výjimek do Objective-C výjimek).
V .NET je zařazování spravovaných výjimek do Objective-C výjimek ve výchozím nastavení vždy povolené.
Část Příznaky v čase sestavení vysvětluje, jak zakázat zachycování, když je výchozí.
Události
Existují dvě události, které jsou vyvolány po zachycení výjimky: Runtime.MarshalManagedException a Runtime.MarshalObjectiveCException.
Oběma událostem se předává EventArgs objekt, který obsahuje Exception vlastnost původní výjimky, která byla vyvolána, a ExceptionMode vlastnost definuje, jak má být výjimka zařazována.
ExceptionMode Vlastnost lze v obslužné rutině události upravit tak, aby se změnilo chování podle libovolného vlastního zpracování provedeného ve správci. Jedním z příkladů by bylo přerušení procesu, pokud dojde k určité výjimce.
ExceptionMode Změna vlastnosti platí pro jednu událost, nemá vliv na žádné výjimky zachycené v budoucnu.
Při zařazování spravovaných výjimek do nativního kódu jsou k dispozici následující režimy:
-
Default: V současné době je to vždyThrowObjectiveCException. Výchozí nastavení se může v budoucnu změnit. -
UnwindNativeCode: Tato možnost není k dispozici při použití CoreCLR (CoreCLR nepodporuje odvíjení nativního kódu, místo toho proces přeruší). -
ThrowObjectiveCException: Převede spravovanou výjimku na výjimku Objective-C a vyvolá výjimku Objective-C. Toto je výchozí hodnota v .NET. -
Abort: Přerušte proces. -
Disable: Zakáže zachycování výjimek. Nemá smysl nastavit tuto hodnotu v obslužné rutině události (jakmile je událost vyvolána, je příliš pozdě zakázat zachycování výjimky). V každém případě, pokud je nastavena, bude se chovat jakoUnwindNativeCode.
Při zařazování výjimek Objective-C do spravovaného kódu jsou k dispozici následující režimy:
-
Default: V současné době to vždy platí v .NET. Výchozí nastavení se může v budoucnu změnit. -
UnwindManagedCode: Toto je předchozí (nedefinované) chování. -
ThrowManagedException: Převeďte výjimku Objective-C na spravovanou výjimku a vyvolejte spravovanou výjimku. Toto je výchozí hodnota v .NET. -
Abort: Přerušte proces. -
Disable: Zakáže zachycování výjimek. Nemá smysl nastavit tuto hodnotu v obslužné rutině události (jakmile je událost vyvolána, je příliš pozdě zakázat zachycování výjimky). V každém případě, pokud je nastavena, bude se chovat jakoUnwindManagedCode.
Pokud se tedy chcete podívat, kdy se zařadí výjimka, můžete to udělat takto:
class MyApp {
static void Main (string args[])
{
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);
};
/// ...
}
}
Návod
V ideálním případě by Objective-C výjimky neměly nastat v dobře chované aplikaci (Apple je považuje za mnohem výjimečné než spravované výjimky: "vyhněte se vyvolání výjimek [Objective-C] v aplikaci, kterou dodáváte uživatelům"). Jedním ze způsobů, jak toho dosáhnout, by bylo přidat obslužnou rutinu události pro událost Runtime.MarshalObjectiveCException , která by protokolovala všechny zařazované výjimky Objective-C pomocí telemetrie (pro ladění nebo místní sestavení možná také nastavit režim výjimky na "Abort"), aby se všechny tyto výjimky detekovaly, aby bylo možné je opravit nebo vyhnout.
příznaky Build-Time
Je možné nastavit následující vlastnosti nástroje MSBuild, které určí, jestli je povolené zachycování výjimek, a nastavit výchozí akci, která by měla nastat:
- MarshalManagedExceptionMode: "default", "unwindnativecode", "throwobjectivecexception", "abort", "disable".
- MarshalObjectiveCExceptionMode: "výchozí", "unwindmanagedcode", "throwmanagedexception", "přerušení", "zakázat".
Příklad:
<PropertyGroup>
<MarshalManagedExceptionMode>throwobjectivecexception</MarshalManagedExceptionMode>
<MarshalObjectiveCExceptionMode>throwmanagedexception</MarshalObjectiveCExceptionMode>
</PropertyGroup>
disableS výjimkou , tyto hodnoty jsou identické s ExceptionMode hodnotami, které jsou předány MarshalManagedException a MarshalObjectiveCException události.
Tato disable možnost většinou zakáže zachycování, s výjimkou případů, kdy nezachytí žádné režijní náklady na spuštění. U těchto výjimek jsou stále vyvolány události zařazování, přičemž výchozí režim je výchozím režimem pro prováděcí platformu.
Omezení
Zachytáváme volání P/Invoke do rodiny funkcí pouze při pokusu o zachycení Objective-C výjimek. To znamená, že P/Invoke do jiné funkce jazyka C, která poté vyvolá jakékoli výjimky Objective-C, stále narazí na staré a nedefinované chování (v budoucnu by to mohlo být vylepšeno).