Handler 源码解析系列文章:

  1. Handler 源码解析(一)—— Handler 的作业流程
  2. Handler 源码解析(二)—— 正确创立 Handler 目标
  3. Handler 源码解析(三)—— Handler 内存走漏

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

很多人说,导致 Handler 内存走漏的原因是:假如 Handler 发送了一个延迟很长时刻或者周期性的音讯,而在音讯处理前 Activity 现已被毁掉,Handler 依然持有对 Activity 的引证,或许导致内存走漏。

咱们都知道匿名内部类会持有外部类的引证,当咱们在 Activity 中创立如下 Handler 实例时,会提示有内存走漏危险:

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

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

咱们再看一个比如:

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

咱们经常运用匿名内部类给控件增加点击事件,但在这儿从未呈现内存走漏危险提示,也从未见过谁剖析此处会存在内存走漏的危险。能够看出,导致 Handler 呈现内存走漏的实质原因并不是匿名内部类持有外部类的引证。依据这个,咱们只是能够知道 Handler 目标持有了 Activity.this

依据可达性剖析,被 GCRoots 直接或直接引证的目标是不能够被收回的。那 Handler 呈现内存走漏时,一定是被某个 GCRoots 直接或直接引证着。

假如不了解 GC,建议先了解一下。 点击阅览:JVM(三)—— 废物收回机制

回过头来,咱们能够得出结论:匿名内部类会持有外部类的引证,但外部类开释时,匿名内部类也会被开释,这并不是导致 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 内存走漏

上述的引证联系会一向保持,直到 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 目标所发出的,则进行第一次循环,否则进行第二次循环。