欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android内存泄漏相关

程序员文章站 2022-04-19 15:37:02
...

一、概念

内存泄漏是指一个不再被使用的对象被一个还存活着的对象引用,此时垃圾回收器会跳过它,不去回收它。比如常见的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泄漏产生。不一会就可以发现在通知栏出现。

Android内存泄漏相关

 

 

很明显MainActivity泄漏了1,8kb点击进去查看具体细节。

Android内存泄漏相关

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就有可能泄漏较大的内存。

Android内存泄漏相关

解决方法:

可以采用静态内部类的方式,静态内部类不会持有外部类的引用。因此只要给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高性能编程。