Conhecendo o Android NDK: integrando código nativo às suas aplicações Android
description
Transcript of Conhecendo o Android NDK: integrando código nativo às suas aplicações Android
Seediscussions,stats,andauthorprofilesforthispublicationat:https://www.researchgate.net/publication/260460628
ConhecendooAndroidNDK:integrandocódigonativoàssuasaplicaçõesAndroid
ARTICLE·JANUARY2014
READS
115
1AUTHOR:
LeandroLuque
CentroPaulaSouza
23PUBLICATIONS1CITATION
SEEPROFILE
Availablefrom:LeandroLuque
Retrievedon:11March2016
1/19
Conhecendo o Android NDK Integrando código nativo às suas aplicações Android
Porque este artigo é útil
Este artigo apresenta um introdução ao Android Native Development Kit - NDK, permitindo
a você tirar o máximo de proveito do dispositivo em aplicações que consomem muitos recursos de
CPU ou que precisam aproveitar um hardware específico. Para muitas aplicações, o desempenho
obtido com o desenvolvimento baseado apenas no Android Software Development Kit - SDK pode
não ser satisfatório. Nestes casos, o uso do NDK contribui para que a aplicação atenda aos
requisitos de desempenho desejados.
O mercado de dispositivos móveis cresce em ritmo acelerado no Brasil e no mundo.
Segundo a International Data Corporation - IDC, o crescimento mundial anual deve ficar em torno
de 7,3% no fim de 2013. No Brasil, o volume de vendas de smartphones no segundo trimestre de
2013 foi 110% superior ao do mesmo período de 2012, ultrapassando, pela primeira vez na história,
a venda de celulares comuns. Quando considerados os tablets, o aumento foi de 151% no mesmo
período.
De cada 10 destes smartphones e tablets vendidos no país, 9 possuem o sistema operacional
Android. No mundo, este número fica em torno de 7 a cada 10 (Tabela 1).
Sistema Operacional Fatia de Mercado (Smartphone) Fatia de Mercado (Tablet)
Android 75,3% 62,5%
iOS 16,9% 32,5%
Windows Phone 3,9% 4,0%
BlackBerry OS 2,7% 0,3%
Outros 1,2% 0,7%
Tabela 1. Fatia do mercado mundial dos sistemas operacionais de dispositivos móveis. Fonte: IDC (2013).
Esse crescimento do mercado, aliado à maior capacidade dos dispositivos móveis, está
impulsionando a demanda por aplicações e jogos cada vez mais complexos, rápidos e interativos.
Isto pode ser facilmente percebido pelo volume de jogos com alta complexidade gráfica que vêm
sendo comercializados e também por aplicações baseadas em processamento de imagens e realidade
aumentada que possibilitam a tradução simultânea de imagens com textos em outros idiomas (p.ex.:
Word Lens e CamTranslator), a pesquisa por informações baseada em imagens (p.ex.: Google
Goggles), a simulação de mobílias em ambientes a partir de imagens da câmera (p.ex.: Home
Design 3D), entre muitas outras.
Para muitas das aplicações citadas, o desempenho obtido com o desenvolvimento baseado
apenas no Android Software Development Kit - SDK pode não ser satisfatório. Nestes casos, o uso
2/19
do Android Native Development Kit – NDK contribui para que a aplicação atenda aos requisitos de
desempenho desejados.
Neste contexto, este artigo apresenta o NDK e como ele pode ser integrado ao SDK, para
que você possa tirar o máximo de proveito do dispositivo em aplicações que consomem muitos
recursos de CPU ou que precisam aproveitar um hardware específico. Será utilizada uma aplicação
exemplo, desenvolvida e comentada passo-a-passo, para esclarecer as etapas necessárias para
atingir o objetivo proposto.
Integração de Aplicações Java com Código Nativo Existem duas alternativas principais para a integração de aplicações Java, não apenas
móveis, com código nativo. A primeira delas envolve a criação de processos do sistema operacional
para a execução do código nativo. Uma forma de fazer isso é por meio da classe ProcessBuilder,
disponível a partir do Java 5 (Listagem 1). O código apresentado nesta listagem é autoexplicativo e
permite a recuperação da saída padrão produzida pelo comando cmd /c dir /ad, executado na pasta
raiz – para sistemas Windows.
Esse mesmo recurso poderia ser utilizado para executar um código nativo que realiza um
cálculo matemático, por exemplo. Entre outras formas, a recuperação do resultado poderia ser feita
a partir da saída padrão, como no exemplo apresentado, ou mesmo por meio de arquivos – o código
nativo gravaria um arquivo como saída e o código Java realizaria a leitura deste arquivo e
recuperaria o resultado.
Listagem 1. Exemplo de código que executa o comando cmd /c dir /ad e exibe as subpastas da pasta raiz (para sistemas Windows). // ... package e imports
public class TesteProcessBuilder {
public static void main(String[] args) {
// Comando que será executado: cmd /c dir /ad
// A array contém em ordem: comando parâmetro1 parâmetro2 ...
String[] comando = {"cmd", "/c", "dir", "/ad"};
// Cria um construtor de processo.
ProcessBuilder construtorProcesso = new ProcessBuilder(comando);
// Pasta de trabalho relacionada ao comando (raiz).
construtorProcesso.directory(new File("c:\\"));
try {
// Tenta iniciar o processo.
Process processo = construtorProcesso.start();
// Aguarda a finalizaçao do processo.
processo.waitFor();
// Lê a saída do comando.
BufferedReader in = new BufferedReader(new
InputStreamReader(processo.getInputStream()));
System.out.println("Saída do comando: cmd /c dir /ad");
String saida;
while ((saida = in.readLine()) != null) {
System.out.println(saida);
}
} catch (IOException erro) {
System.out.println("Sentimos muito. Ocorreu um erro durante a execução do programa.");
// ...
} catch (InterruptedException erro) {
System.out.println("Sentimos muito. Ocorreu um erro durante a execução do programa.");
// ...
}
}
}
Existem algumas implicações relacionadas a esta abordagem. Uma delas é a necessidade do
código nativo ser executável na plataforma em questão. Outra está relacionada à comunicação, que
geralmente envolverá o processamento de textos e conversões. Ainda, não sendo possível acessar
diretamente variáveis e métodos da máquina virtual do Java, as informações que o método nativo
necessita devem ser fornecidas previamente via parâmetros ou arquivos.
3/19
A outra abordagem envolve o uso da JNI – Java Native Interface, um padrão que permite
que bibliotecas, não código executável, sejam integradas ao código Java. É por meio dela que
aplicações Android NDK conseguem executar códigos implementados em C/C++. Diferentemente
da outra abordagem, a JNI possibilita a interação direta com a máquina virtual do Java, não
apresentando as implicações citadas.
A seguir, ela será descrita mais detalhadamente.
Java Native Interface - JNI Para utilizar a Java Native Interface – JNI em um projeto, além do código nativo ter que ser
escrito seguindo algumas convenções, a biblioteca onde o código nativo se encontra deve ser
carregada no código Java, onde também devem ser escritas as assinaturas dos métodos nativos. Por
fim, basta invocar os métodos cujas assinaturas foram definidas e o código nativo será
automaticamente executado.
A seguir, cada uma destas etapas e alguns detalhes importantes sobre elas serão descritos.
Carregamento da biblioteca nativa No código Java, antes de acessar métodos nativos, a biblioteca que os contém deve ser
carregada por meio do comando System.loadLibrary ou System.load. Eles são geralmente
colocados em um bloco estático, pois o carregamento precisa ser feito apenas uma única vez.
(Listagem 2). Caso seja realizado mais de uma vez, os carregamentos subsequentes são
desconsiderados.
Listagem 2. Exemplo de código que carrega uma biblioteca chamada “minhabiblioteca”. //...
public class LadoJava {
// ...
static {
System.loadLibrary("minhabiblioteca");
// ou System.load("caminhoCompleto/minhabiblioteca.dll");
}
// ...
}
No caso do método loadLibrary, o Java segue um padrão específico por plataforma para
encontrar a biblioteca informada. Como exemplo, caso seja um sistema Windows, ele procurará por
uma biblioteca chamada minhabiblioteca.dll. Caso seja um sistema Linux, como no caso do
Android, ele procurará por libminhabiblioteca.so. O caminho onde a biblioteca é procurada é
especificado na variável java.library.path.
Para o caso do método load, uma caminho completo para a biblioteca deve ser informado, o
que pode ser interessante em alguns casos.
Assinaturas nativas no código Java Além do carregamento da biblioteca, o código Java deve criar assinaturas de métodos que
serão associadas ao código nativo. Estas assinaturas exigem o modificador native na sua declaração.
Na Listagem 3, são definidas duas assinaturas, que serão associadas a dois métodos nativos.
Listagem 3. Exemplo assinatura de método nativo no código Java. package br.com.fatec.projetomm;
//...
public class LadoJava {
// ... Carregamento da biblioteca ...
public native void fazAlgumaCoisa();
public native static void fazAlgumaOutraCoisa();
}
A seguir, veremos como o código nativo deve ser criado para que o código Java consiga
executá-lo.
Declarações do código nativo
4/19
Existem duas formas de declarar no código nativo os métodos especificados com native no
código Java. A primeira delas envolve seguir algumas convenções da JNI e a forma mais rápida e
fácil de segui-las é utilizar um utilitário que acompanha o Java SDK e permite a geração do
cabeçalho do código-fonte nativo: o javah.
A Listagem 4 apresenta alguns exemplos de uso deste utilitário – os comentários estão no
formato Windows. Para executar os comandos desta listagem, é necessário que a pasta “bin” do
SDK esteja na variável de ambiente PATH e as classes especificadas como parâmetro estejam na
variável CLASSPATH. Os arquivos gerados pelo utilitário são gravados na pasta de trabalho atual.
Listagem 4. Exemplo de chamada do programa javah. :: Assinatura do comando.
javah [opções] <caminho completo da classe>[...]
:: Criando o cabeçalho C para a classe br.com.fatec.projetomm.LadoJava.
javah –jni br.com.fatec.projetomm.LadoJava
:: A opção –jni é padrão, portanto, pode ser omitida.
javah br.com.fatec.projetomm.LadoJava
A Listagem 5 apresenta o arquivo gerado para a classe br.com.fatec.projetomm.LadoJava.
Listagem 5. Arquivo br_com_fatec_projetomm_LadoJava.h gerado por meio do programa javah. /* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class br_com_fatec_projetomm_LadoJava */
#ifndef _Included_br_com_fatec_projetomm_LadoJava
#define _Included_br_com_fatec_projetomm_LadoJava
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: br_com_fatec_projetomm_LadoJava
* Method: fazAlgumaCoisa
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_br_com_fatec_projetomm_LadoJava_fazAlgumaCoisa
(JNIEnv *, jobject);
/*
* Class: br_com_fatec_projetomm_LadoJava
* Method: fazAlgumaOutraCoisa
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_br_com_fatec_projetomm_LadoJava_fazAlgumaOutraCoisa
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
Como pode ser percebido no código gerado pelo javah, além da inclusão da biblioteca
“jni.h” e de algumas definições, todo método nativo inicia com JNIEXPORT <tipo de retorno>
JNICALL e é nomeado com o prefixo “Java_”, seguido por um nome de pacote, de classe e de
método. Os pontos “.” presentes nos nomes devem ser substituídos pelo sinal underscore “_”. Entre
os nomes do pacote, da classe e do método também deve existir um underscore. Estes nomes
correspondem aos da classe Java na qual foi implementado o método com o modificador native.
As referências a JNIEXPORT e JNICALL podem ser omitidas no Linux, pois não têm
função, mas são geralmente mantidas para compatibilidade com o Windows, onde eliminam a
necessidade de arquivos adicionais para a definição de módulo, entre outras coisas.
A outra forma de declarar os métodos nativos seria, ao invés de seguir este padrão de
nomeação, sobrescrever o método JNI_OnLoad(JavaVM* vm, void* reserved), executado
automaticamente quando a biblioteca é carregada, e nele registrar os métodos nativos por meio da
5/19
função JNI RegisterNatives. Neste caso, os nomes dos métodos poderiam ser arbitrários no código
nativo. Esta função deve retornar um número inteiro com a versão do JNI exigida pela biblioteca.
Além destas convenções, para permitir que métodos nativos acessem os recursos disponíveis
na máquina virtual onde foi iniciada a sua execução, dois parâmetros são passados para eles: um
ponteiro de interface, que aponta para um array de funções JNI, e uma referência para o objeto
(jobject) onde a chamada foi realizada no código Java – veja a definição de fazAlgumaCoisa.
Caso o código nativo seja executado a partir de um contexto estático no código Java, não há
objeto associado a chamada e, neste caso, a classe (jclass) do objeto é passada por parâmetro – veja
a definição de fazAlgumaOutraCoisa. No código nativo em C, jobject pode ser utilizado no lugar de
jclass, porque este é apenas um apelido para jobject. Este não é o caso do C++, onde há uma
definição de tipos e subtipos.
No exemplo apresentado, o objeto passado para fazAlgumaCoisa seria da classe LadoJava,
onde foi declarada a assinatura do método nativo.
Estes parâmetros são transparentes para o código Java que invoca o método nativo
(Listagem 6), ou seja, não precisam ser explicitamente passados pelo programador. Desta forma, as
chamadas ao código nativo podem ser feitas como se a interação fosse com outro código Java
qualquer.
Listagem 6. Demonstração da passagem implícita de parâmetros na invocação de código nativo. //...
public class LadoJava {
// ... Carregamento da biblioteca ...
// ... Declaração da assinatura dos métodos nativos ...
void outroMetodoQualquer() {
// Perceba que, embora a implementação nativa do método "fazAlgumaCoisa" receba 2 parâmetros,
// eles são passados implicitamente pelo Java.
System.out.println(fazAlgumaCoisa());
}
}
O ponteiro de interface é do tipo JNIEnv e possibilita o acesso a diversas funções JNI. Entre
outras coisas, estas funções permitem a criação de objetos na memória controlada pela máquina
virtual. Alguns exemplos de funções JNI são apresentados no código da Listagem 7.
Mapeamento de dados e parâmetros Para que haja compatibilidade entre as variáveis criadas no código Java e no nativo, foram
criados mapeamentos no código nativo para os tipos primitivos e objetos Java, conforme listado na
Tabela 2.
Tipo Java Tipo Nativo
Object jobject
Class jclass
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
6/19
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray
Throwable jthrowable
String jstring
Tabela 2. Mapeamento entre tipos Java e nativos.
Conforme comentado anteriormente para o relacionamento entre jclass e jobject em C, todos
os tipos de classes (arrays, jstring, jthrowable) são apenas apelidos para jobject.
Antes de prosseguir, vejamos um exemplo que envolve o que foi apresentado até então.
Suponha que seja necessário criar e preencher uma ArrayList com algumas Strings no código nativo
e retorná-la para a aplicação Java que o executou (Listagem 7).
Listagem 7. Código nativo que cria e preenche uma ArrayList. //...
JNIEXPORT jobject JNICALL Java_br_com_fatec_projetomm_Principal_criarArrayList(
JNIEnv *env, jobject objeto) {
// Recupera uma referência para a classe java.util.ArrayList.
jclass classeArrayList = (*env)->FindClass(env, "java/util/ArrayList");
// Recupera uma referência para o construtor vazio da classe.
jmethodID metodoConstrutorVazio = (*env)->GetMethodID(env, classeArrayList,
"<init>", "()V");
// Cria uma nova ArrayList.
jobject objetoArrayList = (*env)->NewObject(env, classeArrayList,
metodoConstrutorVazio);
// Recupera uma referência para o método add da classe ArrayList.
jmethodID metodoAdd = (*env)->GetMethodID(env, classeArrayList, "add",
"(Ljava/lang/Object;)Z");
// Insere dez inteiros na ArrayList.
int i = 0;
for(i = 0; i < 10; i++) {
jstring elemento = (*env)->NewStringUTF(env, "Elemento");
(*env)->CallObjectMethod(env, objetoArrayList, metodoAdd, elemento);
}
// Retorna a ArrayList.
return objetoArrayList;
}
Neste código, são utilizadas diversas funções JNI: FindClass, GetMethodID, NewObject,
NewStringUTF e CallObjectMethod, além de vários tipos: jclass, jmethodID, jobject e jstring.
Como pode ser observado, todos os métodos invocados a partir do ponteiro de funções JNI
recebem o ponteiro como primeiro parâmetro.
As duas primeiras funções JNI invocadas são FindClass e GetMethodID, que recuperam,
respectivamente, uma referência para uma classe e um método, necessárias para a criação de um
objeto. Desta forma, para criar uma ArrayList, foram inicialmente recuperadas referências para a
classe java.util.ArrayList e para o método construtor vazio.
A recuperação da referência para a classe utiliza barras “/” ao invés de pontos “.”. No caso
de métodos, seu nome deve ser utilizado, mas, por tratar-se de um construtor, foi utilizado o nome
“<init>”. Além disso, para métodos, deve ser informada a assinatura, com parâmetros entre
parênteses, seguidos do tipo de retorno, após os parênteses. Para o exemplo apresentado, a
assinatura é “()V”, indicando que ele não recebe parâmetros, “()”, e não tem tipo de retorno, “V” de
void.
7/19
Em seguida, um objeto é criado por meio da função NewObject, que recebe as referências e
eventuais parâmetros – no caso, nenhum foi passado por tratar-se do construtor vazio.
Por fim, é recuperada uma referência para o método add e são adicionadas 10 Strings criadas
em um loop à ArrayList. As strings são criadas por meio da função NewStringUTF, que recebe
como parâmetro o valor da String.
Perceba que, na assinatura utilizada para a recuperação da referência para o método add, foi
utilizado o nome da classe, precedido de “L” e sucedido por “;”. Sempre que existirem classes na
assinatura, esse padrão deve ser seguido.
É importante que fique claro que escrever códigos nativos que simplesmente invocam
funções Java, como no exemplo apresentado, não resulta em um ganho de desempenho - pelo
contrário. No entanto, na maioria das vezes, será necessário criar algum objeto ou executar algumas
funções do Java a partir do código nativo.
Sobrecarga de métodos nativos Caso exista sobrecarga de métodos, devem ser acrescentados ao nome do método nativo
dois caracteres underscore “__”, seguidos dos tipos dos parâmetros que o método recebe. Como
exemplo, veja o resultado gerado pelo comando javah (Listagem 9) a partir do código Java da
Listagem 8.
Listagem 8. Código Java com assinatura de métodos nativos sobrecarregados. //...
public class LadoJavaSobrecarga {
//...
public native int metodoSobrecarregado();
public native int metodoSobrecarregado(int a);
public native int metodoSobrecarregado(int a, int b);
public native int metodoSobrecarregado(double a, int b);
public native int metodoSobrecarregado(boolean a);
public native int metodoSobrecarregado(short a);
public native int metodoSobrecarregado(long a);
public native int metodoSobrecarregado(char a);
public native int metodoSobrecarregado(String a);
//...
}
Listagem 9. Código nativo gerado pelo programa javah para o código Java da Listagem 8. //...
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__
(JNIEnv *, jobject);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__I
(JNIEnv *, jobject, jint);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__II
(JNIEnv *, jobject, jint, jint);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__DI
(JNIEnv *, jobject, jdouble, jint);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__Z
(JNIEnv *, jobject, jboolean);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__S
(JNIEnv *, jobject, jshort);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__J
(JNIEnv *, jobject, jlong);
JNIEXPORT jint JNICALL Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__C
(JNIEnv *, jobject, jchar);
JNIEXPORT jint JNICALL
Java_br_com_fatec_luque_wm_LadoJavaSobrecarga_metodoSobrecarregado__Ljava_lang_String_2
(JNIEnv *, jobject, jstring);
//...
Como pode ser observado no código desta listagem, após os dois underscores, a assinatura
dos parâmetros é definida. Quando se trata de tipo primitivo, apenas uma letra é utilizada, conforme
Tabela 3. Para o caso de classes, o nome completo dela é utilizado, precedido de L, substituindo o
ponto “.” por underscore - como no caso da String, Ljava_lang_String – e inserindo um “_2” no
8/19
final. O “_2” representa um caractere “;”, que não pode aparecer em nomes de métodos. Além dele,
“_1” é utilizado para “_” e “_3” para “[“.
Tipo Nativo Assinatura
jboolean Z
jbyte B
jchar C
jshort S
jint I
jlong J
jfloat F
jdouble D
void V
Tabela 3. Assinaturas de alguns tipos nativos.
O conteúdo abordado até então apresentou uma visão geral de JNI e como ela pode ser
utilizada para criar projetos Java que interagem com código nativo. No caso do Android, para que
isso seja possível, é necessário preparar o ambiente para o NDK. A seguir, veremos como isso pode
ser feito e estudaremos uma aplicação exemplo que usa o que foi discutido até então para melhorar
o desempenho de aplicações Android.
Preparação do Ambiente Nesta seção, serão apresentados todos os passos para a configuração de um projeto que usa o
NDK.
Estamos assumindo que você já possui o ambiente para desenvolvimento com o Android
SDK instalado – se este não for o caso, acesse o site do desenvolvedor Android, copie o Android
Developer Tools - ADT bundle e o instale. Estamos assumindo também que a versão de referência é
aquela distribuída com o Eclipse, não com o IntelliJ IDEA.
Para instalar o NDK, o mesmo site do desenvolvedor pode ser acessado e a última versão do
NDK pode ser copiada e instalada – a instalação envolve simplesmente a descompactação do
arquivo copiado em alguma pasta. Após terminar a instalação, inicie o Eclipse ADT, acesse o menu
de preferências (Window> Preferences) e, dentro da guia Android, selecione NDK. Defina a pasta
onde você o descompactou.
O próximo passo envolve a criação de uma pasta chamada JNI no seu projeto. Nesta pasta,
serão colocados os arquivos-fonte nativos e o makefile, com informações sobre a compilação do
código nativo. Esta pasta deve ficar no nível logo abaixo do projeto e não em src.
Em seguida, converta o projeto em um projeto misto Java e C/C++. Para isso, pressione
Ctrl+N ou selecione File > New > Other, e escolha a opção C/C++ > Convert to a C/C++ Project
(Adds C/C++ Nature) (Figura 1). Clique em próximo (Next >) e selecione o projeto que deseja
converter e o tipo C Project.
Na próxima tela, selecione Makefile Project e Other Toolchain e, então, Finish (Figura 2).
Isso fará com que a IDE pergunte a você se deseja abrir a perspectiva C/C++.
Mesmo após esta etapa, você continuará podendo trabalhar com código Java no seu projeto
– ela apenas adiciona a possibilidade de trabalhar com código nativo em C/C++.
9/19
Figura 1. Seleção do projeto que será convertido para C/C++.
Figura 2. Configuração do projeto que será convertido para C/C++.
Para poder configurar o script de construção do código C/C++ no Eclipse, você deve inserir
na variável de ambiente PATH o caminho onde este script se encontra – a pasta de instalação do
NDK. Tendo feito isso, acesse o menu de propriedades do seu projeto – Alt+Enter sobre o projeto
ou botão direito > Properties -, selecione C/C++ Build e, na aba Builder Settings, desmarque a caixa
de verificação “Use default build command” e preencha a caixa Build command com ‘ndk-build’
(Figura 3).
10/19
Figura 3. Configuração do script para a construção do código nativo no Eclipse.
Em seguida, selecione a aba Behaviour e marque a caixa de verificação ‘Build on resource
save (Auto build)”, para que o código nativo seja automaticamente reconstruído quando o projeto
for gravado (Figura 4).
Figura 4. Configuração da construção do código nativo no Eclipse.
Após selecionar “OK”, caso você não tenha configurado a variável PATH para o script, o
Eclipse informará a existência de um erro no projeto: ‘Error: Program "ndk-build" is not found in
PATH’.
11/19
Agora, precisamos incluir as bibliotecas em C/C++ para a escrita do código fonte. Para isso,
selecione Properties > C/C++ General> Paths and symbols. Clique em Add, marque as os locais
“all-configurations” e “all-languages” e insira o caminho da pasta “include” contida dentro do
NDK. Como utilizaremos a versão 9 do Android no nosso projeto, esta pasta está localizada em
<raiz do NDK>\platforms\android-9\arch-arm\usr\include.
Crie um arquivo chamado Android.mk na pasta jni do projeto, com instruções para a
compilação do código nativo. Um exemplo de arquivo é apresentado na Listagem 10. Entre outras
coisas, estão definidos nele o nome da biblioteca, que será utilizado no comando
System.loadLibrary, os arquivos fontes e as bibliotecas utilizadas nos arquivos fontes.
Listagem 10. Exemplo de makefile Android.mk. LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := bibliotecanativa
LOCAL_SRC_FILES := Biblioteca.c
LOCAL_LDLIBS := -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)
Seguidas estas etapas, o seu ambiente já está pronto para permitir a criação de projetos
Android que utilizam código nativo. A seguir, será apresentado um exemplo de aplicação deste tipo.
A Aplicação Exemplo Para exemplificar o uso do Android NDK, desenvolveremos uma aplicação simples para
melhorar a qualidade de imagens por meio de duas técnicas de Processamento de Imagens:
filtragem da mediana e normalização. Nesta aplicação, a visualização e interação com o usuário
ficarão sob responsabilidade do código Java, enquanto o processamento será realizado pelo código
nativo, escrito em C.
Para o entendimento dos códigos que serão apresentados, é necessário conhecer alguns
conceitos básicos de Processamento de Imagens, discutidos a seguir. Uma imagem digital
monocromática pode ser entendida como uma função f(x,y) que mapeia uma posição (x,y) para uma
intensidade de brilho, geralmente representada no intervalo [0, 255]. Essa função pode ser
visualizada como uma matriz, conforme apresentado na Figura 5 e cada célula desta matriz é
conhecida como pixel (Picture Element).
10 114 15 2 76
216 68 251 110 22
198 3 134 198 3
Figura 5. Exemplo de matriz de imagem monocromática.
Um valor próximo a 0 indica uma intensidade de brilho baixa (escuro), enquanto um valor
próximo a 255 indica uma intensidade de brilho alta (claro).
Em se tratando de imagens coloridas, ao invés de um único valor de intensidade de brilho
para cada pixel, são utilizados valores para várias cores que juntas compõem a cor do pixel. O
modelo mais comum de cores para imagens é o RGB, no qual cada pixel possui três valores no
intervalo [0, 255] – um para o vermelho (Red), outro para o verde (Green) e um último para o azul
(Blue). Assim sendo, nestas imagens, ao invés de uma única matriz, temos três matrizes, podendo
ser acrescentada à estas uma matriz com valores de transparência, conhecida como alpha.
Existem diversas técnicas que permitem a correção de características indesejáveis em
imagens por meio da manipulação das suas matrizes. Uma delas é a Filtragem da Mediana, que
permite a redução de ruídos – variações aleatórias no padrão de cores da imagem. A Figura 6
apresenta um exemplo de imagem com ruídos e a mesma imagem após a filtragem da mediana.
12/19
Figura 6. À esquerda, imagem com ruído. À direita, a mesma imagem após a filtragem da mediana.
O filtro da mediana consiste em substituir a intensidade de cor de cada pixel pela mediana
das intensidades de cor na vizinhança do pixel. A mediana de um conjunto de pixels é igual ao
elemento central do conjunto ordenado (se o número de elementos for ímpar) ou a média aritmética
dos dois elementos centrais (se o número de elementos for par).
O tamanho da vizinhança é determinado por uma matriz conhecida como máscara. Deve-se
posicionar a máscara centralizada no pixel que se deseja filtrar e todos os pixels que ficarem abaixo
dela participarão do cálculo da mediana.
Desta forma, esta técnica pode ser entendida como o deslocamento de uma janela (máscara)
pelos pixels da imagem e pela substituição de seus valores a partir do cálculo da mediana entre seus
vizinhos. Veja o exemplo seguinte para uma máscara de tamanho 3x3. Será utilizada uma imagem
monocromática (Figura 7), mas a mesma técnica poderia ser empregada em imagens coloridas,
bastando que o mesmo procedimento fosse repetido independentemente em cada uma das matrizes
R, G e B.
13/19
Figura 7 Exemplo de aplicação do filtro da mediana com máscara 3x3.
Outra técnica é a normalização, que permite uma melhor distribuição dos valores dos pixels
no intervalo válido – entre 0 e 255, geralmente resultando em um aumento no contraste da imagem.
Para aplicá-la, basta utilizar uma regra de três simples procurando mapear o intervalo de valores de
uma imagem para o intervalo [0, 255].
Para fazer isso, devemos encontrar o maior (max) e menor (min) valor da matriz da imagem
e, para cada um dos seus pixels (p), calcular um novo valor (x) a partir da fórmula da Figura 8.
Figura 8. Fórmula de normalização.
A Figura 9 apresenta um exemplo de imagem com baixo contraste e o resultado após a
normalização.
14/19
Figura 9. Exemplo de aplicação da normalização. Imagem com baixo contraste (à esquerda) e normalizada (à direita).
Entendidos estes conceitos, veremos como funcionará a aplicação. Ela possui um único
layout (Figura 10), onde são exibidos rótulos, duas imagens – uma que é aberta pelo usuário e outra
para exibir o resultado – e três botões. Um dos botões permite o carregamento de imagens a partir
da galeria do dispositivo e do GDrive. Os outros dois aplicam a filtragem da mediana e a
normalização, nesta ordem, sendo que um deles executa todo o processamento em Java e o outro em
C.
Figura 10. Representação gráfica do layout da aplicação exemplo.
O tempo de processamento é calculado e exibido logo acima da imagem resultante,
permitindo a comparação de desempenho entre o código Java e o nativo.
Uma imagem em Android é representada pela classe Bitmap e pode ser desenhada em
ImageViews, como os presentes na interface da aplicação. Um Bitmap possui vários métodos que
permitem a recuperação e alteração dos valores dos seus pixels.
15/19
No processamento utilizado, foram utilizados os métodos getPixels e setPixels. O primeiro
deles retorna os pixels da imagem como uma array de números inteiros, na qual, em cada posição,
estão armazenados os valores R, G e B de cada pixel. Para recuperá-los deve ser utilizado o
deslocamento de bits.
O código que aplica o filtro da mediana e a normalização (Listagem 11 e 12) envolve as
seguintes etapas: recuperação dos pixels da imagem em uma array de inteiros, passagem por todos
os pixels da imagem e cálculo da mediana da vizinhança de cada pixel. Após o cálculo da mediana,
o maior e o menor valor da matriz resultante são recuperados. Então, passe-se novamente pelos
pixels e é aplicada a normalização. Por fim, os pixels são desenhados novamente na imagem.
Listagem 11. Código Java que aplica o filtro da mediana e a normalização à imagem. /**
* Reduz ruídos (filtro da mediana) e tenta aumentar o contraste da imagem
* por meio da normalização - versão nativa.
* @param imagem um array com os pixels da imagem original.
* @param largura a largura da imagem original;
* @param altura a altura da imagem original.
* @param linhas o número de linhas da matriz da máscara.
* @param colunas o número de colunas da matriz da máscara.
* @return um array com os pixels da imagem original após ela ter sido
* filtrada com o filtro da mediana e seu contraste ter sido
* aumentado com a normalização.
*/
public static native int[] melhorarQualidadeNativo(int[] pixels,
int largura, int altura, int linhas, int colunas);
public static int[] melhorarQualidade(int[] pixels, int largura,
int altura, int linhas, int colunas) {
// Calcula o número de pixels da imagem.
int tamanho = largura * altura;
// Calcula quantos pixels acima, abaixo, à direita e à esquerda deverão
// ser visitados para calcular a mediana.
int deltaLinha = (linhas - 1) / 2;
int deltaColuna = (colunas - 1) / 2;
int tamanhoMascara = linhas * colunas;
// Cria um array onde serão armazenados os pixels resultantes.
int pixelsResultantes[] = new int[tamanho];
// Variáveis para armazenar o maior e menor elemento para normalizar a imagem.
int maiorR = Integer.MIN_VALUE;
int menorR = Integer.MAX_VALUE;
// ... o mesmo para G e B.
// Para cada pixel da imagem, calcula a mediana entre os pixels sobrepostos pela máscara.
int i = 0, x = 0, y = 0;
int conjuntoMedianaR[] = new int[tamanhoMascara];
// conjuntoMedianaG, conjuntoMedianaB ...
for (i = 0; i < tamanho; i++) {
// Número de pixels válidos que serão utilizados no cálculo da mediana.
int elementos = 0;
// Passa pelos pixels vizinhos.
for (x = -deltaLinha; x <= deltaLinha; x++) {
for (y = -deltaColuna; y <= deltaColuna; y++) {
// Verifica se o pixel é válido, ou seja, se está dentro da imagem.
int indice = i + x + (y * largura);
if (indice >= 0 && indice < tamanho) {
// Recupera uma referência para o pixel e seus
// componentes
// R, G e B.
int pixel = pixels[indice];
int R = (pixel >> 16) & 0xff;
int G = (pixel >> 8) & 0xff;
int B = pixel & 0xff;
// Armazena o elemento que será utilizado no cálculo da
// mediana.
conjuntoMedianaR[elementos] = R;
// conjuntoMedianaG, conjuntoMedianaB ...
elementos++;
}
}
16/19
}
// Finaliza o cálculo da mediana
int medianaR = mediana(conjuntoMedianaR, elementos);
// ... o mesmo para G e B.
// Encontra o maior e menor elemento para normalizar a imagem.
maiorR = medianaR > maiorR ? medianaR : maiorR;
menorR = medianaR < menorR ? medianaR : menorR;
// ... o mesmo para G e B.
// Armazena o pixel resultante.
pixelsResultantes[i] = (255 << 24) | (medianaR << 16)
| (medianaG << 8) | medianaB;
}
// Normaliza a imagem para corrigir o contraste.
double fatorR = 255.0 / (maiorR - menorR);
double fatorG = 255.0 / (maiorG - menorG);
double fatorB = 255.0 / (maiorB - menorB);
for (i = 0; i < tamanho; i++) {
// Recupera uma referência para o pixel e os valores R, G e B
// para ele.
int pixel = pixelsResultantes[i];
// ... Recupera o valor dos pixels, como anteriormente.
// Calcula o novo valor para o pixel.
int novoValorPixelR = (int) ((R - menorR) * fatorR);
// ... o mesmo para G e B.
pixelsResultantes[i] = (255 << 24) | (novoValorPixelR << 16)
| (novoValorPixelG << 8) | novoValorPixelB;
}
return pixelsResultantes;
}
Comparando o código Java e o nativo, a única diferença existente é aquela relacionada à
recuperação e retorno da array. No código nativo, para não ter que invocar funções JNI de
recuperação do valor de cada pixel, invocamos inicialmente uma função que armazena a array em
formato diretamente acessível pelo código nativo. No retorno, fazemos o contrário, para também
não ter que definir o valor dos pixels um a um invocando funções JNI.
Listagem 12. Código nativo que aplica o filtro da mediana e a normalização à imagem. #include <jni.h>
#include <android/bitmap.h>
// ...
// Os parâmetros são equivalentes ao código já apresentado em Java.
// Alguns comentários foram omitidos por serem equivalentes aos já apresentados
// no código Java.
JNIEXPORT jobject JNICALL Java_br_com_fatec_projetomm_ImagemUtil_melhorarQualidadeNativo(
JNIEnv *env, jclass classe, jintArray imagem, jint largura, jint altura,
jint linhas, jint colunas) {
// Calcula algumas medidas importantes.
int tamanho = largura * altura;
int deltaLinha = (linhas - 1) / 2;
int deltaColuna = (colunas - 1) / 2;
int tamanhoMascara = linhas * colunas;
// Recupera os pixels da imagem.
jint* pixels = (*env)->GetIntArrayElements(env, imagem, 0);
jint pixelsResultantes[tamanho];
int maiorR = INT32_MIN;
int menorR = INT32_MAX;
// ...
// Para cada pixel da imagem,
int i = 0, x = 0, y = 0;
int conjuntoMedianaR[tamanhoMascara];
// ...
for (i = 0; i < tamanho; i++) {
int elementos = 0;
for (x = -deltaLinha; x <= deltaLinha; x++) {
17/19
for (y = -deltaColuna; y <= deltaColuna; y++) {
jint indice = i + x + (y * largura);
if (indice >= 0 && indice < tamanho) {
jint pixel = pixels[indice];
int R = (pixel >> 16) & 0xff;
int G = (pixel >> 8) & 0xff;
int B = pixel & 0xff;
conjuntoMedianaR[elementos] = R;
// ...
elementos++;
}
}
}
int medianaR = mediana(conjuntoMedianaR, elementos);
// ...
maiorR = medianaR > maiorR ? medianaR : maiorR;
maiorG = medianaG > maiorG ? medianaG : maiorG;
// ...
pixelsResultantes[i] = (ALFA << 24) | (medianaR << 16) | (medianaG << 8)
| medianaB;
}
double fatorR = 255.0 / (maiorR - menorR);
// ...
for (i = 0; i < tamanho; i++) {
int pixel = pixelsResultantes[i];
// ... Recupera os valores dos pixels, como anteriormente.
int novoValorPixelR = (int) ((R - menorR) * fatorR);
// ...
pixelsResultantes[i] = (ALFA << 24) | (novoValorPixelR << 16)
| (novoValorPixelG << 8) | novoValorPixelB;
}
// Preenche um array com os pixels resultantes.
jintArray novosPixels = (*env)->NewIntArray(env, tamanho);
(*env)->SetIntArrayRegion(env, novosPixels, 0, tamanho, pixelsResultantes);
return novosPixels;
}
Como o processamento para a criação de objetos na máquina virtual a partir do código
nativo é maior do que aquele necessário para fazer o mesmo a partir do Java, optou-se pelo método
não retornar uma nova imagem Bitmap, mas sim uma array de pixels, que é, nos dois casos,
desenhada na imagem por meio do código Java (Listagem 13).
Listagem 13. Código que invoca o processamento para o cálculo da mediana e normalização. /**
* Trata o evento de clique no botão de melhoria da qualidade.
*
* @param origem O botão onde o evento foi gerado.
*/
public void tratarEventoMelhoriaQualidade(View origem) {
// Se o usuário ainda não tiver selecionado uma imagem, exibe
// uma mensagem de erro e cancela a operação.
if (!validarImagemOrigem()) {
return;
}
// Marca o tempo de início do processamento.
long inicio = System.currentTimeMillis();
// Recupera os pixels da imagem.
int[] pixels = new int[imagemCarregada.getWidth()
* imagemCarregada.getHeight()];
imagemCarregada.getPixels(pixels, 0, imagemCarregada.getWidth(), 0, 0,
imagemCarregada.getWidth(), imagemCarregada.getHeight());
// Tenta melhorar a qualidade gráfica da imagem
// >>> Aqui vai a chamada para o método de processamento escrito em Java ou nativo.
int[] resultado = ImagemUtil.melhorarQualidade(pixels,
18/19
imagemCarregada.getWidth(), imagemCarregada.getHeight(), 3, 3);
// Exibe a imagem resultante.
Bitmap resultante = Bitmap.createBitmap(imagemCarregada.getWidth(),
imagemCarregada.getHeight(), Bitmap.Config.ARGB_8888);
resultante.setPixels(resultado, 0, imagemCarregada.getWidth(), 0, 0,
imagemCarregada.getWidth(), imagemCarregada.getHeight());
componenteImagemResultante.setImageBitmap(resultante);
// Marca o tempo de término do processamento.
long termino = System.currentTimeMillis();
// Exibe o tempo total de processamento em milisegundos.
tempoProcessamento.setText("Demorou " + (termino - inicio)
+ " milisegundos.");
}
Em testes realizados em um Samsung Galaxy Tab, o código nativo foi duas vezes mais
rápido que o Java para máscaras com tamanho 3x3. Esta diferença tende a crescer com o aumento
do tamanho da máscara.
O código completo do projeto está disponível no site da revista.
Conclusões Este artigo apresentou uma introdução ao NDK e como ele pode contribuir para a melhoria
do desempenho de suas aplicações Android. A JNI, utilizada no NDK, é muito extensa e apenas
uma breve introdução foi apresentada. Recomendamos a leitura dos artigos e livros na seção de
links para um aprofundamento sobre o tema.
Leandro Luque [email protected] É professor da FATEC Mogi das Cruzes, onde desenvolve pesquisas na área de Interação Humano-Computador, Engenharia de Software e Processamento de Imagens. Bacharel em Ciência da Computação pela Universidade de Mogi das Cruzes e mestre em Computação Aplicada pelo Instituto Nacional de Pesquisas Espaciais (INPE), trabalha com Java há 13 anos, atuando no desenvolvimento de aplicações de grande porte, tanto no segmento empresarial quanto governamental.
19/19
Eron Silva [email protected] É graduando em Análise e Desenvolvimento de Sistemas pela FATEC Mogi das Cruzes. Tem dois anos de experiência em desenvolvimento Android, principalmente de aplicações para o processamento de imagens e reconhecimento de códigos de barras.
Links Especificação da JNI
http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html
Página de download do Android NDK
http://developer.android.com/tools/sdk/ndk/index.html
Artigo da IBM com um exemplo de aplicação desenvolvida passo-a-passo para o Android
NDK
http://www.ibm.com/developerworks/br/opensource/tutorials/os-androidndk/index.html
Livros Pro Android C++ with the NDK
Livro “Pro Android C++ with the NDK” sobre programação em C++ com o NDK.
Android Native Development Kit Cookbook
Livro de receitas sobre o Android NDK.