domingo, 6 de marzo de 2016

Android NDK (Native Development Kit - Sistema de Desarrollo Nativo)

NDK nos va a permitir llamar a librerías escritas en C y C++ desde nuestra aplicacióna través de JNI (Java Native Interface). Aunque para la mayoría de las aplicaciones no es necesario existen casos en los que necesaitaremos usar esta funcionalidad. Por ejemplo si queremos realizar aplicaciones con gran carga de gráficos en cuyo caso será mejorar el tiempo de respuesta de nuestra aplicación.
En cualquier caso no debemos usar NDK por simple gusto pues la programación podría resultar mas compleja y aumentar la posibilidad de cometer errores.
A continuación veamos un ejemplo de como crear una aplicación que use NDK. Lo primero es tener instalada junto con la SDK de Android la NDK.
Ahora creamos nuestro proyecto, en este caso he creado un sencillo proyecto con un texto y un botón, cuando pulsemos el botón se sustituirar el texto por uno devuelto por una librería creada en C.
La definición de la pantalla sería la siguiente:
<? xml version= "1.0" encoding= "utf-8" ?> 
< RelativeLayout xmlns: android = "http://schemas.android.com/apk/res/android"  
xmlns: tools = "http://schemas.android.com/tools"  
  android :layout_width= "match_parent"  
  android :layout_height= "match_parent"  
  android :paddingBottom= "@dimen/activity_vertical_margin"
  android :paddingLeft= "@dimen/activity_horizontal_margin"  
  android :paddingRight= "@dimen/activity_horizontal_margin"
  android :paddingTop= "@dimen/activity_vertical_margin"
  tools :context= "com.losga.ndkexample.MainActivity">
    < TextView  
      android :layout_width= "wrap_content"  
      android :layout_height= "wrap_content"  
      android :text= "Mensaje"  
      android :layout_alignParentRight= "true"  
      android :layout_alignParentEnd= "true"
      android :layout_alignParentLeft= "true"  
      android :layout_alignParentStart= "true"  
      android :id= "@+id/textView" />
  < Button  
     android :layout_width= "wrap_content"  
     android :layout_height= "wrap_content"
     android :text= "Pulsa aquí"
     android :id= "@+id/button"  
     android :layout_below= "@+id/textView"  
     android :layout_centerHorizontal= "true"  
     android :layout_marginTop= "29dp" />
 </RelativeLayout>
y la clase de nuestra actividad tendrá el siguiente contenido:

package com.losga.ndkexample;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;  
import java.util.Date;

public class MainActivity extends AppCompatActivity {

  native String getMensaje(Date fecha);

  @Override  
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);  
      final TextView texto = (TextView) findViewById(R.id. textView);
      final Button button = (Button) findViewById(R.id. button);
      button.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
          texto.setText(getMensaje(new Date()));
        }
      });
    }
  }
En esta clase vemos que está la siguiente línea:
native String getMensaje(Date fecha);
Con el modificador native estamos indicando que dicha función es una llamada a una librería en C, que posteriormente vamos a crear.
A continuación vamos a crear una carpeta con el nombre jni, en Android Studio ya tenemos dicha opción.
El siguiente paso es crear un archivo con el nombre Android.mk dentro de la carpeta que acabamos de crear con el siguiente contenido:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := jniUtils
LOCAL_SRC_FILES := jniUtils.c
include $(BUILD_SHARED_LIBRARY)
A continuación usaremos JavaH para crear la cabecera que tendremos que incluir en nuestra clase, previamente debemos hacer un build del proyecto pues javaH necesitará de la clase java donde se encuentra la llamada a jni. La línea a ejecutar es:
javah -jni -classpath <ruta_donde_este_el_archivo>/android.jar;<ruta_proyecto>/build/intermediates/classes/debug com.losga.ndkexample.MainActivity
como vemos s enos creará un archivo con la definición de la función nativa, en este caso el archivo se llama com_losga_ndkexample_MainActivity.h y que tendrá el siguiente contenido:
/* DO NOT EDIT THIS FILE - it is machine generated */
 #include <jni.h>  
/* Header for class com_losga_ndkexample_MainActivity */
#ifndef _Included_com_losga_ndkexample_MainActivity
#define _Included_com_losga_ndkexample_MainActivity  
#ifdef __cplusplus extern "C" {  
#endif  
/* 
 * Class: com_losga_ndkexample_MainActivity
 * Method: getMensaje
 * Signature: (Ljava/util/Date;)Ljava/lang/String;  
 */
 JNIEXPORT jstring JNICALL Java_com_losga_ndkexample_MainActivity_getMensaje
    (JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif  
#endif
A continuación debemos crear el archivo en C que tendrá la implementación de la función:

#include <jni.h>
#include "jniUtils.h"
#include <com_losga_ndkexample_MainActivity.h>
JNIEXPORT jstring JNICALL Java_com_losga_ndkexample_MainActivity_getMensaje
        (JNIEnv *env, jobject obj1, jobject obj2)
{
    return (*env)->NewStringUTF(env, "Buenos días " );
}

El siguiente paso es añadir al path la localización del ndk-build, para ello debemos modificar la variables de entorno de nuestro sistema.
Normalmente nuestro ndk estará junto a la sdk de Android:
<Ruta_de_lasdk>\sdk\ndk-bundle
Ahora nos vamos a la carpeta jni y ejecutamos lo siguiente:
ndk-build
vemos entonces que se generan una serie de archivos, en este caso se nos generan las carpetas libs y obj, en este momento tenemos que crear una carpeta con el nombre jniLibs y copiar el contenido de obj\local\ tal como vemos en la imagen.
Antes de compilar nuestro proyecto ahora debemos modificar el archivo gradle.properties y añadirle la siguiente línea:
android.useDeprecatedNdk= true
finalmente modificamos el archivo build.grade para añadir lo siguiente:
... 
sourceSets {
    main {
        jni.srcDirs = []
    }
}
...
Debemos de tener en cuenta que si modificamos nuestro archivo en C deberemos volver a ejecutar el ndk-bunble y volver a copiar las librerías en la carpeta jniLibs.