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

Handler 源码解析系列文章:

  1. Handler 源码解析(一)------ Handler 的工作流程
  2. Handler 源码解析(二)------ 正确创建 Handler 对象
  3. Handler 源码解析(三)------ Handler 内存泄漏

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

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

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

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

我们再看一个例子:

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

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

如果不了解 GC,建议先了解一下。 点击阅读:JVM(三)------ 垃圾回收机制

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

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

2. Handler 内存泄漏原因

2.1 在主线程中创建 Handler 对象

主线程中创建 Handler 对象:

java 复制代码
// 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 进行分析,会发现内存发生泄漏,引用链:

2.1.1 sMainLooper 作为 GCRoot

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

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

java 复制代码
// 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:

java 复制代码
// 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 的构造函数中:

java 复制代码
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 的构造函数:

java 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

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

java 复制代码
// Looper.java
final MessageQueue mQueue;

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

java 复制代码
public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
    ...
}

第3行,Looper.prepareMainLooper(),进一步查看 Looper 的 prepareMainLooper() 方法:

java 复制代码
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 对象。

java 复制代码
// Looper.java
private static Looper sMainLooper;

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

2.1.2 活动中的线程作为 GCRoots

其实还存在另外一条引用链, 查看prepareMainLooper()prepare() 方法:

java 复制代码
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 方法:

java 复制代码
// 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中:

java 复制代码
// 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 ,不能被回收。如果线程一直处于运行中,则一直会存在如下引用链:

2.2 在子线程中创建 Handler 对象

java 复制代码
// 仅作为测试代码
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 弱引用持有外部类,保证外部类能被回收。

java 复制代码
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内消息队列

java 复制代码
@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}

最终会调用 MessageQueue 中的 removeCallbacksAndMessages()

java 复制代码
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 对象对应的消息。

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

相关推荐
darkb1rd1 小时前
五、PHP类型转换与类型安全
android·安全·php
gjxDaniel1 小时前
Kotlin编程语言入门与常见问题
android·开发语言·kotlin
csj501 小时前
安卓基础之《(22)—高级控件(4)碎片Fragment》
android
峥嵘life2 小时前
Android16 【CTS】CtsMediaCodecTestCases等一些列Media测试存在Failed项
android·linux·学习
闻哥2 小时前
从测试坏味道到优雅实践:打造高质量单元测试
java·面试·单元测试·log4j·springboot
stevenzqzq3 小时前
Compose 中的状态可变性体系
android·compose
似霰3 小时前
Linux timerfd 的基本使用
android·linux·c++
南风知我意9574 小时前
【前端面试5】手写Function原型方法
前端·面试·职场和发展
darling3315 小时前
mysql 自动备份以及远程传输脚本,异地备份
android·数据库·mysql·adb
java1234_小锋5 小时前
Java高频面试题:SpringBoot如何自定义Starter?
java·spring boot·面试