Vraag Waarom veroorzaken nul / NULL-blokken busfouten tijdens het uitvoeren?


Ik begon heel veel blokken te gebruiken en merkte al snel dat nul-blokken busfouten veroorzaken:

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

Dit lijkt in te gaan tegen het gebruikelijke gedrag van Objective-C dat berichten negeert tot nul objecten:

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

Daarom moet ik de gebruikelijke nulcontrole gebruiken voordat ik een blok gebruik:

if (aBlock != nil)
    aBlock();

Of gebruik dummy-blokken:

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

Is er een andere optie? Is er een reden waarom nul blokken niet eenvoudigweg een nadeel kunnen zijn?


69
2017-11-10 13:52


oorsprong


antwoorden:


Ik wil dit graag wat meer uitleggen, met een completer antwoord. Laten we eerst deze code eens bekijken:

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

Als je dit uitvoert, zie je een crash op de block() regel die er ongeveer zo uitziet (bij uitvoering op een 32-bits architectuur - dat is belangrijk):

EXC_BAD_ACCESS (code = 2, adres = 0xc)

Dus, waarom is dat? Nou ja, de 0xc is het belangrijkste deel. De crash betekent dat de processor heeft geprobeerd de informatie op het geheugenadres te lezen 0xc. Dit is bijna zeker een volkomen onjuist iets om te doen. Het is onwaarschijnlijk dat daar iets is. Maar waarom probeerde het deze geheugenlocatie te lezen? Nou, het komt door de manier waarop een blok eigenlijk onder de motorkap is gebouwd.

Wanneer een blok is gedefinieerd, maakt de compiler daadwerkelijk een structuur op de stapel, van dit formulier:

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

Het blok is dan een verwijzing naar deze structuur. Het vierde lid, invoke, van deze structuur is de interessante. Het is een functieaanwijzer, wijzend op de code waar de implementatie van het blok wordt gehouden. Dus de processor probeert naar die code te springen wanneer een blok wordt opgeroepen. Merk op dat als u het aantal bytes in de structuur vóór de meet invoke lid, je zult zien dat er 12 in decimaal zijn, of C in hexadecimaal.

Dus wanneer een blok wordt opgeroepen, neemt de processor het adres van het blok, voegt 12 toe en probeert de waarde te laden die op dat geheugenadres wordt bewaard. Vervolgens probeert het naar dat adres te springen. Maar als het blok nul is, zal het proberen het adres te lezen 0xc. Dit is duidelijk een duff-adres, dus we krijgen de segmentatiefout.

Nu is de reden dat het zo'n crash moet zijn in plaats van stilzwijgend falen als een Objective-C berichtoproep, echt een ontwerpkeuze. Omdat de compiler het werk doet om te beslissen hoe het blok opgeroepen moet worden, zou het overal waar een blok wordt aangeroepen nul controlecode moeten injecteren. Dit zou de codegrootte doen toenemen en tot slechte prestaties leiden. Een andere optie zou zijn om een ​​trampoline te gebruiken die de nulcontrole uitvoert. Dit zou echter ook een prestatievergoeding opleveren. Object-C berichten gaan al door een trampoline omdat ze de methode moeten opzoeken die daadwerkelijk wordt aangeroepen. De runtime maakt luie injectie van methoden en het veranderen van methode-implementaties mogelijk, dus het gaat sowieso al door een trampoline. De extra boete voor het uitvoeren van de nulcontrole is in dit geval niet belangrijk.

Ik hoop dat dat een beetje helpt om de redenering uit te leggen.

Zie mijn voor meer informatie blog  posts.


137
2018-02-05 09:10



Matt Galloway's antwoord is perfect! Geweldig gelezen!

Ik wil alleen toevoegen dat er enkele manieren zijn om het leven gemakkelijker te maken. U kunt een macro als deze definiëren:

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

Er kunnen 0 - n argumenten voor nodig zijn. Voorbeeld van gebruik

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");

Als je wilt de retourwaarde van het blok en je weet niet zeker of het blok bestaat of niet, dan ben je waarschijnlijk beter af als je gewoon typt:

block ? block() : nil;

Op deze manier kunt u gemakkelijk de fallback-waarde definiëren. In mijn voorbeeld 'nul'.


38
2017-10-23 18:52



Waarschuwing: ik ben geen expert in blokken.

Blocks zijn  objectief-c objecten maar het noemen van een blok is niet Een boodschap, hoewel je het nog steeds kon proberen [block retain]ing a nil blokkeren of andere berichten.

Hopelijk helpt dat (en de links).


8
2017-11-10 14:18



Dit is mijn eenvoudigste leuke oplossing ... Misschien is het mogelijk om een ​​universele run-functie te schrijven met die c var-args, maar ik weet niet hoe ik dat moet schrijven.

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

void runWith(void (^block)(id), id value) {
    if (block)block(value);
}

2
2018-05-07 07:54