Este artigo foi originalmente publicado no Medium pessoal da autora. Confira aqui.
Alguns leitores estão familiarizados com bibliotecas como Specta, OCMock e ReactiveCocoa. Em um primeiro momento pode parecer mágica, mas essas ferramentas na verdade exploram ao máximo o fato de o Objective-C ser uma linguagem dinâmica, isto é, o código em questão toma decisões em tempo de execução (“runtime”) sempre que possível.
Essa característica oferece uma flexibilidade que pode ser aproveitada de diversas maneiras. Uma das mais conhecidas é consultar informações sobre um dado objeto por meio de métodos como isKindOfClass:, respondsToSelector:, conformsToProtocol:, e assim por diante. Pode-se também adicionar novos métodos, chamá-los e até mudar a sua implementação em runtime. Isso é comumente utilizado no OCMock quando, por exemplo, um objeto “mockado” não depende mais de um serviço para obter a resposta de uma chamada, mas retorna um JSON que foi previamente setado.
Embora o runtime funcione na maior parte do tempo por debaixo dos panos, é interessante estudar mais sobre o assunto não só para entender melhor como a estrutura da linguagem funciona, mas também para entender a razão pela qual alguns passos são realizados.
Vamos mostrar aqui uma visão geral sobre o Runtime. Para mais detalhes, recomendo a leitura do “Objective-C Runtime Programming Guide”.
Algumas definições
Para saber como o runtime funciona, é importante entender algumas definições. Selector, método e implementação podem, em um primeiro momento, parecer que são a mesma coisa, mas na verdade são diferentes etapas de um processo feito em tempo de execução. Os termos mais importantes serão descritos abaixo. Não se esqueça de importar a biblioteca <objc/runtime.h> caso queira explorar os parâmetros a seguir.
Selector
Um selector (typedef struct objc_selector *SEL) nada mais é do que o nome de um método, como viewDidAppear:, setObject:forKey: etc. Note que o “:” faz parte do selector e serve para identificar quando é preciso passar parâmetros para um método. Para trabalhar diretamente com um selector, basta fazer, por exemplo:
SEL selector = @selector(viewDidAppear:) //ou SEL aSelector = NSSelectorFromString(@"viewDidAppear:")
Method
Um método (typedef struct objc_method *Method) é a combinação de um selector e sua implementação. Para acessar um método de uma instância ou classe, basta fazer:
// Instance Method Class class = [self class]; Method method = class_getInstanceMethod(class, selector); // Class Method Class class = object_getClass(self); Method method = class_getClassMethod(class, selector);
O método class_getInstanceMethod(class, selector) retorna o método de instância que corresponde à implementação de um selector em uma dada classe, ou NULL, caso, por exemplo, a classe ou a classe pai não tiver o método de instância para um selector específico.
Implementation
Uma implementação (id (*IMP)(id, SEL, …)) é basicamente o que está escrito dentro do bloco de um código. Um objeto do tipo IMP é um tipo de dado que aponta para o início da função que implementa o método. O primeiro argumento (id) aponta para a memória de uma dada instância de uma classe (ou, no caso de um método de classe, um ponteiro para uma metaclasse), também chamado de “receiver” (aquele que recebe o método), o segundo é o nome do método (SEL) e os restantes são os parâmetros que um método requer. A implementação pode ser adquirida da seguinte forma:
IMP implementation = method_getImplementation(method);
Message
Enviar uma mensagem é invocar um selector junto com os parâmetros que serão enviados para um “receiver”. Por exemplo, ao fazer:
[button setTitle:@"title" forState:UIControlStateNormal];
o compilador chama a seguinte função:
objc_msgSend(button, @selector(setTitle:forState:), @"title", UIControlStateNormal);
e, assim, a mensagem enviada para o “receiver” button é o selector “setTitle:forState:” mais os argumentos “title” e “UIControlStateNormal”. É possível guardar uma mensagem em um objeto do tipo NSInvocation para invocá-la posteriormente.
Method Signature
A assinatura de um método (NSMethodSignature) representa os tipos de dados que são aceitos e retornados por um método. Pode ser obtido por:
NSMethodSignature *signature = [receiver methodSignatureForSelector:selector];
no qual o receiver é o objeto que implementa o método, e o selector é o nome do método, como já foi discutido anteriormente.
Invocation
Um objeto do tipo NSInvocation é usado para guardar e enviar mensagens para um dado objeto. Ele contém todos os elementos necessários para enviar uma mensagem: um “receiver”, um selector, parâmetros de envio e o valor que será retornado. Um exemplo de como implementar um objeto desse tipo é:
void invokeSelector(id receiver, SEL selector, NSArray *arrayArguments) { if (receiver != nil && [receiver respondsToSelector:selector]) { NSMethodSignature *signature = [receiver methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:receiver]; [invocation setSelector:selector]; for(int i = 0; i < [signature numberOfArguments] - 2; i++) { id arg = [arrayArguments objectAtIndex:i]; [invocation setArgument:&arg atIndex:i+2]; // The first two arguments are the hidden arguments self and _cmd } [invocation invoke]; // Invoke the selector } } - (void)invocationExample { NSMutableDictionary *dictionary = [NSMutableDictionary new]; invokeSelector(dictionary, NSSelectorFromString(@"setObject:forKey:"), @[@"keyString", @"valueString"]); }
Note que os argumentos são setados a partir do índice 2. Isso porque os índices 0 e 1 são reservados para os argumentos self e _cmd, respectivamente, e devem ser setados explicitamente como mostrado acima, onde self é igual ao receiver e _cmd é o selector.
Se o você estiver familiarizado com testes unitários e usou o OCMock, já deve ter se deparado com a seguinte situação:
[[[managerMock expect] andDo:^(NSInvocation *invocation) { void (^successBlock)(NSString *aString) = nil; [invocation getArgument:&successBlock atIndex:2]; successBlock(@"42"); }] successBlock:[OCMArg any] errorBlock:[OCMArg any]];
Juntando o quebra-cabeça
Com essas definições em mente, fica mais fácil entender como o processo em runtime funciona e como esses conceitos estão relacionados.
No Objective-C, a estrutura de uma classe possui:
- Um ponteiro para a classe pai (ou superclass)
- Uma “dispatch table”, na qual cada entrada associa um selector a uma implementação.
Os objetos instanciados, por sua vez, possuem um ponteiro para a estrutura de classe, chamado isa, que dá ao objeto acesso à sua classe e, por meio da classe, a todas as classes que herda. Ao enviar uma mensagem a um objeto, a função objc_msgSend(receiver, selector, arg1, arg 2, etc…) segue o isa que aponta para a estrutura de classe e tenta encontrar o selector na “dispatch table”. Caso não encontre, a função segue o ponteiro que aponta para a superclass e tenta encontrar o selector na “dispatch table” dela. Falhas sucessivas fazem com que objc_msgSend vá subindo na hierarquia de classes até chegar na classe NSObject. Uma vez localizado o selector, o objc_msgSend chama o método correspondente e repassa os parâmetros, caso contrário, ocorre uma exceção. Dessa forma, as implementações são escolhidas em tempo de execução.
imagem: https://developer.apple.com
Para acelerar o processo, o sistema possui um cache para cada classe, que associa os selectors às implementações assim que vão sendo usadas. Quando uma mensagem for enviada, a função objc_msgSend checa primeiro esse cache antes de verificar a “dispatch table”. Assim, quanto mais tempo o programa for executado, mais rapidamente as mensagens serão enviadas.
Método Swizzling
Uma das formas de se aplicar esses conceitos sobre runtime é por meio do método Swizzling, que consiste em modificar a “dispatch table”, trocando as implementações de dois métodos entre si.
Imagine que você quer, por exemplo, adicionar um log toda vez que uma tela aparece:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self writeLogWhenTheViewAppeared]; } - (void)writeLogWhenTheViewAppeared { NSLog(@"The view of viewController %@ appeared", self); }
Agora imagine repetir esse processo para uma UIViewController, UITableViewController, UINavigationViewController etc. A técnica mostrada a seguir visa a evitar esse tipo de repetição.
Resumidamente, implementamos o método extensionViewDidAppear: (que contém a implementação do log) e será trocada a implementação dele com a do viewDidAppear:. Assim, quando a mensagem for enviada para a viewController com o selector viewDidAppear:, será na verdade executada a implementação de extensionViewDidAppear:. Além do log, o método extensionViewDidAppear: irá chamar [self extensionViewDidAppear:]. Como os selectors estão trocados, a implementação de viewDidAppear: será executada e o processo poderá continuar normalmente. O código a seguir pode ser encontrado no GitHub.
O primeiro passo é criar uma categoria e importar a biblioteca <objc/runtime.h>:
#import "UIViewController+Swizzling.h" #import <objc/runtime.h> @implementation UIViewController (Swizzling)
O método a seguir aplicará a técnica Swizzling propriamente dita:
#pragma mark - Swizzling viewDidAppear: + (void)swizzlingViewDidAppear { }
Dentro desse método será escrito:
Class class = [self class]; SEL selectorOriginal = @selector(viewDidAppear:); SEL selectorSwizz = @selector(extensionViewDidAppear:); Method methodOriginal = class_getInstanceMethod(class, selectorOriginal); Method methodSwizz = class_getInstanceMethod(class, selectorSwizz); /* Class class = object_getClass(self); SEL selectorOriginal = @selector(...); SEL selectorSwizz = @selector(...); Method methodOriginal = class_getClassMethod(class, selectorOriginal); Method methodSwizz = class_getClassMethod(class, selectorSwizz); */
Primeiro, resgatamos a classe do objeto no qual o método viewDidAppear: é implementado. Depois, guardamos os selectors e métodos de viewDidAppear: eextensionViewDidAppear:. As linhas comentadas acima mostram como fazer esse mesmo processo para métodos de classe. Feito isso, pegamos a implementação dos respectivos métodos:
MP implementationOriginal = method_getImplementation(methodOriginal); IMP implementationSwizz = method_getImplementation(methodSwizz);
Agora, tentamos adicionar à classe UIViewController (class) um método com o nome de viewDidAppear: (selectorOriginal), mas com a implementação de extensionViewDidAppear:. O último parâmetro trata-se de um array de caracteres que correspondem aos tipos dos argumentos que serão passados para o método.
BOOL didAddMethod = class_addMethod(class, selectorOriginal, implementationSwizz, method_getTypeEncoding(methodSwizz));
Em caso de sucesso, o próximo passo será fazer com que a classe em questão adicione um método com o nome de extensionViewDidAppear:, mas com a implementação do método original. Caso contrário, significa que a classe já contém uma implementação com o dado nome. Nesse caso, é necessário apenas trocar suas implementações:
if (didAddMethod) { class_replaceMethod(class, selectorSwizz, implementationOriginal, method_getTypeEncoding(methodOriginal)); } else { method_exchangeImplementations(methodOriginal, methodSwizz); }
Agora, é preciso escrever o método extensionViewDidAppear: propriamente dito:
#pragma mark - viewDidAppear: extension - (void)extensionViewDidAppear:(BOOL)animated { NSLog(@"The view of viewController %@ appeared", self); [self extensionViewDidAppear:animated]; }
Lembre-se de que após terminar a implementação é preciso chamar o método original. Isso será feito ao escrever [self extensionViewDidAppear:].
Para finalizar, é necessário chamar o método swizzlingViewDidAppear no método load, que por sua vez é chamado assim que a classe for inicializada:
#pragma mark - Load + (void)load { static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ [self swizzlingViewDidAppear]; }); }
Como o método Swizzling muda o estado global da classe, é preciso garantir que o código seja executado apenas uma vez, e por isso utilizamos dispatch_once.
Cuidados a serem tomados
Embora os conceitos acima ofereçam ferramentas poderosas, deve-se tomar o máximo de cautela antes de aplicá-los. Além de não ser necessário usá-los explicitamente a maior parte do tempo, não entender a fundo como o processo funciona, ou caso haja alguma modificação interna da linguagem, pode fazer com que o aplicativo quebre quando menos se espera. No caso do método Swizzling, há o problema de haver conflito com métodos com mesmo nome, além de tornar o código mais difícil de debugar. A dica final é: pense em todas as possibilidades antes de utilizar os exemplos citados acima.
Ficou alguma dúvida ou tem algum comentário a fazer? Aproveite os campos abaixo!