美文网首页一些收藏
Android内存优化二:内存泄漏

Android内存优化二:内存泄漏

作者: Archer_J | 来源:发表于2021-10-13 17:23 被阅读0次

    Android内存优化一:java垃圾回收机制
    Android内存优化二:内存泄漏
    Android内存优化三:内存泄漏检测与监控
    Android内存优化四:OOM
    Android内存优化五:Bitmap优化

    内存泄漏

    内存泄漏指的是不需要的对象无法被回收。

    其本质是对象生命周期的不一致性导致的,即生命周期较长的对象持有生命周期较短的对象的引用。

    对象回收时机:

    1. 根据Java垃圾回收机制,Java 虚拟机使用可达性分析算法来判断对象是否可被回收,以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
    • 当一个不需要的对象,仍然可达(从GC Roots到此对象具有完整的引用链),则无法被回收,导致此对象仍然占用着内存,就发生了内存泄漏
    1. 根据Java垃圾回收机制,判定对象是否可被回收与引用有关。
    • 强引用关联的对象不会被回收。
    • 软引用关联的对象只有在内存不够的情况下才会被回收。
    • 弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
    • 虚引用关联的对象,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

    注:通过软引用可以解决大部分内存泄漏问题。只有当对象只被软引用关联时,gc发生时,才会被回收;如果对象同时被强引用和弱引用关联,则无法被回收。

    类型

    内部类

    内部类(包括匿名内部类)会隐式持有外部类对象的引用

    /**
     * 外部类
     */
    public class Outer {
    
        private int count;
    
        private Inner inner = new Inner();
    
        public void doAction() {
            inner.doSomething();
        }
    
        /**
         * 内部类
         */
        private class Inner {
    
            public void doSomething() {
                count++;
            }
        }
    
    }
    

    编译后的字节码

    class Outer$Inner
    {
      private Outer$Inner(Outer paramOuter) {}
    
      public void doSomething()
      {
        Outer.access$108(this.this$0);
      }
    }
    
    • 可以看到,编译器为内部类单独生成了一个.class文件,构造函数被定义为私有的,且传入了外部类的实例,所以它持有了外部类实例的强引用,也因此,内部类可以调用到外部类的方法和属性。
    • 当内部类生命周期长于外部类时,比如外部类为Activity,内部类为Thread,当Activity退出时,Thread仍未执行完,导致Activity仍被Thread强引用着无法被回收而出现了内存泄漏

    解决方法

    解决方法是使用静态内部类 + 弱引用,静态内部类编译后的字节码文件,编译器并没有为它添加额外的构造函数,所以它其实和我们的外部类没有任何关系,这是写在同一个.java源文件中而已。

    • 由于静态内部类未持有外部类引用,所以无法调用外部类的方法及属性,解决方式是将外部类通过弱引用传递给静态内部类,当外部类未被强引用时,则GC的时候,外部类会被回收,此时通过弱引用取出外部类的引用则为null
    /**
     * 外部类
     */
    public class Outer {
    
        private int count;
    
        private Inner inner = new Inner();
    
        public void doAction() {
            inner.doSomething();
        }
    
        /**
         * 静态内部类
         */
        private static class Inner {
    
                WeakReference<Outer> ref;
    
                Inner(Outer outer){
                    ref = new WeakReference<Outer>(outer);
            }
    
            public void doSomething() {
                // 由于静态内部类未持有外部类引用,所以无法调用外部类的方法及属性
                // 这里通过弱引用获取到外部类的引用
                Outer outer = ref.get();
                // 当外部类被回收时,取出的引用为null
                if (outer != null){
                     outer.count++;
                }
            }
        }
    
    }
    

    Handler

    通过handler发送一个延迟消息,由于匿名内部类Runnable持有了外部类的引用,且其生命周期大于外部类,所以造成了内存泄漏。

    public void post(){
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // todo
            }
        },500);
    }
    

    通过handler发送非延迟消息,也会造成内存泄漏。

    这是因为消息会插入到消息队列MessageQueue中,如果在这个消息之前有其他消息产生阻塞,或者存在同步屏障,会导致这个消息延迟执行,所以其生命周期同样会大于外部类。

    public void post(){
        Handler handler = new Handler();
        handler.post(new Runnable() {
            @Override
            public void run() {
                // todo    
            }
        });
    }
    

    同样,如下这种方式也会产生泄漏,这里的new Handler()创建的也是一个匿名内部类

    public void post(){
        Handler handler = new Handler(){
    
            @Override
            public void handleMessage(@NonNull Message msg) {
                 super.handleMessage(msg);
                 // todo
            }
        };
        Message msg = Message.obtain();
        msg.what = 1;
        handler.sendMessage(msg);
    }
    

    引用链

    1. 通过post调用,会将runnable存入创建的Message中,使Message持有runnable
    // Handler
    public final boolean post(@NonNull Runnable r) {
       return sendMessageDelayed(getPostMessage(r), 0);
    }
    
    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
    

    当发送一个Message,最终都会调用enqueueMessage将Message插入MessageQueue中

    同时将当前handler对象赋值给Message的target变量,使Message持有handler

    // Handler
    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
                long uptimeMillis) {
        // 这里的this为Handler
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
    
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
    
    1. MessageQueue是一个单向链表,在Looper初始化使创建
    // Looper
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    
    1. Looper用于为线程运行消息循环的类,一个线程只有一个Looper,主线程的Loop在ActivityThread中创建,并开启循环,作用于整个应用的生命周期中。
    // ActivityThread
    public static void main(String[] args) {
            ....
            // 创建主线程的Looper
            Looper.prepareMainLooper();
        // 开启循环
        Looper.loop();
        ....
    }
    

    引用链为:

    Looper(****GC Roots****) -> MessageQueue -> Message -> Runnable(或Handler) -> Activity(外部类)

    所以插入到MessageQueue中的Message如果还未得到执行,其生命周期相当于整个应用的生命周期,且其间接持有了外部类的引用,所以造成了外部类的泄漏

    解决方法

    使用静态内部类+弱引用的方式 可以解决Handler造成外部类泄漏的问题,但是并没有从根本上解决泄漏,因为MessageQueue仍然持有Message,仍会造成Message的泄漏。

    可以如下方式移除当前handler创建的Message

    handler.removeCallbacksAndMessages(null);
    

    静态变量

    静态变量导致内存泄漏的原因是因为短生命周期对象被声明成为长生命周期对象,静态变量是GC Roots的一种

    • 静态变量是属于类的不是属于实例对象的,存储在方法区中
    • 方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高,主要是对常量池的回收和对类的卸载。

    类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

    • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。

    • 加载该类的 ClassLoader 已经被回收。

    • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

    所以一般来说,静态变量很少会被GC回收的

    比如:

    private static Context context;
    

    当将Activity赋值给静态的context变量时,Activity成为了长生命周期对象GC Roots,而Activity 的生命周期可能很短,用户一打开就关闭了。导致Activity无法被回收而产生泄漏

    解决方法

    如果需要静态 Context, 可以考虑使用 ApplicationContext,ApplicationContext的生命周期为整个应用的生命周期,不会产生泄漏问题。

    单例模式

    单例模式产生泄漏的原因是因为长生命周期对象持有了短生命周期对象的引用

    比如:

    final class SingleTon{
    
        private static SingleTon instance;
        private Context context;
    
    }
    

    单例对象实际是一个静态变量,持有了context的引用,当context为Activity时,导致Activity无法被回收而产生泄漏

    解决方法

    如果需要单例需要引用 Context, 可以考虑使用 ApplicationContext,ApplicationContext的生命周期为整个应用的生命周期,不会产生泄漏问题。

    资源未释放

    1. 忘了注销 BroadcastReceiver

    2. 忘了关闭数据库游标(Cursor)

    3. 忘了关闭流

    4. 忘了调用 recycle() 方法回收创建的 Bitmap 使用的内存

    5. 忘了在 Activity 退出时取消 RxJava 或协程所开启异步任务

    6. Webview

    不同的 Android 版本的 Webview 会有差异,加上不同厂商定制 ROM 的 Webview 的差异,导致 Webview 存在很大的兼容问题,一般情况下,在应用中只要使用一次 Webview,它占用的内存就不会被释放,解决方案:WebView内存泄漏--解决方法小结

    相关文章

      网友评论

        本文标题:Android内存优化二:内存泄漏

        本文链接:https://www.haomeiwen.com/subject/gwkeoltx.html