Die Objective C Runtime

12. Mai 2018

Thema: Objc Runtime Basics, Metaklassen, Properties & iVars, method swizzling

„Dies ist ein Sparring-Programm. Ähnlich wie die programmierte Realität der Matrix. Es folgt den gleichen Gesetzen. Der Schwerkraft zum Beispiel. Im Grunde unterscheiden sich diese Gesetze nicht von denen eines Computerprogramms. Einige kann man umgehen, andere kann man brechen.“
-- Morpheus im Film Matrix (1999)
(Drehbuch, Wikipedia)

Es ist schwer zu erklären, was die Objective C Runtime ist. Man muss es selbst erfahren.

Normalerweise ist die Runtime für den Entwickler unsichtbar.

Das 3. Clarkesche Gesetz besagt, dass jede hinreichend fortschrittliche Technologie nicht von Magie unterscheidbar ist. Man kann jahrelang als iOS Developer arbeiten, ohne sich explizit mit der Objective C Runtime beschäftigen zu müssen. Dass man eine Technologie anwenden kann, ohne deren genaue Funktionsweise zu verstehen, ist ein Zeichen dafür, dass diese Technologie sehr stabil läuft und ein intuitives Frontend hat. Doch wie auch schon Joel Spolsky angemerkt hat, kommt man immer irgendwann an einen Punkt, an dem ein tieferes Verständnis notwendig ist, um weiter zu kommen. Z.B. wenn man einen Bug in einem Framework findet, zu dem man den Sourcecode nicht hat; oder wenn man eine Anforderung umsetzen muss, die es erfordert, eine undokumentierte Methode aufzurufen, und man weiß nicht einmal den Namen der Methode. Ein iOS Senior Developer weiß sich in solchen Fällen zu helfen, und kann auf ein Repertoire an Tricks zugreifen, um auch unmöglich erscheinende Aufgaben souverän zu lösen. Darum geht es in diesem Blog-Artikel. Er richtet sich an iOS Developer, die bereits einiges an Erfahrung im Bereich App Development haben, und die ein tieferes Verständnis für die Funktionsweise der iOS Plattform erlangen wollen. Ein Beispiel für die praktische Anwendung dieses Wissens folgt dann in einem späteren Blog-Artikel.

Wenn man Objective C lernt, stellt man sich vor, dass der Compiler die eigenen Anweisungen in Maschinencode übersetzt. Tatsächlich jedoch übersetzt der Compiler die eigenen Anweisungen in Maschinencode, der mit der Objective C Runtime kommuniziert. Jeder Methodenaufruf wird zu einem Aufruf der C-Funktion objc_msgSend übersetzt (oder objc_msgSend_stret, objc_msgSendSuper, ...). Alle Objective C-spezifischen Vorgänge kann man auch mit Runtime-Funktionen realisieren.

Man kann eine Klasse entweder - wie üblich - zur Compile-Time anlegen, oder aber auch zur Laufzeit mit objc_allocateClassPair erzeugen. (Gewöhnlich erstellte Klassen rufen nicht im Hintergrund objc_allocateClassPair auf, aber vom Resultat her ist kein Unterschied zwischen einer vom Compiler erzeugten Klasse und einer Klasse, die zur Laufzeit erzeugt wurde.)

Man kann Methoden normal compilieren lassen, oder man kann sie zur Laufzeit mit class_addMethod anlegen.

Durch diese Dynamizität ergeben sich viele Möglichkeiten:

  • Apple verwendet sie z.B. für Meta-Programming: die Proxy-Objekte für RPC-Schnittstellen werden zur Laufzeit generiert.
  • Key Value Coding verwendet die “Reflection API”, also den Teil der Objective C Runtime, der dafür zuständig ist, zur Laufzeit Informationen über den Aufbau des Programms zur Verfügung zu stellen, die normalerweise nur zur Compile Time zur Verfügung stehen.
  • Das NSZombie Entwicklertool verändert dynamisch die Klassenzugehörigkeit von Objekten, anstatt sie zu deallozieren.

Auch wir können die Objective C Runtime auf kreative Weise benutzen. Wir können

  • undokumentierte APIs finden
  • Klassen zur Laufzeit patchen (z.B. mit method swizzling)
  • Die Eigenschaften von Objekten zur Laufzeit untersuchen
  • undokumentierte APIs und private Funktionen verwenden (z.B. mit Key Value Coding)

In diesem kurzen Tutorial möchte ich (passend zum Matrix-Motto) den Teil der Objective C Runtime betrachten, der zum Hacken relevant ist. Wir wollen mehr über iOS erfahren, undokumentierte APIs erkunden und nutzen, und besser verstehen, wie Objective C intern funktioniert. Daher werde ich einige Themen nicht behandeln:

  • Wie man in Objective C programmiert. Das ist kein Anfänger-Tutorial.
  • Automatic Reference Counting. ARC ist ausführlich hier und hier beschrieben. ARC ist auch sehr relevant, wenn man lernen will, komplexe Programm-Abstürze zu debuggen. Ich war in der Vergangenheit oft in Situationen, wo ich einen Absturz beseitigen sollte, und wo ich das Problem vor allem durch ein genaues Wissen über die Interna von ARC beseitigen konnte. Natürlich hat nicht jeder Absturz etwas mit ARC zu tun, aber ARC ist ein Teil des Puzzles. (Und nein, mit “manual reference counting” wars nicht besser, sondern noch viel schlimmer. ARC funktioniert perfekt, Abstürze haben immer einen Entwicklerfehler als Ursache, und damit meine ich nicht die ARC-Entwickler.)
  • Weak References. Ein interessantes und komplexes Thema, aber fürs Hacken nicht wirklich relevant.
  • Die rechtliche Seite. Alles was ich hier beschreibe ist legal. D.h. aber nicht, dass es legal ist, jede Information, die man auf diese Weise gewinnt, zu veröffentlichen. Das gilt insbesondere dann, wenn man mit Beta- oder Entwickler-Software arbeitet und einem NDA zugestimmt hat. Legal heißt auch nicht, dass Apple einen in den Appstore lassen muss. Die rechtliche Situation ist komplex, und würde leicht einen ganzen Blog-Artikel füllen. Fürchten braucht man sich aber nicht. Die Datenschutzgrundverordnung der EU ist rechtlich gesehen gefährlicher und für die App-Entwicklung auch sehr relevant.

Einleitung

Reflection („lesen“)

Die Sprache Objective C bietet eine umfangreiche Reflection API an. Man kann zur Laufzeit eine Liste aller Klassen abfragen. Zu jeder Klasse kann man alle Instanz-Methoden und alle Klassen-Methoden herausfinden. Konzepte wie “public” und “private” gibt es für die Objective C Runtime nicht. Alles ist jederzeit verfügbar. Auch die Methodensignaturen lassen sich auslesen, und ebenfalls die Namen und Datentypen von Instanz-Variablen.

Monkey Patching („schreiben“)

Mit Hilfe von Runtime API Funktionen lässt sich zur Laufzeit nicht nur alles lesen, sondern auch anpassen. Ein Beispiel: Methodenaufrufe verwenden immer “dynamic dispatch”. Man kann jederzeit neue Methoden zu einer Klasse hinzufügen, oder die Implementierung einer Methode austauschen. Es lassen sich sogar zur Laufzeit neue Klassen anlegen. Alle diese Funktionen werden in der Praxis in Produktivcode verwendet.

Nutzen der Runtime Funktionen

Die Runtime-Funktionen wurden nicht erschaffen, um es Hackern leichter zu machen, iPhones zu hacken. Dafür sind sie nicht wirklich geeignet. Sie werden jedoch von einigen System-Funktionen genutzt, z.B.:

  • Interface Builder: Key Value Coding, Target/Action, Binding
  • Key Value Observing: Isa-Swizzling, Neue Klassen anlegen
  • Delegate handling: Weak references
Ein Verständnis dafür, wofür die einzelnen Runtime-Funktionen produktiv eingesetzt werden, hilft auch sehr dabei, wenn man diese Funktionen dann für „alternative Zwecke“ benutzen will, also fürs Hacken.

Hacken

Unter „hacken“ verstehe ich Aktivitäten, die dazu führen, dass man die Interna eines Systems besser versteht; insbesondere dann, wenn keine Dokumentation verfügbar ist; sowie die Benutzung von APIs, die nicht genutzt werden wollen. „Hacken“ hat in den meisten Fällen nichts mit „Security“ oder dem Einbrechen in fremde Systeme zu tun. Runtime-Funktionen lassen sich nutzen, um an Informationen zu undokumentierten Teilen eines Systems zu gelangen. Und “method swizzling” wird gerne verwendet, um das System auf Arten zu beeinflussen, die nicht standardmäßig vorgesehen sind.

Swift

Apple betont gerne, dass Swift eine eigene Sprache ist, unabhängig von Objective C, und dass sie nicht auf Objective C aufbaut. Das ist zwar theoretisch richtig, aber praktisch nicht immer eine hilfreiche Vorstellung, weil es naheliegt, dass einem ein Verständnis für die Objective C Runtime wenig bringt, wenn man ohnehin die meiste Zeit nur Swift verwendet. Es ist besser, wenn man sich vorstellt, dass Swift auf Objective C aufbaut, und eine dünne Schicht ist, die eine neue Syntax über die Objective C Runtime legt. Die meisten Apple-Klassen sind immer noch in Objective C geschrieben, z.B. auch der UIViewController. Wenn eine Swift-Klasse von UIViewController (oder jeder anderen Objective C Klasse) ableitet, dann ist sie aus Kompatibilitätsgründen wieder eine Objective C Klasse. Wenn sie z.B. -viewWillAppear: ableitet, dann wird jeder viewWillAppear(_:) Aufruf über objc_msgSend per dynamic dispatch aufgerufen. Tatsächlich sind auf allen Apple Plattformen Swift und Objective C sehr eng miteinander verwoben. Die Objective C Runtime Library enthält Code, der sich mit Swift-Interoperabilität beschäftigt, und vice versa. Außerhalb des Apple-Universums mag das anders sein. Aber wer verwendet Swift außerhalb des Apple-Universums?

Hauptteil

Genug philosophiert! Jetzt wird es technisch. Ich beschreibe die wichtigsten Funktionen und Konzepte. Um die hier beschriebenen Ideen praktisch umzusetzen, ist es aber auf jeden Fall weiterhin notwendig, die offizielle Dokumentation zu konsultieren.

=> Objective C Runtime Programming Guide
=> Objective C Runtime Reference

objc_msgSend

Diese C-Funktion ist das Herz der Objective C Runtime Library. Die Funktion ist potentiell sehr komplex. Da sie aber sehr umfangreich optimiert wurde ist sie trotzdem effizient. Viele Funktionen der Runtime gibt es nur aus dem einzigen Grund, dass dadurch objc_msgSend effizienter arbeiten kann (z.B. den objc method cache; die customRR-flags um objc_msgSend in vielen Fällen komplett zu umgehen; tagged pointers).

Jeder objc-Methodenaufruf wird vom Compiler in einen Aufruf der C-Funktion objc_msgSend() umgewandelt. D.h. aus

[value performAction:@"foo"
       withCreditCard:cc];

wird

objc_msgSend(value,
             @selector(performAction:withCreditCard:),
             @"foo",
             cc);

objc_msgSend ist auf allen Plattformen in Assembler geschrieben, und zwar nicht nur aus Performance-Gründen, sondern auch, weil es nicht anders geht. Die Funktion durchsucht die Klasse von value nach einer Methode mit dem Namen performAction:withCreditCard: und springt dann dort hin. Durch den Sprung erhält die Methode dann genau die selben Parameter wie objc_msgSend, und der Aufruf von objc_msgSend() ist im Stacktrace nicht mehr sichtbar. Deshalb hat jede Methode zwei implizite Parameter: self (id) und _cmd (SEL). Da die Parameter bei der C Calling Convention immer in umgekehrter Reihenfolge auf den Stack gepusht werden, ist es möglich, eine variable Anzahl von Argumenten zu unterstützen, ohne dass objc_msgSend wissen muss, wie viele Parameter ihm übergeben wurden.

Wenn objc_msgSend die Methode nicht in der Klasse findet, dann durchsucht es rekursiv auch die Superklassen. Die Methodenliste kann nicht nur von objc_msgSend ausgelesen werden, sondern von jedem der es versteht, die objc-runtime-Funktionen zu benutzen. Es ist sogar möglich, die Methodenliste zur Laufzeit zu verändern, also Methoden hinzuzufügen, zu löschen, oder zu vertauschen. Deshalb ist monkey patching in Objective-C so leicht möglich.

Die Entwickler von Objective C haben diese Dynamizität gemacht, weil sie viele Basistechnologien erst ermöglicht. Dazu gehören z.B. NSProxy forwarding, Key Value Coding, Key Value Observing oder Zombie Objects.

Hinweis: Tatsächlich wird nicht jeder Methodenaufruf in einen Aufruf von objc_msgSend umgewandelt. Tatsächlich gibt es einen ganzen Haufen von Funktionen, die etwas ähnliches tun: objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, um nur die 3 wichtigsten zu nennen. Es handelt sich hier um Spezialfälle von Methodenaufrufe, z.B. die _stret Aufrufe werden verwendet, wenn der Rückgabewert eine Struktur (z.B. CGRect) ist (stret steht für “structure return”.)

Aufbau einer Objective C Klasse

Im Gegensatz zu C++ sind bei Objective C Informationen zu jeder Klasse zur Laufzeit verfügbar. Jedes Objekt enthält einen isa-Pointer, der den Link zur Klasse darstellt. Man kann sich die Situation so vorstellen:

// Achtung Pseudocode.. kompiliert nicht

class Class {
    Class isa; // link to meta class
    Class superclass;
    String name;
    MutableDictionary<SEL, Method> methodList;
    MutableDictionary<String, PropertyDescription> propertyList;
}

class NSObject {
    Class isa; // link to class object
    int retainCount;
}

Alles, was eine Nachricht empfangen kann, muss mit einem isa-Pointer beginnen. objc_msgSend erwartet als ersten Parameter ein Objekt, das als erstes Feld einen solchen (isa-Pointer-)Link auf eine Klasse hat. Diese Klasse bestimmt dann, wie objc_msgSend die Methode aufrufen soll, weil objc_msgSend die Methodenliste der Klasse durchsucht. Für objc_msgSend macht es daher keinen Unterschied, ob der erste Parameter ein Objekt oder eine Klasse ist. Eine Klasse ist auch nur ein Objekt vom Typ „Klasse“. Genauer gesagt ist eine Klasse ein Objekt vom Typ „Metaklasse“.

Während ich diesen Blog-Beitrag schrieb, wurde mir klar, dass mein Wissen über Metaklassen ziemlich bruchstückhaft war. Ich wusste genug um auch Klassenmethoden verswizzeln zu können. Aber es war mehr eine Anwendung eines Kochrezeptes, ohne tieferes Verständnis. Um meine Wissenslücke zu füllen, habe ich ein bisschen experimentiert, und mit Hilfe des Debuggers die Klassenhierarchie untersucht. Am Ende stellte sich heraus, dass Meta-Klassen nur so funktionieren können.

Wie hängen also Klassen und Metaklassen zusammen? Ein Bild sagt mehr als tausend Worte:
 

Den violetten „isa“-Pfeilen folgt man mit object_getClass(), also z.B. object_getClass(viewController) == [UIViewController class], oder object_getClass([UIResponder class]) == die UIResponder Metaklasse.

Den schwarzen „super“-Pfeilen folgt man mit class_getSuperclass(), also z.B. class_getSuperclass([UIViewController class]) == [UIViewController superclass] == [UIResponder class].

Der isa-Pointer eines Objektes zeigt auf die Klasse zu dem dieses Objekt gehört. Der isa-Pointer einer Klasse zeigt auf die Metaklasse dieser Klasse. Auch Metaklassen haben Metaklassen, wobei die NSObject-Metaklasse ihre eigene Metaklasse ist.

Was ist eine Metaklasse?

Eine normale Klasse enthält als Methodenliste eine Liste von Instanzmethoden. Eine Metaklasse enthält als Methodenliste eine Liste von Klassenmethoden. Aus Sicht des objc messaging Systems, deren primäre Implementierung in objc_msgSend liegt, ist kein Unterschied, ob man eine Nachricht an ein Objekt oder an eine Klasse schickt. Wenn man eine Nachricht an ein Objekt schickt (z.B. [viewController viewWillAppear:]), dann sieht objc_msgSend in der Methodenliste der UIViewController-Klasse nach (weil viewController->isa ist die UIViewController-Klasse). Wenn man eine Nachricht an eine Klasse schickt (z.B. [UIViewController new]), dann sieht objc_msgSend in der Methodenliste der UIViewController-Metaklasse nach (weil UIViewController->isa ist die UIViewController-Metaklasse). In beiden Fällen wird die Suche rekursiv in der Superklasse fortgesetzt, solange bis die Methode gefunden wurde oder bis man bei einer Root-Klasse angelangt ist, also bei einer Klasse die keine Superklasse mehr hat.

Ohne die Runtime-Funktionen kommt man normalerweise nicht an die Metaklassen-Objekte. Das sieht man am besten, indem man sich den NSObject Sourcecode ansieht:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

D.h. wenn wir die Instanzmethoden einer Klasse c wissen wollen, so bekommen wir diese mit class_copyMethodList(c, &count). Wenn wir die Klassenmethoden einer Klasse c wissen wollen brauchen wir hingegen class_copyMethodList(object_getClass(c), &count).

Properties und iVars

Viele iOS-Entwickler, insbesondere diejenigen die erst nach iOS 6 eingestiegen sind, kennen den Unterschied zwischen Properties und Instanz-Variablen nicht so richtig. Für viele sind Properties einfach die flexiblere Form einer Instanz-Variable. Auf eine Instanz-Variable kann man z.B. nicht einfach so key-value-observing machen, auf eine Property schon. Ich habe auch schon Entwickler gesehen, die Properties gemacht haben, und dann Key Value Observing benutzt haben, um darauf reagieren zu können, wenn sich die Property ändert; wohl gemerkt von innerhalb der Klasse; also ein Objekt das sich selbst beobachtet; ich würde das dann “self introspection” nennen. Das ist nicht im Sinne der Erfinder.

Eine Instanz-Variable (oder „iVar“) ist genau das, eine Variable, die einen festen Platz in jedem Objekt eines bestimmten Typs hat.

Eine gewöhnliche Read-Write-Property besteht aus 4 Elementen: einer iVar, einem Getter, einem Setter, und einer speziellen Property-Syntax. Nehmen wir z.B. die Property foregroundColor der Klasse MyWidget. Sie besteht aus:

  1. Der InstanzVariable UIColor *_foregroundColor
  2. Dem Getter - (UIColor *)foregroundColor { return _foregroundColor; }
  3. Dem Setter - (void)setForegroundColor:(UIColor *)c { _foregroundColor = c; }
  4. Der speziellen Property-Syntax:
    widget.foregroundColor wird übersetzt zu [widget foregroundColor].
    widget.foregroundColor = [UIColor orangeColor] wird übersetzt zu [widget setForegroundColor:[UIColor orangeColor]];
Bei einer Property geschieht also im Hintergrund viel mehr als bei einer iVar. Das hat natürlich auch Performance-Implikationen. iVars sind immer schneller.

Was heißt das für uns? Wir brauchen die Property-Liste einer Klasse nicht unbedingt, um sie zu analysieren. Diese Liste ist zwar verfügbar (class_copyPropertyList), aber wir wissen jetzt, dass es für jede Property sowieso Getter und Setter gibt (für readonly Properties nur Getter).

Zurück zum Beispiel von vorhin. Ich habe geschrieben, dass es keine gute Idee ist, wenn ein Objekt sich selbst „beobachtet“. Um z.B. zu erkennen, wenn jemand die Vordergrundfarbe geändert hat, würde man einfach eine Zeile zum Setter hinzufügen, also in etwa so:

- (void)setForegroundColor:(UIColor *)c {
    _foregroundColor = c;
    [self setNeedsDisplay];
}

Das ist wieder um einiges weniger aufwendig als Key Value Observing. Es geht hier nicht nur um Performance. Ein Programm das weniger komplex ist, ist auch weniger fehleranfällig. Und falls doch ein Fehler auftritt, ist dieser schneller zu finden. Bei einer Property gibt es einfach mehr Fehlerquellen: man weiß beim Lesen nie, ob hinter der Property nur eine iVar steckt oder echter Code ausgeführt wird. Man weiß auch nicht sicher, ob irgend jemand Key Value Observing auf die Property macht. Und selbst wenn man für eine Klasse den Überblick hat, weiß man nicht, was eventuelle Subklassen anstellen.

Monkey patching

Beim monkey patching wird nicht das Binary manipuliert, sondern das Programm wird zur Laufzeit im Speicher angepasst. Im Fall von Objective C wird kein Binärcode angegriffen. Assembler-Kenntnisse sind also definitiv nicht nötig. Da jeder Methodenaufruf im wesentlichen eine Abfrage in einer read/write dispatch table ist, bedeutet monkey patching hier einfach, dass man Methoden vertauscht.

Ich möchte das kurz an einem Beispiel illustrieren:

Wenn man einen Aufruf macht wie z.B. [obj slideAwayAnimated:YES]; dann kann man sich das so vorstellen:

obj->isa.methodList["sildeAnimated:"](obj, "slideAnimated:", YES);

Wenn ich die slideAnimated:-Methode so patchen will, dass sie nie animiert, also dass sie sich immer so verhält als würde sie mit NO als Argument aufgerufen werden, dann erzeuge ich einfach eine 2. Methode, die sich so verhält wie gewünscht:

obj->isa.methodList["slideAnimated_hacked:"] = function(self, _cmd, animated) {
    // call original method, but change 'animated' arg to NO
    [self slideAnimated_hacked:NO];
}

Und ich vertausche die beiden Methoden, so dass jedes Mal wenn ich die Methode slideAnimated: aufrufe, statt dessen slideAnimated_hacked: aufgerufen wird:

SWAP(obj->isa.methodList["slideAnimated"], 
     obj->isa.methodList["slideAnimated_hacked:"])

D.h. die Original-Methode heißt jetzt slideAnimated_hacked:. Diesen Vorgang nennt man in Objective C...

Method Swizzling

Method swizzling kann sehr einfach sein. Die objc runtime stellt dafür eine eigene Funktion zur Verfügung: method_exchangeImplementations()

+ (void)swizzle {
    Method originalMethod = class_getInstanceMethod(
        [MyAnimatableObject class], @selector(slideAnimated:));
    Method swizzledMethod = class_getInstanceMethod(
        [MyAnimatableObject class], @selector(slideAnimated_hacked:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

Dadurch werden zwei Methoden vertauscht. Genauer gesagt, tauschen die Methoden den Namen (den Selector) unter dem sie erreichbar sind. Das führt dann zu der paradoxen Situation, dass es so aussieht, als ob wir eine unendliche Rekursion hätten, was aber natürlich nicht der Fall ist. Denn aus

- (void)slideAnimated:(BOOL)animated {
    // ... 100 lines of implementation ommitted ...
}

- (void)slideAnimated_hacked:(BOOL)animated {
    [self slideAnimated_hacked:NO];
}

wird nach dem swizzeln folgendes Programm:

- (void)slideAnimated_hacked:(BOOL)animated {
    // ... 100 lines of implementation ommitted ...
}

- (void)slideAnimated:(BOOL)animated {
    [self slideAnimated_hacked:NO];
}

Das sieht nicht nur einfach aus, es ist auch einfach. Allerdings gibt es auch Risiken und Nebenwirkungen. Daher will ich auch die Packungsbeilage nicht verschweigen:

  1. Die Methodenliste einer Klasse kennt keine Vererbung. Wenn ich z.B. eine Klasse MyViewController habe, die von UIViewController erbt, und diese Klasse implementiert die -viewWillAppear: Methode nicht, dann kann ich -[MyViewController viewWillAppear:] auch nicht verswizzeln. Der richtige Weg besteht dann darin, einfach -[MyViewController viewWillAppear:] zu implementieren, und super aufzurufen. Das ist normale Vererbung. Swizzeln darf man nur in Fällen, wo normale Vererbung nicht ausreicht.
  2. Swizzeln ist eine fortgeschrittene Vorgehensweise. Wenn man hier Fehler macht, dann kann das zu schwer debugbaren Fehlern führen. Man muss stets im Einzelfall entscheiden, ob es eine gute Idee ist. Es gelten die üblichen Vorsichtsmaßnahmen für den professionellen Umgang mit Hacks (mehr dazu in einem zukünftigen Blog-Beitrag.)
  3. Für weitere Infos zum Thema verweise ich auf diesen Artikel von Matt Thompson.

Isa Swizzling

Beim Isa Swizzling geht man noch einen Schritt weiter. Anstatt die Methodenliste einer Klasse zu manipulieren, erzeugt man gleich eine neue Subklasse, überladet die Methoden die man ändern will, und schreibt dann gezielt den isa-Pointer des Objektes um, dessen Verhalten man ändern möchte.

Beim Method Swizzling ändert man das Verhalten aller Objekte einer Klasse, sowie aller Objekte von Subklassen dieser Klasse. Beim Isa Swizzling verändert man gezielt nur die Objekte die man ändern will. Die Technik wird vom Foundation.framework verwendet um Key Value Observing zu ermöglichen.

Key Value Coding

KVC verwendet die Dynamizität von Objective C. Es wäre ohne diese Dynamizität nicht möglich. KVC ermöglicht es, auf Properties lesend oder schreibend zuzugreifen, und zwar nur über den Namen. Wenn ich z.B. aus irgendeinem Grund weiß, dass das UIApplication Objekt eine Property namens statusBarWindow hat, kann ich mit KVC darauf zugreifen. Beachte, dass [UIApplication sharedApplication].statusBarWindow nicht funktioniert, da statusBarWindow im Header nicht definiert ist. Man könnte zwar eine Category dafür anlegen. KVC ist aber unter Umständen einfacher zu handhaben:

UIApplication *app = [UIApplication sharedApplication];
id statusBarServer = [app valueForKey:@"statusBarWindow"];

KVC verwendet genau die Reflection API von Objective C und sucht Properties, Methoden und Instanzvariablen mit dem angegebenen Namen. Für eine genaue Beschreibung siehe die Doku von Apple.

Schluss

Ursprünglich wollte ich einen Blog-Beitrag über ein ganz anderes Thema schreiben. Es sollte darum gehen, wie man die Status Bar beeinflussen kann oder wie man Informationen aus der Status Bar ausliest (z.B. den Namen des Telefon-Providers.) Beim Schreiben bemerkte ich, dass ich dabei viele Informationen voraussetzen muss, die vermutlich ein Großteil meiner Leser nicht haben wird. Wenn ich dazu komme, schreibe ich noch einen Beitrag über die Status Bar, in dem ich die hier beschriebenen Konzepte dann praktisch anwenden werde. (Sobald ich den Beitrag geschrieben habe, kommt hier ein Link her!)

Wissen über die Objective C Runtime ist meiner Meinung nach das, was einen Senior Developer von einem Junior Developer unterscheidet, zumindest in Bezug auf iOS Entwicklung. Natürlich gibt es aber auch Dinge, die wichtiger sind. Z.B. ein Verständnis dafür, wie unter iOS Design Patterns angewendet werden (MVC, Delegation). Oder ganz ganz wichtig: Dokumentation!! Ohne Dokumentation sinkt der Wert eines Programmes dramatisch. (Darüber könnte man auch wieder einen ganzen Blog-Beitrag schreiben.) Dokumentation schreiben ist ein Skill, den man üben muss, und die Fähigkeiten die man dabei erwirbt, sind auch in anderen Bereichen sehr nützlich.

Und was habe ich gelernt? Dass die Objective C Runtime um einiges komplexer ist als mir klar war. Die Objective C Runtime API benötigt man zwar nur selten, aber für manche Anwendungen ist sie unerlässlich. Ich habe sie in der Vergangenheit immer wieder gebraucht: um Apple-Frameworks so zu patchen, dass sie stabil laufen; um Informationen aus der Status Bar auszulesen; oder für ein Hotreloading-System für Bild-Dateien unter iOS.

Das Titelbild ist eine Matrix, in blau statt in grün, im Hintergrund sieht man ein iPhone 7.
Download großes Bild, Download flaches Bild