Android常见内存泄漏及其修复

  • 一、定义
  • 二、案例
  • 1.Handler发送延迟消息
  • 解决方案:onDestroy()中移除延迟发送的消息
  • 2.Handler执行耗时任务
  • 解决方案:onDestroy()中移除耗时任务
  • 3.RxJava2的internal任务未停止
  • 解决方案1:onDestroy()中手动关闭任务
  • 解决方案2:绑定RxLifeCycle
  • 4.Timer任务未停止
  • 解决方案:onDestroy()中移除Timer任务
  • 5.static变量内存泄漏:static Context
  • 解决方案:onDestroy()中将静态变量置空
  • 6.static变量内存泄漏:static View
  • 解决方案:onDestroy()中将静态变量置空
  • 附:静态变量和实例变量的区别
  • 三、内存回收的两种算法
  • 1.引用计数法:iOS和Python使用的此方法回收内存
  • 图示
  • 引用计数法处理循环引用图示
  • 解决方式1:根据具体业务逻辑,当其中一个变量不再需要时,将其主动置空,这样就解除了互相引用
  • 解决方式2:将其中一个变量设置为弱引用,弱引用的对象在内存回收时必定被回收,这样就解除了互相引用
  • 2.可达性分析法:Android和Java使用的此方法回收内存
  • 图示
  • GC Root
  • 四、强引用、弱引用、软引用、虚引用
  • 1.强引用( StrongReference ):
  • 2.软引用(SoftReference):
  • 3. 弱引用(WeakReference):
  • 4.虚引用(PhantomReference):
  • 表格对比
  • 图示
  • 五、内存泄漏检测工具--leakcanary
  • 使用方式


一、定义

Android内存泄漏是指:在页面销毁之后,分配的内存未释放完

二、案例

1.Handler发送延迟消息

handler?.sendEmptyMessageDelayed(123, 10000)

解决方案:onDestroy()中移除延迟发送的消息

handler?.removeMessages(123)

2.Handler执行耗时任务

handler.post {
    Thread(Runnable {
        Thread.sleep(10000)
    }).start()
}

解决方案:onDestroy()中移除耗时任务

handler.removeCallbacks(runnable)

3.RxJava2的internal任务未停止

Observable.interval(0, 1, TimeUnit.SECONDS)
           .subscribeOn(Schedulers.newThread())
           .observeOn(AndroidSchedulers.mainThread())
           .subscribe {
                Log.d("~~~", "$it")
            }

解决方案1:onDestroy()中手动关闭任务

disposable?.dispose()

解决方案2:绑定RxLifeCycle

.compose(bindUntilEvent(ActivityEvent.DESTROY))

4.Timer任务未停止

var time = 0
Timer().schedule(object:TimerTask(){
    override fun run() {
        Log.d("~~~", "${time++}")
    }
},0,1000)

解决方案:onDestroy()中移除Timer任务

timer.cancel()

5.static变量内存泄漏:static Context

companion object {
    var context: Context? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    context = this
}

解决方案:onDestroy()中将静态变量置空

context = null

6.static变量内存泄漏:static View

companion object {
    var button: Button? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    button = btn_test
}

解决方案:onDestroy()中将静态变量置空

button = null

附:静态变量和实例变量的区别

class StaticInstanceClass {
    companion object {
        var staticVar = 0
    }

    private var instanceVar = 0

    init {
        staticVar++
        instanceVar++
        Log.d("~~~", "staticVar = $staticVar, instanceVar = $instanceVar")
    }
}

执行以下代码:

for(i in 0..9){
    StaticInstanceClass()
}

打印Log如下:

~~~: staticVar = 1, instanceVar = 1
~~~: staticVar = 2, instanceVar = 1
~~~: staticVar = 3, instanceVar = 1
~~~: staticVar = 4, instanceVar = 1
~~~: staticVar = 5, instanceVar = 1
~~~: staticVar = 6, instanceVar = 1
~~~: staticVar = 7, instanceVar = 1
~~~: staticVar = 8, instanceVar = 1
~~~: staticVar = 9, instanceVar = 1
~~~: staticVar = 10, instanceVar = 1

这是因为静态变量的内存地址是静态的,创建一次之后,此静态变量就一直存在,如果不手动置空,它的生命周期将贯穿整个app。
所以尽管代码中创建了10个此对象,但staticVar其实都指向同一个变量,每次都在原来的基础上加1;而实例变量的内存地址每次都动态分配,所以instanceVar的值每次都从0开始加1

三、内存回收的两种算法

1.引用计数法:iOS和Python使用的此方法回收内存

引用计数法就是对每个对象被引用的次数进行计数,当计数为0,则表示没有被引用,判断为可回收状态。
此方法存在的问题是循环引用,即A持有B的引用,B持有A的引用, A、B同时不再使用时,无法回收A、B,就会发生内存泄漏。

图示

android 内存空间 android 内存清理_常见内存泄漏


当对象M被A使用时,M的被引用次数+1

当对象M被B使用时,M的被引用次数+1

当A不再使用M后,M的被引用次数-1

当B不再使用M后,M的被引用次数-1

此时M的被引用次数=0,所以M被判定为可回收状态,当回收内存时,M就会被回收了

引用计数法处理循环引用图示

android 内存空间 android 内存清理_Android内存泄漏_02


当A和B互相引用时,A和B的被引用次数都为1,如果A和B同时不再需要使用了,由于引用次数不为0,所以内存回收时,无法将A和B进行回收。

解决方式1:根据具体业务逻辑,当其中一个变量不再需要时,将其主动置空,这样就解除了互相引用

解决方式2:将其中一个变量设置为弱引用,弱引用的对象在内存回收时必定被回收,这样就解除了互相引用

2.可达性分析法:Android和Java使用的此方法回收内存

可达性分析就是从一些GC Root 对象出发,去遍历包含此对象引用的对象,再依次递归。类似树形结构,从根向叶子标记可达的对象。最后没有标记到的对象即为可回收对象。解决了循环引用的问题。

图示

android 内存空间 android 内存清理_Android内存泄漏_03


如上图,当内存回收时,从GC Roots出发,Object1 ~ 4都是可达的,所以不会被回收,Object5 ~ 7是不可达的,就会被回收掉

GC Root

所有正在运行的线程的栈上的引用变量。所有的全局变量。所有ClassLoader等等,具体如下:

1.System Class
2.JNI Local
3.JNI Global
4.Thread Block
5.Thread
6.Busy Monitor
7.Java Local
8.Native Stack
9.Finalizable
10. Unfinalized
11.Unreachable
12.Java Stack Frame
13.Unknown

四、强引用、弱引用、软引用、虚引用

1.强引用( StrongReference ):

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它

2.软引用(SoftReference):

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

3. 弱引用(WeakReference):

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

4.虚引用(PhantomReference):

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

表格对比

引用类型

被垃圾回收时间

用途

生存时间

强引用

从来不会

对象的一般状态

JVM停止运行时终止

软引用

在内存不足时

对象缓存

内存不足时终止

弱引用

在垃圾回收时

对象缓存

gc运行后终止

虚引用

Unknown

Unknown

Unknown

图示

android 内存空间 android 内存清理_android 内存空间_04

五、内存泄漏检测工具–leakcanary

leakcanary是square公司开源的检测Android程序内存泄漏的工具
Github地址:https://github.com/square/leakcanary

使用方式

在build.gradle中添加依赖库:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'
}

在Application中初始化:

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        initLeakCanary()
    }

    /**
     * 初始化内存泄漏检测
     */
    private fun initLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return
        }
        LeakCanary.install(this)
    }
}

leakcanary-android库是用来检测内存泄漏的库,用debugImplementation导入表示只在编译时导入此库
leakcanary-android-no-op是用releaseImplementation导入的,表示发布正式版本时,导入此库,no-op表示No operation(无操作),导入的主要目的是打包时不需要手动删除leakcanary相关代码,也可以编译通过