Análisis del código fuente MMKV de optimización del rendimiento del almacenamiento
Su nacimiento más temprano se debió principalmente a un error importante en WeChat iOS, es decir, un texto especial puede causar que WeChat iOS falle, y esto sucedió más de una vez. Para contar la frecuencia de aparición de dichos caracteres de falla y realizar el filtrado, debido a demasiadas ocurrencias, se descubrió que el componente de almacenamiento del par clave-valor original NSUserDefaults no podía cumplir con los requisitos en absoluto, lo que provocaría un retraso en el deslizamiento de la celda. .
Por lo tanto, iOS comenzó a crear un nuevo componente de almacenamiento de valores clave de alto rendimiento. Al mismo tiempo, SharedPreferences en Android también tiene algunas deficiencias:
Por lo tanto, Android también comenzó a reutilizar el MMKV de iOS, y luego Android necesitaba escribir datos en múltiples procesos, y el equipo de Android mejoró esto.
Aquí está el cuadro oficial de comparación de rendimiento:
Puedes ver que MMKV es cientos de veces más rápido que los componentes que utilizamos para el desarrollo.
Por lo tanto, este artículo girará en torno a por qué el rendimiento de MMKV es tan alto (desde la perspectiva del código fuente) y por qué SharePrefences puede ser un ANR.
Tenga en cuenta que el siguiente artículo utilizará el código fuente MMKV versión 1.1.1 como ejemplo para el análisis. Si tiene algún problema, consulte este artículo /p/c12290a9a3f7 para analizarlo.
Como siempre, echemos un vistazo a cómo usar MMKV. mmkv es en realidad lo mismo que SharePrefences, con cuatro operaciones: agregar, eliminar, verificar y cambiar.
Como componente de almacenamiento clave-valor, MMKV también está optimizado para la serialización de objetos almacenados. Los métodos más utilizados incluyen json y Twitter's Serial. MMKV utiliza la solución de serialización de código abierto de Google:
Protocol Buffers es una solución más avanzada que json:
Puedes leer el siguiente artículo para aprender a usarlo: /p/e8712962f0e9
La siguiente es una comparación de elementos entre varias serializaciones de objetos
MMKV eligió Protocol Buffers como el núcleo del caché de objetos debido a su bajo costo de tiempo.
Inicialización antes de su uso:
Por supuesto, MMKV puede escribir en estos tipos básicos además de SharePrefences, y siempre que SharePrefences lo admita, debe poder admitirlo.
Como se mencionó anteriormente, el tipo de datos leído por cada clave es el nombre del tipo correspondiente a decodexxx. Es muy sencillo de utilizar.
Posibilidad de eliminar el valor correspondiente a una única clave, así como cada valor correspondiente a varias claves. containsKey determina si la clave correspondiente existe en la memoria caché del disco de mmkv.
Al igual que SharePrefences, mmkv también puede dividir los archivos de caché correspondientes según módulos y servicios:
Aquí, se crea una instancia con ID a en el disco para almacenar en caché los datos.
Cuando se requiere almacenamiento en caché multiproceso:
MMKV puede usar la memoria anónima de Ashmem para acelerar la transferencia de objetos grandes:
Proceso 1:
El punto más importante es que MMKV migra el caché de SharePrefences a MMKV, y el uso posterior es el mismo que SharePrefences.
Aquí está la migración de los datos myData de SharedPreferences a MMKV. Por supuesto, si queremos mantener el uso de SharePreferences sin cambios, debemos personalizar SharePreferences nosotros mismos.
El uso de mmkv es muy sencillo, centrémonos en cómo funciona.
Primero veamos la inicialización de MMKV.
Puedes ver que la inicialización en realidad se divide en los siguientes pasos:
Puedes ver que en realidad está tomando esta determinación. Porque esto está configurado en el paquete libc. En este momento, BuildConfig.FLAVOR es StaticCpp, que no cargará c _shared. Por supuesto, si ya estamos usando la biblioteca c _shared, no es necesario empaquetarla, usando defaultPublishConfig "SharedCppRelease" intentará encontrar una manera y su tamaño se reducirá en 2M.
Tenga en cuenta un requisito previo, es decir, después de llamar a System.loadLibrary, la inicialización de JNI llamará a dlsym, y dlsym llamará al método JNI_OnLoad en JNI después de cargarlo en la memoria a través de dlopen.
De hecho, el propósito de esto es muy simple:
Puede ver todos los métodos de almacenamiento MMKV en estos métodos nativos y configurar el soporte de almacenamiento ashemem de la memoria disfrutada por Dios. , así como soporte para solicitudes malloc locales para acceder directamente a la memoria
A continuación, todo lo que necesita hacer es obtener el método de inicialización oficial MMKV.
Este método en realidad llama al método pthread_once. Por lo general, este método selecciona un subproceso para la inicialización en un entorno de subprocesos múltiples según la política de programación del kernel.
El algoritmo aquí es realmente muy simple:
defaultMMKV En este punto, se llama a getDefaultMMKV, que es un método nativo que de forma predeterminada utiliza el modo de proceso único. Como puede adivinar por el diseño aquí, getDefaultMMKV crea una instancia de un objeto MMKV desde la capa nativa y permite que el objeto MMKV de la capa Java instanciado lo contenga. Posteriormente, los métodos de capa Java y los métodos de capa nativa se asignan uno por uno para implementar objetos Java que operan directamente sobre objetos nativos.
Echemos un vistazo al mmkvWithID de MMKV.
Es algo similar a defaultMMKV. También llama al método de capa local para la inicialización y permite que el objeto MMKV de la capa Java contenga la capa local. Entonces, ¿podemos pensar que estas dos instancias llaman al mismo método bajo el capó, solo que con un conjunto de identificación adicional?
Puede consultar el archivo MMKV.h:
Aquí puede ver que la especulación anterior es correcta y que mmkvWithID eventualmente será llamado para la creación de instancias tan pronto como se complete la creación de instancias. La identificación de mmkv predeterminada es mmkv.default. Android establece un tamaño de página predeterminado, digamos 4kb.
Todos los identificadores de mmkv y las instancias de MMKV correspondientes se almacenan en la tabla hash g_instanceDic instanciada previamente. Cada ID de mmkv corresponde a una ruta de archivo, y la ruta se procesa de la siguiente manera:
Si se descubre que el mmkv en la ruta correspondiente se ha almacenado en caché en la tabla hash, se devolverá directamente. De lo contrario, la ruta relativa se guarda, se pasa a MMKV para la creación de instancias y se guarda en la tabla hash g_instanceDic.
Echemos un vistazo a cómo el constructor MMKV inicializa varios campos clave.
mmkvID es la ruta al archivo de caché después de md5.
Como puede ver, la identificación se inicializa de acuerdo con el modo actual. Si no se crea en el modo de memoria de **** anónimo ashmem, la identificación será similar a la anterior. id es la ruta al archivo de caché después de md5.
Tenga en cuenta que el modo está configurado en MMKV_ASHMEM, lo que significa que el modo de memoria **** anónima de ashmem se crea de la siguiente manera:
Este es en realidad el archivo de memoria en el dirección del directorio del controlador.
A continuación, después de usar **** en el constructor para habilitar la protección de bloqueo de archivos, llame a loadFromFile para inicializar aún más los datos dentro de MMKV.
Ahora que tenemos una visión general de las responsabilidades de cada campo en MMKV, explicaremos en detalle cómo funciona.
Aquí encontramos lo que parece ser la clase principal MemoryFile, con un nombre similar al archivo de memoria anónimo mapeado en la capa de Java descrito en el artículo sobre memoria anónima habilitada para **** de Ashmem.
Veamos primero la inicialización de MemoryFile.
MemeoryFile se inicializa en dos modos:
El procesamiento aquí es muy simple:
Puede ver que la llamada al sistema mmap se llama en este momento, por Establezca el bit de bandera en lectura-escritura y active el modo MAP_SHARED. De esta manera, el archivo se asigna a un segmento de memoria de 4 kilobytes en el kernel. Cuando acceda al archivo en el futuro, podrá acceder directamente a la memoria asignada por el archivo sin pasar por el kernel.
Para el análisis del código fuente de la llamada al sistema mmap, consulte este artículo "Inicialización de los principios de mapeo del controlador de Binder".
Puede ver que el proceso en realidad todavía está expandiendo la capacidad mediante ftruncate y luego llamando a zeroFillFile, que primero mueve el puntero al final de la capacidad actual mediante lseek y luego lo llena con datos nulos "\0 "El resto. La dirección a la que apunta la asignación final es válida y primero se desasignará y luego se reasignará.
¿Por qué dar el último paso? Si lees el artículo sobre el código fuente que analicé para mmap, puedes ver que el archivo usa el modo MAP_SHARED para vincular esencialmente un bloque de memoria asignada de vma a la estructura del archivo. ftruncate solo escala la estructura del archivo, pero no la memoria virtual vinculada correspondiente, por lo que es necesario desasignarla y luego reasignarla.
Si usa el modo Ashmem para abrir MMKV:
El siguiente paso es loadFromFile. Se puede decir que este método es el método principal de MMKV. Toda lectura, escritura o expansión debe realizarse. Siga este método. La memoria del archivo mapeado se almacena en caché en la memoria MMKV.
Después de ingresar este método, el proceso es el siguiente:
Aquí nos encontramos con un campo bastante oscuro m_version, que por su nombre se parece un poco al número de versión de MMKV.
En realidad, se refiere al estado actual del MMKV, representado por un objeto de enumeración:
Tenga en cuenta que m_vector es una matriz de caracteres de longitud 16. En realidad, es muy sencillo copiar una copia de 16 bits del m_vector del archivo en el m_vector de m_metaInfo. Esto se debe a que el cifrado aes debe ser múltiplo de 16 para funcionar correctamente.
La inicialización se divide en estos 6 puntos. Discutiremos la lógica central de la inicialización de MMKV a partir de los últimos tres puntos. También debemos comenzar a comprender la estructura de almacenamiento de memoria de MMKV.
Como puedes ver, lo primero que debes hacer es obtener la dirección del puntero mapeado de m_file y luego leer los datos de 4 bits. Estos 4 bits de datos son datos reales de tamaño real. Sin embargo, si m_version de m_metaInfo es mayor o igual a 3, se obtiene el tamaño real almacenado en m_metaInfo.
El método de verificación es comparar el crcDigest guardado en m_metaInfo con el crcDigest leído desde m_file. Si son iguales, devuelve verdadero. Si es coherente, devuelve verdadero y establece loadFromFile en verdadero.
De hecho, aquí solo se procesa el estado donde m_version de m_metaInfo es mayor o igual a 3. Repasemos que en el método readActualSize, la lectura de la longitud de los datos almacenados actualmente se divide en dos lógicas de lectura. Si es mayor o igual a 3, obtenido de m_metaInfo.
El error en la suma de comprobación de CRC indica que se produjo una excepción mientras escribíamos. Necesitamos forzar la restauración de los datos.
Lo primero que debe hacer es aclarar qué ha verificado la suma de verificación CRC:
MMKV ha tomado los siguientes pasos para manejar solo casos con nivel de estado MMKVVersionActualSize. Para este caso, la información del último MMKV quedará registrada en m_metaInfo. Por lo tanto, puede verificar la longitud de los datos almacenados a través de m_metaInfo y luego actualizar la longitud de los datos del registro real.
Finalmente, lea los datos de la copia de seguridad no actualizados y la longitud del campo de suma de verificación crc en el último MMKV a través de writeActualSize y regístrelos en la memoria asignada.
Si la última suma de verificación compensada sigue siendo un error de suma de verificación cc, el método de devolución de llamada final onMMKVCRCCheckFail. Este método refleja la estrategia de manejo de excepciones implementada en la capa Java.
En el caso de OnErrorRecover, tanto loadFromFile como needFullWriteback se configuran en verdadero para recuperar la mayor cantidad de datos posible. Por supuesto, en el caso de OnErrorDiscard, se descartarán todos los datos.