20
See discussions, stats, and author profiles for this publication at: https://www.researchgate.net/publication/260460628 Conhecendo o Android NDK: integrando código nativo às suas aplicações Android ARTICLE · JANUARY 2014 READS 115 1 AUTHOR: Leandro Luque Centro Paula Souza 23 PUBLICATIONS 1 CITATION SEE PROFILE Available from: Leandro Luque Retrieved on: 11 March 2016

Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

  • Upload
    leluque

  • View
    30

  • Download
    4

Embed Size (px)

DESCRIPTION

Artigo da Revista MundoJ sobre a customização de um framework MVVM para Android (AndroidBinding).

Citation preview

Page 1: 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

Page 2: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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

Page 3: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 4: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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

Page 5: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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

Page 6: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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

Page 7: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 8: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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

Page 9: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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++.

Page 10: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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).

Page 11: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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’.

Page 12: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 13: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 14: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 15: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 16: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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++;

}

}

Page 17: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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++) {

Page 18: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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,

Page 19: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.

Page 20: Conhecendo o Android NDK: integrando código nativo às suas aplicações Android

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.