11

Tengo un ContentProvider personalizado que gestiona el acceso a una base de datos SQLite. Para cargar el contenido de una tabla de base de datos en un ListFragment, utilizo el LoaderManager con un CursorLoader y una CursorAdapter:Android ContentProvider llama ráfagas de setNotificationUri() a CursorAdapter cuando se insertan muchas filas con una operación por lotes

public class MyListFragment extends ListFragment implements LoaderCallbacks<Cursor> { 
    // ... 
    CursorAdapter mAdapter; 

    @Override 
    public void onActivityCreated(Bundle savedInstanceState) { 
     super.onActivityCreated(savedInstanceState); 
     mAdapter = new CursorAdapter(getActivity(), null, 0); 
     setListAdapter(mAdapter); 
     getLoaderManager().initLoader(LOADER_ID, null, this); 
    } 

    public Loader<Cursor> onCreateLoader(int id, Bundle args) { 
     return new CursorLoader(getActivity(), CONTENT_URI, PROJECTION, null, null, null); 
    } 

    public void onLoadFinished(Loader<Cursor> loader, Cursor c) { 
     mAdapter.swapCursor(c); 
    } 

    public void onLoaderReset(Loader<Cursor> loader) { 
     mAdapter.swapCursor(null); 
    } 
} 

La base de datos SQLite se actualiza mediante una tarea de fondo, que se vende a un número de elementos de un servicio web y inserta estos elementos en la base de datos a través de ContentProvider operaciones por lotes (ContentResolver#applyBatch()).

Incluso si se trata de una operación por lotes, ContentProvider#insert() invocan por cada fila se inserta en la base de datos y, en la implementación actual, las llamadas ContentProvidersetNotificationUri() para cada comando de inserción.

El resultado es que el CursorAdapter recibe ráfagas de notificaciones, lo que hace que la interfaz de usuario se actualice con demasiada frecuencia con el consiguiente efecto de parpadeo molesto.

Idealmente, cuando una operación por lotes está en progreso, debería haber una manera de notificar ContentObserver solo al final de cualquier operación por lotes y no con cada comando de inserción.

¿Alguien sabe si esto es posible? Tenga en cuenta que puedo cambiar la implementación de ContentProvider y anular cualquiera de sus métodos.

+4

Considere hacer/basar su proveedor en algo como [SQLiteContentProvider] (http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android-apps/2.2_r1.1 /com/android/providers/calendar/SQLiteContentProvider.java/?v=source): proporciona muchos de los fundamentos de un proveedor agradable y funcional basado en SQLite, y si lo hace, simplemente debe usar bulkInsert o applyBatch para hacer tus "inserciones masivas". El beneficio adicional es que sus inserciones masivas se realizarán en una transacción que les acelera bastante. – Jens

+0

@Jens muchas gracias por el puntero, esto parece exactamente lo que estaba buscando. Solo una pregunta al respecto: ¿quién ha desarrollado y publicado este código? Desde el encabezado, parece ser parte del Proyecto de Código Abierto de Android, pero, si este es el caso, ¿por qué Google no lo ha lanzado con el SDK estándar de Android? –

+2

Es parte del proyecto AOSP pero no está publicado en el SDK, por lo que no sé, ya que incluirlo hubiera evitado que mucha gente escribiera sus propias implementaciones 'ContentProvider'. – Jens

Respuesta

7

Para solucionar este problema exacto, anulé applyBatch y establecí una bandera que impedía que otros métodos enviasen notificaciones.

volatile boolean applyingBatch=false; 
    public ContentProviderResult[] applyBatch(
     ArrayList<ContentProviderOperation> operations) 
     throws OperationApplicationException { 
    applyingBatch=true; 
    ContentProviderResult[] result; 
    try { 
     result = super.applyBatch(operations); 
    } catch (OperationApplicationException e) { 
     throw e; 
    } 
    applyingBatch=false; 
    synchronized (delayedNotifications) { 
     for (Uri uri : delayedNotifications) { 
      getContext().getContentResolver().notifyChange(uri, null); 
     } 
    } 
    return result; 
} 

expuse un método para notificaciones "almacenar" para ser enviados cuando el lote fue completa:

protected void sendNotification(Uri uri) { 
    if (applyingBatch) { 
     if (delayedNotifications==null) { 
      delayedNotifications=new ArrayList<Uri>(); 
     } 
     synchronized (delayedNotifications) { 
      if (!delayedNotifications.contains(uri)) { 
       delayedNotifications.add(uri); 
      } 
     } 
    } else { 
     getContext().getContentResolver().notifyChange(uri, null); 
    } 
} 

Y cualquiera de los métodos que envían notificaciones emplean SendNotification, en lugar de disparar directamente una notificación.

Es posible que existan mejores formas de hacerlo, ciertamente parece que deberían estarlo, pero eso es lo que hice.

+1

Muchas gracias por su respuesta. +1 para la idea, que es una de las opciones que tenía en mente, pero prefiero usar el enfoque más flexible y genérico de derivar del resumen ['SQLiteContentProvider'] (http://grepcode.com/file_/repository .grepcode.com/java/ext/com.google.android/android-apps/2.2_r1.1/com/android/providers/calendar/SQLiteContentProvider.java /? v = source) sugerido por Jens. –

13

Encontré una solución más simple que descubrí de la aplicación de E/S de Google. Solo tiene que anular el método applyBatch en su ContentProvider y realizar todas las operaciones en una transacción. Las notificaciones no se envían hasta que se confirma la transacción, lo que se traduce en la reducción de la cantidad de cambio ContentProvider-notificaciones que se envían:

@Override 
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 
     throws OperationApplicationException { 

    final SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); 
    db.beginTransaction(); 
    try { 
     final int numOperations = operations.size(); 
     final ContentProviderResult[] results = new ContentProviderResult[numOperations]; 
     for (int i = 0; i < numOperations; i++) { 
      results[i] = operations.get(i).apply(this, results, i); 
     } 
     db.setTransactionSuccessful(); 
     return results; 
    } finally { 
     db.endTransaction(); 
    } 
} 
+0

Gracias @Dia, parece la mejor y más simple solución. Y como es demasiado simple y lo suficientemente genérico, me pregunto por qué google no lo hace como predeterminado para todas las operaciones por lotes – xandy

+0

Para mí, esto desencadena tantos eventos porque sigue llamando al método insert (...) varias veces y por lo tanto llamando a notifyChange (uri, null) que está en el método de inserción (...). – startoftext

+0

Solo un comentario, "que resulta en minimizar el número", de hecho, solo pospone la notificación hasta el final de la ejecución. Gracias por la solución propuesta! – Juancho

0

En un comentario a la respuesta original, Jens nos dirigió hacia SQLiteContentProvider en los PSE. Una razón por la que esto no es (¿todavía?) En el SDK puede ser que el AOSP parece contener múltiples variaciones de este código.

Por ejemplo, com.android.browser.provider.SQLiteContentProvider parece ser una solución un poco más completa, incorporando el principio de "notificaciones retrasadas" propuesto por Phillip Fitzsimmons manteniendo el proveedor seguro para hilos utilizando un ThreadLocal para el indicador de lote y sincronizando el acceso a la notificación demorada conjunto.

Sin embargo, a pesar de que el acceso al conjunto de URI a notificar de cambio está sincronizado, todavía puedo imaginar que las condiciones de carrera pueden ocurrir.Por ejemplo, si una operación larga publica algunas notificaciones, una operación por lotes más pequeña la supera, lo que activa las notificaciones y borra el conjunto antes de que la primera operación lo confirme.

Aún así, la versión anterior parece ser la mejor opción como referencia al implementar su propio proveedor.

Cuestiones relacionadas