Práctica de optimización del rendimiento (3): reflexiones sobre la optimización de Caton
El sistema Android envía una señal VSYNC cada 16 ms para activar la representación de la interfaz de usuario. Si cada representación tiene éxito, se pueden lograr los 60 fps necesarios para una imagen fluida, lo que también significa que la mayoría de las operaciones de la interfaz. programa Todo debe completarse en 16 ms. Si no se puede completar, los fotogramas se eliminarán y el fotograma anterior se mostrará repetidamente, lo que provocará un retraso visual.
Desde la perspectiva de todo el proceso de representación de la vista:
Surfaceflinger es un proceso independiente iniciado por init y proporciona servicios del sistema para vistas sintéticas. Si Surfaceflinger se bloquea, se reiniciará zygote.
En el método init de Surfaceflinger, se crean instancias de HWComposer y dos EventThreads.
HWComposer: Responsable de emitir señales Vsync generadas por hardware o simuladas por software.
EventThread: Responsable de distribuir vsync a Choreographer y SurfaceFlinger. Entre ellos, mEventThread corresponde a Choreographer y mSFEventThread: corresponde a SurfaceFlinger.
Hay dos suscriptores principales a la señal VSYNC: SurfaceFlinger y Choreographer.
SurfaceFlinger: Recibe señales para ejecutar el proceso de capa de síntesis.
Coreógrafo: recibe señales para controlar las tres operaciones de la interfaz de usuario de procesamiento sincrónico de entrada (Input), animación (Animation) y dibujo (Draw).
Choreographer notifica a la capa de aplicación que dibuje y SurfaceFlinger es responsable de sintetizar la vista. Se agrega un cierto desplazamiento antes de las dos para garantizar que estén sincronizadas.
En este proceso, la CPU es responsable de procesar la vista en polígonos y texturas. La GPU es responsable de rasterizar polígonos y texturas en datos de píxeles para su visualización.
1 Problema a nivel de vista
Incluyendo que el nivel de diseño es demasiado profundo y hay demasiadas Vistas, la Vista es demasiado compleja, se repite el dibujo, ListView no está optimizado y la animación el diseño no es razonable, etc.
Esto es lo primero que se debe verificar cuando se encuentran problemas de retraso. Algunos problemas se pueden evitar mediante especificaciones de codificación en la etapa de desarrollo.
1) El nivel de diseño es demasiado profundo y hay demasiadas vistas: se puede detectar a través de Lint: mediante el uso de contenedores razonables, primero reduzca el nivel y luego reduzca el número de vistas. y reutilizar tanto como sea posible.
2) La Vista es demasiado compleja: si es una Vista personalizada, entonces la optimización debe considerarse desde dos aspectos: la vista es demasiado profunda y la Vista es demasiada. Si se trata de una Vista madura: como una Vista pesada, como WebView y VideoView, intente reutilizar y administrar el ciclo de vida tanto como sea posible.
3) Dibujo repetido: habilite el sobredibujo de GPU en Configuración; el modo de renderizado de GPU puede comprender la relación jerárquica de la vista actual. Por supuesto, esta parte también es inseparable de los dos puntos anteriores. preste atención a los cambios Excepto por el fondo no requerido en xml.
4) Optimización de ListView, esta parte es principalmente la reutilización de convertView, que puede reducir el uso de ViewHolder, reducir la búsqueda y asignación de View y acelerar la velocidad de carga de la página; : controla los datos cargados a la vez. De esta manera, la velocidad de carga será más rápida y la presión de la memoria será relativamente pequeña.
5) Animación: Diseñe las animaciones de forma racional y trate de no utilizar animaciones de fotogramas si es posible, porque las imágenes ocupan más memoria, especialmente cuando hay una gran cantidad de ellas. Además, para la animación de propiedades, se puede combinar una serie de animaciones de la misma vista usando Keyframe PropertyValuesHolder para usar solo un ObjectAnimator. La animación de múltiples vistas se puede combinar y ordenar usando AnimatorSet.
2 Tiempo relacionado con los mensajes
Todos sabemos que las operaciones que consumen mucho tiempo se realizan en subprocesos y el identificador se devuelve al hilo principal para actualizar la interfaz de usuario. Sin embargo, el mensaje en sí también consume mucho tiempo, principalmente en dos aspectos: 1) El mensaje en sí tarda en ejecutarse, 2) La ejecución del mensaje se retrasa. El tiempo de ejecución del mensaje en sí es el del hilo principal. La ejecución del mensaje se retrasa en la cola de mensajes, debido a demasiados mensajes anteriores o un tiempo de ejecución demasiado largo, la operación actual que necesita actualizar la interfaz de usuario no se puede procesar a tiempo. , especialmente 16,6 ms. Bajo estándares rígidos, los fotogramas inevitablemente se perderán una vez retrasados.
3 Consumo de tiempo del hilo principal
De lo que quiero hablar en esta parte no es de realizar operaciones que consumen mucho tiempo en el hilo principal, sino de analizar el problema que requiere mucho tiempo. desde la perspectiva de la programación de la CPU, en otras palabras, por ejemplo, si el subproceso principal tarda 500 ms, o cuánto tiempo ha estado ejecutándose, o si hay estados como Dormir y Dormir ininterrumpidamente, la CPU se adelanta durante este período y allí. No es momento de realizar su operación. Si hay una escena, se puede ver claramente capturando systrace.
4 El evento de entrada en sí consume mucho tiempo.
Hay tres miembros importantes en todo el sistema de entrada de Android: Eventhub, InputReader y InputDispatcher. Cada uno de ellos tiene diferentes responsabilidades. Eventhub es responsable de monitorear /dev/input para generar eventos de entrada. InputReader es responsable de leer eventos de Eventhub y enviar los eventos leídos a InputDispatcher y distribuirlos al teléfono móvil actual de acuerdo con las necesidades reales. La ventana real con foco finalmente se entrega a ActivityThread para su procesamiento a través de mensajes.
Perspectiva del sistema:
El proceso de distribución de eventos de InputDispatcher a Window es una comunicación entre procesos, y obtener la ventana correspondiente en sí puede llevar mucho tiempo.
Perspectiva de la aplicación:
El cliente que recibe el mensaje del evento puede tardar mucho tiempo y retrasarse, lo que vuelve a la categoría de mensaje que consume mucho tiempo.
5 Tiempo de retención del bloqueo
Este es un problema en el nivel de lógica empresarial. El más simple es el punto muerto del hilo principal, o el hilo principal está esperando el bloqueo y luego el. El bloqueo actual está bloqueado por otros subprocesos que se mantienen realizando operaciones que consumen mucho tiempo, etc.
6 GC frecuente
Sabemos que al realizar una operación de GC, cualquier operación de todos los subprocesos deberá suspenderse. Una vez completada la operación de GC, otras operaciones pueden continuar ejecutándose. . En términos generales, un solo GC no toma mucho tiempo, pero una gran cantidad de operaciones de GC sin parar ocuparán significativamente el tiempo del intervalo de cuadros (16 ms). Si se realizan demasiadas operaciones de GC durante el intervalo de fotogramas, naturalmente habrá menos tiempo disponible para otros cálculos similares, renderizado y otras operaciones.
Hay dos razones para la ejecución frecuente de GC:
1) Jitter de la memoria, que se puede ver claramente en el monitor de memoria. Se crea una gran cantidad de objetos en un corto período de tiempo. tiempo y luego liberado rápidamente.
Por ejemplo: empalmar cadenas en un bucle for en un método. Se generará una gran cantidad de objetos String descartados y se reciclarán en un corto período de tiempo, por lo que es fácil causar fluctuaciones. En su lugar, se pueden usar StringBuilder/StringBuffer. Se implementan como matrices dinámicas con una longitud inicial de 128. Si no es suficiente, la longitud se puede aumentar mediante copia de matriz. Los objetos se administran de manera unificada, lo que no causará el problema de una gran cantidad de creación y destrucción en un corto período de tiempo. Al mismo tiempo, agregar es más seguro.
2) Generar una gran cantidad de objetos instantáneamente ocupará seriamente el área de memoria de Young Generation. Cuando se alcanza el umbral y el espacio restante no es suficiente, también se activará GC. Incluso si cada objeto asignado ocupa una pequeña cantidad de memoria, su apilamiento aumentará la presión sobre el montón, lo que desencadenará más otros tipos de GC. Esta operación puede afectar la velocidad de cuadros y causar problemas de rendimiento percibidos por el usuario.
1 motivo de la memoria
Cuando la memoria del sistema es muy baja, la experiencia general es: cuando MemAvailable es inferior a MemTotal 1/10, es fácil provocar un retraso causado por la memoria. No es más que cuando el kernel asigna memoria cuando la memoria es baja, es difícil obtener directamente páginas del tamaño apropiado de la memoria física (en este momento, se activarán operaciones de reciclaje, como la compactación de la memoria). (compacto) y reciclaje de páginas anónimas (intercambio), reciclaje de páginas de archivos (sucio=escritura, limpieza=descartar) y otras operaciones. Estas operaciones de reciclaje son más lentas y, por tanto, requieren más tiempo. Este proceso se refleja principalmente cuando se inicia una nueva aplicación y el proceso de bifurcación del cigoto solicita memoria.
2 Tiempo de retención del bloqueo del servicio del sistema
Utilice la llamada de carpeta para solicitar servicios del sistema. En términos generales, los métodos correspondientes a los servicios del sistema como AMS y WMS deben ignorarse al principio. En primer lugar, es un bloqueo grande. En muchos casos, operaciones específicas provocarán que el bloqueo requiera mucho tiempo. Analice los problemas específicos.
3 problemas de programación de CPU
Este tipo de situación es poco común, pero existe. Durante un determinado ciclo de dibujo, la CPU tiene prioridad y no puede iniciar la operación de dibujo a tiempo. Esto se divide en varias partes. En primer lugar, si se adelanta mediante un determinado proceso, como dex2oat. O si el uso de la CPU es muy alto durante este período, puede ser que el núcleo grande esté lleno, pero el núcleo pequeño esté relativamente inactivo. Esto puede indicar un problema con la programación del sistema, etc.
Por ejemplo: cuando ocurre dex2oat, todas las CPU disponibles están ocupadas (la política predeterminada es iniciar tantos subprocesos como núcleos haya), se extraerá el archivo dex en el archivo original y se mostrarán las instrucciones. juzgar uno por uno, luego traduce y genera una gran cantidad de contenido intermedio, que no se puede guardar en la memoria, por lo que se adopta el mecanismo de intercambio, cuanto menos memoria, más fácil es intercambiar, por lo que también puede causar IO. embotellamiento.
Puede configurar las propiedades del sistema: dalvik.vm.bg-dex2oat-threads y dalvik.vm.dex2oat-threads. Estas dos propiedades del sistema sirven para establecer el número de subprocesos que limitan la ejecución de dex2oat. el frente y la parte posterior respectivamente, correspondientes a 8. Para la CPU central, por ejemplo, configurar el frente y el fondo en 4 respectivamente prolongará el tiempo de ejecución de dex2oat, pero se aliviará el retraso.
Por supuesto, otra situación es que cuando la temperatura del teléfono móvil es demasiado alta, lo que hace que la CPU reduzca la frecuencia, también se producirá un retraso del sistema.
Este artículo solo proporciona una pequeña idea inmadura para el análisis de retrasos.
Continuaré actualizando a medida que avance mi aprendizaje.
Para conocer el contenido relacionado con el diseño y el dibujo repetido involucrado en este artículo, consulte mi artículo: Optimización del diseño.
Para conocer las herramientas de optimización del rendimiento relacionadas involucradas en este artículo, consulte mi serie de artículos: Resumen de herramientas de optimización del rendimiento