Introdução sobre Runtime em Objective-C
Vamos mostrar aqui uma visão geral sobre o Runtime.
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]];
No caso, o objeto manager terá o retorno do bloco de resposta “mockado” toda vez que o método successBlock:errorBlock: for chamado. Note que o primeiro argumento é referenciado com o índice 2, o segundo a 3 e assim por diante (caso houvesse), devido ao fato que foi discutido acima.
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!









