Dev & EngARTIGO

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.

runtime

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.

img-1

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!

É desenvolvedora iOS na Concrete Solutions, bacharel em Física e mestre em Astronomia pela USP. Adora saber as novidades sobre tecnologia e descobertas científicas.

Ver perfil →