Warum verursachen Null- / NULL-Blöcke beim Ausführen Busfehler?

73

Ich begann viel mit Blöcken zu arbeiten und bemerkte bald, dass keine Blöcke Busfehler verursachen:

typedef void (^SimpleBlock)(void);
SimpleBlock aBlock = nil;
aBlock(); // bus error

Dies scheint gegen das übliche Verhalten von Objective-C zu verstoßen, bei dem Nachrichten an keine Objekte ignoriert werden:

NSArray *foo = nil;
NSLog(@"%i", [foo count]); // runs fine

Daher muss ich auf die übliche Nullprüfung zurückgreifen, bevor ich einen Block verwende:

if (aBlock != nil)
    aBlock();

Oder verwenden Sie Dummy-Blöcke:

aBlock = ^{};
aBlock(); // runs fine

Gibt es eine andere Option? Gibt es einen Grund, warum Nullblöcke nicht einfach ein NOP sein können?

Zoul
quelle

Antworten:

144

Ich möchte dies etwas näher erläutern und eine vollständigere Antwort geben. Betrachten wir zunächst diesen Code:

#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {    
    void (^block)() = nil;
    block();
}

Wenn Sie dies ausführen, sehen Sie einen Absturz in der block()Zeile, der ungefähr so ​​aussieht (wenn Sie auf einer 32-Bit-Architektur ausgeführt werden - das ist wichtig):

EXC_BAD_ACCESS (Code = 2, Adresse = 0xc)

Warum ist das so? Nun, das 0xcist das Wichtigste. Der Absturz bedeutet, dass der Prozessor versucht hat, die Informationen an der Speicheradresse zu lesen 0xc. Dies ist fast definitiv eine völlig falsche Sache. Es ist unwahrscheinlich, dass dort etwas ist. Aber warum hat es versucht, diesen Speicherort zu lesen? Nun, es liegt an der Art und Weise, wie ein Block tatsächlich unter der Haube aufgebaut ist.

Wenn ein Block definiert ist, erstellt der Compiler tatsächlich eine Struktur auf dem Stapel in der folgenden Form:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

Der Block ist dann ein Zeiger auf diese Struktur. Das vierte Mitglied invokedieser Struktur ist das interessante. Es ist ein Funktionszeiger, der auf den Code zeigt, in dem die Implementierung des Blocks gespeichert ist. Der Prozessor versucht also, zu diesem Code zu springen, wenn ein Block aufgerufen wird. Beachten Sie, dass, wenn Sie die Anzahl der Bytes in der Struktur vor dem invokeElement zählen, Sie feststellen, dass 12 in Dezimalzahl oder C in Hexadezimalzahl vorhanden sind.

Wenn also ein Block aufgerufen wird, nimmt der Prozessor die Adresse des Blocks, addiert 12 und versucht, den an dieser Speicheradresse gespeicherten Wert zu laden. Es wird dann versucht, zu dieser Adresse zu springen. Wenn der Block jedoch Null ist, wird versucht, die Adresse zu lesen 0xc. Dies ist eindeutig eine Duff-Adresse, und so erhalten wir den Segmentierungsfehler.

Der Grund, warum es ein Absturz wie dieser sein muss, anstatt stillschweigend zu scheitern, wie es ein Objective-C-Nachrichtenaufruf tut, ist wirklich eine Entwurfsentscheidung. Da der Compiler entscheidet, wie der Block aufgerufen werden soll, muss er überall dort, wo ein Block aufgerufen wird, keinen Prüfcode einfügen. Dies würde die Codegröße erhöhen und zu einer schlechten Leistung führen. Eine andere Möglichkeit wäre die Verwendung eines Trampolins, das die Nullprüfung durchführt. Dies würde jedoch auch zu Leistungseinbußen führen. Objective-C-Nachrichten durchlaufen bereits ein Trampolin, da sie die Methode aufrufen müssen, die tatsächlich aufgerufen wird. Die Laufzeit ermöglicht das verzögerte Einfügen von Methoden und das Ändern von Methodenimplementierungen, sodass sie ohnehin schon ein Trampolin durchläuft. Die zusätzliche Strafe für die Nullprüfung ist in diesem Fall nicht signifikant.

Ich hoffe, das hilft ein wenig, die Gründe zu erklären.

Weitere Informationen finden Sie in meinen Blog- Beiträgen .

mattjgalloway
quelle
39

Matt Galloways Antwort ist perfekt! Tolle Lektüre!

Ich möchte nur hinzufügen, dass es einige Möglichkeiten gibt, das Leben leichter zu machen. Sie können ein Makro wie folgt definieren:

#define BLOCK_SAFE_RUN(block, ...) block ? block(__VA_ARGS__) : nil

Es kann 0 - n Argumente annehmen. Anwendungsbeispiel

typedef void (^SimpleBlock)(void);
SimpleBlock simpleNilBlock = nil;
SimpleBlock simpleLogBlock = ^{ NSLog(@"working"); };
BLOCK_SAFE_RUN(simpleNilBlock);
BLOCK_SAFE_RUN(simpleLogBlock);

typedef void (^BlockWithArguments)(BOOL arg1, NSString *arg2);
BlockWithArguments argumentsNilBlock = nil;
BlockWithArguments argumentsLogBlock = ^(BOOL arg1, NSString *arg2) { NSLog(@"%@", arg2); };
BLOCK_SAFE_RUN(argumentsNilBlock, YES, @"ok");
BLOCK_SAFE_RUN(argumentsLogBlock, YES, @"ok");

Wenn Sie den Rückgabewert des Blocks erhalten möchten und nicht sicher sind, ob der Block vorhanden ist oder nicht, sollten Sie wahrscheinlich nur Folgendes eingeben:

block ? block() : nil;

Auf diese Weise können Sie den Fallback-Wert einfach definieren. In meinem Beispiel 'nil'.

hfossli
quelle
1
VA_ARGS macht Probleme in .mm-Dateien
BergP
9

Vorsichtsmaßnahme: Ich bin kein Experte für Blöcke.

Blöcke sind Objective-C-Objekte, aber das Aufrufen eines Blocks ist keine Nachricht , obwohl Sie immer noch versuchen könnten, [block retain]einen nilBlock oder andere Nachrichten zu erstellen.

Hoffentlich hilft das (und die Links).

Stephen Furlani
quelle
Vielen Dank, interessante Links. Ich weiß, dass das Aufrufen eines Blocks nicht dasselbe ist wie das Senden einer Nachricht, aber konzeptionell wäre es schön, wenn keine Blöcke so verzeihend wären wie keine Objekte.
Zoul
Möglicherweise können Sie dem __blockTyp eine Kategorie hinzufügen ... aber ich bin mir nicht sicher. #define nilBlock ^{}kann auch Ihr Leben leichter machen.
Stephen Furlani
Ich habe über den nilBlockAnsatz nachgedacht , leider stört die Eingabe - das Erstellen eines separaten Nullwerts für jeden Blocktyp macht nicht viel Spaß.
Zoul
Ich habe keine Ahnung, ob Sie Blöcke unterordnen können, aber das Hinzufügen einer Nachricht, [block call]die eine interne Prüfung durchführt, kann hilfreich sein. Nicht sicher, wie nahe Blöcke an ObjC-Objekten sind.
Stephen Furlani
3
Ich blockiere im Allgemeinen nur? block (): nil; Das ist knapp genug für mich und transparent, was Sie tun ...
Patrick Pijnappel
2

Dies ist meine einfach schönste Lösung… Vielleicht gibt es eine Möglichkeit, eine universelle Ausführungsfunktion mit diesen c var-args zu schreiben, aber ich weiß nicht, wie ich das schreiben soll.

void run(void (^block)()) {
    if (block)block();
}

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}
Renetik
quelle
IST das: Block? block (): nil; besser?
Tibin Thomas