versao - versoes ios




executeSelector pode causar um vazamento porque seu seletor é desconhecido (13)

Estou recebendo o seguinte aviso pelo compilador ARC:

"performSelector may cause a leak because its selector is unknown".

Veja o que estou fazendo:

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Por que recebo este aviso? Eu entendo que o compilador não pode verificar se o seletor existe ou não, mas por que isso causaria um vazamento? E como posso mudar meu código para não receber mais esse aviso?


Não suprima avisos!

Não há menos de 12 soluções alternativas para mexer com o compilador.
Enquanto você está sendo inteligente no momento da primeira implementação, poucos engenheiros na Terra podem seguir seus passos, e esse código acabará quebrando.

Rotas Seguras:

Todas essas soluções funcionarão, com algum grau de variação de sua intenção original. Suponha que param pode ser nil se você assim desejar:

Rota segura, mesmo comportamento conceitual:

// GREAT
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

Rota segura, comportamento ligeiramente diferente:

(Veja this resposta)
Use qualquer thread em vez de [NSThread mainThread] .

// GOOD
[_controller performSelector:selector withObject:anArgument afterDelay:0];
[_controller performSelector:selector withObject:anArgument afterDelay:0 inModes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorInBackground:selector withObject:anArgument];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

Rotas Perigosas

Requer algum tipo de silenciamento de compilador, que é obrigado a quebrar. Note-se que no momento presente, ele se quebrar em Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];

Solução

O compilador está alertando sobre isso por um motivo. É muito raro que esse aviso seja simplesmente ignorado e seja fácil trabalhar por aí. Veja como:

if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);

Ou mais concisa (embora difícil de ler e sem o guarda):

SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);

Explicação

O que está acontecendo aqui é que você está pedindo ao controlador pelo ponteiro da função C o método correspondente ao controlador. Todos os NSObject s respondem a methodForSelector: mas você também pode usar class_getMethodImplementation no class_getMethodImplementation do Objective-C (útil se você tiver apenas uma referência de protocolo, como id<SomeProto> ). Esses ponteiros de função são chamados de IMP s, e são simples ponteiros de função tipados ( id (*IMP)(id, SEL, ...) ) 1 . Isso pode estar próximo da assinatura do método real do método, mas nem sempre corresponderá exatamente.

Depois de ter o IMP , você precisa convertê-lo em um ponteiro de função que inclua todos os detalhes que o ARC precisa (incluindo os dois argumentos ocultos implícitos self e _cmd de cada chamada de método do Objective-C). Isso é tratado na terceira linha (o (void *) no lado direito simplesmente diz ao compilador que você sabe o que está fazendo e não gera um aviso, pois os tipos de ponteiro não coincidem).

Finalmente, você chama o ponteiro de função 2 .

Exemplo Complexo

Quando o seletor aceita argumentos ou retorna um valor, você terá que mudar um pouco as coisas:

SEL selector = NSSelectorFromString(@"processRegion:ofView:");
IMP imp = [_controller methodForSelector:selector];
CGRect (*func)(id, SEL, CGRect, UIView *) = (void *)imp;
CGRect result = _controller ?
  func(_controller, selector, someRect, someView) : CGRectZero;

Raciocínio para aviso

O motivo desse aviso é que, com o ARC, o tempo de execução precisa saber o que fazer com o resultado do método que você está chamando. O resultado pode ser qualquer coisa: void , int , char , NSString * , id , etc. Normalmente, o ARC obtém essas informações do cabeçalho do tipo de objeto com o qual você está trabalhando. 3

Existem apenas 4 coisas que o ARC consideraria para o valor de retorno: 4

  1. Ignorar tipos não-objeto ( void , int , etc)
  2. Retenha o valor do objeto, depois libere quando não for mais usado (suposição padrão)
  3. Libere novos valores de objetos quando não ns_returns_retained mais usados ​​(métodos na família init / copy ou atribuídos com ns_returns_retained )
  4. Não faça nada e assuma que o valor do objeto retornado será válido no escopo local (até que o conjunto de liberação mais interno seja drenado, atribuído com ns_returns_autoreleased )

A chamada para methodForSelector: assume que o valor de retorno do método que está chamando é um objeto, mas não retém / libera. Então você pode acabar criando um vazamento se o seu objeto deve ser lançado como no item 3 acima (ou seja, o método que você está chamando retorna um novo objeto).

Para os seletores que você está tentando chamar esse retorno void ou outros não-objetos, você pode habilitar os recursos do compilador para ignorar o aviso, mas isso pode ser perigoso. Eu vi Clang passar por algumas iterações de como ele lida com valores de retorno que não são atribuídos a variáveis ​​locais. Não há nenhuma razão para que o ARC esteja ativado e não possa reter e liberar o valor do objeto retornado de methodForSelector: mesmo que você não queira usá-lo. Da perspectiva do compilador, é um objeto afinal. Isso significa que se o método que você está chamando, someMethod , estiver retornando um objeto não (incluindo void ), você pode acabar com um valor de ponteiro de lixo sendo mantido / liberado e travado.

Argumentos Adicionais

Uma consideração é que esse é o mesmo aviso que ocorrerá com performSelector:withObject: e você pode ter problemas semelhantes ao não declarar como esse método consome parâmetros. O ARC permite declarar parâmetros consumidos , e se o método consome o parâmetro, você provavelmente enviará uma mensagem a um zumbi e falhará. Há maneiras de contornar isso com a conversão de ponte, mas realmente seria melhor simplesmente usar a metodologia de ponteiro de função e IMP acima. Como os parâmetros consumidos raramente são um problema, é provável que isso não aconteça.

Seletores estáticos

Curiosamente, o compilador não irá reclamar sobre seletores declarados estaticamente:

[_controller performSelector:@selector(someMethod)];

A razão para isso é porque o compilador é realmente capaz de registrar todas as informações sobre o seletor e o objeto durante a compilação. Não precisa fazer suposições sobre nada. (Eu chequei isso há um ano atrás, olhando para a fonte, mas não tenho uma referência agora.)

Supressão

Ao tentar pensar em uma situação em que a supressão desse aviso seria necessária e um bom design de código, estou chegando em branco. Alguém, por favor, compartilhe se eles tiveram uma experiência em que silenciar este aviso foi necessário (e o acima não lida corretamente com as coisas).

Mais

É possível criar um NSMethodInvocation para lidar com isso também, mas fazer isso requer muito mais digitação e também é mais lento, então não há razão para isso.

História

Quando o performSelector: family of methods foi adicionado pela primeira vez ao Objective-C, o ARC não existia. Ao criar o ARC, a Apple decidiu que um aviso deveria ser gerado para esses métodos como uma forma de orientar os desenvolvedores a usar outros meios para definir explicitamente como a memória deveria ser manipulada ao enviar mensagens arbitrárias através de um seletor nomeado. Em Objective-C, os desenvolvedores podem fazer isso usando moldes de estilo C em ponteiros de função brutos.

Com a introdução do Swift, a Apple documentou o performSelector: família de métodos como "intrinsecamente inseguro" e eles não estão disponíveis para o Swift.

Com o tempo, vimos essa progressão:

  1. As primeiras versões do Objective-C permitem performSelector: (gerenciamento de memória manual)
  2. Objective-C com ARC avisa para uso do performSelector:
  3. O Swift não tem acesso ao performSelector: e documenta esses métodos como "intrinsecamente inseguros"

A idéia de enviar mensagens com base em um seletor nomeado não é, no entanto, um recurso "inerentemente inseguro". Esta ideia tem sido usada com sucesso há muito tempo no Objective-C, assim como em muitas outras linguagens de programação.

1 Todos os métodos Objective-C possuem dois argumentos ocultos, self e _cmd que são incluídos implicitamente quando você chama um método.

2 Chamar uma função NULL não é seguro em C. A proteção usada para verificar a presença do controlador garante que tenhamos um objeto. Portanto, sabemos que obteremos um IMP de methodForSelector: (embora possa ser _objc_msgForward , entrada no sistema de encaminhamento de mensagens). Basicamente, com o guarda no lugar, sabemos que temos uma função para chamar.

3 Na verdade, é possível obter as informações incorretas se declarar seus objetos como id e se você não está importando todos os cabeçalhos. Você pode acabar com falhas no código que o compilador acha que está bem. Isso é muito raro, mas pode acontecer. Normalmente, você só receberá um aviso de que não sabe qual das duas assinaturas de método escolher.

4 Consulte a referência do ARC em valores de retorno retidos e valores de retorno não retidos para obter mais detalhes.


A resposta de Matt Galloway neste tópico explica o porquê:

Considere o seguinte:

id anotherObject1 = [someObject performSelector:@selector(copy)];
id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];

Agora, como o ARC pode saber que o primeiro retorna um objeto com uma contagem de retenções de 1, mas o segundo retorna um objeto que é liberado automaticamente?

Parece que geralmente é seguro suprimir o aviso se você estiver ignorando o valor de retorno. Não sei qual é a melhor prática se você realmente precisa obter um objeto retido do performSelector - diferente de "don't do that".


Aqui está uma macro atualizada com base na resposta dada acima. Este deve permitir que você envolva seu código mesmo com uma declaração de retorno.

#define SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(code)                        \
    _Pragma("clang diagnostic push")                                        \
    _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")     \
    code;                                                                   \
    _Pragma("clang diagnostic pop")                                         \


SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(
    return [_target performSelector:_action withObject:self]
);

Como uma solução alternativa até que o compilador permita substituir o aviso, você pode usar o tempo de execução

objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

ao invés de

[_controller performSelector:NSSelectorFromString(@"someMethod")];

Você terá que

#import <objc/message.h>


Em seu projeto Build Settings , sob Other Warning Flags ( WARNING_CFLAGS ), adicione
-Wno-arc-performSelector-leaks

Agora, apenas certifique-se de que o seletor que você está chamando não faça com que seu objeto seja retido ou copiado.


Estranho, mas verdadeiro: se aceitável (ou seja, o resultado é inválido e você não se importa em deixar o ciclo de runloop uma vez), adicione um atraso, mesmo que seja zero:

[_controller performSelector:NSSelectorFromString(@"someMethod")
    withObject:nil
    afterDelay:0];

Isso remove o aviso, presumivelmente porque reafirma ao compilador que nenhum objeto pode ser retornado e, de alguma forma, mal gerenciado.


Meu palpite sobre isso é o seguinte: como o seletor é desconhecido para o compilador, o ARC não pode impor o gerenciamento de memória adequado.

Na verdade, há momentos em que o gerenciamento de memória é vinculado ao nome do método por uma convenção específica. Especificamente, estou pensando em construtores de conveniência versus métodos de fabricação ; o primeiro retorna por convenção um objeto autoreleased; o último, um objeto retido. A convenção é baseada nos nomes do seletor, portanto, se o compilador não conhecer o seletor, ele não poderá impor a regra de gerenciamento de memória adequada.

Se isso estiver correto, acho que você pode usar seu código com segurança, desde que tenha certeza de que tudo está correto no gerenciamento de memória (por exemplo, que seus métodos não retornam objetos que eles alocam).


Para a posteridade, decidi jogar meu chapéu no ringue :)

Recentemente eu tenho visto cada vez mais uma reestruturação longe do paradigma target / selector , em favor de coisas como protocolos, blocos, etc. No entanto, há um substituto para o performSelector que eu usei algumas vezes agora :

[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];

Estes parecem ser uma substituição limpa, ARC-safe, e quase idêntica para performSelector sem ter muito a ver com objc_msgSend() .

No entanto, não tenho ideia se existe um analógico disponível no iOS.


Para ignorar o erro apenas no arquivo com o seletor de execução, adicione um #pragma da seguinte maneira:

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

Isso ignoraria o aviso nessa linha, mas ainda permitiria o restante do seu projeto.


Em vez de usar a abordagem de bloco, o que me deu alguns problemas:

    IMP imp = [_controller methodForSelector:selector];
    void (*func)(id, SEL) = (void *)imp;

Vou usar o NSInvocation, assim:

    -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button 

    if ([delegate respondsToSelector:selector])
    {
    NSMethodSignature * methodSignature = [[delegate class]
                                    instanceMethodSignatureForSelector:selector];
    NSInvocation * delegateInvocation = [NSInvocation
                                   invocationWithMethodSignature:methodSignature];


    [delegateInvocation setSelector:selector];
    [delegateInvocation setTarget:delegate];

    // remember the first two parameter are cmd and self
    [delegateInvocation setArgument:&button atIndex:2];
    [delegateInvocation invoke];
    }

Como você está usando o ARC, você deve estar usando o iOS 4.0 ou posterior. Isso significa que você pode usar blocos. Se, em vez de lembrar o seletor para executar, você pegasse um bloco, o ARC seria capaz de rastrear melhor o que realmente está acontecendo e você não teria que correr o risco de introduzir acidentalmente um vazamento de memória.


Você também pode usar um protocolo aqui. Então, crie um protocolo assim:

@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end

Em sua turma que precisa chamar seu seletor, você tem uma @property.

@interface MyObject
    @property (strong) id<MyProtocol> source;
@end

Quando você precisar chamar @selector(doSomethingWithObject:)uma instância do MyObject, faça o seguinte:

[self.source doSomethingWithObject:object];






automatic-ref-counting