Je voudrais expliquer cela un peu plus, avec une réponse plus complète. D'abord, examinons ce code:
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
void (^block)() = nil;
block();
}
Si vous exécutez cela, alors vous verrez un accident sur la block()
ligne qui ressemble à quelque chose comme ça (lorsqu'il est exécuté sur une architecture 32 bits - ce qui est important):
EXC_BAD_ACCESS (code = 2, adresse = 0xc)
Alors, pourquoi est-ce? Eh bien, le 0xc
est le bit le plus important.Le crash signifie que le processeur a essayé de lire les informations à l'adresse mémoire 0xc
. C'est presque définitivement une chose complètement incorrecte à faire. Il est peu probable qu'il y ait quelque chose là-bas. Mais pourquoi a-t-il essayé de lire cet emplacement de mémoire? Eh bien, c'est en raison de la façon dont un bloc est réellement construit sous le capot.
Lorsqu'un bloc est défini, le compilateur crée en fait une structure sur la pile, de cette forme:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
Le bloc est alors un pointeur vers cette structure. Le quatrième membre, invoke
, de cette structure est l'intéressant. C'est un pointeur de fonction, pointant vers le code où l'implémentation du bloc est tenue. Ainsi, le processeur essaie de sauter à ce code lorsqu'un bloc est appelé. Notez que si vous comptez le nombre d'octets dans la structure avant le membre invoke
, vous constaterez qu'il existe 12 en décimal, ou C en hexadécimal.
Ainsi, lorsqu'un bloc est appelé, le processeur prend l'adresse du bloc, ajoute 12 et essaie de charger la valeur contenue à cette adresse mémoire. Il essaie ensuite de sauter à cette adresse. Mais si le bloc est nul alors il va essayer de lire l'adresse 0xc
. Ceci est une adresse duff, clairement, et ainsi nous obtenons la faute de segmentation. Maintenant, la raison pour laquelle il doit s'agir d'un crash comme celui-ci plutôt que d'échouer silencieusement comme un appel de message Objective-C est vraiment un choix de conception. Puisque le compilateur fait le travail de décider comment invoquer le bloc, il devrait injecter le code de vérification nul partout où un bloc est invoqué. Cela augmenterait la taille du code et conduirait à de mauvaises performances. Une autre option serait d'utiliser un trampoline qui fait la vérification nulle. Cependant, cela entraînerait également une pénalité de performance. Les messages Objective-C passent déjà par un trampoline car ils doivent rechercher la méthode qui sera réellement appelée. L'exécution permet l'injection paresseuse des méthodes et le changement des implémentations de la méthode, de sorte qu'elle passe déjà par un trampoline. La pénalité supplémentaire de faire la vérification nulle n'est pas significative dans ce cas. J'espère que cela aide un peu à expliquer la logique. Pour plus d'informations, voir mon blogposts.
Merci, liens intéressants. Je sais que l'appel d'un bloc n'est pas la même chose que l'envoi d'un message, mais conceptuellement ce serait bien si les blocs nuls étaient aussi indulgents que les objets nuls. – zoul
Vous pouvez ajouter une catégorie au type '__block' ... mais je ne suis pas sûr. '#define nilBlock^{}' peut aussi vous faciliter la vie. –
Je pensais à l'approche 'nilBlock', malheureusement le typage entrave la création - créer une valeur nulle différente pour chaque type de bloc n'est pas très amusant. – zoul