fy>
Clique aqui para ler todos os artigos desta edição
Otimização de aplicações Java ME
Em qualquer ambiente computacional, a aplicação de técnicas de otimização é uma tarefa de extrema relevância. Na área de computação móvel, a importância dessa tarefa chega a ser fundamental e às vezes decisiva no processo de desenvolvimento de software. As limitações impostas pelo hardware dos dispositivos móveis acabam por promover um grande impacto para o desenvolvedor proveniente de ambientes mais robustos. Preocupações como o tamanho final da aplicação e o espaço disponível para persistência podem eventualmente não ter muita relevância em computadores desktops ou grandes servidores, mas, por outro lado, são uma constante durante o desenvolvimento de software para dispositivos móveis.
Uma das linguagens de programação mais utilizadas no desenvolvimento de softwares para estes dispositivos é o Java ME. Essa versão Java foi projetada especificamente para ser utilizada em dispositivos computacionalmente limitados. Mesmo com essas características o desenvolvedor precisa estar atento ao seu código e no quanto de recursos de hardware será demandado.
Esse artigo aborda alguns detalhes a respeito de otimizações de código JME com o propósito de aumentar a performance e reduzir o tamanho do aplicativo ou da sua área de execução. Algumas técnicas de otimização serão apresentadas e discutidas, e eventualmente serão tratadas através de exemplos. Também, serão apresentadas técnicas de mensuração do tempo de execução de códigos JME, permitindo conhecer onde é realmente necessário otimizar.
Quando otimizar
O termo otimizar, segundo o dicionário da língua portuguesa, significa estabelecer o valor ótimo de algo ou alguma coisa, através da criação das condições mais favoráveis possíveis para chegar a esse objetivo. Esse é um conceito geral, entretanto na área de desenvolvimento de software, é necessário atentar para algumas considerações antes de prematuramente determinar a otimização como uma prioridade.
Em primeiro lugar, em alguns momentos, desenvolver pensando em otimizar certamente irá aumentar o tempo para o término de uma determinada tarefa. Quando se tem como meta principal a obtenção de um algoritmo rápido e leve, certamente o tempo de desenvolvimento dele será acrescido. Isso devido ao fato de que parte desse tempo estaria sendo “desperdiçado” aprimorando código que ainda nem foi testado. Nesse caso, o esforço principal não estaria na funcionalidade e sim na sua performance e na sua otimização. Com isso, a otimização pode tornar-se inimiga do software funcional. Tentar otimizar um código que nem ao menos foi testado ou sequer teve sua performance avaliada é um erro. Martin J. Wells em J2ME Game Programming é enfático ao afirmar: “Não tente otimizar código que você pensa que é lento, otimize aquilo que você sabe que é lento!”.
Outro fator importante a considerar é que essa preocupação precoce em otimização pode inclusive introduzir erros sérios e difíceis de descobrir nos algoritmos e também tornar esse código impossível de ser mantido.
Existe ainda um detalhe muito importante, todas essas afirmações são válidas quando o objetivo da otimização é o melhoramento da performance de um determinado algoritmo. Eventualmente, a preocupação principal é acompanhar o tamanho final de uma aplicação. Essa preocupação é uma constante quando se trata de desenvolvimento de software para dispositivos computacionalmente limitados.
Nesse caso, é vital atentar para técnicas de otimização de tamanho enquanto o desenvolvimento de um algoritmo avança. Muitas vezes, o tamanho máximo de uma aplicação JME voltada para aparelhos celulares não deve ultrapassar uma determinada quantidade de Kbytes (varia entre 60 e 100 Kbytes). Para não ultrapassarmos esse limite, é necessário um acompanhamento constante do crescimento do código executável da aplicação.
À medida que novos recursos são incorporados à aplicação ou novos algoritmos são desenvolvidos, o tamanho da aplicação precisa ser checado, senão podem acontecer surpresas como, ter um código ou uma aplicação inteiramente pronta e absolutamente funcional sem condições de ser executado na plataforma alvo, simplesmente por estouro de memória.
Assim, quando se trata de otimização de performance é interessante primeiro ter o algoritmo ou a aplicação em um estado absolutamente funcional para a seguir poder aplicar técnicas de melhoramento de performance. Agora, se existe também a preocupação com um limite para o tamanho final da aplicação, é importante atentar antecipadamente para técnicas de otimização do tamanho e aplicá-las onde forem necessárias mesmo antes da aplicação ter sido finalizada e está funcionando.
Otimização de alto nível vs Otimização de baixo nível
Existem diversas técnicas que objetivam efetuar a otimização de performance e de tamanho. Tais técnicas poderiam ser agrupadas em duas grandes áreas. Otimização de Alto Nível onde o foco principal seria o design do código em si e dos seus algoritmos, atentando bastante para a estrutura geral do que está sendo desenvolvido. A outra técnica consiste em Otimização de Baixo Nível, onde a atenção é direcionada para pequenos trechos de código, muitas vezes mudanças leves em uma linha de programação apenas.
Otimização de Alto Nível
Nesse nível, talvez a principal consideração a ser levantada seja escolher o algoritmo correto ou a solução mais adequada para um determinado problema.
Podem ser encontradas as mais diversas maneiras de se resolver um mesmo problema, mas sem dúvida alguma, em se tratando de performance de execução, existirão diferenças entre tais soluções. Dessa forma, a escolha do algoritmo correto para resolver um problema seria mais produtiva do que tentar aperfeiçoar algoritmos medíocres utilizando técnicas de baixo nível.
Imaginando o exemplo de uma situação onde é necessário ordenar um array de strings de tamanho de até 20 elementos. Existem classes básicas em outras edições Java (JEE e JSE) que implementam funcionalidades de ordenação. Porém, em JME não há classes ou métodos com tais funcionalidades e no caso dessa necessidade a alternativa mais interessante seria a criação de um método em alguma classe da sua aplicação para implementar algum algoritmo de ordenação.
Um exemplo de algoritmo de ordenação é o algoritmo da bolha. Ele é um dos mais controversos algoritmos de ordenação, pois é de implementação simples e eficiente quando se trata de ordenação de um pequeno conjunto de valores. Contudo, não é aconselhado para ordenação de um conjunto grande de dados. Para isso, existem diversos outros métodos de ordenação como o quicksort.
No caso da ordenação dos 20 elementos, o método da bolha torna-se o mais adequado devido ao fato do conjunto de dados ser pequeno. Esse tipo de decisão que leva em consideração o contexto em que o algoritmo será aplicado consiste em uma decisão de otimização de alto nível.
Mas, pode acontecer também de uma determinada funcionalidade requerida já existir implementada em alguma classe de algum pacote que ainda não faça parte da aplicação. Se tal funcionalidade estiver presente em apenas um método dessa mesma classe, é interessante considerar a possibilidade de implementá-la na própria aplicação, copiando-a como um novo método. Assim, evita-se usar uma classe inteira quando o que se precisa é de apenas um método existente em uma classe.
Novamente, escolher o algoritmo correto é extremamente importante e partindo-se do princípio de que quase tudo, em se tratando de algoritmos básicos de programação, já foi desenvolvido e suficientemente testado, é sempre bom perder algum tempo consultando materiais em sites ou revistas especializadas e aproveitar o conhecimento presente nessas fontes.
Otimização de Baixo Nível
A otimização de baixo nível normalmente trata de técnicas de codificação mais específicas, onde na maioria das vezes a atenção é voltada para o que pode ser melhorado na implementação de métodos, na sua declaração e nos seus parâmetros de entrada. Por vezes, mudanças minuciosas podem trazer benefícios consideráveis em performance mas também podem proporcionar pouco ou até mesmo nenhum resultado prático.
Olhando atentamente para um pequeno bloco de código é possível identificar pequenas mudanças que podem trazer resultados práticos de fato na sua performance. Mudar o tipo de um determinado campo de String para inteiro, reduzir número de parâmetros de entrada em métodos, ponderar sobre a existência de algumas declarações dentro de laços de repetição são exemplos de mudanças que podem otimizar a execução de um bloco de código qualquer.
Às vezes, a decisão sobre onde otimizar pode recair em apenas uma linha de código e o resultado pode vir a ser altamente proveitoso. A preocupação principal na otimização de baixo nível está justamente nas opções disponíveis e viáveis de melhorar a performance de um pequeno bloco de código ou até mesmo uma única linha. Nesse momento é extremamente importante a utilização de alguma ferramenta que possibilite medir o quanto realmente um dado bloco de código está influenciando na performance da aplicação, deixando-a lenta. Assim, o uso de profiles permite calcular a performance do seu código a cada nova mudança, orientando-o até que ponto houve alguma melhora.
Aplicação exemplo
De posse das possíveis abordagens para se otimizar uma aplicação, vamos buscar ilustrar como elas podem ser aplicadas através de um exemplo. A aplicação exemplo utilizada tem o propósito de executar a movimentação horizontal de duas imagens na tela, uma no topo e outra na parte de baixo da mesma. Cada imagem é movimentada em uma velocidade diferente.
Essa aplicação tem apenas duas classes. A classe RapidoLeve (veja a Listagem 1) é um MIDlet simples, utilizado para chamar a classe Apresenta que mostra uma animação utilizando a classe de baixo nível Canvas. Veja o código da classe Apresenta na Listagem 2.
A animação é totalmente implementada em uma linha de execução paralela (thread) implementada na classe pela interface Runnable e consiste apenas em uma mesma imagem se movimentando horizontalmente em dois locais da janela do dispositivo: na parte superior à velocidade de 2 pixels a cada 10ms e na parte inferior à uma velocidade de 1 pixel a cada 10ms. Também para cada uma das imagens é mantido um controle sobre o número de passagens da mesma pela janela do dispositivo e o tempo da última passagem completa. Um contador mantém o controle sobre o número máximo de descolamentos das imagens, que nesse caso está setado para
Listagem 1. Classe que estende uma MIDlet para controlar a aplicação.
1: /* RapidoLeve.java
2: */
3:
4: import javax.microedition.midlet.*;
5: import javax.microedition.lcdui.*;
6:
7: /**
8: * @author Eloi Jr
9: */
10:
11: public class RapidoLeve extends MIDlet {
12: private Apresenta cvApresenta;
13:
14: public RapidoLeve() {
15: cvApresenta = new Apresenta(this);
16: }
17:
18: public void startApp() {
19: ...