Historia
Możliwości refleksji Swift zostały zasadniczo przeprojektowane podczas inicjatywy stabilności ABI w Swift 5.0. Wcześniej refleksja opierała się na niestabilnych wewnętrznych strukturach kompilatora, które zmieniały się z każdą wersją zestawu narzędzi. API Mirror zostało wprowadzone, aby zapewnić stabilny, publiczny interfejs do inspekcji typów w czasie wykonywania, umożliwiając narzędziom debugującym i ogólnemu logowaniu bez znajomości typów w czasie kompilacji. Wymagało to formatu metadanych zdolnego do przetrwania ewolucji biblioteki, gdzie układy struktur mogły się zmieniać między wersjami.
Problem
Gdy struktura jest oznaczona jako odporna (domyślnie dla publicznych typów w trybie ewolucji biblioteki), kompilator nie może twardo kodować stałych przesunięć pamięci dla jej właściwości przechowywanych. Twarde kodowanie złamałoby zgodność binarną, jeśli autor biblioteki doda, usunie lub zmieni kolejność pól w przyszłej wersji. Dodatkowo, system refleksji musi ujawniać wystarczające metadane, aby odtworzyć nazwy i typy pól typu w czasie wykonywania, szanując jednocześnie odporną granicę, która ukrywa szczegóły implementacji przed bezpośrednim dostępem.
Rozwiązanie
Kompilator Swift emituje deskriptory pól do sekcji __swift5_fieldmd metadanych binarnych. Te deskriptory nie zawierają statycznych offsetów; zamiast tego przechowują odniesienia do offsetów względnych lub obliczenia układów w czasie instancjonowania, które rozwiązują rzeczywistą lokalizację pamięci w czasie wykonywania. Dla typów odpornych metadane zawierają wektor offsetów pól, który jest wypełniany, gdy typ jest instancjonowany w bieżącym procesie. Ta indykcja pozwala API Mirror na przeszukiwanie właściwości przy użyciu obliczonych offsetów, które dostosowują się do konkretnej wersji biblioteki załadowanej w czasie wykonywania, zachowując zarówno stabilność ABI, jak i możliwości refleksji.
import Foundation struct ResilientConfig { let timeout: Double private let apiKey: String // Dostępne w Mirror mimo 'private' } let config = ResilientConfig(timeout: 30.0, apiKey: "secret") let mirror = Mirror(reflecting: config) for child in mirror.children { print("Właściwość: \(child.label ?? "nienazwana"), Wartość: \(child.value)") }
Modularna architektura aplikacji iOS oddziela moduł Networking (zamknięty kod źródłowy SDK) od modułu Analytics (wewnętrzny). Moduł Networking zwraca złożone struktury konfiguracyjne zawierające prywatne tokeny uwierzytelniające, które nie powinny być ujawniane przez publiczne metody, ale zespół Analytics wymaga logowania wszystkich parametrów konfiguracyjnych w celu debugowania sporadycznych problemów z czasem oczekiwania.
Rozwiązanie 1: Publiczna Konwersja na Słownik
Zespół Networking mógłby ujawnić metodę toDictionary(), która ręcznie mapuje pola na stringi.
Zalety: Bezpieczeństwo typów w czasie kompilacji, jawna kontrola nad ujawnionymi danymi, szybka wydajność.
Wady: Wymaga utrzymania przy każdej zmianie struktury; nie może odzwierciedlać nowych pól dodanych w aktualizacjach SDK bez rekompilacji klienta; ujawnia wrażliwe pola, jeśli deweloper zapomni o ich filtrowaniu.
Rozwiązanie 2: Introspekcja w Czasie Wykonywania Objective-C
Wykorzystując valueForKey: przez most NSObject.
Zalety: Znajome dla deweloperów z tłem Objective-C.
Wady: Struktury Swift nie są podklasami NSObject; wymuszenie zgodności @objc zmienia semantykę wartości na semantykę odniesienia i znacząco zwiększa rozmiar binarny; nie działa z natywnymi typami Swift.
Rozwiązanie 3: Refleksja Swift przez Mirror
Implementacja ogólnego loggera przy użyciu Mirror(reflecting:) do iteracji po wszystkich właściwościach przechowywanych, niezależnie od kontroli dostępu.
Zalety: Automatycznie dostosowuje się do nowych właściwości w aktualizacjach SDK bez rekompilacji; szanuje granice odporności; działa z typami wartości i kodem ogólnym.
Wady: Mirror alokuje pamięć na stercie dla swojej wewnętrznej pamięci, co czyni go nieodpowiednim do szybkiego logowania; omija kontrolę dostępu, potencjalnie ujawniając prywatne sekrety, jeśli nie zostanie przefiltrowany przez CustomReflectable; nie może odzwierciedlać pól bitowych w C lub właściwości obliczeniowych.
Wybrane rozwiązanie
Zespół przyjął Rozwiązanie 3 z opakowaniem, które sprawdza zgodność z CustomReflectable, aby umożliwić SDK Networking dostarczenie zsanitowanego widoku. Moduł Networking zaimplementował customMirror, aby wykluczyć apiKey, jednocześnie ujawniając timeout i inne bezpieczne pola.
Wynik
Moduł Analytics pomyślnie zarejestrował stany konfiguracyjne przez trzy główne aktualizacje SDK bez zmian łamiących. Jednak gdy zespół Networking dodał opakowanie struktury C dla niskopoziomowych opcji gniazd z polami bitowymi, te konkretne pola pojawiły się jako puste w logach. To wymagało dokumentacji, aby wyjaśnić ograniczenie Mirror, podczas gdy reszta konfiguracji nadal odzwierciedlała się automatycznie.
Jak Mirror zapobiega nieskończonej rekurencji podczas odzwierciedlania struktur danych z odniesieniami do siebie i jaka odpowiedzialność spoczywa na deweloperze przy implementacji CustomReflectable?
Mirror wykrywa cykle odniesienia, śledząc tożsamość instancji klas podczas przechodzenia refleksji. Gdy natrafia na instancję klasy, sprawdza, czy ten obiekt już znajduje się w bieżącym stosie rekurencji; jeśli tak, przestaje przechodzić, aby zapobiec przepełnieniu stosu. Dla typów wartości rekurencja występuje tylko wtedy, gdy zawierają odniesienia, które tworzą cykle. Jednak gdy deweloper wdraża CustomReflectable i ręcznie konstruuje Mirror z children, czas wykonywania nie może wykrywać cykli w tej niestandardowej konstrukcji. Deweloper musi upewnić się, że sekwencja children nie tworzy nieskończonych pętli, na przykład, sprawdzając limit głębokości rekurencji lub utrzymując własny zestaw odwiedzonych przy budowaniu niestandardowej refleksji dla struktur przypominających grafy.
Dlaczego refleksja na struktural przez Mirror czasami zgłasza różne układy pamięci w porównaniu do rzeczywistego skompilowanego układu, szczególnie przy użyciu C zawierających pola bitowe lub unie?
Metadane refleksji Swift są zaprojektowane dla typów Swift i używają metadanych importera Clang dla interoperacyjności z C. C pola bitowe i unie nie mapują się na odrębne właściwości przechowywane w Swift z stabilnymi adresami; są reprezentowane jako nieprzezroczysty magazyn lub wypełnienie wewnętrzne w tłumaczeniu typów importera Clang. API Mirror wymaga adresowalnych pól do skonstruowania swojej kolekcji children. W konsekwencji pola bitowe są niewidoczne dla refleksji, ponieważ brakuje im deskriptorów pól w sekcji __swift5_fieldmd, a członkowie unii mogą występować jako nakładające się lub niepoprawnie typowane, ponieważ metadane opisują kontener unii, a nie poszczególne przypadki. To jest fundamentalne ograniczenie: Mirror odzwierciedla Swift widok typu, a nie podstawowy C układ.
Jaki jest koszt wydajności dostępu do właściwości przez Mirror w porównaniu do bezpośredniego dostępu i dlaczego koszt jest asymetryczny między odczytem liczby właściwości a odczytem wartości właściwości?
Dostęp do właściwości przez Mirror jest o rzędy wielkości wolniejszy niż dostęp bezpośredni, ponieważ wiąże się z wyszukiwaniami metadanych w czasie wykonywania, alokacją pamięci na stercie dla instancji Mirror i pośrednimi wywołaniami przez funkcje dostępu do pól przechowywane w metadanych typu. Odczyt liczby children wymaga parsowania metadanych deskriptorów pól, aby określić liczbę przechowywanych właściwości, co jest stosunkowo szybkim skanowaniem sekcji __swift5_fieldmd. Jednak uzyskanie rzeczywistych wartości wymaga wywołania witnessów wartości lub wyspecjalizowanych funkcji dostępu dla każdego pola, co może wiązać się z kopiowaniem danych, zarządzaniem liczbą odniesień dla typów ARC oraz przekraczaniem granic odporności. Dla klas ten koszt obejmuje kontrole czasu wykonywania Objective-C. Dlatego iterowanie przez mirror.children, aby wydobyć wartości, ma wyższe narzuty niż po prostu sprawdzenie mirror.children.count, co czyni Mirror nieodpowiednim do gorących ścieżek, mimo jego użyteczności w debugowaniu.