Como melhorar drasticamente seu aplicativo com uma tarde e instrumentos – Sam Soffes
Postado em
Eu era se gabando no Twitter sobre como acabei de melhorar meu aplicativo com alguns ajustes simples. Eu queria escrever uma postagem rápida sobre o que fiz que realmente ajudou e que provavelmente ajudará a maioria das pessoas. Esse material é um pouco específico do aplicativo, mas acho que você verá paralelos com o seu aplicativo.
Sintomas
Meu aplicativo extrai uma tonelada de dados da rede e os coloca no Core Data quando você faz login pela primeira vez. Ao usar o aplicativo, percebi que o desempenho é péssimo no início e depois volta ao normal. (Todas as visualizações da minha tabela rolam a 60fps, mas vou guardar isso para outro post. Desculpe. Tive que incluir isso aí. Estou muito orgulhoso.) Isso foi preocupante, pois geralmente funciona muito bem (ok, agora cansei de me gabar de minhas células), então investiguei.
Só para você saber, estou fazendo toda a minha rede, análise de dados e inserção no Core Data em threads de segundo plano via NSOperationQueue.
Os problemas
Depois de executar Instruments com o instrumento de alocação de objetos, percebi que estava usando cerca de 22 MB de memória enquanto baixava todos esses dados. Na minha opinião, isso é muito alto. Vou adicionar isso à lista de coisas para mexer.
Notei também que meu NSDate A categoria para analisar strings de data ISO8601 (maneira padrão de colocar uma data em JSON) demorava cerca de 7,4 segundos usando o instrumento de cronômetro. Totalmente inaceitável. Adicionado à lista.
Depois de brincar um pouco mais, percebi que muito tempo estava sendo gasto em um dos meus NSString categorias, especificamente em NSRegularExpression. Isso parece irritante, então vou deixar isso para o final.
As soluções
Memória
Eu tinha alguns palpites sobre como reduzir o uso de memória ao converter grandes quantidades de strings JSON em NSManagedObjectS. Meu palpite era que uma tonelada de objetos precisavam ser liberados automaticamente, mas o NSAutoreleasePool não estava sendo drenado até que a operação terminasse. A solução simples para isso adicione um bem colocado NSAutoreleasePool em torno do código do problema. Foram necessárias algumas tentativas para chegar ao lugar certo. Eu o colocaria onde acho que a maioria dos objetos temporários estava sendo criada e depois observaria o instrumento de alocação de objetos para ter certeza de que ficou mais plano.
Aqui foi minha primeira tentativa:

Vê como ele sobe e desce um pouco e depois aumenta por um tempo e finalmente diminui? Isso é um sinal de que há outro loop aninhado mais abaixo que deve ter um pool ao seu redor. No primeiro fez um pouco e depois esgotou (provavelmente porque fez menos coisa naquela operação). Como a segunda protuberância gigante (observe que o pico é de 23 MB ou mais) não diminui por um tempo, sei que devo procurar outro loop mais profundo. Espero que isso faça sentido. Assim que você chegar lá, você será atingido de repente depois de tropeçar um pouco. Você verá.
Depois de movê-lo para um loop mais aninhado, aqui está o resultado:

Assim que coloquei no lugar certo, estava usando menos de 2 MB de memória para todo o processo! Pontuação! Próximo problema.
Coisas de data
A coisa do encontro me deixou perplexo por um tempo. Eu estava usando ISO8601Parser (uma subclasse de NSFormatter) que estava funcionando muito, muito bem em comparação com NSDateFormatter. Depois de observar o instrumento temporizador, percebi que a maior parte desse tempo era gasto em classes do sistema como NSCFCalendar. Presumi que houvesse uma maneira melhor. Eu tentei voltar para NSDateFormattermas isso não funcionou bem e ainda não era uma boa memória e velocidade.
Como isenção de responsabilidade, meu foco é o Objective-C. Eu amo isso. Não sou um daqueles engenheiros que diz “ei, deveríamos reescrever isso em C” o tempo todo, mas ei, deveríamos reescrever isso em C. Eu fiz… e o resultado foi surpreendente!
Aqui está o código:
#include
+ (NSDate *)dateFromISO8601String:(NSString *)string {
if (!string) {
return nil;
}
struct tm tm;
time_t t;
strptime((string cStringUsingEncoding:NSUTF8StringEncoding), "%Y-%m-%dT%H:%M:%S%z", &tm);
tm.tm_isdst = -1;
t = mktime(&tm);
return (NSDate dateWithTimeIntervalSince1970:t + ((NSTimeZone localTimeZone) secondsFromGMT));
}
- (NSString *)ISO8601String {
struct tm *timeinfo;
char buffer(80);
time_t rawtime = (self timeIntervalSince1970) - ((NSTimeZone localTimeZone) secondsFromGMT);
timeinfo = localtime(&rawtime);
strftime(buffer, 80, "%Y-%m-%dT%H:%M:%S%z", timeinfo);
return (NSString stringWithCString:buffer encoding:NSUTF8StringEncoding);
}
Veja, não é muito louco. Usar o material de data C levou minha análise de data de 7,4 segundos para 300 ms. Fale sobre um aumento de desempenho! (Atualizei a categoria NSDate do SSTokit para usar este novo código.)
Expressão regular
eu tenho vários NSString categorias em meu aplicativo para fazer várias coisas. Alguns deles foram chamados durante o processo que eu estava tentando otimizar. Eu me aprofundei no instrumento de perfil de tempo e percebi que (NSRegularExpression regularExpressionWith...) estava demorando muito. Isso faz muito sentido, já que compila seu regex para usar mais tarde e eu fazia isso todas as vezes. Solução simples:
- (NSString *)camelCaseString {
static NSRegularExpression *regex = nil;
if (!regex) {
regex = ((NSRegularExpression alloc) initWithPattern:@"(?:_)(.)" options:0 error:nil);
}
// Use regex...
return string;
}
Esta foi realmente a parte mais fácil 🙂
Conclusões
Portanto, usar Instrumentos para rastrear códigos lentos ou incorretos é realmente fácil quando você pega o jeito. Comece com o instrumento de vazamentos se você for novo. Você não deve ter nenhum vazamento (conhecido) em seu aplicativo.
Depois de entender isso (ou ficar tão frustrado ao tentar rastreá-lo, você desiste e passa para outra coisa), faça o próximo instrumento de alocação de objetos. Você pode observar o gráfico e ver quantos objetos você tem vivos. Se você observar um grande pico que nunca diminui, provavelmente você tem uma tonelada de memória que provavelmente não precisa, mas ainda tem uma referência para que ela não apareça em vazamentos. Adicionar pools de liberação automática em torno de loops que realizam muito processamento sempre ajuda.
Por fim, use o instrumento de perfil de tempo para ver o que está demorando muito e otimizar tudo isso. Isso é o mais divertido, pois é fácil ver o que está acontecendo e o quanto você melhorou com as alterações que acabou de fazer. A chave para tornar este instrumento útil são as caixas de seleção à esquerda. Ativar apenas o Objective-C ou alternar a árvore de pilha invertida é realmente útil.
Isso é difícil
Não se sinta mal, especialmente se você for novo nisso. Essa coisa é difícil. Todas as minhas soluções listadas acima são bastante simples. Passei quase um dia inteiro pensando nessas poucas coisas. A maior parte do tempo que você gasta será rastreando problemas. Consertá-los geralmente é bem simples, especialmente depois de fazer isso algumas vezes. Isso é difícil. Você é inteligente. 🙂
