Implementação simples de LeakCanary manuscrita

prefácio

Este artigo foi escrito em 08/04/2022. A partir de agora, a versão do leakcanary é v2.8.1, que não deveria ter sido atualizada por mim.

Este artigo não é uma análise de código-fonte, mas uma implementação simples de vazamento de memória baseada no princípio de implementação do LeakCanary.

Conhecimento teórico pré-requisito

Um vazamento de memória ocorre quando um objeto que não está mais em uso deveria ter sido recuperado, mas não foi.

No Android, quais são os objetos que não são mais usados?

Essa pergunta é mais fácil de responder, como o objeto Activity que chamou de volta onDestory, o Fragment que chamou de volta onFragmentDestroyed, a caixa de diálogo que chamou de volta onViewDetachedFromWindow e assim por diante.

Então, como detectar se esses objetos foram reciclados?

Geralmente, para evitar vazamentos de memória causados ​​por referências fortes a alguns objetos, optamos por usar a camada de pacote WeakReference, usando o seguinte construtor:

public WeakReference(T referent) {
    super(referent);
}
复制代码

E WeakReference tem outro construtor:

public WeakReference(T referent, ReferenceQueue<? super T> queue) {
  super(referent, queue);
}
复制代码

Observe o segundo parâmetro aqui. Este parâmetro especifica uma fila de referência para ele. Quando o referente é coletado como lixo, o referente será colocado na fila de fila de referência associada.

Então não podemos usar WeakReference para carregar o objeto Activity de onDestory, e especificar um ReferenceQueue, e então ver se existe um objeto Activity no ReferenceQueue para saber se a Activity foi reciclada?

OK, depois de conhecer o princípio, você pode começar.

Implementação simples

Os comentários são muito claros:

object MyAppWatcher {
​
  /** 引用队列 */
  private val queue = ReferenceQueue<Any>()
​
  /** 待观察的对象 */
  private val watchedObjects = mutableMapOf<String, MyKeyedWeakReference>()
​
  /**
   * 注册方法,可以在 Application#onCreate 里调用或者直接使用 ContentProvider 注册
   */
  fun install(app: Application) {
    // 这里以检测 Activity 泄露为例,注册 Activity#onDestory 回调
    app.registerActivityLifecycleCallbacks(object :
      Application.ActivityLifecycleCallbacks by noOpDelegateMy() {
​
      override fun onActivityDestroyed(activity: Activity) {
        // 1. 在 Activity#onDestory 时,把 Activity 添加到观察列表里
        // !!注意这里的 queue,如果 activity 被回收了,activity 会被添加到 queue 里面
        val reference =
          MyKeyedWeakReference(activity, UUID.randomUUID().toString(), "activity", queue)
        watchedObjects[reference.key] = reference
        Log.i(TAG, "onActivityDestroyed: activity 即将被回收")
        // 2. 主线程延迟 5s,之所以延迟 5s,是因为两次 gc 最小间隔时间是 5s
        Handler(Looper.getMainLooper()).postDelayed({
          // 3. 看看对象有没有被回收
          removeRefIfCollect()
          // 4. 可能泄漏了,手动 GC 一下再试试
          Log.i(TAG, "gc: ")
          Runtime.getRuntime().gc()
          try {
            // 直接粗暴的睡 100ms,让对象能够有足够的时间添加到弱引用队列里面
            Thread.sleep(100)
          } catch (e: InterruptedException) {
            throw AssertionError()
          }
          System.runFinalization()
          // 5. 再次看看对象有没有被回收
          removeRefIfCollect()
          val leakRef = watchedObjects[reference.key]
          // 6. 如果不为 null,也就是没有从观察对象列表里面移除
          // 说明对象被其他对象引用着,也就是存在泄漏
          if (leakRef != null) {
            Log.i(TAG, "对象泄露了: ${leakRef.desc}")
            // 7. dump 堆转储文件并分析
            dumpHeap(activity.applicationContext)
          }
        }, 5000)
      }
    })
  }
​
  /**
   * 如果对象被回收了,则把它从观察对象列表里面移除
   * 核心原理:当对象被回收时,会把它添加到其引用队列里面
   */
  private fun removeRefIfCollect() {
    var ref: MyKeyedWeakReference?
    do {
      ref = queue.poll() as MyKeyedWeakReference?
      if (ref != null) {
        Log.i(TAG, "removeRefIfCollect: ${ref.desc}")
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
​
  /**
   * dump hprof 文件并使用 shark 库分析,输出结果
   */
  private fun dumpHeap(context: Context) {
    val handlerThread = HandlerThread("Dump-Heap-Thread")
    handlerThread.start()
    Handler(handlerThread.looper).post {
      val storageDirectory = File(context.cacheDir, "leakcanary")
      if (!storageDirectory.exists()) {
        storageDirectory.mkdir()
      }
      val fileName = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'.hprof'", Locale.US).format(Date())
      val file = File(storageDirectory, fileName)
      // dump 出堆转储文件
      Debug.dumpHprofData(file.absolutePath)
      Log.i(TAG, "dumpHeap: ${file.absolutePath}")
      // 使用 Shark 库里的 HeapAnalyzer 来分析
      val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener { step ->
        Log.i(TAG, "Analysis in progress, working on: ${step.name}")
      })
      val heapAnalysis = heapAnalyzer.analyze(
        heapDumpFile = file,
        leakingObjectFinder = FilteringLeakingObjectFinder(
          AndroidObjectInspectors.appLeakingObjectFilters
        ),
        referenceMatchers = AndroidReferenceMatchers.appDefaults,
        computeRetainedHeapSize = true,
        objectInspectors = AndroidObjectInspectors.appDefaults.toMutableList(),
        proguardMapping = null,
        metadataExtractor = AndroidMetadataExtractor
      )
      Log.i(TAG, "dumpHeap: \n$heapAnalysis")
    }
  }
}
​
private const val TAG = "MyAppWatcher"
复制代码

O código inteiro não tem outras dependências, exceto o método dumpHeap, que precisa depender da biblioteca leakcanary-shark.O método dumpHeap é adequado para despejar diretamente o arquivo hprof e analisar a cadeia de referência vazada quando ocorre um vazamento de memória.

teste simples

Antes de tudo, você precisa se registrar, o leakcanary está registrado no ContentProvider, usamos Application para registrar aqui, principalmente para simular facilmente vazamentos de memória:

open class ExampleApplication : Application() {
  // 泄漏的 View
  val leakedViews = mutableListOf<View>()
​
  override fun onCreate() {
    super.onCreate()
    MyAppWatcher.install(this)
  }
}
复制代码

Faça um vazamento de SecondActivity:

class SecondActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)
​
    val app = application as ExampleApplication
    app.leakedViews.add(findViewById(R.id.tv_text))
  }
}
复制代码

Quando SecondActivity estiver onDestory, SecondActivity não será reciclado porque o aplicativo contém uma forte referência à exibição, ou seja, ocorre um vazamento de memória. Vamos ver quais logs são gerados.

saída de log

O log, que é o log impresso pelo MyAppWatcher acima:

2022-04-08 14:25:00.030 25500-25500/com.example.leakcanary I/MyAppWatcher: onActivityDestroyed: activity 即将被回收
2022-04-08 14:25:05.036 25500-25500/com.example.leakcanary I/MyAppWatcher: gc: 
2022-04-08 14:25:05.212 25500-25500/com.example.leakcanary I/MyAppWatcher: 对象泄露了: activity
2022-04-08 14:25:07.004 25500-25569/com.example.leakcanary I/MyAppWatcher: dumpHeap: /data/user/0/com.example.leakcanary/cache/leakcanary/2022-04-08_14-25-05_220.hprof
2022-04-08 14:25:07.127 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: PARSING_HEAP_DUMP
2022-04-08 14:25:08.916 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: EXTRACTING_METADATA
2022-04-08 14:25:09.026 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: FINDING_RETAINED_OBJECTS
2022-04-08 14:25:12.094 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: FINDING_PATHS_TO_RETAINED_OBJECTS
2022-04-08 14:25:16.039 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: INSPECTING_OBJECTS
2022-04-08 14:25:16.073 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: COMPUTING_NATIVE_RETAINED_SIZE
2022-04-08 14:25:16.289 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: COMPUTING_RETAINED_SIZE
2022-04-08 14:25:16.431 25500-25569/com.example.leakcanary I/MyAppWatcher: Analysis in progress, working on: BUILDING_LEAK_TRACES
2022-04-08 14:25:16.443 25500-25569/com.example.leakcanary I/MyAppWatcher: dumpHeap: 
    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    118361 bytes retained by leaking objects
    Signature: 63f8e699b71efe361be3e4edf804ad0ff76866a1
    ┬───
    │ GC Root: System class
    │
    ├─ android.provider.FontsContract class
    │    Leaking: NO (ExampleApplication↓ is not leaking and a class is never leaking)
    │    ↓ static FontsContract.sContext
    ├─ com.example.leakcanary.ExampleApplication instance
    │    Leaking: NO (Application is a singleton)
    │    mBase instance of android.app.ContextImpl
    │    ↓ ExampleApplication.leakedViews
    │                         ~~~~~~~~~~~
    ├─ java.util.ArrayList instance
    │    Leaking: UNKNOWN
    │    Retaining 118.4 kB in 1069 objects
    │    ↓ ArrayList[0]
    │               ~~~
    ├─ android.widget.TextView instance
    │    Leaking: YES (View.mContext references a destroyed activity)
    │    Retaining 118.4 kB in 1067 objects
    │    View is part of a window view hierarchy
    │    View.mAttachInfo is null (view detached)
    │    View.mWindowAttachCount = 1
    │    mContext instance of com.example.SecondActivity with mDestroyed = true
    │    ↓ View.mContext
    ╰→ com.example.SecondActivity instance
    •     Leaking: YES (Activity#mDestroyed is true)
    •     Retaining 24.4 kB in 74 objects
    •     mApplication instance of com.example.leakcanary.ExampleApplication
    •     mBase instance of android.app.ContextImpl
    ====================================
    0 LIBRARY LEAKS
    
    A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
    See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
    ====================================
    1 UNREACHABLE OBJECTS
    
    An unreachable object is still in memory but LeakCanary could not find a strong reference path
    from GC roots.
    
    android.view.ViewRootImpl instance
    •  Leaking: YES (ViewRootImpl#mView is null)
    •  mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.example.SecondActivity with mDestroyed = true
    •  mWindowAttributes.mTitle = "com.example.leakcanary/com.example.SecondActivity"
    •  mWindowAttributes.type = 1
    ====================================
    METADATA
    
    Please include this in bug reports and Stack Overflow questions.
    
    Build.VERSION.SDK_INT: 30
    Build.MANUFACTURER: OPPO
    LeakCanary version: Unknown
    App process name: com.example.leakcanary
    Stats: LruCache[maxSize=3000,hits=37016,misses=78089,hitRate=32%] RandomAccess[bytes=3892247,reads=78089,travel=30739517820,range=20956324,size=26877108]
    Analysis duration: 7820 ms
    Heap dump file path: /data/user/0/com.example.leakcanary/cache/leakcanary/2022-04-08_14-25-05_220.hprof
    Heap dump timestamp: 1649399116436
    Heap dump duration: Unknown
    ====================================
​
复制代码

Dessa forma, concluímos uma detecção simples de vazamento de memória.

É altamente recomendável que todos baixem o leakcanary, executem o projeto de amostra e o depurem. É muito útil para entender o leakcanary, e você pode até entendê-lo diretamente sem ler nenhum artigo de análise de código-fonte.

Acho que você gosta

Origin juejin.im/post/7084115960793137165
Recomendado
Clasificación