• Android内存泄漏


    什么是内存泄漏?

    生命周期较长的对象引用了生命周期较短的对象,导致生命周期较短的对象无法被GC及时回收,从而导致被占用的内存无法释放。

    Failure to release unused objects from the memory
    在这里插入图片描述

    内存泄漏的危害

    性能降低、导致OOM

    从基础开始

    RAM:Stack & Heap

    Random access memory,是Android设备用于存储当前运行的应用程序及其数据的内存。RAM中有两个重要的角色:Stack、Heap。
    在这里插入图片描述

    Stack用于静态内存分配,Heap用于动态内存分配。

    内存分配示例

    在这里插入图片描述
    接下来的图,代表了应用的Heap和Stack,展示了应用运行时每个对象指向和存储的位置:
    在这里插入图片描述
    在这里插入图片描述
    Line 1 - JVM为main方法创建一个Stack内存
    在这里插入图片描述
    Line 2 - 创建一个局部变量,这个变量会在main方法的Stack中创建和存储
    在这里插入图片描述
    Line 3 - new了一个Object。创建的Object保存在Heap中,而Object在Heap中的内存地址保存在Stack中
    在这里插入图片描述
    Line 4 - 同Line 3
    在这里插入图片描述
    Line 5 - JVM为foo方法创建一个Stack内存
    在这里插入图片描述
    Line 6 - 在foo方法的Stack中创建一个Object,该Object存储在第5行传递的Object在Heap中的内存地址
    在这里插入图片描述
    Line 7 - 在foo方法的Stack中新建一个str,存储string pool在Heap中的内存地址
    在这里插入图片描述
    Line 8 - foo方法结束,在foo方法Stack中的Objects会被自动释放和回收
    在这里插入图片描述
    在这里插入图片描述
    Line 9 - main方法结束,在main方法Stack中的Objects会被自动释放和回收
    在这里插入图片描述

    结论:Stack中的Object的存在是短暂的,方法结束后会被自动释放和回收。

    Heap与Stack不同,如果要释放和回收Heap中的Objects,需要GC(Garbage Collector)的帮助。GC会自动检测无用的Objects,然后释放和回收。而GC是怎么工作的呢?

    GC会寻找无用的或无法触达的Objects,当Heap中的某个Object无任何引用指向它的时候,GC会释放和回收它。
    在这里插入图片描述
    如图,GC Roots是引用树的根,树中的每个Object都有一个或多个根Object,只要应用程序或GC Roots可以触达那些Objects,就说明整棵树可触达。如果Object变得不可触达,将被视为无用的或无法触达的Object。

    main方法结束,GC后:
    在这里插入图片描述

    常见内存泄漏

    非静态内部类(匿名类)Leak

    非静态内部类(匿名类)默认就持有外部类的引用,当非静态内部类(匿名类)对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。

    • Handler引起内存泄漏
      如果Handler中有 延迟任务 或者 等待执行的任务队列过长,可能因为Handler继续执行而导致Activity发生泄漏。
    • 非静态的Handler类会默认持有外部类的引用,如Activity等。
    • 还未处理完的消息(Message)中会持有Handler的引用。
    • 还未处理完的消息会处于消息队列中,即消息队列MessageQueue会持有Message的引用。
    • 消息队列MessageQueue位于Looper中,Looper的生命周期跟应用一致。
      引用链:Looper -> MessageQueue -> Message -> Handler -> Activity
      解决方法
    • 静态内部类+弱引用

    静态内部类默认不持有外部类的引用,所以改成静态内部类即可。若Handler中需要Context,采用弱引用来持有Activity的引用。
    在这里插入图片描述

    • Activity退出时,清除队列信息

    清除队列信息后,Handler将会跟Activity生命周期同步。

    在这里插入图片描述
    2. 线程引起内存泄漏

    在Activity销毁前,Thread的任务没有执行完毕。

    public class ThreadActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            //...
            new DownloadTask().start();
        }
        
        private class DownloadTask extends Thread {
            @Override
            public void run() {
               SystemClock.sleep(2000 * 10);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    众所周知,内部类会持有外部类的引用,类似于:

    public class ThreadActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            //....
            new DownloadTask(this).start();
        }
        
        private class DownloadTask extends Thread {
            
            private final ThreadActivity threadActivity;
            
            public DownloadTask(ThreadActivity threadActivity) {
                this.threadActivity = threadActivity;
            }
    
            @Override
            public void run() {
                SystemClock.sleep(2000 * 10);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    正常流程:
    在这里插入图片描述
    在onCreate中start DownlaodTask后,线程在后台运行。用户等待20s,等 DownloadTask执行完,run()方法的stack会被释放,DownloadTask对外部类ThreadActivity的引用也会解除,如图:
    在这里插入图片描述
    当用户关闭ThreadActivity后,GC就可正常回收Heap中的DownloadTask和ThreadActivity。

    异常流程:
    若DownlaodTask未执行结束,用户关闭了ThreadActivity或屏幕旋转,导致ThreadActivity重建:
    在这里插入图片描述
    此时Stack释放了所有的Object,但是由于run()方法还在执行,DownloadTask无法释放。而DownloadTask持有外部类ThreadActivity的引用,导致GC无法从Heap中释放ThreadActivity,从而导致内存泄漏。若Activity引用了大量的UI资源,会造成严重的内存泄漏。

    解决方法:

    • 静态内部类

    静态内部类不持有外部类的引用

    在这里插入图片描述

    静态变量内存泄漏

    静态变量的生命周期跟整个程序的生命周期一致。只要静态变量没有被销毁也没有置为null,其对象就一直被保持引用,也就不会被垃圾回收,从而出现内存泄漏。

    1. 静态变量持有Activity引用
    public class MainActivity extends AppCompatActivity {
      private ActivityMainBinding binding;
    
      private static TestLeak sTest;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
    
        sTest = new TestLeak(this);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    public class TestLeak {
      private Context context;
    
      public TestLeak(Context context) {
        this.context = context;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    内存泄漏log:

     HEAP ANALYSIS RESULT
        ====================================
        1 APPLICATION LEAKS
        
        References underlined with "~~~" are likely causes.
        Learn more at https://squ.re/leaks.
        
        127719 bytes retained by leaking objects
        Signature: f0d5226936119ad526c8706acdf0ccecdbf662fb
        ┬───
        │ GC Root: Local variable in native code
        │
        ├─ android.os.HandlerThread instance
        │    Leaking: NO (PathClassLoader↓ is not leaking)
        │    Thread name: 'LeakCanary-Heap-Dump'
        │    ↓ Thread.contextClassLoader
        ├─ dalvik.system.PathClassLoader instance
        │    Leaking: NO (MainActivity↓ is not leaking and A ClassLoader is never leaking)
        │    ↓ ClassLoader.runtimeInternalObjects
        ├─ java.lang.Object[] array
        │    Leaking: NO (MainActivity↓ is not leaking)
        │    ↓ Object[].[144]
        ├─ com.demo.leak.MainActivity class
        │    Leaking: NO (a class is never leaking)
        │    ↓ static MainActivity.sTest
        │                          ~~~~~
        ├─ com.demo.leak.TestLeak instance
        │    Leaking: UNKNOWN
        │    Retaining 127.7 kB in 2628 objects
        │    context instance of com.demo.leak.MainActivity with mDestroyed = true
        │    ↓ TestLeak.context
        │               ~~~~~~~
        ╰→ com.demo.leak.MainActivity instance
        ​     Leaking: YES (ObjectWatcher was watching this because com.demo.leak.MainActivity received Activity#onDestroy()
        ​     callback and Activity#mDestroyed is true)
        ​     Retaining 127.7 kB in 2627 objects
        ​     key = 99067c93-630f-4aca-a27e-926cd09f664d
        ​     watchDurationMillis = 5162
        ​     retainedDurationMillis = 158
        ​     mApplication instance of android.app.Application
        ​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
        ====================================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    解决方法:

    • 在静态变量无用时置为空
    • 使用Application的Context
    • 使用弱引用
    WeakReference<Activity> weakReference = new WeakReference<>(this);
    Activity activity = weakReference.get();
    
    • 1
    • 2
    1. 单例类

    单例模式其生命周期跟应用一样,所以使用单例模式时传入的参数需要注意一下,避免传入Activity等对象造成内存泄漏。

    public class Singleton {
      private static Singleton instance;
      private Context context;
      
      private Singleton(Context context) {
        this.context = context;
      }
      
      public static Singleton getInstance(Context context) {
        if (instance == null) {
          instance = new Singleton(context);
        }
        return instance;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    bindService/unbindService

    若Activity中执行了bindService,而在Activity销毁时未执行unbindService,则造成内存泄漏。
    说明:bindService可以跟Activity生命周期联动,在Activity销毁时系统会自动解绑Service(具体代码:LoadedApk.java的removeContextRegistrations()),但若未显式调用unbindService,仍然会内存泄漏。

    324109 bytes retained by leaking objects
        Displaying only 1 leak trace out of 2 with the same signature
        Signature: 8420f46333a4dc28cc99f28a0bc2da2f3891f
        ┬───
        │ GC Root: System class
        │
        ├─ android.provider.FontsContract class
        │    Leaking: NO (Application↓ is not leaking and a class is never leaking)
        │    ↓ static FontsContract.sContext
        ├─ android.app.Application instance
        │    Leaking: NO (Application is a singleton)
        │    mBase instance of android.app.ContextImpl
        │    ↓ Application.mLoadedApk
        │                  ~~~~~~~~~~
        ├─ android.app.LoadedApk instance
        │    Leaking: UNKNOWN
        │    Retaining 341.2 kB in 7121 objects
        │    mApplication instance of android.app.Application
        │    Receivers
        │    ..Application@317996008
        │    ....VisibilityTracker@319156072
        │    ↓ LoadedApk.mServices
        │                ~~~~~~~~~
        ├─ android.util.ArrayMap instance
        │    Leaking: UNKNOWN
        │    Retaining 340.0 kB in 7100 objects
        │    ↓ ArrayMap.mArray
        │               ~~~~~~
        ├─ java.lang.Object[] array
        │    Leaking: UNKNOWN
        │    Retaining 340.0 kB in 7098 objects
        │    ↓ Object[].[0]
        │               ~~~
        ╰→ com.demo.leak.SettingsActivity instance
        ​     Leaking: YES (ObjectWatcher was watching this because com.demo.leak.SettingsActivity received
        ​     Activity#onDestroy() callback and Activity#mDestroyed is true)
        ​     Retaining 162.3 kB in 3366 objects
        ​     key = d6ccb489-a1a2-4196-b7ac-a591a81e5d5f
        ​     watchDurationMillis = 87872
        ​     retainedDurationMillis = 82869
        ​     mApplication instance of android.app.Application
        ​     mBase instance of androidx.appcompat.view.ContextThemeWrapper
        ====================================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    解决方法:
    Activity销毁时,显示调用unbindService

    内存泄漏检测方法

    LeakCanary

    debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.7’
    在这里插入图片描述

    Profiler

    查看MEMORY:
    在这里插入图片描述
    Capture heap dump:
    在这里插入图片描述
    在这里插入图片描述
    Show activity/fragment Leaks:
    在这里插入图片描述
    Heamp Dump右侧四列的意思:
    Allocations:Java堆中的实例个数
    Native Size:Native层分配的内存大小
    Shallow Size:Java堆中分配实际大小
    Retained Size:这个类的所有实例保留的内存总大小(并非实际大小)

  • 相关阅读:
    2023/9/20 -- C++/QT
    Convert Office Documents to PDF/A Formats
    无头链表二级指针方式实现(C语言描述)
    思腾云计算
    【OpenCV 例程300篇】234. 特征提取之主成分分析(PCA)
    BBS第一天,项目开发流程,BBS项目功能分析,表设计,表字段的编写和数据库的迁移
    1-1 如何实现前端低代码? - 戴向天
    ECMA Script Module(ES module)知识点
    Linux安装Nexus(图文解说详细版)
    java高级用法之:JNA中的Function
  • 原文地址:https://blog.csdn.net/mxiaomi/article/details/126365632