performSelector pode causar um vazamento porque seu seletor é desconhecido

1258

Estou recebendo o seguinte aviso do compilador ARC:

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

Aqui está o que estou fazendo:

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

Por que recebo esse aviso? 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 alterar meu código para não receber mais esse aviso?

Eduardo Scoz
fonte
3
O nome da variável é dinâmico, depende de muitas outras coisas. Existe o risco de eu chamar algo que não existe, mas esse não é o problema.
Eduardo Scoz
6
@matt Por que chamar um método dinamicamente em um objeto seria uma prática ruim? Não é todo o objetivo do NSSelectorFromString () apoiar essa prática?
Eduardo Scoz
7
Você deve / pode também testar [_controller respondsToSelector: mySelector] antes de defini-lo via performSelector:
mattacular
50
@mattacular Gostaria de votar: "Isso ... é uma prática ruim".
Ctpenrose
6
Se você sabe que a string é literal, use @selector () para que o compilador possa dizer qual é o nome do seletor. Se o seu código real estiver chamando NSSelectorFromString () com uma string que é construída ou fornecida em tempo de execução, você deve usar o NSSelectorFromString ().
Chris Page

Respostas:

1211

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 NSObjectrespondem methodForSelector:, mas você também pode usar class_getMethodImplementationno 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 IMPs e são simples typedefponteiros 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 selfe _cmdtodas 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

  1. Ignorar tipos não-objetos ( void, int, etc.)
  2. Reter o valor do objeto e solte quando não for mais usado (suposição padrão)
  3. Libere novos valores de objeto quando não forem mais usados ​​(métodos na família init/ copyou atribuídos a 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 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 voidou 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 someMethodretornar 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 IMPmetodologia 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 NSMethodInvocationpara 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:

  1. As versões anteriores do Objective-C permitem performSelector:(gerenciamento manual de memória)
  2. Objective-C com ARC alerta para o uso de performSelector:
  3. 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, selfe _cmdque são adicionados implicitamente quando você chamar um método.

2 A chamada de uma NULLfunçã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 IMPde 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 ide 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.

wbyoung
fonte
@wbyoung Se o seu código resolve o problema de retenção, eu me pergunto por que os performSelector:métodos não são implementados dessa maneira. Eles possuem assinatura de método rigorosa (retornando id, tendo um ou dois ids), portanto, nenhum tipo primitivo precisa ser tratado.
Tricertops
1
@ Todo o argumento é tratado com base na definição do protótipo do método (não será retido / liberado). A preocupação é baseada principalmente no tipo de retorno.
Wbyoung
2
O "Exemplo complexo" apresenta um erro Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'ao usar o Xcode mais recente. (5.1.1) Ainda assim, aprendi muito!
Stan James
2
void (*func)(id, SEL) = (void *)imp;não compila, substituí-o com #void (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl
1
alterar void (*func)(id, SEL) = (void *)imp;para <…> = (void (*))imp;ou<…> = (void (*) (id, SEL))imp;
Isaak Osipovich Dunayevsky,
1182

No compilador LLVM 3.0 no Xcode 4.2, você pode suprimir o aviso da seguinte maneira:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.ticketTarget performSelector: self.ticketAction withObject: self];
#pragma clang diagnostic pop

Se você estiver recebendo o erro em vários locais e quiser usar o sistema de macro C para ocultar os pragmas, poderá definir uma macro para facilitar a supressão do aviso:

#define SuppressPerformSelectorLeakWarning(Stuff) \
    do { \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

Você pode usar a macro assim:

SuppressPerformSelectorLeakWarning(
    [_target performSelector:_action withObject:self]
);

Se você precisar do resultado da mensagem executada, poderá fazer o seguinte:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);
Scott Thompson
fonte
Esse método pode causar vazamentos de memória quando a otimização é definida como algo diferente de Nenhum.
Eric
4
@ Eric Não, não pode, a menos que você esteja invocando métodos engraçados como "initSomething" ou "newSomething" ou "somethingCopy".
precisa saber é o seguinte
3
@ Julian Isso funciona, mas desativa o aviso para todo o arquivo - você pode não precisar ou querer isso. Envolvê-lo com os pragmas pope pushé muito mais limpo e seguro.
Emil
2
Tudo o que isso faz é silenciar o compilador. Isso não resolve o problema. Se o seletor não existir, você está muito ferrado.
Andra Todorescu
2
Isso deve ser usado apenas quando envolvido por uma if ([_target respondsToSelector:_selector]) {lógica ou similar.
208

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

De fato, há momentos em que o gerenciamento de memória está vinculado ao nome do método por uma convenção específica. Especificamente, estou pensando em construtores de conveniência versus métodos make ; o primeiro retorno por convenção de um objeto liberado automaticamente; o último um objeto retido. A convenção é baseada nos nomes do seletor; portanto, se o compilador não conhecer o seletor, 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 tudo esteja correto quanto ao gerenciamento de memória (por exemplo, que seus métodos não retornem objetos que eles alocam).

sergio
fonte
5
Obrigado pela resposta, analisarei mais detalhes para ver o que está acontecendo. Alguma idéia de como posso ignorar o aviso e fazê-lo desaparecer? Eu odiaria ter o aviso no meu código para sempre para o que é uma chamada segura.
Eduardo Scoz
84
Então, recebi confirmação de alguém da Apple em seus fóruns de que esse é realmente o caso. Eles adicionarão uma substituição esquecida para permitir que as pessoas desativem esse aviso em versões futuras. Obrigado.
Eduardo Scoz
5
Essa resposta levanta algumas questões, como se o ARC tentasse determinar quando liberar algo com base em nomes de convenções e métodos, então como é a "contagem de referência"? O comportamento que você descreve soa apenas marginalmente melhor do que completamente arbitrário, se o ARC estiver assumindo que o código segue uma certa convenção, em vez de realmente acompanhar as referências, independentemente da convenção seguida.
Aroth
8
O ARC automatiza o processo de adição de retenções e lançamentos na compilação. Não é coleta de lixo (é por isso que é tão incrivelmente rápido e com pouca sobrecarga). Não é arbitrário. As regras padrão são baseadas em convenções ObjC bem estabelecidas, aplicadas de maneira consistente há décadas. Isso evita a necessidade de adicionar explicitamente um __attributea todos os métodos que explicam seu gerenciamento de memória. Mas também torna impossível para o complier lidar adequadamente com esse padrão (um padrão que costumava ser muito comum, mas foi substituído por padrões mais robustos nos últimos anos).
Rob Napier
8
Portanto, não podemos mais ter um ivar do tipo SELe atribuir seletores diferentes, dependendo da situação? Caminho a percorrer, linguagem dinâmica ...
Nicolas Miari
121

No seu Build Build Settings , em Other Warning Flags ( WARNING_CFLAGS), adicione
-Wno-arc-performSelector-leaks

Agora, verifique se o seletor que você está chamando não faz com que seu objeto seja retido ou copiado.

0xced
fonte
12
Observe que você pode adicionar o mesmo sinalizador para arquivos específicos, em vez de todo o projeto. Se você olhar em Fases de Construção-> Origens de Compilação, poderá definir Sinalizadores do Compilador por arquivo (como deseja excluir arquivos do ARC). No meu projeto, apenas um arquivo deve usar seletores dessa maneira, então apenas o excluí e deixei os outros.
Michael
111

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>

jluckyiv
fonte
8
O ARC reconhece as convenções de cacau e, em seguida, adiciona retenções e lançamentos com base nessas convenções. Como o C não segue essas convenções, o ARC obriga a usar técnicas manuais de gerenciamento de memória. Se você criar um objeto CF, deverá CFRelease (). Se você dispatch_queue_create (), você deve dispatch_release (). Conclusão: se você quiser evitar os avisos do ARC, poderá evitá-los usando objetos C e gerenciamento manual de memória. Além disso, você pode desativar o ARC por arquivo usando o sinalizador do compilador -fno-objc-arc nesse arquivo.
precisa saber é
8
Não sem elenco, você não pode. Varargs não é o mesmo que uma lista de argumentos digitada explicitamente. Geralmente funcionará por coincidência, mas não considero "por coincidência" correto.
bbum 27/09
21
Não faça isso [_controller performSelector:NSSelectorFromString(@"someMethod")];e objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));não é equivalente! Dê uma olhada nas incompatibilidades de assinatura de método e Uma grande fraqueza na digitação fraca do Objective-C está explicando o problema em profundidade.
0xced
5
@ 0xced Nesse caso, tudo bem. objc_msgSend não criará uma incompatibilidade de assinatura de método para nenhum seletor que funcione corretamente no performSelector: ou em suas variantes, pois eles apenas recebem objetos como parâmetros. Desde que todos os seus parâmetros sejam ponteiros (incluindo objetos), duplos e NSInteger / long e seu tipo de retorno seja nulo, ponteiro ou longo, então objc_msgSend funcionará corretamente.
Matt Gallagher
88

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 o permitiria durante o restante do seu projeto.

Barlow Tucker
fonte
6
Acho que você também pode ativar o aviso imediatamente após o método em questão #pragma clang diagnostic warning "-Warc-performSelector-leaks". Sei que, se eu desligar um aviso, gosto de ligá-lo o mais rápido possível, para não deixar escapar acidentalmente outro aviso imprevisto. É improvável que isso seja um problema, mas é apenas minha prática sempre que desativo um aviso.
Rob
2
Você também pode restaurar o estado anterior da configuração do compilador usando #pragma clang diagnostic warning pushantes de fazer alterações e #pragma clang diagnostic warning poprestaurar o estado anterior. Útil se você estiver desativando cargas e não quiser ter muitas linhas pragma de reativação no seu código.
deanWombourne
Ele ignorará apenas a seguinte linha?
Hfossli
70

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

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

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

mate
fonte
2
Você sabe se isso realmente resolve os problemas relacionados ao gerenciamento de memória ou tem os mesmos problemas, mas o Xcode não é inteligente o suficiente para avisá-lo com esse código?
Aaron Brager
Semanalmente, isso não é a mesma coisa! O uso do performSelector: withObject: AfterDelay: executará o seletor na próxima execução do runloop. Portanto, esse método retorna imediatamente.
Florian
10
@ Florian Claro que não é o mesmo! Leia minha resposta: digo se aceitável, porque o resultado é nulo e o ciclo de execução é executado. Essa é a primeira frase da minha resposta.
Matt
34

Aqui está uma macro atualizada com base na resposta dada acima. Este deve permitir que você agrupe 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]
);
syvex
fonte
6
returnnão precisa estar dentro da macro; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);também funciona e parece mais saudável.
UASI
31

Este código não envolve sinalizadores de compilador ou chamadas diretas de tempo de execução:

SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];

NSInvocationpermite que vários argumentos sejam definidos, ao contrário, performSelectorisso funcionará em qualquer método.

Benedict Cohen
fonte
3
Você sabe se isso realmente resolve os problemas relacionados ao gerenciamento de memória ou tem os mesmos problemas, mas o Xcode não é inteligente o suficiente para avisá-lo com esse código?
precisa saber é o seguinte
1
Você poderia dizer que resolve os problemas de gerenciamento de memória; mas isso ocorre porque basicamente permite especificar o comportamento. Por exemplo, você pode optar por deixar a chamada reter os argumentos ou não. Para meu conhecimento atual, ele tenta corrigir os problemas de incompatibilidade de assinatura que podem aparecer, confiando que você sabe o que está fazendo e não fornece dados incorretos. Não tenho certeza se todas as verificações podem ser executadas em tempo de execução. Como mencionado em outro comentário, mikeash.com/pyblog/… explica bem o que as incompatibilidades podem fazer.
Mihai Timar
20

Bem, muitas respostas aqui, mas como isso é um pouco diferente, combinando algumas respostas que pensei em colocar. Estou usando uma categoria NSObject que verifica se o seletor retorna nulo e também suprime o compilador Atenção.

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Debug.h" // not given; just an assert

@interface NSObject (Extras)

// Enforce the rule that the selector used must return void.
- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object;
- (void) performVoidReturnSelector:(SEL)aSelector;

@end

@implementation NSObject (Extras)

// Apparently the reason the regular performSelect gives a compile time warning is that the system doesn't know the return type. I'm going to (a) make sure that the return type is void, and (b) disable this warning
// See http://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://stackoverflow.com/questions/14602854/objective-c-is-there-a-way-to-check-a-selector-return-value
    Method m = class_getInstanceMethod([self class], aSelector);
    char type[128];
    method_getReturnType(m, type, sizeof(type));

    NSString *message = [[NSString alloc] initWithFormat:@"NSObject+Extras.performVoidReturnSelector: %@.%@ selector (type: %s)", [self class], NSStringFromSelector(aSelector), type];
    NSLog(@"%@", message);

    if (type[0] != 'v') {
        message = [[NSString alloc] initWithFormat:@"%@ was not void", message];
        [Debug assertTrue:FALSE withMessage:message];
    }
}

- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // Since the selector (aSelector) is returning void, it doesn't make sense to try to obtain the return result of performSelector. In fact, if we do, it crashes the app.
    [self performSelector: aSelector withObject: object];
#pragma clang diagnostic pop    
}

- (void) performVoidReturnSelector:(SEL)aSelector {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector: aSelector];
#pragma clang diagnostic pop
}

@end
Chris Prince
fonte
O 'v' deve ser substituído por _C_VOID? _C_VOID é declarado em <objc / runtime.h>.
Rik Renich
16

Por uma questão de posteridade, eu decidi jogar meu chapéu no ringue :)

Recentemente, tenho visto cada vez mais reestruturações fora do target/ selectorparadigma, a favor de coisas como protocolos, blocos, etc. No entanto, há um substituto substituto performSelectorque já usei algumas vezes agora:

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

Estes parecem ser um substituto limpo, seguro para ARC e quase idêntico, performSelectorsem ter que se preocupar muito com isso objc_msgSend().

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

Patrick Perini
fonte
6
Obrigado por incluindo este .. Ele está disponível em iOS: [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Eu olhei para ele uma vez, mas parece meio estranho usar uma classe relacionada à interface do usuário no meio do seu domínio ou serviço apenas para fazer uma chamada dinâmica. Obrigado por incluir isso!
Eduardo Scoz
2
Ai credo! Ele terá mais sobrecarga (já que precisa verificar se o método está disponível e subir a cadeia de respostas, se não estiver) e terá um comportamento de erro diferente (subindo na cadeia de respostas e retornando NÃO se não encontrar nada que responde ao método, em vez de simplesmente travar). Também não funciona quando você deseja o idfrom-performSelector:...
tc.
2
@tc. Ele não "sobe a cadeia de respostas" a menos que to:seja nulo, o que não é. Apenas vai direto ao objeto alvo sem verificação prévia. Portanto, não há "mais sobrecarga". Não é uma ótima solução, mas o motivo que você dá não é o motivo. :)
matt
15

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ção 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 - além de "não faça isso".

c roald
fonte
14

O @ c-road fornece o link correto com a descrição do problema aqui . Abaixo você pode ver meu exemplo, quando performSelector causa um vazamento de memória.

@interface Dummy : NSObject <NSCopying>
@end

@implementation Dummy

- (id)copyWithZone:(NSZone *)zone {
  return [[Dummy alloc] init];
}

- (id)clone {
  return [[Dummy alloc] init];
}

@end

void CopyDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy copy];
}

void CloneDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy clone];
}

void CopyDummyWithLeak(Dummy *dummy, SEL copySelector) {
  __unused Dummy *dummyClone = [dummy performSelector:copySelector];
}

void CloneDummyWithoutLeak(Dummy *dummy, SEL cloneSelector) {
  __unused Dummy *dummyClone = [dummy performSelector:cloneSelector];
}

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    Dummy *dummy = [[Dummy alloc] init];
    for (;;) { @autoreleasepool {
      //CopyDummy(dummy);
      //CloneDummy(dummy);
      //CloneDummyWithoutLeak(dummy, @selector(clone));
      CopyDummyWithLeak(dummy, @selector(copy));
      [NSThread sleepForTimeInterval:1];
    }} 
  }
  return 0;
}

O único método, que causa vazamento de memória no meu exemplo, é CopyDummyWithLeak. O motivo é que o ARC não sabe, que copySelector retorna o objeto retido.

Se você executar a Ferramenta de vazamento de memória, poderá ver a seguinte imagem: insira a descrição da imagem aqui ... e não haverá vazamento de memória em nenhum outro caso: insira a descrição da imagem aqui

Pavel Osipov
fonte
6

Para tornar a macro de Scott Thompson mais genérica:

// String expander
#define MY_STRX(X) #X
#define MY_STR(X) MY_STRX(X)

#define MYSilenceWarning(FLAG, MACRO) \
_Pragma("clang diagnostic push") \
_Pragma(MY_STR(clang diagnostic ignored MY_STR(FLAG))) \
MACRO \
_Pragma("clang diagnostic pop")

Em seguida, use-o assim:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )
Ben Flynn
fonte
FWIW, eu não adicionei a macro. Alguém adicionou isso à minha resposta. Pessoalmente, eu não usaria a macro. O pragma está lá para solucionar um caso especial no código e os pragmas são muito explícitos e diretos sobre o que está acontecendo. Prefiro mantê-los no lugar em vez de escondê-los ou abstraí-los por trás de uma macro, mas sou apenas eu. YMMV.
21815 Scott Thompson
@ScottThompson Isso é justo. Para mim, é fácil procurar essa macro na minha base de código e geralmente também adiciono um aviso não silenciado para lidar com o problema subjacente.
Ben Flynn
6

Não suprima avisos!

Existem pelo menos 12 soluções alternativas para mexer no compilador.
Enquanto você está sendo esperto 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 em relação à sua intenção original. Suponha que parampode ser, nilse você 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 esta 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 do compilador, que está fadado a quebrar. Note-se que no momento presente, ele fez pausa em Swift .

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];
SwiftArchitect
fonte
3
A redação está muito errada. As rotas seguras não são mais seguras do que perigosas. É sem dúvida mais perigoso, porque esconde o aviso implicitamente.
Bryan Chen
Vou consertar o texto para não ser ofensivo, mas mantenho minha palavra. A única vez que acho aceitável o aviso de silenciamento é se eu não possuo o código. Nenhum engenheiro pode manter com segurança o código silenciado sem entender todas as conseqüências, o que significaria ler esse argumento, e essa prática é claramente arriscada; especialmente se você considerar as 12, inglês simples, alternativas robustas.
SwiftArchitect
1
Não. Você não entendeu meu argumento. Usar nãoperformSelectorOnMainThread é uma boa maneira de silenciar o aviso e tem efeitos colaterais. (isso não resolve o vazamento de memória) O extra suprime explicitamente o aviso de uma maneira muito clara. #clang diagnostic ignored
Bryan Chen
É verdade que executar um seletor em um - (void)método não é o problema real.
SwiftArchitect
e como você chama um seletor com vários argumentos por meio disso e fica seguro ao mesmo tempo? @SwiftArchitect
Catalin
4

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

honus
fonte
Na verdade, os blocos facilitam a criação acidental de um ciclo de retenção que o ARC não resolve. Eu ainda gostaria que houvesse um aviso do compilador quando você usou implicitamente selfatravés de um ivar (por exemplo, em ivarvez de self->ivar).
tc.
Você quer dizer como -Wimplicit-retem-se?
18415 OrangeDog
2

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];
    }
supersabbath
fonte
1

Se você não precisar passar nenhum argumento, uma solução fácil é usar valueForKeyPath. Isso é possível em um Classobjeto.

NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
    UIColor *brightPink = [uicolor valueForKeyPath:colorName];
    ...
}
arsênio
fonte
-2

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

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

Na sua turma que precisa chamar seu seletor, você tem uma propriedade @.

@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];
Damon
fonte
2
Ei Wu, obrigado, mas o objetivo de usar o NSSelectorFromString é quando você não sabe qual seletor deseja chamar durante o tempo de execução.
Eduardo Scoz