A interface do usuário trava até mesmo com performSelectorOnMainThread e waitUntilDone: NO

No meu viewController , eu tenho um timer que dispara a cada 20 segundos e chama uma atualização de local. então, quando a localização é atualizada, eu chamo um método via performSelectorOnMainThread . Por alguma razão, apesar de eu ter incluído waitUntilDone: NO , minha interface de usuário continua travando enquanto a verificação é executada. Alguém sabe como eu posso melhorar isso para que a tela não congele a cada 20 segundos? Obrigado!

-(void)viewDidLoad {

    NSTimer* myTimer = [NSTimer scheduledTimerWithTimeInterval: 20.0 target: self 
    selector: @selector(callAfterSixtySecond:) userInfo: nil repeats: YES];

    //other code
}

-(void) callAfterSixtySecond:(NSTimer*) t {

    locationManagerProfile.delegate = self;
    locationManagerProfile.desiredAccuracy = kCLLocationAccuracyBest; 
    [locationManagerProfile startUpdatingLocation];  
}

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation 
*)newLocation fromLocation:(CLLocation *)oldLocation {

    CLLocation *currentLocation = newLocation;

    if (currentLocation != nil) {

        [self performSelectorOnMainThread:@selector(buttonUpdate) withObject:nil 
        waitUntilDone:NO];   
    }
}

Por fim, minha interface é atualizada com este método:

-(void) buttonUpdate {

    [locationManagerProfile stopUpdatingLocation];
    NSString *userLatitude =[(PDCAppDelegate *)[UIApplication sharedApplication].delegate 
    getUserLatitude];
    NSString *userLongitude =[(PDCAppDelegate *)[UIApplication sharedApplication].delegate 
    getUserLongitude];

    NSString *placeLatitude = [[NSUserDefaults standardUserDefaults]
    stringForKey:@"savedLatitude"];

    NSString *placeLongitude = [[NSUserDefaults standardUserDefaults]
    stringForKey:@"savedLongitude"];

    NSString *distanceURL = [NSString stringWithFormat:@"http://www.website.com/page.php?
    lat1=%@&lon1=%@&lat2=%@&lon2=%@",userLatitude, userLongitude, placeLatitude, 
    placeLongitude];

    NSData *distanceURLResult = [NSData dataWithContentsOfURL:[NSURL 
    URLWithString:distanceURL]];

    NSString *distanceInFeet = [[NSString alloc] initWithData:distanceURLResult 
    encoding:NSUTF8StringEncoding];

    if ([distanceInFeet isEqualToString:@"1"]) {

        UIBarButtonItem *btnGo = [[UIBarButtonItem alloc] initWithTitle:@"Button A" 
        style:UIBarButtonItemStyleBordered target:self action:@selector(actionA)];
        self.navigationItem.rightBarButtonItem = btnGo;
        [self.navigationItem.rightBarButtonItem setTintColor:[UIColor 
        colorWithRed:44.0/255.0 green:160.0/255.0 blue:65.0/255.0 alpha:1.0]];  
        UIBarButtonItem *btnGoTwo = [[UIBarButtonItem alloc] initWithTitle:@"Button B" 
        style:UIBarButtonItemStyleBordered target:self action:@selector(actionB)];
        self.navigationItem.rightBarButtonItem = btnGoTwo; 
        self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:btnGo, 
        btnGoTwo, nil];
    }
    if ([distanceInFeet isEqualToString:@"0"]) {
        UIBarButtonItem *btnGo = [[UIBarButtonItem alloc] initWithTitle:@"Button C" 
        style:UIBarButtonItemStyleBordered target:self action:@selector(actionC)];
        self.navigationItem.rightBarButtonItem = btnGo;
        [self.navigationItem.rightBarButtonItem setTintColor:[UIColor 
        colorWithRed:44.0/255.0 green:160.0/255.0 blue:65.0/255.0 alpha:1.0]];        
        UIBarButtonItem *btnGoTwo = [[UIBarButtonItem alloc] initWithTitle:@"Button B" 
        style:UIBarButtonItemStyleBordered target:self action:@selector(actionB)];
        self.navigationItem.rightBarButtonItem = btnGoTwo;   
        self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:btnGo, 
        btnGoTwo, nil];
    }       
}
0

7 Respostas

Parece que você está buscando alguns dados de uma URL usando dataWithContentsOfURL: enquanto estiver no thread principal.

Isso parece que seria responsável pelo congelamento.

Tente movê-lo para que você só ligue para o thread principal quando os dados que você deseja apresentar na interface do usuário estiver pronto.

4
adicionado

viewDidLoad gets called on the main thread, you're adding the timer here so the selector is called on the main thread. You aren't in background. There are many ways to make the method execute concurrently, for example you can call dispatch_async inside it:

-(void) callAfterSixtySecond:(NSTimer*) t {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ^{
        locationManagerProfile.delegate = self;
        locationManagerProfile.desiredAccuracy = kCLLocationAccuracyBest; 
        [locationManagerProfile startUpdatingLocation];  
    });
}

Ou adicione o temporizador de forma assíncrona:

-(void)viewDidLoad {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ^{
        NSTimer* myTimer = [NSTimer scheduledTimerWithTimeInterval: 20.0 target: self 
        selector: @selector(callAfterSixtySecond:) userInfo: nil repeats: YES];
    });

    //other code
}
3
adicionado
Em que linha você está recebendo este erro?
adicionado o autor Ramy Al Zuhouri, fonte
obrigado! Eu estou recebendo um erro que diz passagem void (^) (void) para paramater do tipo incompatível 'unsigned long'. Alguma ideia de como consertar isso?
adicionado o autor user2492064, fonte

viewDidLoad gets called on the main thread, you're adding the timer here so the selector is called on the main thread. You aren't in background. There are many ways to make the method execute concurrently, for example you can call dispatch_async inside it:

-(void) callAfterSixtySecond:(NSTimer*) t {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ^{
        locationManagerProfile.delegate = self;
        locationManagerProfile.desiredAccuracy = kCLLocationAccuracyBest; 
        [locationManagerProfile startUpdatingLocation];  
    });
}

Ou adicione o temporizador de forma assíncrona:

-(void)viewDidLoad {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ^{
        NSTimer* myTimer = [NSTimer scheduledTimerWithTimeInterval: 20.0 target: self 
        selector: @selector(callAfterSixtySecond:) userInfo: nil repeats: YES];
    });

    //other code
}
3
adicionado
Em que linha você está recebendo este erro?
adicionado o autor Ramy Al Zuhouri, fonte
obrigado! Eu estou recebendo um erro que diz passagem void (^) (void) para paramater do tipo incompatível 'unsigned long'. Alguma ideia de como consertar isso?
adicionado o autor user2492064, fonte

A ideia com CLLocationManager é que você crie, configure e inicie uma vez quando seu aplicativo se interessar pelo local. Então, com base na configuração que você definir, ele ligará de volta ( locationManager: didUpdateToLocation: fromLocation: ou, melhor se puder, locationManager: didUpdateLocations: ) sempre que o local Mudou. Repetidamente iniciar e parar o gerenciador de localização é ineficiente e é improvável que você obtenha informações de localização precisas.

Depois de resolver isso, você provavelmente ficará bem.

Depois disso, você precisa refazer o encadeamento, pois o temporizador será acionado no encadeamento principal, portanto, voltar para o encadeamento principal realmente não altera nada (apenas adiciona um pequeno atraso).

Além disso, não escreva muito código. Particularmente, se é o mesmo código repetidamente. Por exemplo, se você deseja obter várias coisas dos padrões do usuário, não faça:

[[NSUserDefaults standardUserDefaults] stringForKey:@"savedLatitude"]
[[NSUserDefaults standardUserDefaults] stringForKey:@"savedLongitude"]

Melhor fazer:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]
[defaults stringForKey:@"savedLatitude"]
[defaults stringForKey:@"savedLongitude"]

(padrões do usuário é apenas um exemplo, isso se aplica a qualquer chamada de método que você está fazendo várias vezes sem motivo ...)

É um pequeno ganho de cada vez, mas tudo se soma (tanto ao custo de execução quanto à legibilidade/manutenção).

3
adicionado
A resposta de Seán Labastille é também um excelente ponto e contribuirá para a questão.
adicionado o autor Wain, fonte
@BillBurgess [NSUserDefaults standardDefaults] pode não ser o melhor exemplo, mas como regra geral a abordagem é válida, pois evitará o custo de qualquer processamento que o método do acessador faça.
adicionado o autor Wain, fonte
Usar [NSUserDefaults standardDefaults] ou atribuí-lo a uma variável terá o mesmo endereço de memória e não custa nada para a pilha de memória, apenas se torna uma preferência pessoal. Você quer linhas extras de código ou linhas ligeiramente mais longas.
adicionado o autor Bill Burgess, fonte

A ideia com CLLocationManager é que você crie, configure e inicie uma vez quando seu aplicativo se interessar pelo local. Então, com base na configuração que você definir, ele ligará de volta ( locationManager: didUpdateToLocation: fromLocation: ou, melhor se puder, locationManager: didUpdateLocations: ) sempre que o local Mudou. Repetidamente iniciar e parar o gerenciador de localização é ineficiente e é improvável que você obtenha informações de localização precisas.

Depois de resolver isso, você provavelmente ficará bem.

Depois disso, você precisa refazer o encadeamento, pois o temporizador será acionado no encadeamento principal, portanto, voltar para o encadeamento principal realmente não altera nada (apenas adiciona um pequeno atraso).

Além disso, não escreva muito código. Particularmente, se é o mesmo código repetidamente. Por exemplo, se você deseja obter várias coisas dos padrões do usuário, não faça:

[[NSUserDefaults standardUserDefaults] stringForKey:@"savedLatitude"]
[[NSUserDefaults standardUserDefaults] stringForKey:@"savedLongitude"]

Melhor fazer:

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]
[defaults stringForKey:@"savedLatitude"]
[defaults stringForKey:@"savedLongitude"]

(padrões do usuário é apenas um exemplo, isso se aplica a qualquer chamada de método que você está fazendo várias vezes sem motivo ...)

É um pequeno ganho de cada vez, mas tudo se soma (tanto ao custo de execução quanto à legibilidade/manutenção).

3
adicionado
A resposta de Seán Labastille é também um excelente ponto e contribuirá para a questão.
adicionado o autor Wain, fonte
@BillBurgess [NSUserDefaults standardDefaults] pode não ser o melhor exemplo, mas como regra geral a abordagem é válida, pois evitará o custo de qualquer processamento que o método do acessador faça.
adicionado o autor Wain, fonte
Usar [NSUserDefaults standardDefaults] ou atribuí-lo a uma variável terá o mesmo endereço de memória e não custa nada para a pilha de memória, apenas se torna uma preferência pessoal. Você quer linhas extras de código ou linhas ligeiramente mais longas.
adicionado o autor Bill Burgess, fonte

Quando o retorno do cronômetro é chamado, você está no thread principal. Então, quando você chamar o método buttonUpdate usando "performSelectorOnMainThread", simplesmente enfileire essa mensagem no loop de execução principal e continue a execução. Então, quando é hora de buttonUpdate ser executado, ele bloqueia a interface do usuário devido à chamada de rede síncrona "dataWithContentsOfURL" executada dentro do thread principal. A melhor maneira de corrigir seu problema é chamar "buttonUpdate" imediatamente (não use performSelectorOnMainThread como você já está no thread principal) e chame seu initWithURL de dentro de um GCD async ou NSOperationQueue. Enquanto espera que os dados sejam atualizados, você pode mostrar uma "mensagem de atualização" no botão e assim que seu bloco assíncrono ou NSOperation concorrente terminar, você pode finalmente atualizar o botão para seu estado final (não se esqueça de fazer a atualização o thread principal).

2
adicionado

Quando o retorno do cronômetro é chamado, você está no thread principal. Então, quando você chamar o método buttonUpdate usando "performSelectorOnMainThread", simplesmente enfileire essa mensagem no loop de execução principal e continue a execução. Então, quando é hora de buttonUpdate ser executado, ele bloqueia a interface do usuário devido à chamada de rede síncrona "dataWithContentsOfURL" executada dentro do thread principal. A melhor maneira de corrigir seu problema é chamar "buttonUpdate" imediatamente (não use performSelectorOnMainThread como você já está no thread principal) e chame seu initWithURL de dentro de um GCD async ou NSOperationQueue. Enquanto espera que os dados sejam atualizados, você pode mostrar uma "mensagem de atualização" no botão e assim que seu bloco assíncrono ou NSOperation concorrente terminar, você pode finalmente atualizar o botão para seu estado final (não se esqueça de fazer a atualização o thread principal).

2
adicionado