Android内存泄漏--基础介绍与延伸解析

    摘要:Android中内存泄漏的的分析。

Android的内存基础知识

    Android系统在安装、加载一个apk文件时,会在系统内存中划出一部分作为该apk的运行内存。

这个运行内存的大小,目前随着Android设备的进化,也已经适量增大。从早期默认的90M左右到现在200M、300M。当你在设定属性android:largeheap = "true"时,内存大小基本还会翻倍。如果想要得到具体可用内存,可在代码中获取具体数值:

Runtime runtimeMemory=Runtime.getRuntime();
long maxMemory=runtimeMemory.maxMemory()/(1024*1024);

    在apk可用内存增大的情况下,你仍然需要注意合理的分配内存,使用内存。虽然在大内存的情况下,可能将一些内存使用的隐患隐藏起来,没有造成apk崩溃等,但如果apk中存在内存使用不善的情况,如内存泄漏,仍会影响apk的运行效率,严重的情况下,apk会发生内存溢出,导致崩溃。

    如果将apk可用内存比喻成一只水桶,apk运行时占用的内存比喻成桶里的水。那么现在这个桶变大变高了,正常情况下桶里的水是不会满溢的,Android与Java一样,会隐式的进行GC垃圾对象回收(你可以显示调用GC方法回收)。但当apk中存在内存泄漏的情况下,每一次泄漏导致无法GC回收,桶里的水位就会慢慢增长,直至满溢,造成内存溢出。

内存溢出是日常代码编写中,因不易发现,会导致很多线上问题的产生。代码编写的规范与良好习惯,是避免这个问题的主要办法。

Android中常见的内存泄漏情景

    内存泄漏的产生过程:apk运行时,操作系统为apk中的各种变量以及对象实例分配内存。假设程序运行后,产生了两个对象:长生命周期对象A,短生命周期对象B,其中A持有了B的引用。在B生命周期结束后,理论上系统GC应该要释放B占用的内存,但因为引用被A持有,导致内存无法释放,这就造成了内存泄漏。

Android中我们常见的泄漏场景,列出代表性的如下:

1、静态变量持有短生命周期的对象引用

    示例如下:object是一个静态变量,在onCreate中将当前activity的引用赋值给了object。因为static变量赋值后,会将引用保存在整个app的方法区。生命周期是与整个app是一致的,会一直持有这个引用,导致当前activity即使onDestroy后,引用也无法被GC释放,造成内存泄漏。

public class StaticViewTestActivity extends AppCompatActivity {

    private static Object object;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        object = this;
    }
}

    解法:(1)尽量不要将生命周期短暂的对象赋值给static变量,谨慎使用 (2)如果业务有这个需求,以上述代码为例,在使用完object后,在onDestroy请将object置为null。

   谨慎使用静态变量也要分清何时使用,不能总是担心引发问题而不用。

   静态相关延伸-1

   静态方法是否会造成内存泄漏呢?看下面这段代码:

public class StaticMethodTestActivity extends AppCompatActivity {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        test(this);
    }

    private static void test(Activity activity){
        Object object = activity;
    }
}

  先给出结论:这是不会造成内存泄漏的。原因在于java、android中,调用方法时(无论是静态方法还是非静态方法),JVM的虚机栈都会为这个方法创建一个方法栈帧。你可以理解为:一个线程中,有多个方法时,会产生多个栈帧,存于这个线程的栈帧队列中。当方法被调用完毕后,该栈帧被弹出销毁。方法中的局部变量等,也都将被释放,因此不会存在内存泄漏情况。

  静态相关延伸-2

  当一个类中,同时存在静态变量、静态方法、普通方法时,关系如下:

 

    如图中所述:一个类中静态变量、静态方法的生命周期与该类的实例化对象是没有关系的。

public static void main(String[] args){
        A a = new A();
        a.d();
        A.c();
        A.b = 1;
    }

执行以上这段代码后,对象a的实例将会存在内存中的堆中,静态方法c与静态变量b将会存在内存的方法区。

  静态相关延伸-3

  还有哪些常见场景是静态变量持有短生命周期的引用,会引发泄漏?

   (1)单例模式,持有短生命周期的context

public class Test {
    private static Test INSTANCE;
    private Context context;

    private Test(Context context){
        this.context = context;
    }

    public static Test getInstance(Context context) {
        if (INSTANCE == null) {
            synchronized (Test.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Test(context);
                }
            }
        }
        return INSTANCE;
    }
}

      单例模式下getInstance中如果传入的Context对象引用是activity的引用,因为单例模式内部INSTANCE是静态对象,没有赋值为null前,都会长存于内存中,context作为该对象的属性,也不会释放,引发内存泄漏。

      解法:如果单例中必要传入Context对象,使用Application的Context对象,因为这个是与整个app生命周期同步的。

    (2) 静态集合类引发的内存泄漏

public class Test {
    private static List<Object> ls;
    void operationList(){
         ls = new ArrayList<>();
      
         for(int i=0;i<100;i++){
            Object o = new Object();
             ls.add(o);
             o = null;
         }
    }
}

    以上代码虽然在循环中,每次在集合ls添加Object对象o后,都将o对象置为null,但集合ls仍持有o的引用,不会释放。

    Object o = new Object(); 这个语句细分为三个阶段。

     Object o:声明引用变量o,并在内存中分配空间;

     new Object():创建Object对象,并在内存的堆中分配空间存放它;

     = :等号,是个指向,将引用变量o指向创建好的Object对象。

    以此分析,上面的代码在循环中,只是把引用变量本身的指向置为null,在这之前,已经把引用存入到了静态集合中,所以不会释放。

     解法:不使用这种写法。

2、非静态内部类持有短生命周期的对象引用

     内部类概述:

     内部类包含静态内部类和非静态内部类。

     非静态内部类包含匿名内部类以及内部类(有类名)。

     Java与Android中不存在顶层的静态类,所有的静态类都是指静态内部类。     

      内部类有以下几种场景:

public class Test {
    //局部变量
    private int val = 1;

    //一个成员内部类
    class Inner{
        public void testInInner(){
            System.out.println("这是一个成员内部类的方法");
            System.out.println("可以直接引用外部类Test的变量,val=" + val);
            System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
        }
    }

    public void test1(){
        //匿名内部类
        //此处有匿名内部类, new Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("这是一个匿名内部类的方法");
                System.out.println("可以直接引用外部类Test的变量,val=" + val);
                System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
            }
        }).start();
    }

    public void test2(){
        class MethodInner{
            public void testInMethodInner(){
                System.out.println("这是一个方法内部类的方法");
                System.out.println("可以直接引用外部类Test的变量,val=" + val);
                System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.this.val);
            }
        }
    }


    static class StaticInner{
        public void testInStatic(){
            System.out.println("这是一个成员内部类的方法");
            System.out.println("与外部类无关,不可以直接引用外部类Test的非静态变量val");
        }
    }
}

        

(1)非静态内部类为何容易造成内存泄漏

     主要原因在于:非静态内部类隐性的持有外部类的引用,当内部类中进行耗时等操作时,外部类的引用会被一直持有,无法被释放。

     将上面的Test类做编译操作(javac),能看到同级目录下生成了5个字节码文件:Test$1.class 、Test$1MethodInner.class 、Test$Inner.class、Test$StaticInner.class、Test.class;

     以Test$1.class为例,字节码文件中默认生成的构造函数中,参数是外部类的对象引用。除静态内部类外,其他的内部类也是类似的构造,因此说非静态内部类隐性的持有外部类的引用。

class Test$Inner {
    Test$Inner(Test var1) {
        this.this$0 = var1;
    }

    public void testInInner() {
        System.out.println("这是一个成员内部类的方法");
        System.out.println("可以直接引用外部类Test的变量,val=" + Test.access$000(this.this$0));
        System.out.println("可以直接引用外部类Test的变量,该写法是在内部类中有同名变量时使用,val=" + Test.access$000(this.this$0));
    }
}

    其中access$000是编译器默认给外部类生成的方法。

(2)内部类的泄漏,有哪些场景

     - 如上面举例的new Runnable(){...},如果再run中有耗时操作,在耗时操作未结束前,就退出页面,因持有外部类引用并不释放,就会造成内存泄漏。

public class ThreadTestActivity extends AppCompatActivity {
    private static final String TAG = "ThreadTestActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        Log.d(TAG, "onCreate-this:"+this.toString());
        testThread();
    }

    private void testThread(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, "testThread-this:"+this.toString());
                SystemClock.sleep(10*1000);
            }
        }).start();
    }
}

    解法:改写方法,如必要这么写,定义静态内部类实现Runnable接口。

  - 如定时器 TimeTask

new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                 while (true);
            }
        },3 * 1000);

  解法:在相对位置,对定时器做cancel,或者改为静态内部类实现。

 - 如Handler。下方例子:匿名内部类new Handler(){...}持有外部类Test的引用。handlerOp方法执行后,主线程的消息队列在60s内都会持有handler的引用。handler又持有了外部类Test的引用,导致Test对象无法回收,造成内存泄漏。

public class Test {

Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };


    public void handlerOp(){
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // doSomeThing
            }
        },60 * 1000);
    }
}

 解法:静态内部类实现,或者handler改为弱引用,或者在相对位置对handler做remove,语句(Handler.removeCallbacksAndMessages(null);)  

  - 其他场景,如AsyncTask等,类似处理。

3、资源对象未释放

   资源对象未释放,也是出现内存泄漏的一个常见场景。

   (1)如文件未关闭,导致分配给该文件引用的缓冲未及时释放。如果多次未关闭,文件句柄太多没有被关闭(Could not read input channel file descriptors from parcel)

   (2)如数据库操作的游标 Cursor未关闭,频繁操作后会导致(android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed)

   如下示例,频繁操作而out不做处理,可能就会导致句柄过多,内存泄漏直至溢出。

public class ResourceTestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak_test);
        test();
    }

    private void test(){
        String filename = "app.txt";
        File file = new File(getExternalCacheDir(),filename);
        try{
            file.createNewFile();
            FileOutputStream out = new FileOutputStream(file);
        } catch (FileNotFoundException e){

        } catch (IOException e){

        }
    }
}

   解法:针对以上等情况,在正常流程或异常流程中,对资源关闭做好处理,如finally中做好关闭操作。

   总结的说,内存泄漏本质就是本该被回收的内存,没有被回收,多关注生命周期。

   内存泄漏不经意间就会被你写出,时时轻拂拭,莫使惹尘埃。

猜你喜欢

转载自blog.csdn.net/yangzhaomuma/article/details/105608848