en Android

Desarrollo Android: StringBuilder, JSON y sus problemas de Out Of Memory

A veces pensamos que los datos más primitivos de Java son “económicos” en el uso de memoria. Pero no necesariamente tiene que ser así dependiendo del escenario.

Veamos un ejemplo muy concreto:

1- Pido datos a un servicio REST

2- Obtengo la salida en un InputStream

3- Ese InputStream lo convierto a String con StringBuilder

4- Esa salida (en este ejemplo) al ser un JSON lo parsero con la clase nativa que tenemos presente.

5- Los datos obtenidos los meto a mi objecto

Si la salida son unos 300 o 400KB de texto (o más) posiblemente existan teléfonos lleguen al límite de la memoria asignada por el sistema rápidamente y se produzca un crash. Esto sucede porque tenemos memoria asignada para el InputStream, para un enorme String y luego para una serie de objetos Json que ocupan mucha memoria. y, finalmente, nuestro propio objeto donde guardamos los datos modelados. Esto es sumamente peligroso en dispositivos que disponene de pocos o menos recursos que los dispositivos actuales.

Vamos a intentar siempre hacer nuestras aplicaciones compatibles y funcionales en muchos dispositivos, por lo que debemos tener en cuenta esta clase de cosas para que no ocurran excepciones inesperadas.

Evidentemente las soluciones pueden ser muchas para este caso, como por ejemplo parsear poco a poco la salida en vez de construir un gran String (complejo), limitar la salida del servicio para que retorne menos datos (no siempre es optimizable), o…

Utilizar un parser de Json que tome el Stream y lo convierta en nuestro objeto

¿¿¿Cómo??? Lo dicho. Vamos a utilizar un parser que tome directamente el InputStream y una clase de referencia (mi objeto) y nos de todo hecho. Suena difícil, ¿no? Pues no.

Gson

Esta librería es una maravilla de proyecto creada por Google que nos va a hacer la vida más sencilla y nos ahorrará quebraderos de cabeza. Su utilización es muy sencilla y el resultado es óptimo. Podemos encontrarla en http://code.google.com/p/google-gson/

Ventajas

– Velocidad

– Flexibilidad

– Optimización de recursos

Vamos con un ejemplo
Para este ejemplo vamos a parsear la salida del API de Twitter que nos devuelve resultados en formato JSON para una búsqueda de palabra clave, por ejemplo: http://search.twitter.com/search.json?q=minube

Por motivos obvios he cortado la captura porque a partir de ahí se repite varias veces el patrón de la salida.

Ahora lo que vamos a hacer es crear tres clases que van a representar nuestra salida convertida a objeto. La primera va a ser la clase ApiOutput que va a contener un List de objetos Result y, estos últimos, van a contener un objeto Metadata.

Manos a la obra

ApiOutput.java

package com.example.gsonproject;

import java.util.List;
import com.google.gson.annotations.SerializedName;

public class ApiOutput
{
    public List<Result> results;

    @SerializedName("max_id")
    public long maxId;

    @SerializedName("refresh_url")
    public String refreshUrl;

    @SerializedName("next_page")
    public String nextPage;
}

Como se puede ver en el código anterior, Gson utiliza anotaciones en el código para referenciar los datos que queremos extraer de la salida. Por ejemplo
@SerializedName(“refresh_url”) va a tomar ese dato de la salida y lo va a almacenar en la variable refreshUrl.

Result.java

package com.example.gsonproject;

import com.google.gson.annotations.SerializedName;

public class Result
{
    @SerializedName("from_user_id_str")
    public String fromUserIdStr;

    @SerializedName("profile_image_url")
    public String profileImageUrl;

    @SerializedName("created_at")
    public String createdAt;

    @SerializedName("from_user")
    public String fromUser;

    @SerializedName("id_str")
    public String idStr;

    public Metadata metadata;

    @SerializedName("to_user_id")
    public String toUserId;

    @SerializedName("text)
    public String text;

    public long id;

    @SerializedName("from_user_id")
    public String from_user_id;

    @SerializedName("iso_language_code")
    public String isoLanguageCode;

    @SerializedName("to_user_id_str")
    public String toUserIdStr;

    @SerializedName("source")
    public String source;
}

Metadata.java

package com.example.gsonproject;

import com.google.gson.annotations.SerializedName;

public class Metadata
{
    @SerializedName("result_type")
    public String resultType;
}

Ahora que ya tenemos definidas todas las clases que van a representar la salida de este ejemplo, vamos a ver cómo usar Gson.

package com.example.gsonproject;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

import com.google.gson.Gson;
import com.example.gsonproject.Result;
import com.example.gsonproject.ApiOutput;

public class GsonExampleActivity extends Activity {

    String url = "http://search.twitter.com/search.json?q=minube";

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        InputStream source = retrieveStream(url);

        Gson gson = new Gson();

        Reader reader = new InputStreamReader(source);

        ApiOutput response = gson.fromJson(reader, ApiOutput.class);

        Toast.makeText(this, response.query, Toast.LENGTH_SHORT).show();

        List<Result> results = response.results;

        for (Result result : results)
        {
            Toast.makeText(this, result.fromUser, Toast.LENGTH_SHORT).show();
        }

    }

    private InputStream retrieveStream(String url)
    {
        DefaultHttpClient client = new DefaultHttpClient();

        HttpGet getRequest = new HttpGet(url);

        try {

           HttpResponse getResponse = client.execute(getRequest);
           final int statusCode = getResponse.getStatusLine().getStatusCode();

           if (statusCode != HttpStatus.SC_OK) {
              Log.w(getClass().getSimpleName(),
                  "Error " + statusCode + " for URL " + url);
              return null;
           }

           HttpEntity getResponseEntity = getResponse.getEntity();
           return getResponseEntity.getContent();

        }
        catch (IOException e) {
           getRequest.abort();
           Log.w(getClass().getSimpleName(), "Error accediendo a " + url, e);
        }

        return null;
     }
}

Lo que hemos hecho básicamente es crear una nueva instancia de Gson y pasarle el InputStream obtenido con el cliente Http diciéndole que la salida corresponde a nuestro modelo ApiOutput.java. El ya se encarga del resto 🙂

Esto nos va a ahorrar crear un String de la salida con StringBuilder y también nos va a hacer prescindir de parsear “manualmente” la salida con JSONObject, JSONArray, etc…

Finalmente el ejemplo lo que hace es sacar varios toast con los nombres de los usuarios de twitter que han incluído el término minube en su twitt.

Como se puede observar es muy sencillo e intuitivo y nos va a ahorrar varios quebraderos de cabeza porque hace un uso muy eficiente de la memoria utilizando directamente el stream de datos.

Como nota adicional y para finalizar, he de comentar que si añadimos en nuestro modelo anotaciones de datos que no están presentes en la salida, el parser no lanza una excepción. Esto es muy bueno para salidas de dos métodos que pueden variar levemente en un pequeño puñado de datos.

Escribe un comentario

Comentario

  1. A partir de que API se usan todas las librerias mencionadas? la clase SearchResponse me da error y me temo que es porque estoy con android 2.2

  2. Javi, perdona pero me he equivocado, no es SearchResponse sino ApiOutput = gson.fromJson(reader, ApiOutput.class);

  3. Hola a todos. El articulo es interesante al 100%, pero tengo problemas al hacer un fromJson y parsearlo a mi objeto. Lo que yo hago es usar una cadena JSON en vez de un reader…. y me esta dando problemas. ¿al hacer esto con una string hay que hacer algo aparte?