Odpowiedź Vladimira jest właściwie całkiem dobra, jednak chciałbym tutaj podać trochę więcej podstawowej wiedzy. Może któregoś dnia ktoś znajdzie moją odpowiedź i uzna ją za pomocną.
Kompilator przekształca pliki źródłowe (.c, .cc, .cpp, .m) na pliki obiektowe (.o). Istnieje jeden plik obiektowy na plik źródłowy. Pliki obiektów zawierają symbole, kod i dane. Pliki obiektowe nie są bezpośrednio używane przez system operacyjny.
Teraz podczas budowania biblioteki dynamicznej (.dylib), frameworka, ładowalnego pakietu (.bundle) lub wykonywalnego pliku binarnego, te pliki obiektowe są łączone ze sobą przez konsolidator w celu utworzenia czegoś, co system operacyjny uważa za „użyteczne”, np. Coś, co może bezpośrednio załadować do określonego adresu pamięci.
Jednak podczas budowania biblioteki statycznej wszystkie te pliki obiektowe są po prostu dodawane do dużego pliku archiwum, stąd rozszerzenie bibliotek statycznych (.a dla archiwum). Zatem plik .a to nic innego jak archiwum plików obiektowych (.o). Pomyśl o archiwum TAR lub archiwum ZIP bez kompresji. Po prostu łatwiej jest skopiować pojedynczy plik .a niż całą masę plików .o (podobnie jak w Javie, gdzie pakujesz pliki .class do archiwum .jar w celu łatwej dystrybucji).
Podczas łączenia pliku binarnego z biblioteką statyczną (= archiwum), konsolidator otrzyma tablicę wszystkich symboli w archiwum i sprawdzi, do których z tych symboli odwołują się pliki binarne. Tylko pliki obiektowe zawierające symbole, do których istnieją odniesienia, są w rzeczywistości ładowane przez konsolidator i są uwzględniane w procesie łączenia. Np. Jeśli twoje archiwum ma 50 plików obiektowych, ale tylko 20 zawiera symbole używane przez plik binarny, tylko tych 20 jest ładowanych przez konsolidator, pozostałe 30 są całkowicie ignorowane w procesie łączenia.
Działa to całkiem dobrze w przypadku kodu C i C ++, ponieważ języki te starają się robić jak najwięcej w czasie kompilacji (chociaż C ++ ma również pewne funkcje tylko w czasie wykonywania). Jednak Obj-C to inny rodzaj języka. Obj-C w dużym stopniu zależy od funkcji środowiska uruchomieniowego, a wiele funkcji Obj-C to w rzeczywistości funkcje tylko w czasie wykonywania. Klasy Obj-C w rzeczywistości mają symbole porównywalne z funkcjami C lub globalnymi zmiennymi C (przynajmniej w obecnym środowisku wykonawczym Obj-C). Konsolidator może sprawdzić, czy istnieje odwołanie do klasy, czy nie, więc może określić, która klasa jest używana, czy nie. Jeśli używasz klasy z pliku obiektowego w bibliotece statycznej, ten plik obiektowy zostanie załadowany przez konsolidator, ponieważ konsolidator widzi używany symbol. Kategorie są funkcją tylko w czasie wykonywania, kategorie nie są symbolami, takimi jak klasy lub funkcje, a to oznacza również, że konsolidator nie może określić, czy kategoria jest używana, czy nie.
Jeśli konsolidator ładuje plik obiektowy zawierający kod Obj-C, wszystkie jego części Obj-C są zawsze częścią etapu łączenia. Więc jeśli plik obiektowy zawierający kategorie jest ładowany, ponieważ dowolny jego symbol jest uważany za „używany” (czy to klasa, czy to funkcja, czy to zmienna globalna), kategorie są również ładowane i będą dostępne w czasie wykonywania . Jednak jeśli sam plik obiektowy nie zostanie załadowany, kategorie w nim zawarte nie będą dostępne w czasie wykonywania. Plik obiektowy zawierający tylko kategorie nigdy nie jest ładowany, ponieważ nie zawiera symboli, które konsolidator kiedykolwiek uznałby za „w użyciu”. I to jest cały problem tutaj.
Zaproponowano kilka rozwiązań i teraz, gdy już wiesz, jak to wszystko gra razem, spójrzmy jeszcze raz na proponowane rozwiązanie:
Jednym z rozwiązań jest dodanie -all_load
do wywołania konsolidatora. Co właściwie zrobi ta flaga konsolidatora? Właściwie to mówi linkerowi, co następuje: „ Załaduj wszystkie pliki obiektowe wszystkich archiwów, niezależnie od tego, czy widzisz jakiś używany symbol, czy nie .” Oczywiście to zadziała, ale może również dać dość duże pliki binarne.
Innym rozwiązaniem jest dodanie -force_load
do wywołania konsolidatora, w tym ścieżki do archiwum. Ta flaga działa dokładnie tak samo -all_load
, ale tylko dla określonego archiwum. Oczywiście to również zadziała.
Najpopularniejszym rozwiązaniem jest dodanie -ObjC
do wywołania konsolidatora. Co właściwie zrobi ta flaga konsolidatora? Ta flaga mówi konsolidatorowi " Załaduj wszystkie pliki obiektów ze wszystkich archiwów, jeśli zobaczysz, że zawierają one kod Obj-C ". „Każdy kod Obj-C” obejmuje kategorie. To również zadziała i nie wymusi ładowania plików obiektowych, które nie zawierają kodu Obj-C (są one nadal ładowane tylko na żądanie).
Innym rozwiązaniem jest dość nowe ustawienie kompilacji Xcode Perform Single-Object Prelink
. Co zrobi to ustawienie? Jeśli ta opcja jest włączona, wszystkie pliki obiektowe (pamiętaj, że jest jeden na plik źródłowy) są łączone razem w jeden plik obiektowy (to nie jest prawdziwe łączenie, stąd nazwa PreLink ) i ten pojedynczy plik obiektowy (czasami nazywany także „obiektem głównym plik ”) jest następnie dodawany do archiwum. Jeśli teraz jakikolwiek symbol głównego pliku obiektowego jest uważany za używany, cały główny plik obiektowy jest uważany za używany, a zatem wszystkie jego części Objective-C są zawsze ładowane. A ponieważ klasy są zwykłymi symbolami, wystarczy użyć jednej klasy z takiej biblioteki statycznej, aby uzyskać również wszystkie kategorie.
Ostatnim rozwiązaniem jest sztuczka, którą Vladimir dodał na samym końcu swojej odpowiedzi. Umieść „ fałszywy symbol ” w dowolnym pliku źródłowym, deklarując tylko kategorie. Jeśli chcesz użyć którejkolwiek z kategorii w czasie wykonywania, upewnij się, że w jakiś sposób odwołujesz się do fałszywego symbolu w czasie kompilacji, ponieważ powoduje to, że plik obiektowy jest ładowany przez konsolidator, a tym samym cały kod Obj-C w nim. Np. Może to być funkcja z pustym ciałem funkcji (która nie zrobi nic po wywołaniu) lub może to być zmienna globalna, do której można uzyskać dostęp (np.int
po przeczytaniu lub napisaniu jest to wystarczające). W przeciwieństwie do wszystkich innych rozwiązań powyżej, to rozwiązanie przenosi kontrolę nad tym, które kategorie są dostępne w czasie wykonywania, na skompilowany kod (jeśli chce, aby były one połączone i dostępne, uzyskuje dostęp do symbolu, w przeciwnym razie nie uzyskuje dostępu do symbolu i konsolidator zignoruje) to).
To wszystko ludzie.
Och, czekaj, jest jeszcze jedna rzecz:
konsolidator ma opcję o nazwie -dead_strip
. Co robi ta opcja? Jeśli konsolidator zdecyduje się załadować plik obiektowy, wszystkie symbole pliku obiektowego staną się częścią połączonego pliku binarnego, niezależnie od tego, czy są używane, czy nie. Np. Plik obiektowy zawiera 100 funkcji, ale tylko jedna z nich jest używana przez plik binarny, wszystkie 100 funkcji jest nadal dodawanych do pliku binarnego, ponieważ pliki obiektowe są albo dodawane jako całość, albo w ogóle nie są dodawane. Częściowe dodawanie pliku obiektowego nie jest zwykle obsługiwane przez konsolidatory.
Jednakże, jeśli powiesz konsolidatorowi "martwy pasek", konsolidator najpierw doda wszystkie pliki obiektowe do pliku binarnego, rozwiąże wszystkie odniesienia i na koniec przeskanuje plik binarny w poszukiwaniu symboli nieużywanych (lub używanych tylko przez inne symbole nie w posługiwać się). Wszystkie symbole, które nie są używane, są następnie usuwane w ramach etapu optymalizacji. W powyższym przykładzie 99 nieużywanych funkcji zostało ponownie usuniętych. Jest to bardzo przydatne, jeśli używasz opcji takich jak -load_all
, -force_load
lub Perform Single-Object Prelink
ponieważ te opcje mogą w niektórych przypadkach łatwo znacznie zwiększyć rozmiary binarne, a martwe usuwanie usunie nieużywany kod i dane ponownie.
Dead stripping działa bardzo dobrze w przypadku kodu C (np. Nieużywane funkcje, zmienne i stałe są usuwane zgodnie z oczekiwaniami), a także działa całkiem dobrze w C ++ (np. Nieużywane klasy są usuwane). Nie jest doskonały, w niektórych przypadkach niektóre symbole nie są usuwane, mimo że byłoby w porządku, aby je usunąć, ale w większości przypadków działa całkiem dobrze w tych językach.
A co z Obj-C? Zapomnij o tym! Nie ma martwego strippingu dla Obj-C. Ponieważ Obj-C jest językiem funkcji środowiska uruchomieniowego, kompilator nie może powiedzieć w czasie kompilacji, czy symbol jest rzeczywiście używany, czy nie. Np. Klasa Obj-C nie jest używana, jeśli nie ma bezpośredniego odniesienia do niej, prawda? Źle! Możesz dynamicznie zbudować ciąg zawierający nazwę klasy, zażądać wskaźnika klasy dla tej nazwy i dynamicznie przydzielić klasę. Np. Zamiast
MyCoolClass * mcc = [[MyCoolClass alloc] init];
Mógłbym też pisać
NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];
W obu przypadkach mmc
jest to odniesienie do obiektu klasy „MyCoolClass”, ale w drugim przykładzie kodu nie ma bezpośredniego odniesienia do tej klasy (nawet nazwa klasy jako ciąg statyczny). Wszystko dzieje się tylko w czasie wykonywania. I to pomimo tego, że klasy są w rzeczywistości prawdziwymi symbolami. Jeszcze gorzej jest z kategoriami, ponieważ nie są one nawet prawdziwymi symbolami.
Więc jeśli masz bibliotekę statyczną z setkami obiektów, ale większość plików binarnych potrzebuje tylko kilku z nich, możesz nie chcieć używać powyższych rozwiązań (1) do (4). W przeciwnym razie otrzymasz bardzo duże pliki binarne zawierające wszystkie te klasy, mimo że większość z nich nigdy nie jest używana. W przypadku klas zwykle nie potrzebujesz żadnego specjalnego rozwiązania, ponieważ klasy mają prawdziwe symbole i tak długo, jak odwołujesz się do nich bezpośrednio (nie jak w drugim przykładzie kodu), konsolidator samodzielnie zidentyfikuje ich użycie. Jednak w przypadku kategorii rozważ rozwiązanie (5), ponieważ umożliwia ono uwzględnienie tylko tych kategorii, których naprawdę potrzebujesz.
Np. Jeśli chcesz mieć kategorię dla NSData, np. Dodając do niej metodę kompresji / dekompresji, możesz utworzyć plik nagłówkowy:
// NSData+Compress.h
@interface NSData (Compression)
- (NSData *)compressedData;
- (NSData *)decompressedData;
@end
void import_NSData_Compression ( );
i plik implementacji
// NSData+Compress
@implementation NSData (Compression)
- (NSData *)compressedData
{
// ... magic ...
}
- (NSData *)decompressedData
{
// ... magic ...
}
@end
void import_NSData_Compression ( ) { }
Teraz po prostu upewnij się, że import_NSData_Compression()
wywoływane jest dowolne miejsce w kodzie . Nie ma znaczenia, gdzie jest nazywany ani jak często się go nazywa. Właściwie to wcale nie musi być wywoływane, wystarczy, że linker tak myśli. Np. Możesz umieścić następujący kod w dowolnym miejscu projektu:
__attribute__((used)) static void importCategories ()
{
import_NSData_Compression();
// add more import calls here
}
Nie musisz nigdy wywoływać importCategories()
swojego kodu, atrybut sprawi, że kompilator i linker uwierzą, że jest wywoływany, nawet jeśli tak nie jest.
I ostatnia wskazówka:
jeśli dodasz -whyload
do końcowego wywołania łącza, konsolidator wydrukuje w dzienniku kompilacji, który plik obiektowy, z której biblioteki załadował z powodu używanego symbolu. Wyświetli tylko pierwszy symbol rozważany jako używany, ale niekoniecznie jest to jedyny symbol używany w tym pliku obiektowym.