Android内存泄漏相关
一、概念
内存泄漏是指一个不再被使用的对象被一个还存活着的对象引用,此时垃圾回收器会跳过它,不去回收它。比如常见的Activity泄漏,Activity组件在Android中扮演着重要的作用,如果它泄漏,很可能导致Activity所引用的其他对象也泄漏。Activity在生命周期中,因为某些原因(配置改变或手机内存不足)可能会被多次销毁再创建,如果每个泄漏的Activity都驻留在内存中会导致可用的堆内存越来越小,堆内存的变小更会引发GC的频繁调度,程序的性能可想而知。
二、准备
这里我采用 LeakCanary来检测Activity的泄漏,LeakCanary使用很简单,分两步。
第一步:build.gradle
dependencies {
...
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'
}
第二步:在自己的Application中
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {//1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}
这要就搞定了。
二、常见的泄漏原因
1、Toast工具类的错误使用
很多时候我们在写Toast工具类时,可能是这样写的。
public class ToastUtil {
private static Toast toast;
public static void show(Context context,String text){
if (toast==null){
toast=Toast.makeText(context,text,Toast.LENGTH_SHORT);
}
else {
toast.setText(text);
}
toast.show();
}
}
这样写必然会带来Activity的泄漏。这里我们尝试着点击Button让他弹出土司,并将Activity返回。注意必须将Activity返回,这样LeakCanary才能检测出是否有Activity泄漏产生。不一会就可以发现在通知栏出现。
很明显MainActivity泄漏了1,8kb点击进去查看具体细节。
Toast的引用是静态的因此Toast对象会一直存在内存中,而Toast对象又持有Activity的引用就是Toast.makeText(context,text,Toast.LENGTH_SHORT)中的context,这就导致即使我们返回了Activity而Toast一直持有Activity的引用,垃圾收集器就会跳过Activity不去回收它。
补充:判断一个对象是否可以被回收主要是看它和GC Roots之间是否存在引用链,如果有则该对象是可达的,系统不会回收它。反之,若是不可达则会回收。
解决方法:
解决办法就是我们让Toast持有应用上下文,而不是Activity。这样当Activity被返回就没有存活着的对象引用它,自然可以被回收
public class ToastUtil {
private static Toast toast;
public static void show(Context context,String text){
if (toast==null){
toast=Toast.makeText(context.getApplicationContext(),text,Toast.LENGTH_SHORT);
}
else {
toast.setText(text);
}
toast.show();
}
}
2、非静态内部类的错误使用
很时候我们会用到内部类,因为它可以访问到外部类的成员,非常方便。但是我们需要知道,其实非静态内部类是具有外部类的一个引用。我们可能像下面这样写代码。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyThread().start();
}
class MyThread extends Thread{
@Override
public void run() {
//这里模拟处理一些耗时任务
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
当线程中处理一些耗时的任务,还没结束,我们就退出Activity了,这时由于MyThread还在执行,并且具有外部类MainActivity的一个引用就会导致MainActivity无法被回收,从而产生泄漏。LeakCanary检测如下:可以看到只是一个简单的Activity就有可能泄漏较大的内存。
解决方法:
可以采用静态内部类的方式,静态内部类不会持有外部类的引用。因此只要给MyThread加上static关键字就行了。
3、单例的错误使用
public class Singleton {
private static Singleton singleton;
private Callback callback;
private Singleton(){}
public static Singleton getInstance(){
if (singleton==null){
synchronized (Singleton.class){
if (singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
public void setCallback(Callback callback){
this.callback=callback;
}
interface Callback{
void callback();
}
}
然后再MainActivity实现Callback接口
public class Main2Activity extends AppCompatActivity implements Singleton.Callback{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Singleton.getInstance().setCallback(this);
}
@Override
public void callback() {
//其他操作
}
}
这样写也会导致Activity的内存泄漏
原因很简单,单例的生命周期和应用程序是一致的,这会导致它一直驻留在内存中,而该单例持有的Callback的引用,肯定就导致Callback对象无法被垃圾回收器回收。
解决方法:
解决方法很简单可以结合弱引用来实现,不了解弱引用的可以看Java的四种引用。Singleton修改如下
public class Singleton {
private static Singleton singleton;
private WeakReference<Callback> callback;
private Singleton(){}
public static Singleton getInstance(){
if (singleton==null){
synchronized (Singleton.class){
if (singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
public void setCallback(Callback c){
this.callback=new WeakReference<>(c);
}
interface Callback{
void callback();
}
}
当Activity返回之后,Activity就只有弱引用了,当垃圾收集器只要扫描到具有弱引用的对象,不管内存是否充足,都会回收该对象,因此弱引用不能让对象豁免被垃圾收集器回收的可能。
4、匿名内部类的错误使用
对于上面单例产生的泄漏,也许有人不用弱引用,也不让Activity实现Callback,而是直接new Callback(),如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
Singleton.getInstance().setCallback(new Singleton.Callback() {
@Override
public void callback() {
//其他操作
}
});
}
这样看起来好像单例不再持有Main2Activity的引用了,其实不然。匿名内部类持有当前外部类的一个隐式引用,而匿名内部类又被单例所持有,这任然是一个引用链,GC Roots到Main2Activity对象之间任然可达,因此GC还是不会回收,内存泄漏依旧存在。
5、Handler的错误引用
public class Main2Activity extends AppCompatActivity {
private Handler handler=new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
handler.postDelayed(new Runnable() {
@Override
public void run() {
//其他操作
}
},10000);
}
}
上面代码也会产生内存泄漏。原因:我们把任务发送出去并且延迟10s执行,在任务还没结束时我们退出了Activity,但是由于匿名内部类Runnable持有外部类的引用,而Runnable被Message所持有,Message被MessageQueue所持有。这是一条完整的引用链,消息队列一直存活于内存,因此Main2Activity到GC Roots之间还是可达的,GC不会回收它。这样就导致了内存泄漏。
解决方法:可以将Runnable直接申明为static这样就不会持有外部类Main2Activity的引用了。
public class Main2Activity extends AppCompatActivity {
private Handler handler=new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
handler.postDelayed(runnable,10000);
}
static Runnable runnable=new Runnable() {
@Override
public void run() {
//其他操作
}
};
}
此外我们也可以这样写:在Activity被销毁时,移除所有的消息。这样也能防止内存泄漏。
public class Main2Activity extends AppCompatActivity {
private Handler handler=new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
handler.postDelayed(new Runnable() {
@Override
public void run() {
//其他操作
}
},10000);
}
@Override
protected void onDestroy() {
super.onDestroy();
//在Activity销毁时移除队列中所有的消息
handler.removeCallbacksAndMessages(null);
}
}
6、静态字段导致的内存泄漏
public class Main2Activity extends AppCompatActivity {
private static TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
textView= (TextView) findViewById(R.id.textView);
}
}
这个代码看起来很简单,但它一样导致了内存泄漏。因为TextView被申明为静态的,并且TextView在实例化时持有了Main2Activity对象。
...
public TextView(Context context) {
this(context, null);
}
public TextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.android.internal.R.attr.textViewStyle);
}
public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
...
虽然还有很多导致内存泄漏的错误代码,其实原因都是类似的:直白点说就是一个不再被使用的对象被一个还存活着的对象引用,此时垃圾回收器会跳过它,不去回收它。不直白的说,是因为Java采用可达性分析算法来判断一个对象是否存活,从起始点也就是一系列的GC Roots向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots之间有引用链相连是可达的,那么就证明这个对象是有用的GC不会回收它,反之没有引用链、不可达就认为对象是不可用的,GC会回收它。
大四狗,知识浅薄。如有错误,欢迎指正。
参考资料:深入理解Java虚拟机、Android高性能编程。
上一篇: 有趣的编程