likes
comments
collection
share

Handler 源码解析(三)—— Handler 内存泄漏

作者站长头像
站长
· 阅读数 3

Handler 源码解析系列文章:


1. 匿名内部类是导致 Handler 内存泄漏的本质原因吗?

很多人说,导致 Handler 内存泄漏的原因是:如果 Handler 发送了一个延迟很长时间或者周期性的消息,而在消息处理前 Activity 已经被销毁,Handler 仍然持有对 Activity 的引用,可能导致内存泄漏。

我们都知道匿名内部类会持有外部类的引用,当我们在 Activity 中创建如下 Handler 实例时,会提示有内存泄漏风险:

Handler 源码解析(三)—— Handler 内存泄漏

那么导致该风险的根本原因是匿名内部类持有外部类的引用吗?

我们再看一个例子:

Handler 源码解析(三)—— Handler 内存泄漏

我们经常使用匿名内部类给控件添加点击事件,但在这里从未出现内存泄漏风险提示,也从未见过谁分析此处会存在内存泄漏的风险。可以看出,导致 Handler 出现内存泄漏的本质原因并不是匿名内部类持有外部类的引用。根据这个,我们仅仅可以知道 Handler 对象持有了 Activity.this

根据可达性分析,被 GCRoots 直接或间接引用的对象是不可以被回收的。那 Handler 出现内存泄漏时,一定是被某个 GCRoots 直接或间接引用着。

回过头来,我们可以得出结论:匿名内部类会持有外部类的引用,但外部类释放时,匿名内部类也会被释放,这并不是导致 Handler 发生内存泄漏的本质原因,但可以作为一个间接原因。

那么GCRoots 又是谁,下文接着分析。

2. Handler 内存泄漏原因

2.1 在主线程中创建 Handler 对象

主线程中创建 Handler 对象:

// MainActivity.java
Handler handler = new Handler(){  
    @Override  
    public void handleMessage(@NonNull Message msg) {  
        // 修改 TextView 内容
    }  
};  
  
new Thread(new Runnable() {  
    @Override  
    public void run() {  
        Message msg = Message.obtain();  
        ...
        handler.sendMessageDelayed(msg, 20000);  
    }  
}).start();

然后在子线程中发送一个延迟消息,立刻销毁 MainActivity ,会发生内存泄漏。 操作手顺:

  1. 点击按钮,打开 MainActivity 页面。
  2. 在 20s 内销毁 MainActivity 页面。
  3. 手动 GC。

通过 Profiler 进行分析,会发现内存发生泄漏,引用链:

Handler 源码解析(三)—— Handler 内存泄漏

2.1.1 sMainLooper 作为 GCRoot

通过匿名内部类持有外部类的对象,我们可以知道Handler 持有了 MainActivity.this

在上一篇文章中,我们提到 Handler 中消息入队方法 enqueueMessage()

// Handler.java
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

第4行处,msg.target 引用了 Handler 的对象。也就是 Message 的对象 msg 持有了 handler 的引用

第10行处,调用了 MessageQueue 的 enqueueMessage() 方法并传入了 msg:

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {  
    if (msg.target == null) {  
        throw new IllegalArgumentException("Message must have a target.");  
    }  

    synchronized (this) {  
        if (msg.isInUse()) {  
        throw new IllegalStateException(msg + " This message is already in use.");  
    }  

    if (mQuitting) {  
        IllegalStateException e = new IllegalStateException(  
        msg.target + " sending message to a Handler on a dead thread");  
        Log.w(TAG, e.getMessage(), e);  
        msg.recycle();  
        return false;  
    }  

    msg.markInUse();  
    msg.when = when;  
    Message p = mMessages;  
    boolean needWake;  
    if (p == null || when == 0 || when < p.when) {  
        // New head, wake up the event queue if blocked.  
        msg.next = p;  
        mMessages = msg;  
        needWake = mBlocked;  
    } else {  
        // Inserted within the middle of the queue. Usually we don't have to wake  
        // up the event queue unless there is a barrier at the head of the queue  
        // and the message is the earliest asynchronous message in the queue.  
        needWake = mBlocked && p.target == null && msg.isAsynchronous();  
        Message prev;  
        for (;;) {  
            prev = p;  
            p = p.next;  
            if (p == null || when < p.when) {  
                break;  
            }  
            if (needWake && p.isAsynchronous()) {  
                needWake = false;  
            }  
        }  
        msg.next = p; // invariant: p == prev.next  
        prev.next = msg;  
    }  

    // We can assume mPtr != 0 because mQuitting is false.  
    if (needWake) {  
    nativeWake(mPtr);  
    }  
    }  
    return true;  
}

消息被添加至消息队列后,MessageQueue 中的 mMessages 会有对该消息的引用,所有待处理的消息被组织成一个单向链表使用 next 属性来指示下一个消息的位置。MessageQueue 对象持 有了 msg 的引用

MessageQueue 又被谁持有呢,在 Handler 的构造函数中:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {  
    mLooper = looper;  
    mQueue = looper.mQueue;  
    mCallback = callback;  
    mAsynchronous = async;  
}

第3行处,根据 mQueue = mLooper.mQueue,推测应该在 Looper 中进行了赋值,接着看 Looper 的构造函数:

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

可以发现,在创建 Looper 对象时,同时创建了一个 MessageQueue 实例。 Looper 对象持有了 MessageQueue 对象。同时可以发现,Looper 中的 mQueue 为 final 对象,Looper 对应的 mQueue 不可以被修改:

// Looper.java
final MessageQueue mQueue;

Looper 对象又被谁持有了呢?查看 ActivityThread 中的 main() 方法,其中:

public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
    ...
}

第3行,Looper.prepareMainLooper(),进一步查看 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();
    }
}

第 2 行,通过 prepare 方法创建了 Looper 对象,在第 7 行,sMainLooper 持有了创建的 Looper 对象。

// Looper.java
private static Looper sMainLooper;

sMainLooper 是 static 修饰的,就是我们所说的 GCRoot。综上,存在如下引用链:

Handler 源码解析(三)—— Handler 内存泄漏

2.1.2 活动中的线程作为 GCRoots

其实还存在另外一条引用链, 查看prepareMainLooper()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));
}

第5行,new 出一个 Looper 对象并调用了 ThreadLocal 的 set 方法:

// ThreadLocal.java
public void set(T value) {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        map.set(this, value);  
    } else {  
        createMap(t, value);  
    }  
}

// ThreadLocal.java
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}

// Thread,java
ThreadLocal.ThreadLocalMap threadLocals = null;

通过 set 方法可以发现,Looper 对象放到了 ThreadLocalMap 中,而第4行的 ThreadLocalMap 对象,是通过当前线程获得的。当前 thread 持有 ThreadLocalMap 对象。ThreadLocalMap 对象通过 Entry 持有 Looper 对象

这里有个容易搞错的点,Looper 对象是 Entry 节点中的一个 value,并不是被 sThreadLocal 持有。通过 sThreadLocal.set(new Looper(quitAllowed)) 将 Looper 对象作为 value 值添加到 ThreadLocalMap 的 Entry中:

// ThreadLocal.java  ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {  
    ...
    tab[i] = new Entry(key, value);  
    ...
}

// ThreadLocal.java  ThreadLocalMap
static class Entry extends WeakReference<ThreadLocal<?>> {  
    /** The value associated with this ThreadLocal. */  
    Object value;  

    Entry(ThreadLocal<?> k, Object v) {  
        super(k);  
        value = v;  
    }  
}

private Entry[] table;

我们可以发现,Entry 通过弱引用去引用 key 值(ThreadLocal 对象),通过强引用去引用 value 值(Looper 对象)

所以 ThreadLocal.ThreadLocalMap 通过 Entry 强引用了 Looper 对象。而当前 Thread 持有了 ThreadLocalMap 对象。

活动的线程也是 GCRoot ,不能被回收。如果线程一直处于运行中,则一直会存在如下引用链:

Handler 源码解析(三)—— Handler 内存泄漏

2.2 在子线程中创建 Handler 对象

// 仅作为测试代码
new Thread(new Runnable() {  
    @Override  
    public void run() {  
        Looper.prepare();  
        handler = new Handler(Looper.myLooper()){  
            @Override  
            public void handleMessage(@NonNull Message msg) {  
                if (msg.what == 1){  
                    binding.text.setText("111111111");  
                }  
            }  
        };  
        handler.sendEmptyMessageDelayed(1, 20000);  
        Looper.loop();  
    }  
}).start();

同样发送延迟消息,只不过本次的 Handler 对象是在子线程中创建的,子线程中的 Looper 对象是通过第5行调用 Looper.prepare() 直接创建的。

与主线程不同的是,这次不会调用 prepareMainLooper() 方法了,自然也就不存在以 sMainLooper 为 GCRoot 的引用链。另一条引用链同主线程分析时一样,存在。只要创建 Looper 对象的线程存在,就会存在如下引用链,从而导致内存泄漏:

上述的引用关系会一直保持,直到 Handler 消息队列中的所有消息被处理完毕。在 Handler 消息队列还有未处理的消息 / 正在处理消息时,此时若需销毁外部类 MainActivity ,但由于上述引用关系,垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏。

造成内存泄露的两个关键条件:

  1. 存在 “未被处理 / 正处理的消息 -> Handler 实例 -> 外部类” 的引用关系
  2. Handler 的生命周期 > 外部类的生命周期

3. Handler 内存泄露的解决方案

3.1 静态内部类 + 弱引用

Handler的子类设置成静态内部类。静态内部类不持有外部类的引用。此外,可使用 WeakReference 弱引用持有外部类,保证外部类能被回收。

public class MainActivity extends AppCompatActivity {
    private UIHandler mHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new UIHandler(this, Looper.myLooper());
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                Message msg = Message.obtain();  
                msg.what = 1;  
                msg.obj = "Hello";  

                handler.sendMessage(msg);  
            }  
        }).start();
    }
    // 设置为:静态内部类
    private static class UIHandler extends Handler{
        // 定义弱引用实例
        private final WeakReference<Activity> mReference;
        // 在构造方法中传入需持有的Activity实例
        public UIHandler(Activity activity, Looper looper) {
            super(looper);
            // 使用 WeakReference 弱引用持有 Activity 实例
            mReference = new WeakReference<Activity>(activity);
        }
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    ...
                    break;
                case 2:
                    ...
                    break;
            }
      }
    }
  }

3.2 清空消息队列

当外部类结束生命周期时,清空Handler内消息队列

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

最终会调用 MessageQueue 中的 removeCallbacksAndMessages()

void removeCallbacksAndMessages(Handler h, Object object) {  
    if (h == null) {  
        return;  
    }  

    synchronized (this) {  
        Message p = mMessages;  

        // Remove all messages at front.  
        while (p != null && p.target == h  
            && (object == null || p.obj == object)) {  
                Message n = p.next;  
                mMessages = n;  
                p.recycleUnchecked();  
                p = n;  
        }  

        // Remove all messages after front.  
        while (p != null) {  
            Message n = p.next;  
            if (n != null) {  
                if (n.target == h && (object == null || n.obj == object)) {  
                    Message nn = n.next;  
                    n.recycleUnchecked();  
                    p.next = nn;  
                    continue;  
                }  
            }  
        p = n;  
        }  
    }  
}

该函数中为什么进行两次循环?

一个线程中,消息队列与 Handler 实例的比例为 1:n。如下图所示,一个消息队列中的消息可能由不同的 Handler 对象发送过来的,而 mHandler.removeCallbacksAndMessages(null) 移除的是指定 Handler 对象对应的消息。

Handler 源码解析(三)—— Handler 内存泄漏

若当前消息队列队头消息 mMessages 为想要清空 Handler 对象所发出的,则进行第一次循环,否则进行第二次循环。