Solução
O compilador está avisando sobre isso por um motivo. É muito raro que esse aviso seja simplesmente ignorado e é fácil contornar isso. Aqui está como:
if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);
Ou de maneira mais concisa (embora difícil de ler e sem a guarda):
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
Explicação
O que está acontecendo aqui é que você está solicitando ao controlador o ponteiro da função C para o método correspondente ao controlador. Todos NSObject
respondem methodForSelector:
, mas você também pode usar class_getMethodImplementation
no tempo de execução Objective-C (útil se você tiver apenas uma referência de protocolo, como id<SomeProto>
). Esses ponteiros de função são chamados IMP
s e são simples typedef
ponteiros de função ed ( id (*IMP)(id, SEL, ...)
) 1 . Isso pode estar próximo da assinatura real do método, mas nem sempre corresponde exatamente.
Depois de ter o IMP
, você precisará 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
todas as chamadas de método Objective-C). Isso é tratado na terceira linha (o (void *)
lado direito simplesmente informa ao compilador que você sabe o que está fazendo e não gera um aviso, pois os tipos de ponteiro não correspondem).
Finalmente, você chama o ponteiro de função 2 .
Exemplo complexo
Quando o seletor recebe argumentos ou retorna um valor, você precisa 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 poderia ser qualquer coisa: void
, int
, char
, NSString *
, id
, etc. ARC normalmente obtém essas informações do cabeçalho do tipo de objeto que você está trabalhando. 3
Na verdade, existem apenas quatro coisas que a ARC consideraria para o valor de retorno: 4
- Ignorar tipos não-objetos (
void
, int
, etc.)
- Reter o valor do objeto e solte quando não for mais usado (suposição padrão)
- Libere novos valores de objeto quando não forem mais usados (métodos na família
init
/ copy
ou atribuídos a ns_returns_retained
)
- 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 o retém / libera. Portanto, você pode criar um vazamento se seu objeto for 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 de retorno void
ou outros não-objetos, você pode habilitar os recursos do compilador a ignorar o aviso, mas pode ser perigoso. Eu vi o 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á razão para que, com o ARC ativado, ele não possa reter e liberar o valor do objeto retornado, methodForSelector:
mesmo que você não queira usá-lo. Do ponto de vista do compilador, é um objeto, afinal. Isso significa que, se o método que você está chamando someMethod
retornar um objeto não (incluindo void
), você poderá acabar com um valor de ponteiro de lixo sendo retido / liberado e travado.
Argumentos adicionais
Uma consideração é que esse é o mesmo aviso que ocorrerá performSelector:withObject:
e você poderá 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 consumir o parâmetro, você provavelmente enviará uma mensagem para um zumbi e trava. Existem maneiras de contornar isso com a conversão em ponte, mas realmente seria melhor simplesmente usar a IMP
metodologia de ponteiro e de função acima. Como os parâmetros consumidos raramente são um problema, é provável que isso não ocorra.
Seletores estáticos
Curiosamente, o compilador não irá reclamar dos 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 verifiquei isso um ano atrás, olhando a fonte, mas não tenho uma referência no momento.)
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 ficando em branco. Alguém compartilhe se eles tiveram uma experiência em que o silenciamento desse aviso era necessário (e o acima não lida com as coisas corretamente).
Mais
É possível criar um NSMethodInvocation
para lidar com isso também, mas isso exige muito mais digitação e também é mais lento, por isso há poucas razões para fazê-lo.
História
Quando a performSelector:
família de métodos foi adicionada 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 maneira de orientar os desenvolvedores a usar outros meios para definir explicitamente como a memória deve ser manipulada ao enviar mensagens arbitrárias por meio de um seletor nomeado. No Objective-C, os desenvolvedores conseguem fazer isso usando projeções de estilo C em ponteiros de função bruta.
Com a introdução do Swift, a Apple documentou a performSelector:
família de métodos como "inerentemente inseguros" e eles não estão disponíveis para o Swift.
Com o tempo, vimos essa progressão:
- As versões anteriores do Objective-C permitem
performSelector:
(gerenciamento manual de memória)
- Objective-C com ARC alerta para o uso de
performSelector:
- Swift não tem acesso
performSelector:
e documenta esses métodos como "inerentemente inseguros"
A idéia de enviar mensagens com base em um seletor nomeado não é, no entanto, um recurso "inerentemente inseguro". Essa idéia foi usada com sucesso por um longo tempo no Objective-C, bem como em muitas outras linguagens de programação.
1 Todos os métodos de Objective-C tem dois argumentos ocultos, self
e _cmd
que são adicionados implicitamente quando você chamar um método.
2 A chamada de uma NULL
função não é segura em C. O guarda usado para verificar a presença do controlador garante que temos um objeto. Nós, portanto, sei que nós vamos ter uma IMP
de methodForSelector:
(embora possa ser _objc_msgForward
, a entrada no sistema de encaminhamento de mensagens). Basicamente, com a guarda no lugar, sabemos que temos uma função a chamar.
3 Na verdade, é possível obter informações incorretas se você declarar objetos id
e não estiver 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. Geralmente, você recebe um aviso de que não sabe qual das duas assinaturas de método deve escolher.
4 Consulte a referência ARC sobre valores de retorno acumulados e valores de retorno não acumulados para mais detalhes.