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);
}
}
}
众所周知,内部类会持有外部类的引用,类似于:
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);
}
}
}
正常流程:
在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,其对象就一直被保持引用,也就不会被垃圾回收,从而出现内存泄漏。
- 静态变量持有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);
}
public class TestLeak {
private Context context;
public TestLeak(Context context) {
this.context = context;
}
}
内存泄漏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
====================================
解决方法:
- 在静态变量无用时置为空
- 使用Application的Context
- 使用弱引用
WeakReference<Activity> weakReference = new WeakReference<>(this);
Activity activity = weakReference.get();
- 单例类
单例模式其生命周期跟应用一样,所以使用单例模式时传入的参数需要注意一下,避免传入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;
}
}
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
====================================
解决方法:
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:这个类的所有实例保留的内存总大小(并非实际大小)