Best practice per le prestazioni SQLite

Android offre il supporto integrato per SQLite, un database SQL efficiente. Segui queste best practice per ottimizzare il rendimento della tua app, assicurandoti che rimanga veloce e prevedibilmente veloce man mano che i dati aumentano. Utilizzando queste best practice, riduci anche la possibilità di riscontrare problemi di rendimento difficili da riprodurre e risolvere.

Per ottenere prestazioni più veloci, segui questi principi di rendimento:

  • Leggi meno righe e colonne: ottimizza le query per recuperare solo i dati necessari. Riduci al minimo la quantità di dati letti dal database, perché il recupero di dati in eccesso può influire sul rendimento.

  • Trasferisci il lavoro al motore SQLite: esegui operazioni di calcolo, filtro e ordinamento all'interno delle query SQL. L'utilizzo del motore di query di SQLite può migliorare notevolmente il rendimento.

  • Modifica lo schema del database: progetta lo schema del database in modo che SQLite possa creare piani di query e rappresentazioni dei dati efficienti. Indicizza correttamente le tabelle e ottimizza le strutture delle tabelle per migliorare il rendimento.

Inoltre, puoi utilizzare gli strumenti di risoluzione dei problemi disponibili per misurare il rendimento del database SQLite e identificare le aree che richiedono ottimizzazione.

Ti consigliamo di utilizzare la libreria Jetpack Room.

Configurare il database per il rendimento

Segui i passaggi descritti in questa sezione per configurare il database in modo da ottenere prestazioni ottimali in SQLite.

Abilitare la registrazione Write-Ahead

SQLite implementa le mutazioni aggiungendole a un log, che di tanto in tanto compatta nel database. Questa operazione è chiamata registrazione Write-Ahead (WAL).

Abilita WAL a meno che tu non stia utilizzando ATTACH DATABASE.

Rilassare la modalità di sincronizzazione

Quando utilizzi WAL, per impostazione predefinita ogni commit emette un fsync per garantire che i dati raggiungano il disco. In questo modo la durabilità dei dati migliora, ma i commit rallentano.

SQLite ha un'opzione per controllare la modalità sincrona. Se abiliti WAL, imposta la modalità sincrona su NORMAL:

// When opening the database
val paramsBuilder: SQLiteDatabase.OpenParams.Builder = SQLiteDatabase.OpenParams.Builder()
paramsBuilder.journalMode = SQLiteDatabase.SYNC_MODE_NORMAL

// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");

Con questa impostazione, un commit può essere restituito prima che i dati vengano archiviati su un disco. Se si verifica un arresto del dispositivo, ad esempio in caso di interruzione dell'alimentazione o di un kernel panic, i dati di cui è stato eseguito il commit potrebbero andare persi. Tuttavia, grazie alla registrazione, il database non viene danneggiato.

Se si arresta solo l'app, i dati raggiungono comunque il disco. Per la maggior parte delle app, questa impostazione comporta miglioramenti del rendimento senza costi significativi.

Definire schemi di tabelle efficienti

Per ottimizzare il rendimento e ridurre al minimo il consumo di dati, definisci uno schema di tabella efficiente. SQLite crea piani di query e dati efficienti, il che porta a un recupero dei dati più rapido. Questa sezione fornisce le best practice per la creazione di schemi di tabelle.

Prendi in considerazione INTEGER PRIMARY KEY

Per questo esempio, definisci e popola una tabella come segue:

CREATE TABLE Customers(
  id INTEGER,
  name TEXT,
  city TEXT
);
INSERT INTO Customers Values(456, 'John Lennon', 'Liverpool, England');
INSERT INTO Customers Values(123, 'Michael Jackson', 'Gary, IN');
INSERT INTO Customers Values(789, 'Dolly Parton', 'Sevier County, TN');

L'output della tabella è il seguente:

rowid id name city
1 456 John Lennon Liverpool, England
2 123 Michael Jackson Gary, IN
3 789 Dolly Parton Sevier County, TN

La colonna rowid è un indice che conserva l'ordine di inserimento. Le query che filtrano in base a rowid vengono implementate come una ricerca rapida nell'albero B, ma le query che filtrano in base a id sono una scansione lenta della tabella.

Se prevedi di eseguire ricerche in base a id, puoi evitare di archiviare la rowid colonna per ridurre i dati nello spazio di archiviazione e velocizzare il database complessivo:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  city TEXT
);

La tabella ora ha il seguente aspetto:

id name city
123 Michael Jackson Gary, IN
456 John Lennon Liverpool, England
789 Dolly Parton Sevier County, TN

Poiché non devi archiviare la colonna rowid, le query id sono veloci. Tieni presente che la tabella ora è ordinata in base a id anziché all'ordine di inserimento.

Accelerare le query con gli indici

SQLite utilizza gli indici per accelerare le query. Quando filtri (WHERE), ordini (ORDER BY) o aggreghi (GROUP BY) una colonna, se la tabella ha un indice per la colonna, la query viene accelerata.

Nell'esempio precedente, il filtro in base a city richiede la scansione dell'intera tabella:

SELECT id, name
WHERE city = 'London, England';

Per un'app con molte query sulla città, puoi accelerare queste query con un indice:

CREATE INDEX city_index ON Customers(city);

Un indice viene implementato come una tabella aggiuntiva, ordinata in base alla colonna dell'indice e mappata a rowid:

city rowid
Gary, IN 2
Liverpool, England 1
Sevier County, TN 3

Tieni presente che il costo di archiviazione della colonna city è ora doppio, perché è presente sia nella tabella originale sia nell'indice. Poiché utilizzi l'indice, il costo dello spazio di archiviazione aggiunto vale il vantaggio di query più veloci. Tuttavia, non mantenere un indice che non utilizzi per evitare di pagare il costo di archiviazione senza ottenere un miglioramento del rendimento delle query.

Creare indici su più colonne

Se le query combinano più colonne, puoi creare indici su più colonne per accelerare completamente la query. Puoi anche utilizzare un indice su una colonna esterna e lasciare che la ricerca interna venga eseguita come scansione lineare.

Ad esempio, data la seguente query:

SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name

Puoi accelerare la query con un indice su più colonne nello stesso ordine specificato nella query:

CREATE INDEX city_name_index ON Customers(city, name);

Tuttavia, se hai solo un indice su city, l'ordinamento esterno viene comunque accelerato, mentre l'ordinamento interno richiede una scansione lineare.

Questo funziona anche con le query sui prefissi. Ad esempio, un indice ON Customers (city, name) accelera anche il filtro, l'ordinamento e il raggruppamento per city, poiché la tabella degli indici per un indice su più colonne è ordinata in base agli indici specificati nell'ordine specificato.

Prendi in considerazione WITHOUT ROWID

Per impostazione predefinita, SQLite crea una colonna rowid per la tabella, dove rowid è un INTEGER PRIMARY KEY AUTOINCREMENT implicito. Se hai già una colonna INTEGER PRIMARY KEY, questa colonna diventa un alias di rowid.

Per le tabelle che hanno una chiave primaria diversa da INTEGER o una combinazione di colonne, prendi in considerazione WITHOUT ROWID.

Archiviare i dati di piccole dimensioni come BLOB e i dati di grandi dimensioni come file

Se vuoi associare dati di grandi dimensioni a una riga, ad esempio una miniatura di un'immagine o una foto per un contatto, puoi archiviare i dati in una colonna BLOB o in un file e poi archiviare il percorso nella colonna.

In genere, i file vengono arrotondati a incrementi di 4 KB. Per i file molto piccoli, in cui l'errore di arrotondamento è significativo, è più efficiente archiviarli nel database come BLOB. SQLite riduce al minimo le chiamate al file system e in alcuni casi è più veloce del file system sottostante.

Migliorare il rendimento delle query

Segui queste best practice per migliorare il rendimento delle query in SQLite riducendo al minimo i tempi di risposta e massimizzando l'efficienza di elaborazione.

Leggi solo le righe di cui hai bisogno

I filtri ti consentono di restringere i risultati specificando determinati criteri, come intervallo di date, località o nome. I limiti ti consentono di controllare il numero di risultati visualizzati:

db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Process cursor data
    }
}

Leggi solo le colonne di cui hai bisogno

Evita di selezionare colonne non necessarie, che possono rallentare le query e sprecare risorse. Seleziona invece solo le colonne utilizzate.

Nell'esempio seguente, selezioni id, name e phone:

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery(
    """
    SELECT id, name, phone
    FROM customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(1)
        // Further processing
    }
}

Tuttavia, hai bisogno solo della colonna name:

db.rawQuery("""
    SELECT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(0)
        // Further processing
    }
}

Parametrizzare le query

La stringa di query potrebbe includere un parametro noto solo in fase di runtime, ad esempio il seguente:

fun getNameById(id: Long): String?
    db.rawQuery(
        "SELECT name FROM customers WHERE id=$id", null
    ).use { cursor ->
        return if (cursor.moveToFirst()) {
            cursor.getString(0)
        } else {
            null
        }
    }
}

Nel codice precedente, ogni query crea una stringa diversa e quindi non sfrutta la cache delle istruzioni. Ogni chiamata richiede che SQLite la compili prima di poterla eseguire. In alternativa, puoi sostituire l'argomento id con un parametro e associare il valore a selectionArgs:

fun getNameById(id: Long): String? {
    db.rawQuery(
        """
          SELECT name
          FROM customers
          WHERE id=?
        """.trimIndent(), arrayOf(id.toString())
    ).use { cursor ->
        return if (cursor.moveToFirst()) {
            cursor.getString(0)
        } else {
            null
        }
    }
}

Ora la query può essere compilata una sola volta e memorizzata nella cache. La query compilata viene riutilizzata tra le diverse invocazioni di getNameById(long).

Esegui l'iterazione in SQL, non nel codice

Utilizza una singola query che restituisce tutti i risultati mirati, anziché un loop programmatico che esegue l'iterazione sulle query SQL per restituire i singoli risultati. Il loop programmatico è circa 1000 volte più lento di una singola query SQL.

Utilizzare DISTINCT per i valori univoci

L'utilizzo della parola chiave DISTINCT può migliorare il rendimento delle query riducendo la quantità di dati da elaborare. Ad esempio, se vuoi restituire solo i valori univoci di una colonna, utilizza DISTINCT:

db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Only iterate over distinct names in Kotlin
        // Process distinct name
    }
}

Utilizzare le funzioni di aggregazione quando possibile

Utilizza le funzioni di aggregazione per i risultati aggregati senza dati di riga. Ad esempio, il seguente codice verifica se esiste almeno una riga corrispondente:

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT id, name
    FROM Customers
    WHERE city = 'Paris';
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst()) {
        // At least one customer from Paris
        // Handle found
    } else {
        // No customers from Paris
        // Handle not found
}

Per recuperare solo la prima riga, puoi utilizzare EXISTS() per restituire 0 se non esiste una riga corrispondente e 1 se esistono una o più righe corrispondenti:

db.rawQuery("""
    SELECT EXISTS (
        SELECT null
        FROM Customers
        WHERE city = 'Paris';
    );
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
        // At least one customer from Paris
        // Handle found
    } else {
        // No customers from Paris
        // Handle not found
    }
}

Utilizza le funzioni di aggregazione SQLite nel codice dell'app:

  • COUNT: conta il numero di righe in una colonna.
  • SUM: aggiunge tutti i valori numerici in una colonna.
  • MIN o MAX: determina il valore più basso o più alto. Funziona per le colonne numeriche, i tipi DATE e i tipi di testo.
  • AVG: trova il valore numerico medio.
  • GROUP_CONCAT: concatena le stringhe con un separatore facoltativo.

Utilizzare COUNT() anziché Cursor.getCount()

Nell'esempio seguente, la Cursor.getCount() funzione legge tutte le righe del database e restituisce tutti i valori delle righe:

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT id
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    val count = cursor.getCount()
    // Use count
}

Tuttavia, utilizzando COUNT(), il database restituisce solo il conteggio:

db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    cursor.moveToFirst()
    val count = cursor.getInt(0)
    // Use count
}

Annidare le query anziché il codice

SQL è componibile e supporta sottoquery, join e vincoli di chiave esterna. Puoi utilizzare il risultato di una query in un'altra query senza passare per il codice dell'app. In questo modo si riduce la necessità di copiare i dati da SQLite e il motore di database può ottimizzare la query.

Nell'esempio seguente, puoi eseguire una query per trovare la città con il maggior numero di clienti, quindi utilizzare il risultato in un'altra query per trovare tutti i clienti di quella città:

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT city
    FROM Customers
    GROUP BY city
    ORDER BY COUNT(*) DESC
    LIMIT 1;
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst()) {
        val topCity = cursor.getString(0)
        db.rawQuery("""
            SELECT name, city
            FROM Customers
            WHERE city = ?;
        """.trimIndent(),
        arrayOf(topCity)).use { innerCursor ->
            while (innerCursor.moveToNext()) {
                // Process inner cursor data
            }
        }
    }
}

Per ottenere il risultato in metà del tempo dell'esempio precedente, utilizza una singola query SQL con istruzioni nidificate:

db.rawQuery("""
    SELECT name, city
    FROM Customers
    WHERE city IN (
        SELECT city
        FROM Customers
        GROUP BY city
        ORDER BY COUNT (*) DESC
        LIMIT 1;
    );
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToNext()) {
        // Process cursor data
    }
}

Verificare l'unicità in SQL

Se una riga non deve essere inserita a meno che un determinato valore della colonna non sia univoco nella tabella, potrebbe essere più efficiente applicare l'unicità come vincolo di colonna.

Nell'esempio seguente, viene eseguita una query per convalidare la riga da inserire e un'altra per inserirla effettivamente:

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery(
    """
    SELECT EXISTS (
        SELECT null
        FROM customers
        WHERE username = ?
    );
    """.trimIndent(),
    arrayOf(customer.username)
).use { cursor ->
    if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
        throw AddCustomerException(customer)
    }
}
db.execSQL(
    "INSERT INTO customers VALUES (?, ?, ?)",
    arrayOf(
        customer.id.toString(),
        customer.name,
        customer.username
    )
)

Anziché controllare il vincolo univoco in Kotlin, puoi controllarlo in SQL quando definisci la tabella:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  username TEXT UNIQUE
);

SQLite esegue la stessa operazione di quanto segue:

CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);

Ora puoi inserire una riga e lasciare che SQLite controlli il vincolo:

try {
    db.execSql(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        arrayOf(customer.id.toString(), customer.name, customer.username)
    )
} catch(e: SQLiteConstraintException) {
    throw AddCustomerException(customer, e)
}

SQLite supporta indici univoci con più colonne:

CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);

SQLite convalida i vincoli più velocemente e con un overhead inferiore rispetto al codice Kotlin. È una best practice utilizzare SQLite anziché il codice dell'app.

Raggruppare più inserimenti in una singola transazione

Una transazione esegue il commit di più operazioni, il che migliora non solo l'efficienza, ma anche la correttezza. Per migliorare la coerenza dei dati e accelerare il rendimento, puoi raggruppare gli inserimenti:

db.beginTransaction()
try {
    customers.forEach { customer ->
        db.execSql(
            "INSERT INTO Customers VALUES (?, ?, ?)",
            arrayOf(customer.id.toString(), customer.name, "customerValue")
        )
    }
} finally {
    db.endTransaction()
}

Utilizzare gli strumenti di risoluzione dei problemi

SQLite fornisce i seguenti strumenti di risoluzione dei problemi per misurare il rendimento.

Utilizzare il prompt interattivo di SQLite

Esegui SQLite sul tuo computer per eseguire query e imparare. Le diverse versioni della piattaforma Android utilizzano revisioni diverse di SQLite. Per utilizzare lo stesso motore presente su un dispositivo Android, utilizza adb shell ed esegui sqlite3 sul dispositivo di destinazione.

Puoi chiedere a SQLite di cronometrare le query:

sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...

EXPLAIN QUERY PLAN

Puoi chiedere a SQLite di spiegare come intende rispondere a una query utilizzando EXPLAIN QUERY PLAN:

sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers

L'esempio precedente richiede una scansione completa della tabella senza un indice per trovare tutti i clienti di Parigi. Questa operazione è chiamata complessità lineare. SQLite deve leggere tutte le righe e conservare solo quelle che corrispondono ai clienti di Parigi. Per risolvere il problema, puoi aggiungere un indice:

sqlite> CREATE INDEX Idx1 ON Customers(city);
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SEARCH test USING INDEX Idx1 (city=?

Se utilizzi la shell interattiva, puoi chiedere a SQLite di spiegare sempre i piani di query:

sqlite> .eqp on

Per ulteriori informazioni, consulta Pianificazione delle query.

Analizzatore SQLite

SQLite offre l'interfaccia a riga di comando (CLI) sqlite3_analyzer per eseguire il dump di informazioni aggiuntive che possono essere utilizzate per risolvere i problemi di rendimento. Per installare, visita la pagina di download di SQLite.

Puoi utilizzare adb pull per scaricare un file di database da un dispositivo di destinazione alla tua workstation per l'analisi:

adb pull /data/data/<app_package_name>/databases/<db_name>.db

Browser SQLite

Puoi anche installare lo strumento GUI SQLite Browser dalla pagina di download di SQLite.

Logging Android

Android cronometra le query SQLite e le registra per te:

# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR

Tracciamento Perfetto

Quando configuri Perfetto, puoi aggiungere quanto segue per includere le sessioni per le singole query:

data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      atrace_categories: "database"
    }
  }
}

dumpsys meminfo

adb shell dumpsys meminfo <package-name> stamperà le statistiche relative alla memoria utilizzata dall'app, inclusi alcuni dettagli sulla memoria SQLite. Ad esempio, questo è stato tratto dall'output di adb shell dumpsys meminfo com.google.android.gms.persistent sul dispositivo di uno sviluppatore:

DATABASES
      pgsz     dbsz   Lookaside(b) cache hits cache misses cache size  Dbname
PER CONNECTION STATS
         4       52             45     8    41     6  /data/user/10/com.google.android.gms/databases/gaia-discovery
         4        8                    0     0     0    (attached) temp
         4       52             56     5    23     6  /data/user/10/com.google.android.gms/databases/gaia-discovery (1)
         4      252             95   233   124    12  /data/user_de/10/com.google.android.gms/databases/phenotype.db
         4        8                    0     0     0    (attached) temp
         4      252             17     0    17     1  /data/user_de/10/com.google.android.gms/databases/phenotype.db (1)
         4     9280            105 103169 69805    25  /data/user/10/com.google.android.gms/databases/phenotype.db
         4       20                    0     0     0    (attached) temp
         4     9280            108 13877  6394    25  /data/user/10/com.google.android.gms/databases/phenotype.db (2)
         4        8                    0     0     0    (attached) temp
         4     9280            105 12548  5519    25  /data/user/10/com.google.android.gms/databases/phenotype.db (3)
         4        8                    0     0     0    (attached) temp
         4     9280            107 18328  7886    25  /data/user/10/com.google.android.gms/databases/phenotype.db (1)
         4        8                    0     0     0    (attached) temp
         4       36             51   156    29     5  /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
         4       36             97    47    27    10  /data/user/10/com.google.android.gms/databases/context_feature_default.db
         4       36             56     3    16     4  /data/user/10/com.google.android.gms/databases/context_feature_default.db (2)
         4      300             40  2111    24     5  /data/user/10/com.google.android.gms/databases/gservices.db
         4      300             39     3    17     4  /data/user/10/com.google.android.gms/databases/gservices.db (1)
         4       20             17     0    14     1  /data/user/10/com.google.android.gms/databases/gms.notifications.db
         4       20             33     1    15     2  /data/user/10/com.google.android.gms/databases/gms.notifications.db (1)
         4      120             40   143   163     4  /data/user/10/com.google.android.gms/databases/android_pay
         4      120            123    86    32    19  /data/user/10/com.google.android.gms/databases/android_pay (1)
         4       28             33     4    17     3  /data/user/10/com.google.android.gms/databases/googlesettings.db
POOL STATS
     cache hits  cache misses    cache size  Dbname
             13            68            81  /data/user/10/com.google.android.gms/databases/gaia-discovery
            233           145           378  /data/user_de/10/com.google.android.gms/databases/phenotype.db
         147921         89616        237537  /data/user/10/com.google.android.gms/databases/phenotype.db
            156            30           186  /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
             50            57           107  /data/user/10/com.google.android.gms/databases/context_feature_default.db
           2114            43          2157  /data/user/10/com.google.android.gms/databases/gservices.db
              1            31            32  /data/user/10/com.google.android.gms/databases/gms.notifications.db
            229           197           426  /data/user/10/com.google.android.gms/databases/android_pay
              4            18            22  /data/user/10/com.google.android.gms/databases/googlesettings.db

In DATABASES troverai:

  • pgsz: le dimensioni di una pagina del database, in KB.
  • dbsz: le dimensioni dell'intero database, in pagine. Per ottenere le dimensioni in KB, moltiplica pgsz per dbsz.
  • Lookaside(b): la memoria allocata al buffer lookaside di SQLite per connessione, in byte. Questi sono in genere molto piccoli.
  • cache hits: SQLite gestisce una cache delle pagine del database. Questo è il numero di hit della cache delle pagine (conteggio).
  • cache misses: il numero di mancati hit della cache delle pagine (conteggio).
  • cache size: il numero di pagine nella cache (conteggio). Per ottenere le dimensioni in KB, moltiplica questo numero per pgsz.
  • Dbname: il percorso del file DB. Nel nostro esempio, alcuni database hanno (1) o un altro numero aggiunto al nome, per indicare che esiste più di una connessione allo stesso database sottostante. Le statistiche vengono monitorate per connessione.

In POOL STATS troverai:

  • cache hits: SQLite memorizza nella cache le istruzioni preparate e tenta di riutilizzarle quando esegue le query, per risparmiare un po' di impegno e memoria nella compilazione delle istruzioni SQL. Questo è il numero di hit della cache delle istruzioni (conteggio).
  • cache misses: il numero di mancati hit della cache delle istruzioni (conteggio).
  • cache size: a partire da Android 17, questo valore elenca il numero totale di istruzioni preparate nella cache. Nelle versioni precedenti, questo valore è equivalente alla somma di hit e mancati hit elencati nelle altre due colonne, e non rappresenta le dimensioni della cache.

Risorse aggiuntive

Visualizzare i contenuti