it-swarm.dev

Kategorie celu C w bibliotece statycznej

Czy możesz mi pomóc, jak prawidłowo połączyć bibliotekę statyczną z projektem iPhone'a. Używam projektu biblioteki statycznej dodanego do projektu aplikacji jako bezpośredniej zależności (cel -> ogólne -> bezpośrednie zależności) i wszystko działa OK, ale kategorie. Kategoria zdefiniowana w bibliotece statycznej nie działa w aplikacji.

Więc moje pytanie brzmi: jak dodać bibliotekę statyczną z niektórymi kategoriami do innego projektu?

I ogólnie, jaka jest najlepsza praktyka w kodzie projektu aplikacji z innych projektów?

148
Vladimir

Rozwiązanie: Od wersji Xcode 4.2 wystarczy przejść do aplikacji, która łączy się z biblioteką (nie samą bibliotekę) i kliknąć projekt w w Nawigatorze projektu, kliknij cel aplikacji, a następnie skompiluj ustawienia, a następnie wyszukaj „Inne flagi linkera”, kliknij przycisk + i dodaj „-ObjC”. „-all_load” i „-force_load” nie są już potrzebne.

Szczegóły: Znalazłem odpowiedzi na różne fora, blogi i Apple. Teraz próbuję zrobić krótkie podsumowanie mojego wyszukiwania i eksperymenty.

Problem został spowodowany przez (cytat z Apple Techniczne pytania i odpowiedzi QA1490 https://developer.Apple.com/library/content/qa/qa1490/_index.html ):

Cel C nie definiuje symboli linkerów dla każdej funkcji (lub metody, w Objective-C) - zamiast tego symbole linkerów są generowane tylko dla każdej klasy. Jeśli rozszerzysz istniejącą klasę o kategorie, linker nie wie, aby powiązać kod obiektowy implementacji klasy podstawowej i implementacji kategorii. Zapobiega to reagowaniu obiektów utworzonych w wynikowej aplikacji na selektor zdefiniowany w kategorii.

I ich rozwiązanie:

Aby rozwiązać ten problem, biblioteka statyczna powinna przekazać linkerowi opcję -ObjC. Ta flaga powoduje, że linker ładuje każdy plik obiektowy w bibliotece, która definiuje klasę lub kategorię Objective-C. Chociaż ta opcja zwykle powoduje większy plik wykonywalny (z powodu dodatkowego kodu obiektowego załadowanego do aplikacji), pozwoli na pomyślne utworzenie skutecznych bibliotek statycznych Objective-C, które zawierają kategorie w istniejących klasach.

a także FAQ w iPhone Development FAQ:

Jak połączyć wszystkie klasy Objective-C w bibliotece statycznej? Ustaw ustawienie kompilacji Inne flagi linkera na -ObjC.

i opisy flag:

- all_load Ładuje wszystkich członków statycznych bibliotek archiwalnych.

- ObjC Ładuje wszystkich członków statycznych bibliotek archiwalnych, które implementują klasę lub kategorię Objective-C.

- force_load (ścieżka do archiwum) Ładuje wszystkich członków określonej biblioteki archiwum statycznego. Uwaga: -all_load wymusza załadowanie wszystkich członków wszystkich archiwów. Ta opcja pozwala targetować określone archiwum.

* możemy użyć force_load, aby zmniejszyć rozmiar pliku binarnego aplikacji i uniknąć konfliktów, które w niektórych przypadkach może powodować all_load.

Tak, działa z plikami * .a dodanymi do projektu. Miałem jednak problemy z dodaniem projektu lib jako bezpośredniej zależności. Ale później odkryłem, że to moja wina - projekt zależności bezpośredniej prawdopodobnie nie został poprawnie dodany. Po usunięciu i ponownym dodaniu kroków:

  1. Przeciągnij i upuść plik projektu lib w projekcie aplikacji (lub dodaj go za pomocą Project-> Dodaj do projektu…).
  2. Kliknij strzałkę na ikonie projektu lib - wyświetlona nazwa pliku mylib.a, przeciągnij ten plik mylib.a i upuść go w grupie docelowej -> Połącz plik binarny z biblioteką.
  3. Otwórz informacje o celu na stronie pięści (ogólne) i dodaj moją bibliotekę lib do listy zależności

potem wszystko działa OK. W moim przypadku wystarczyło „-ObjC”.

Byłem również zainteresowany pomysłem z http://iphonedevelopmentexperiences.blogspot.com/2010/03/categories-in-static-library.html blog. Autor twierdzi, że może używać kategorii z lib bez ustawiania flagi -all_load lub -ObjC. Po prostu dodaje do plików kategorii h/m pusty interfejs/implementacja klasy fikcyjnej, aby zmusić linker do użycia tego pliku. I tak, ta sztuczka spełnia swoje zadanie.

Ale autor powiedział również, że nawet nie tworzył obiektu obojętnego. Mm… Jak już odkryłem, powinniśmy jawnie wywołać jakiś „prawdziwy” kod z pliku kategorii. Dlatego należy wywołać przynajmniej funkcję klasy. I nawet nie potrzebujemy klasy obojętnej. Pojedyncza funkcja c robi to samo.

Więc jeśli napiszemy pliki lib jako:

// mylib.h
void useMyLib();

@interface NSObject (Logger)
-(void)logSelf;
@end


// mylib.m
void useMyLib(){
    NSLog(@"do nothing, just for make mylib linked");
}


@implementation NSObject (Logger)
-(void)logSelf{
    NSLog(@"self is:%@", [self description]);
}
@end

a jeśli wywołamy useMyLib (); gdziekolwiek w projekcie aplikacji, a następnie w dowolnej klasie możemy zastosować metodę kategorii logSelf;

[self logSelf];

I więcej blogów na temat:

http://t-machine.org/index.php/2009/10/13/how-to-make-an-iphone-static-library-part-1/

http://blog.costan.us/2009/12/fat-iphone-static-libraries-device-and.html

224
Vladimir

Odpowiedź Vladimira jest w rzeczywistości całkiem dobra, jednak chciałbym tutaj podać nieco więcej wiedzy na temat tła. Może któregoś dnia ktoś znajdzie moją odpowiedź i może okazać się pomocny.

Kompilator przekształca pliki źródłowe (.c, .cc, .cpp, .m) w pliki obiektowe (.o). Istnieje jeden plik obiektowy na plik źródłowy. Pliki obiektowe zawierają symbole, kod i dane. Pliki obiektowe nie są bezpośrednio używane przez system operacyjny.

Teraz podczas budowania biblioteki dynamicznej (.dylib), frameworku, pakowalnego pakietu (.bundle) lub wykonywalnego pliku binarnego, te pliki obiektów są łączone razem przez linker, aby stworzyć coś, co system operacyjny uważa za „użyteczny”, np. coś, co może bezpośrednio załadować na określony adres 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). Tak więc plik .a to nic innego jak archiwum plików obiektowych (.o). Pomyśl o archiwum TAR lub Zip bez kompresji. Po prostu łatwiej jest skopiować pojedynczy plik .a niż całą masę plików .o (podobnie jak Java, gdzie pakujesz pliki .class do archiwum .jar w celu łatwej dystrybucji).

Podczas łączenia pliku binarnego z biblioteką statyczną (= archiwum), linker otrzyma tabelę wszystkich symboli w archiwum i sprawdzi, do których z tych symboli odwołują się pliki binarne. Tylko pliki obiektowe zawierające symbole odniesienia są w rzeczywistości ładowane przez linker i są uwzględniane przez proces łączenia. Na przykład. jeśli twoje archiwum ma 50 plików obiektowych, ale tylko 20 zawiera symbole używane przez plik binarny, tylko te 20 są ładowane przez linker, pozostałe 30 są całkowicie ignorowane w procesie łączenia.

Działa to całkiem dobrze w kodzie C i C++, ponieważ te języki starają się zrobić jak najwięcej w czasie kompilacji (chociaż C++ ma również pewne funkcje tylko w czasie wykonywania). Obj-C jest jednak innym rodzajem języka. Obj-C w dużym stopniu zależy od funkcji środowiska wykonawczego, a wiele funkcji Obj-C jest w rzeczywistości funkcjami tylko środowiska wykonawczego. Klasy Obj-C faktycznie mają symbole porównywalne z funkcjami C lub globalnymi zmiennymi C (przynajmniej w bieżącym środowisku wykonawczym Obj-C). Linker może sprawdzić, czy klasa jest przywoływana, czy nie, więc może określić, czy klasa jest w użyciu, czy nie. Jeśli użyjesz klasy z pliku obiektowego w bibliotece statycznej, ten plik obiektowy zostanie załadowany przez linker, ponieważ linker widzi używany symbol. Kategorie są funkcją tylko środowiska wykonawczego, kategorie nie są symbolami, takimi jak klasy lub funkcje, co oznacza również, że linker nie może ustalić, czy kategoria jest używana, czy nie.

Jeśli linker ł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ż każdy z niego symbol jest uważany za „w użyciu” (czy to klasa, czy funkcja, czy 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, zawarte w nim kategorie nie będą dostępne w czasie wykonywania. Plik obiektowy zawierający tylko kategorie jest nigdy załadowany, ponieważ zawiera bez symboli linker kiedykolwiek uważa "za używany" „. I to jest cały problem tutaj.

Zaproponowano kilka rozwiązań, a skoro już wiesz, jak to wszystko się gra, spójrzmy jeszcze raz na proponowane rozwiązanie:

  1. Jednym z rozwiązań jest dodanie -all_load Do wywołania linkera. Co faktycznie zrobi ta flaga linkera? Właściwie mówi linkerowi, że „ Załaduj wszystkie pliki obiektów ze wszystkich archiwów, niezależnie od tego, czy widzisz jakiś używany symbol, czy nie ”. Oczywiście to działa, ale może również generować dość duże pliki binarne.

  2. Innym rozwiązaniem jest dodanie -force_load Do wywołania linkera, w tym ścieżki do archiwum. Ta flaga działa dokładnie tak jak -all_load, Ale tylko dla określonego archiwum. Oczywiście to też zadziała.

  3. Najpopularniejszym rozwiązaniem jest dodanie -ObjC Do wywołania linkera. Co faktycznie zrobi ta flaga linkera? Ta flaga mówi linkerowi „ Załaduj wszystkie pliki obiektów ze wszystkich archiwów, jeśli zobaczysz, że zawierają one dowolny kod Obj-C ”. A „dowolny kod Obj-C” obejmuje kategorie. To również zadziała i nie wymusi ładowania plików obiektowych nie zawierających kodu Obj-C (są one nadal ładowane tylko na żądanie).

  4. Innym rozwiązaniem jest raczej 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 plik źródłowy) są scalane w jeden plik obiektowy (co nie jest prawdziwym łączeniem, stąd nazwa PreLink ), a ten plik pojedynczego obiektu (czasami nazywany również „głównym plikiem obiektowym”) jest następnie dodawany do archiwum. Jeśli teraz jakiś symbol głównego pliku obiektowego jest rozważany w użyciu, cały główny plik obiektowy jest uważany za używany, a zatem wszystkie jego części Celu C są zawsze ładowane. A ponieważ klasy są normalnymi symbolami, wystarczy użyć jednej klasy z takiej biblioteki statycznej, aby uzyskać wszystkie kategorie.

  5. Ostatecznym 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ć dowolnej z kategorii w środowisku wykonawczym, upewnij się, że w jakiś sposób odwołujesz się do fałszywego symbolu w czasie kompilacji, ponieważ powoduje to, że plik obiektowy jest ładowane przez linker, a tym samym cały kod Obj-C w nim. Na przykład. może to być funkcja z pustym ciałem funkcji (która nic nie robi, gdy zostanie wywołana) lub może być zmienną globalną dostępną (np. globalną int raz przeczytaną lub zapisaną, to wystarczy). W przeciwieństwie do wszystkich innych powyższych rozwiązań, rozwiązanie to przesuwa kontrolę nad tym, które kategorie są dostępne w czasie wykonywania do skompilowanego kodu (jeśli chce, aby były połączone i dostępne, uzyskuje dostęp do symbolu, w przeciwnym razie nie uzyskuje dostępu do symbolu, a linker zignoruje to).

To wszystko ludzie.

Och, czekaj, jest jeszcze jedna rzecz:
Linker ma opcję o nazwie -dead_strip. Co robi ta opcja? Jeśli linker zdecydował się załadować plik obiektowy, wszystkie symbole pliku obiektowego stają się częścią połączonego pliku binarnego, niezależnie od tego, czy są używane, czy nie. Na przykład. plik obiektowy zawiera 100 funkcji, ale tylko jedna z nich jest używana przez plik binarny, wszystkie 100 funkcji są nadal dodawane do pliku binarnego, ponieważ pliki obiektowe są albo dodawane jako całość, albo w ogóle nie są dodawane. Częściowe dodanie pliku obiektowego zwykle nie jest obsługiwane przez konsolidatory.

Jeśli jednak powiesz linkerowi „martwy pasek”, linker najpierw doda wszystkie pliki obiektowe do pliku binarnego, rozwiąże wszystkie odwołania i na koniec przeskanuje plik binarny w poszukiwaniu symboli, które nie są używane (lub tylko w użyciu przez inne symbole, których nie ma w posługiwać się). Wszystkie symbole, które okazały się nieużywane, są następnie usuwane w ramach etapu optymalizacji. W powyższym przykładzie 99 nieużywanych funkcji jest ponownie usuwanych. Jest to bardzo przydatne, jeśli używasz opcji takich jak -load_all, -force_load Lub Perform Single-Object Prelink, Ponieważ te opcje mogą z łatwością gwałtownie wysadzić rozmiary binarne w niektórych przypadkach, a martwe usuwanie spowoduje usunięcie nieużywanego kodu i dane ponownie.

Dead stripping działa bardzo dobrze w kodzie 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 to idealne, w niektórych przypadkach niektóre symbole nie są usuwane, chociaż byłoby dobrze je usunąć, ale w większości przypadków działa całkiem dobrze w tych językach.

Co z Obj-C? Zapomnij o tym! Dla Obj-C nie ma martwego rozbiórki. Ponieważ Obj-C jest językiem funkcji wykonawczej, kompilator nie może powiedzieć w czasie kompilacji, czy symbol jest rzeczywiście w użyciu, czy nie. Na przykład. klasa Obj-C nie jest używana, jeśli nie ma kodu bezpośrednio do niej odwołującego, prawda? Źle! Możesz dynamicznie zbudować ciąg zawierający nazwę klasy, zażądać wskaźnika klasy dla tej nazwy i dynamicznie przydzielić klasę. Na przykład. zamiast

MyCoolClass * mcc = [[MyCoolClass alloc] init];

Też bym napisał

NSString * cname = @"CoolClass";
NSString * cnameFull = [NSString stringWithFormat:@"My%@", cname];
Class mmcClass = NSClassFromString(cnameFull);
id mmc = [[mmcClass alloc] init];

W obu przypadkach mmc jest odwołaniem do obiektu klasy „MyCoolClass”, ale w drugiej próbce kodu występuje brak bezpośredniego odwołania do tej klasy (nawet nazwa klasy jako ciąg statyczny). Wszystko dzieje się tylko w czasie wykonywania. I to pomimo tego, że klasy faktycznie prawdziwe symbole. Jest jeszcze gorzej w przypadku kategorii, ponieważ nie są to nawet prawdziwe symbole.

Więc jeśli masz bibliotekę statyczną z setkami obiektów, ale większość twoich plików binarnych potrzebuje tylko kilku z nich, możesz nie chcieć używać powyższych rozwiązań (1) do (4). W przeciwnym razie otrzymujesz bardzo duże pliki binarne zawierające wszystkie te klasy, nawet jeśli 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 tak jak w drugim przykładzie kodu), linker samodzielnie rozpozna ich użycie. W przypadku kategorii rozważ jednak rozwiązanie (5), ponieważ umożliwia to uwzględnienie tylko tych kategorii, których naprawdę potrzebujesz.

Na przykład. jeśli chcesz kategorii dla NSData, np. dodając do niego metodę kompresji/dekompresji, utworzyłbyś plik nagłówka:

// NSData+Compress.h
@interface NSData (Compression)
    - (NSData *)compressedData;
    - (NSData *)decompressedData;
@end

void import_NSData_Compression ( );

oraz plik implementacyjny

// NSData+Compress
@implementation NSData (Compression)
    - (NSData *)compressedData 
    {
        // ... magic ...
    }

    - (NSData *)decompressedData
    {
        // ... magic ...
    }
@end

void import_NSData_Compression ( ) { }

Teraz upewnij się, że gdziekolwiek w twoim kodzie import_NSData_Compression() jest wywołana. Nie ma znaczenia, jak się nazywa i jak często się nazywa. Właściwie to wcale nie musi być wywoływane, wystarczy, jeśli linker tak uważa. Na przykład. możesz umieścić następujący kod w dowolnym miejscu w swoim projekcie:

__attribute__((used)) static void importCategories ()
{
    import_NSData_Compression();
    // add more import calls here
}

Nie musisz nigdy wywoływać importCategories() w swoim kodzie, atrybut sprawi, że kompilator i linker uwierzą, że jest on wywoływany, nawet jeśli tak nie jest.

I ostatnia wskazówka:
Jeśli dodasz -whyload Do ostatniego wywołania łącza, linker wydrukuje w dzienniku kompilacji, który plik obiektowy, z której biblioteki załadował, z powodu używanego symbolu. Wypisze tylko pierwszy symbol rozważany w użyciu, ale niekoniecznie jest to jedyny symbol w użyciu tego pliku obiektowego.

112
Mecki

Ten problem został naprawiony w LLVM . Poprawka jest dostarczana jako część LLVM 2.9 Pierwsza wersja Xcode zawierająca poprawkę to Xcode 4.2 dostarczany z LLVM 3.0. Wykorzystanie -all_load lub -force_load nie jest już potrzebny podczas pracy z XCode 4.2 -ObjC jest nadal potrzebny.

24
tonklon

Oto, co musisz zrobić, aby całkowicie rozwiązać ten problem podczas kompilowania biblioteki statycznej:

Przejdź do Ustawień kompilacji Xcode i ustaw opcję Wykonaj wstępne połączenie z jednym obiektem na TAK lub GENERATE_MASTER_OBJECT_FILE = YES w pliku konfiguracji kompilacji.

Domyślnie linker generuje plik .o dla każdego pliku .m. Tak więc kategorie stają się inne. O. Gdy linker patrzy na pliki .o biblioteki statycznej, nie tworzy indeksu wszystkich symboli na klasę (Runtime zrobi to, nieważne co).

Ta dyrektywa poprosi linkera o spakowanie wszystkich obiektów w jeden duży plik .o, a tym samym zmusi linkera przetwarzającego bibliotekę statyczną do uzyskania indeksu wszystkich kategorii klas.

Mam nadzieję, że to wyjaśnia.

16
amosel

Jednym z czynników, który jest rzadko wymieniany, gdy pojawia się dyskusja na temat statycznego łączenia bibliotek, jest fakt, że Ty muszą również zawierać same kategorie w fazach kompilacji-> kopiuj pliki i kompiluj źródła samej biblioteki statycznej.

Apple również nie podkreśla tego faktu w niedawno opublikowanym Korzystanie z bibliotek statycznych w iOS albo.

Spędziłem cały dzień wypróbowując różnego rodzaju warianty -objC i -all_load itp., Ale nic z tego nie wyszło .. this pytanie zwróciło moją uwagę. (nie zrozum mnie źle ... wciąż musisz robić rzeczy -objC .. ale to coś więcej niż tylko to).

inną czynnością, która zawsze mi pomogła, jest to, że zawsze najpierw buduję dołączoną bibliotekę statyczną ... a następnie tworzę zamykającą aplikację.

9
abbood