Handler-引起的内存泄露分析以及解决方法

10次阅读

共计 4689 个字符,预计需要花费 12 分钟才能阅读完成。

Handler 引起的内存泄露分析以及解决方法

Handler 是 Android 系统提供的一种在子线程更新 UI 的机制,但是使用不当会导致 memory leak。严重的话可能导致 OOM

Java 语言的垃圾回收机制采用了可达性分析来判断一个对象是否还有存在的必要性,如无必要就回收该对象引用的内存区域,

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

}

然后在其他地方来发送一个延迟消息

handler.postDelayed(new Runnable() {
    @Override
    public void run() {}
}, 500);

我们一般使用 Handler 就是扎样,但是这样会当 Activity 销毁后会导致 memory leak.

原因就是 activity 销毁了,但是以为我们的 Handler 对象是一个内部类,因为内部类会持有外部类的一个引用。所以当 activity 销毁了,但是因为 Handler 还持有改 Activity 的引用,导致 GC 启动后,可达性分析发现该 Activity 对象还有其他引用。所以无法销毁改 Activity,

但是 handler 仅仅是 Activity 的一个内存对象。及时他引用了 Activity, 他们之间也只是循环引用而已。而循环引用则不影响 GC 回收内存。

其实真正的原因是 Handler 调用 postDelayed 发送一个延迟消息时:

public final boolean postDelayed(Runnable r, long delayMillis)
{return sendMessageDelayed(getPostMessage(r), delayMillis);
}

而 sendMessageDelayed 的实现是

public final boolean sendMessageDelayed(Message msg, long delayMillis)
{if (delayMillis < 0) {delayMillis = 0;}
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

再往下看

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(this + "sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

最终是将该消息加入到消息队列中。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到,在 enqueueMessage 的实现中。将 msg.target = this;

就是讲改 Handler 对象赋值给了 message 的 target 对象。所以 message 对象就引用了 Handler 对象 ”

进而 messageQueue 对象就引用了 Handler 对象。此次逐渐明朗。就是 messagequeue———message———

Handler——Activity。

所以我们可以在任一环节做文章即可避免 Handler 持有 Activity 对象导致的内存泄露问题。

我们可以在 Activity 销毁时将任务队列清空,或者 在 Activity 销毁时将 Handler 对象销毁。

总之,就是在任一环节将该引用链条切换就好了,这样 GC 就可以销毁 Activity 对象了。

此时还是没有触及到问题的核心,就是为什么 messageQueue 为什么会持有 message 对象进而持有 Handler 对象,导致 Activity 销毁时还有其他引用。为什么 Activity 销毁时 MessageQueue 不销毁呢,这才是问题的核心,如果 messageQueue 销毁了啥问题也没有了。当然我们也可以在 Activity 销毁时手动销毁 messageQueue 对象。这样也可以避免内存泄露。

从这我们可以看出 messagequeue 的生命周期比 Activity 长了。所以才导致这些问题。

其实熟悉 Handler 机制的话就会明白背后的原因了

 final Looper mLooper;
 final MessageQueue mQueue;

public Handler() {this(null, false);
}

public Handler(Callback callback, boolean async) {if (FIND_POTENTIAL_LEAKS) {final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur:" +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()
                        + "that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

从构造方法我们可以看出,无参的构造方法最终调用了两参的构造方法。

mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread()
                        + "that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;

这几行代码才是重中之重。

首先调用 Looper.myLooper() 方法。如果 looper 为 null,说明没有调用 looper.prepare() 方法。从抛出的运行时异常可以看出来。(ps: 所以在子线程使用 handler 时,第一就是要调用 Looper.prepare 方法)

looper 不为空话话,将 looper 复制个 Handler 的 looper 对象,然后将 looper 的 queue 对象赋值给 handler 的 queue 对象。

可以说 Handler 的 looper 字段和 queue 字段都是来着 looper 对象的。

可以看出我们在 Handler 里发送的消息最终发送到了 handler 的 queue 对象所执行的内存区域,而这片内存区域也是 Looper 对象的 queue 对象所指向的。所以说该 queue 对象里所有的 message 对象都收到 Looper 对象的 queue 对象的管理。

真正的大 boss 来了,都是 Looper 搞鬼。

因为我们是在主线程中初始化的 Handler。所以 Handler 引用的 looper 对象是在主线程中创建的。

在代码 ActivityThread.main() 中:

public static void main(String[] args) {
        ....

        // 创建 Looper 和 MessageQueue 对象,用于处理主线程的消息
        Looper.prepareMainLooper();

        // 创建 ActivityThread 对象
        ActivityThread thread = new ActivityThread(); 

        // 建立 Binder 通道 (创建新线程)
        thread.attach(false);

        Looper.loop(); // 消息循环运行
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
 Looper.prepareMainLooper();
 
  public static void prepareMainLooper() {prepare(false);
        synchronized (Looper.class) {if (sMainLooper != null) {throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();}
    }

在 prepareMainLooper 方法中首先调用了 prepare 方法,这就是为什么我们在主线程使用 Handler 时不需要自己手动调动 looper 的 prepare 方法的原因。

private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

在 prepare 方法中首先从 sThreadLocal 对象中取出 looper 对象。如果不为 null. 说明已经初始化过了,直接抛出异常。

没有初始化的话直接初始化然后放到 sThreadLocal 中。sThreadLocal 是一个 ThreadLocal 类型。持有线程的私有数据。

此时,真相大白了。主线程的 ThreadLocal——>looper——>messagequue——>message——>handler——>Acitivity

因为 APP 在活动中,所以主线程一直存在。looper 一直存在,messageQueue 一直存在。所以当我们发送了延迟消息时,而此时 Activity 销毁的话。自然会引起内存泄露的。

解决方法也很明了了。既然我们不能再 looper 层面做文章,就只能在 handler 和 message 层面做文章了。在 Activity 销毁时 将 Handler 手动置为 null, 或者将 messagequeue 清空,或者将 Handler 设置为静态内部类。然后内部通过若引用持有 Activity 对象。总之就是要让 Handler 和 message 改放手时就放手

正文完
 0