Ideas de análisis de fugas de memoria de Android y análisis de casos

análisis de ideas

La pérdida de memoria se refiere a un fenómeno en el proceso de Android en el que algunos objetos ya no se usan, pero algunos objetos con un ciclo de vida más largo hacen referencia a ellos, lo que hace que GC no pueda reciclar los recursos de memoria que ocupan y el uso de memoria continúa aumentando. Las pérdidas de memoria son un factor común que hace que el rendimiento de nuestras aplicaciones disminuya y se congele. La idea central para resolver este tipo de problemas se puede resumir en los dos pasos siguientes:

  1. Simule la ruta de operación de las pérdidas de memoria, observe los cambios en la memoria del montón de aplicaciones y determine la ubicación aproximada del problema;
  2. Realice un análisis en la ubicación específica, encuentre la cadena de referencia completa del objeto filtrado que apunta a la raíz del GC y controle la pérdida de memoria desde la fuente.
Herramienta de análisis: Android Studio Profiler

Hay dos herramientas de análisis de memoria de uso común en Profiler: gráfico de curva de memoria y volcado de montón; la curva de memoria puede observar el estado de uso de la memoria en tiempo real y ayudarnos en el análisis dinámico de la memoria;

Cuando ocurre una pérdida de memoria, el fenómeno típico de la curva de memoria es que presenta forma de escalera, y una vez que sube, es difícil caer; por ejemplo, después de la pérdida de Actividad, el uso de memoria de la página aumentará todo el tiempo. Después de abrir y cerrar la página repetidamente, y después de hacer clic en el ícono de la papelera para GC manual, el uso no se puede reducir. Al nivel anterior a abrir la Actividad, existe una alta probabilidad de pérdida de memoria.

En este momento, podemos volcar manualmente la distribución de memoria en la memoria del montón de la aplicación para realizar un análisis estático:

Descripción de varios indicadores en la interfaz de usuario:

  1. Allocations: el número de instancias de esta clase en la memoria del montón;
  2. Native Size: La memoria ocupada por los objetos nativos a los que hacen referencia todas las instancias de esta clase.
  3. Shallow Size: El uso de memoria real de todas las instancias de esta clase, excluyendo el uso de memoria de los objetos a los que hacen referencia;
  4. Retained Size: Shallow SizeA diferencia de , este número representa la huella de memoria de todas las instancias de la clase y de todos los objetos a los que hace referencia;

Con la ayuda de una imagen, puedes tener una impresión más intuitiva de estos atributos:

Como representaShallow Sizey el punto azulde la memoriaNative Sizetamañose muestra en la figura anterior, el punto rojo representa el Debido a que las pérdidas de memoria a menudo forman un "efecto en cadena", a partir del objeto filtrado, todos los objetos y recursos nativos a los que hace referencia el objeto no se pueden reciclar, lo que resulta en una disminución en la eficiencia del uso de la memoria.Retained SizeRetained Size

Además, Leaksrepresenta el número de posibles instancias de pérdida de memoria; haga clic en una clase en la lista para ver los detalles de la instancia de la clase; en la lista de instancias representa la profundidad de la cadena de llamadas más corta depthalcanzada GC Rootpor la instancia, que Referencese puede ver intuitivamente en la apilar en la columna derecha de la Figura 1 Con la cadena de llamadas completa, puede rastrear todo el camino hacia atrás para encontrar las referencias más sospechosas, analizar la causa de la fuga según el código y recetar el medicamento adecuado para curar el problema.

A continuación, analizaremos algunos casos en los que encontramos algunas pérdidas de memoria típicas en nuestros proyectos:

Analisis de CASO

Caso 1: pérdida de memoria de BitmapBinder

Cuando se trata de escenarios de transmisión de mapas de bits entre procesos, adoptamos un BitmapBindermétodo: debido a que Intent nos permite pasar un Binder personalizado, podemos usar Binder para implementar la transmisión Intent de objetos Bitmap:

// IBitmapBinder AIDL文件 
import android.graphics.Bitmap; 
interface IBitmapInterface { 
    Bitmap getIntentBitmap(); 
}

Sin embargo, Activity1después de pasar un mapa de bits BitmapBinderal uso Activity2, se produjeron dos pérdidas de memoria graves:

  1. Regresa después del salto y Activity1no se puede reciclar durante el final;
  2. Al saltar repetidamente, Bitmaplos Binderobjetos se crearán repetidamente y no se podrán reciclar;

Primero analice el volcado del montón:

Esta es una pérdida de memoria de "instancias múltiples", es decir, cada vez que Activity1se abre el final, se agregará un objeto Actividad y se dejará en el montón y no se puede destruir; es común en escenarios como referencias de clases internas y referencias de matrices estáticas. (como listas de oyentes); según Profiler Dada la cadena de referencia, encontramos BitmapExtesta clase:

suspend fun Activity.startActivity2WithBitmap() {
    val screenShotBitmap = withContext(Dispatchers.IO) { 
        SDKDeviceHelper.screenShot() 
    } ?: return
    startActivity(Intent().apply {
        val bundle = Bundle()
        bundle.putBinder(KEY_SCREENSHOT_BINDER, object : IBitmapInterface.Stub() {
            override fun getIntentBitmap(): Bitmap {
                return screenShotBitmap
            }
        }) 
        putExtra (INTENT_QUESTION_SCREENSHOT_BITMAP, bundle)
    })
}

BitmapExtHay un método de extensión global de Actividad startActivity2WithBitmap, que crea un Binder, arroja en él la captura de pantalla obtenida Bitmap, la envuelve en el Intent y la envía a Activity2; obviamente hay una IBitmapInterfaceclase interna anónima aquí, y parece que se produce la fuga. desde aquí. ;

Pero tengo dos preguntas. Una es que esta clase interna está escrita en el método. Cuando finalice el método, ¿no se borrará la referencia de clase interna en la pila de métodos? En segundo lugar, esta clase interna no hace referencia a Actividad, ¿verdad?

Para comprender estos dos puntos, debemos descompilar el código Kotlin en Java y echar un vistazo:

@Nullable
public static final Object startActivity2WithBitmap(@NotNull Activity $this$startActivity2WithBitmap, boolean var1, @NotNull Continuation var2) {
    ...
    Bitmap var14 = (Bitmap)var10000;
    if (var14 == null) {
        return Unit.INSTANCE;
    } else {
        Bitmap screenShotBitmap = var14;
        Intent var4 = new Intent();
        int var6 = false;
        Bundle bundle = new Bundle();
        // 内部类创建位置:
        bundle.putBinder("screenShotBinder", (IBinder)(new BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1($this$startActivity2WithBitmap, screenShotBitmap)));
        var4.putExtra("question_screenshot_bitmap", bundle);
        Unit var9 = Unit.INSTANCE;
        $this$startActivity2WithBitmap.startActivity(var4);
        return Unit.INSTANCE;
    }
}

// 这是kotlin compiler自动生成的一个普通类:
public final class BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1 extends IBitmapInterface.Stub {
    // $FF: synthetic field
    final Activity $this_startActivity2WithBitmap$inlined; // 引用了activity
    // $FF: synthetic field
    final Bitmap $screenShotBitmap$inlined;

    BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1(Activity var1, Bitmap var2) {
        this.$this_startActivity2WithBitmap$inlined = var1;
        this.$screenShotBitmap$inlined = var2;
    }
    @NotNull
    public Bitmap getIntentBitmap() {
        return this.$screenShotBitmap$inlined;
    }
}

En el archivo Java compilado por Kotlin Compiler, IBitmapInterfacela clase interna anónima se reemplaza por una clase normal BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1, y esta clase normal contiene la Actividad. La razón de esta situación es que para usar las variables del método normalmente dentro de la clase, Kotlin escribe los parámetros de entrada del método y todas las variables creadas sobre el código interno de la clase en las variables miembro de la clase; por lo tanto, el Actividad es la referencia de esta clase; además, el ciclo de vida de Binder es más largo que el de Actividad, por lo que se producen pérdidas de memoria.

La solución es declarar directamente una clase normal para omitir la "optimización" del compilador Kotlin y eliminar la referencia a Actividad.

class BitmapBinder(private val bitmap: Bitmap): IBitmapInterface.Stub() {
    override fun getIntentBitmap( ) = bitmap
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinder(screenShotBitmap))

A continuación, el problema es que Bitmap y Binder se crearán repetidamente y no se podrán reciclar. El fenómeno de la memoria es como se muestra en la figura. Cada vez que salta y se cierra, la memoria aumentará un poco, como una escalera, no se puede liberar. después de GC;

En el montón, 2560x1600, 320densityse puede inferir del tamaño del mapa de bits que se trata de objetos de mapa de bits de captura de pantalla que no se pueden reciclar y que Binder mantiene; sin embargo, al observar la cadena de referencia de Binder, no encontramos ninguna referencia relacionada con nuestra aplicación;

Especulamos que la capa nativa con un ciclo de vida largo debería hacer referencia a Binder, lo cual está relacionado con la implementación de Binder, pero no hemos encontrado un método efectivo para reciclar Binder;

Una solución es reutilizar Binder para garantizar que Binder no se vuelva a crear cada vez que se abre Activity2; además, cambie el BitmapBindermapa de bits a una referencia débil, de modo que incluso si Binder no se puede reciclar, el mapa de bits se puede reciclar a tiempo. Después de todo, el mapa de bits es la casa grande de la memoria.

object BitmapBinderHolder {
    private var mBinder: BitmapBinder? = null // 保证全局只有一个BitmapBinder

    fun of(bitmap: Bitmap): BitmapBinder {
        return mBinder ?: BitmapBinder(WeakReference(bitmap)).apply { mBinder = this }
    }
}

class BitmapBinder(var bitmapRef: WeakReference<Bitmap>?): IBitmapInterface.Stub() {
    override fun getIntentBitmap() = bitmapRef?.get()
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinderHolder.of(screenShotBitmap))

Verificación: al igual que el mapa de memoria, después de un GC, todos los mapas de bits creados se pueden reciclar normalmente.

Caso 2: pérdida de memoria del complemento de escena multimotor de Flutter

Muchos proyectos utilizan soluciones multimotor para implementar el desarrollo híbrido de Flutter. Cuando la página de Flutter está cerrada, para evitar pérdidas de memoria, no solo es necesario desvincular y destruir componentes relacionados como FlutterView, FlutterEngineetc. de manera oportuna, sino que También debe prestar atención a si cada complemento de Flutter se lanza normalmente MessageChannel.

Por ejemplo, en uno de nuestros proyectos multimotor, se descubrió una pérdida de memoria al abrir y cerrar una página repetidamente:

Esta actividad es una página secundaria que utiliza una solución multimotor y se ejecuta en ella. FlutterViewParece ser una pérdida de memoria de "instancia única", es decir, no importa cuántas veces se encienda y apague, la actividad solo retendrá una instancia y no se puede liberar en el montón. Esto es común. El escenario es una referencia a una variable estática global. Este tipo de pérdida de memoria tiene un impacto ligeramente menor en la memoria que una pérdida de instancias múltiples, pero si la actividad es grande y contiene muchos fragmentos y vistas, y estos componentes relacionados se filtran juntos, también se debe centrar la optimización.

A juzgar por la cadena de referencia, esta es FlutterEngineuna pérdida de memoria causada por un canal de comunicación interno; cuando FlutterEnginese crea, cada complemento en el motor creará el suyo propio MessageChannely lo registrará FlutterEngine.dartExecutor.binaryMessengerpara que cada complemento pueda comunicarse con Native de forma independiente.

Por ejemplo, un complemento común podría escribirse así:

class XXPlugin: FlutterPlugin {
    private val mChannel: BasicMessageChannel<Any>? = null

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { // 引擎创建时回调
        mChannel = BasicMessageChannel(flutterPluginBinding.flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME, JSONMessageCodec.INSTANCE)
        mChannel?.setMessageHandler { message, reply ->
            ...
        }
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { // 引擎销毁时回调
        mChannel?.setMessageHandler(null)
        mChannel = null
    }
}

Puede ver que FlutterPluginen realidad contendrá binaryMessengeruna referencia a y binaryMessengertendrá FlutterJNIuna referencia a... Esta serie de cadenas de referencia eventualmente contendrá FlutterPlugin, Contextpor lo que si el complemento no libera la referencia correctamente, inevitablemente se producirá una pérdida de memoria.

loggerChannelEchemos un vistazo a cómo está escrito en la cadena de referencia anterior :

class LoggerPlugin: FlutterPlugin {
    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        LoggerChannelImpl.init(flutterPluginBinding.getFlutterEngine())
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    }
}

object LoggerChannelImpl { // 这是一个单例
    private var loggerChannel: BasicMessageChannel<Any>?= null

    fun init(flutterEngine: FlutterEngine) {
        loggerChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL, JSONMessageCodec.INSTANCE)
        loggerChannel?.setMessageHandler { messageJO, reply ->
            ...
        }
    }
}

En LoggerPlugin.onAttachedToEngine, se FlutterEnginepasa al singleton LoggerChannelImply binaryMessengerlo retiene el singleton, y onDetachedFromEngineel método no ha sido destruido, por lo que el singleton hace referencia a él y el contexto no se puede liberar.

Es posible que este complemento no haya tenido en cuenta el escenario de múltiples motores al diseñarlo. En un solo motor, el complemento equivale onAttachedToEnginea onDetachedFromEngineseguir el ciclo de vida de la aplicación, por lo que no habrá pérdidas de memoria, pero en un En el escenario de múltiples motores, se DartVMusará para cada motor. El motor asigna aislamiento, que es algo similar a un proceso; la memoria del montón de dardos del aislamiento es completamente independiente, por lo que cualquier objeto (incluidos los objetos estáticos) entre motores no es interoperable; por lo tanto, las instancias respectivas se crearán en sus propios aislamientos, lo que FlutterEnginehace que FlutterPlugincada vez que se cree un motor, se repita el ciclo de vida del complemento. Cuando se destruye un motor, el complemento no se recicla normalmente y las referencias relevantes no se publican a tiempo, lo que resulta en una pérdida de memoria Context.FlutterEngine

enmienda:

  1. LoggerChannelImplNo es necesario utilizar escritura singleton, simplemente reemplácela con una clase normal para garantizar que cada motor MessageChannelsea independiente;
  2. LoggerPlugin.onDetachedFromEngineNecesita MessageChannelser destruido y vaciado;
Caso 3: pérdida de memoria de referencia nativa de biblioteca de terceros

Un SDK de lector de terceros está conectado al proyecto. Durante un análisis de memoria, se encontró que cada vez que se abre el lector, la memoria aumentará y no puede disminuir. Desde el archivo de volcado del montón, el Profiler no indicó que haya una pérdida de memoria en el proyecto, pero puede ver que hay una actividad en el montón de la aplicación que tiene una gran cantidad de instancias que no se pueden reciclar y el uso de memoria es grande.

Al observar las referencias de GCRoot, descubrimos que ningún GCRoot conocido hace referencia a estas actividades:

No hay duda de que esta Actividad tiene una pérdida de memoria, porque las páginas relevantes se han terminado y se han GC manualmente durante la operación, por lo que la razón solo puede ser que la Actividad está referenciada por un GCRoot invisible.

De hecho, el volcado de montón de Profiler solo mostrará el GCRoot de la memoria del montón de Java, y el GCRoot del montón nativo no se mostrará en esta lista de referencia. Entonces, ¿es posible que esta Actividad esté a cargo de un objeto Nativo?

Usamos herramientas de análisis dinámico Allocations Recordpara observar las referencias de las clases Java en el montón nativo y, efectivamente, encontramos algunas cadenas de referencia de esta actividad:

Pero desafortunadamente, las cadenas de referencia son todas direcciones de memoria y el nombre de la clase no se muestra. No hay forma de saber dónde se hace referencia a la actividad. Lo intenté más tarde con LeakCanary. Aunque se indicó claramente que la pérdida de memoria fue causada por en la referencia de la capa Nativa, todavía Global Variableno se proporcionaba una ubicación de llamada específica;

Tenemos que volver al código fuente para analizar las posibles ubicaciones de llamadas. Esta DownloadActivityes una página de descarga de libros que creamos para adaptarnos al SDK del lector; cuando no hay libros localmente, el archivo del libro se descargará primero y luego se pasará al SDK para abrir la propia Actividad del SDK; por lo tanto, la función es descargar , DownloadActivitycorrige Verificar, descomprimir libros y manejar algunos procesos de inicio del lector SDK.

De acuerdo con la idea general, primero verificamos los códigos de descarga, verificación y descompresión, y no encontramos dudas, el oyente y similares estaban encapsulados por referencias débiles, por lo que se especula que la pérdida de memoria se debe al método de escritura de el propio SDK.

Se descubre que cuando se inicia el SDK del lector, hay un parámetro de contexto:

class DownloadActivity {
    ... 
    private fun openBook() {
        ... 
        ReaderApi.getInstance().startReader(this, bookInfo) 
    } 
}

Dado que el código fuente de este SDK ha sido ofuscado, solo podemos profundizar en él y startReaderrastrear la cadena de llamadas desde el punto del método hasta el final:

class ReaderApi: void startReader(Activity context, BookInfo bookInfo) 
        ↓ 
class AppExecutor: void a(Runnable var1) 
        ↓ 
class ReaderUtils: static void a(Activity var0, BookViewerCallback var1, Bundle var2) 
        ↓ 
class BookViewer: static void a(Context var0, AssetManager var1) 
        ↓ 
class NativeCpp: static native void initJNI(Context var0, AssetManager var1);

Finalmente, cuando llegamos al método NativeCppde esta clase initJNI, podemos ver que este método local pasa nuestra Actividad. Se desconoce el procesamiento posterior, pero según el análisis de memoria anterior, básicamente podemos concluir que es debido a este método que la referencia de la Actividad se pasa a los objetos nativos de larga duración, lo que provoca pérdidas de memoria en la Actividad.

En cuanto a por qué Native necesita usar el contexto, no hay forma de analizarlo. Solo podemos informar este problema al proveedor del SDK y dejar que él lo maneje más. La solución tampoco es difícil:

  1. Cuando se destruye el lector, la referencia de Actividad se borra rápidamente;
  2. startReaderEl método no necesita especificar el objeto Actividad, simplemente cambie la declaración del parámetro de entrada a Contexto y se podrá Application Contextpasar el parámetro externo.

Para ayudar a todos a comprender la optimización del rendimiento de manera más completa y clara, hemos preparado notas centrales relevantes (incluida la lógica subyacente):https://qr18.cn/FVlo89

Notas fundamentales sobre la optimización del rendimiento:https://qr18.cn/FVlo89

Optimización de inicio

, optimización de memoria,

optimización de UI,

optimización de red,

optimización de mapas de bits y optimización de compresión de imágenes : optimización de concurrencia multiproceso y optimización de la eficiencia de transmisión de datos, optimización de paquetes de volumenhttps://qr18.cn/FVlo89




"Marco de monitoreo del rendimiento de Android":https://qr18.cn/FVlo89

"Manual de aprendizaje del marco de Android":https://qr18.cn/AQpN4J

  1. Proceso de inicio de arranque
  2. Inicie el proceso Zygote al arrancar
  3. Inicie el proceso SystemServer al arrancar
  4. conductor de carpeta
  5. Proceso de inicio de AMS
  6. Proceso de inicio del PMS
  7. Proceso de inicio del lanzador
  8. Android cuatro componentes principales
  9. Servicio del sistema Android: proceso de distribución de eventos de entrada
  10. Representación subyacente de Android: análisis del código fuente del mecanismo de actualización de pantalla
  11. Análisis del código fuente de Android en la práctica

Supongo que te gusta

Origin blog.csdn.net/weixin_61845324/article/details/133355082
Recomendado
Clasificación