你可能没注意到Handler的这些问题


系统为我们提供了 Handler 机制来进行线程之间的消息的发送,其实就是线程的切换,把需要执行的操作切换到另一个线程中执行,通常我们会在主线程中自定义并创建 Handler,然后在子线程中执行完耗时操作后,切换到主线程进行 UI 相关的操作。这大概就是 Handler 常见的用法了,下面说说在使用 Handler 遇到的一些问题。

子线程 Handler 用完后需要退出 Looper

虽说我们一般使用 Handler 只是从子线程发送消息给主线程,但是 Handler 并不只能这样,它同样可以在两个子线程之间发送消息,或者说从一个子线程切换到另一个子线程。要完成这样的需求,代码大体上是这样的:

public void Click(View view) {
    new Thread("Thread1") {
        @Override
        public void run() {
            Looper.prepare();
            final MyHandler myHandler = new MyHandler();

            new Thread("Thread2") {
                @Override
                public void run() {
                    Message message = new Message();
                    message.what = 1;
                    message.obj = "【你好】--来自" + Thread.currentThread().getName() + "的问候!";
                    myHandler.sendMessage(message);
                }
            }.start();

            Looper.loop();
        }
    }.start();
}
private static class MyHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
        if (msg.what == 1) {
            Log.d(TAG, "收到消息了:" + msg.obj 
                + "\n当前线程Thread:" + Thread.currentThread().getName());
        }
    }
}

上面代码,首先在主线程中开启了一个线程 Thread1,在该线程中创建了一个自定义的Handler 对象 MyHandler,需要注意的是,子线程中创建 Handler 需要调用 Looper.prepare() 和 Looper.loop() 方法,具体原因见我另一篇文章–Android基础回顾(2)——从源码角度理解Handler机制。随后,又开启了一个名为 Thread2 的线程,在 Thread2 中,使用 myHandler 发送了一个包含当前线程名信息的 Message 对象。这和我们平时使用 Handler 的唯一不同之处在于创建 Handler 对象的操作放在了子线程,即发送的消息将会在子线程中被 Handler 的 handleMessage 方法处理。

看看打印的日志,更加清楚的认识一下:

D/ThreadLocalTest: 收到消息了:【你好】--来自Thread2的问候!
                   当前线程Thread:Thread1

可以看到消息的发送的线程是 Thread2,接受的线程是 Thread1。

上面演示的是如何在子线程之间通过 Handler 发送消息。

*子线程使用完 Handler 需要退出 Looper

由于 Looper.loop() 方法会不停的从 MessageQueen 中查看是否有新消息,如果有新消息就会立即发给当前线程的 Handler 处理,否则会一直阻塞当前线程。主线程因为需要处理的消息不能停止,所以不用停止处理,而子线程就需要在处理完消息之后退出 Looper,否则该子线程也将不会终止,这样的话,该子线程会一直占用着内存,却没有具体的工作,浪费内存。

因此,在子线程完成了任务后,需要退出该线程的 Looper,终止线程,释放内存。

如上面例子,Thread2在发送完 Message 对象后,就会终止,而 Thread1 由于 Looper.loop() 一直阻塞在哪里,handleMessage 方法处理了消息后,Thread1 并不会终止。我们可以在 handleMessage 方法中获取到 Looper 对象,调用 quit 或 quitSafely 退出 Looper。两者的区别是:quit 会直接退出 Looper,而 quitSafely 会先做一个退出的标记,等待消息队列中已有的消息处理完毕后才安全退出;quitSalely 是 API18 新加的方法。Looper 退出后,通过 Handler 发送的消息会失败,这时候 Handler 的 send 方法会返回 false。

修改 MyHandler 的 handleMessage,这里需要注意的是需要确保所有的消息都处理完,才能调用退出 Looper:

private static class MyHandler extends Handler {

    @Override
    public void handleMessage(Message msg) {
        if (msg.what == 1) {
            Log.d(TAG, "收到消息了:" + msg.obj 
                + "\n当前线程Thread:" + Thread.currentThread().getName());
            // 这里需要确保所有的消息都处理完,才能调用退出 Looper
            getLooper().quit();
        }
    }
}

子线程Toast

在非 UI 线程可以弹吐司吗?

答案是能,但也不是可以直接弹出来的。 一开始看到这里问题,脑中浮现的是,非 UI 线程是不可以操作 UI 的啊,肯定不能弹的,会奔溃的。于是就试了试,果然奔溃了,被自己的聪明才智折服后,却看到了如下报错信息:

java.lang.RuntimeException: 
        Can't create handler inside thread that has not called Looper.prepare()

What?

不是应该报出这样的错吗?

android.view.ViewRootImpl$CalledFromWrongThreadException: 
        Only the original thread that created a view hierarchy can touch its views.

看来 Toast 和普通的界面显示 View 是不同的,从报错信息中可以看出来 Toast 使用到了 Handler。所有在子线程中可以这样来显示 Toast:

new Thread(){
    @Override
    public void run() {
        textView.setText("text_view_thread" + "--" + Thread.currentThread().getName());
        Looper.prepare();
        Toast.makeText(MainActivity.this, "Toast" , Toast.LENGTH_SHORT).show();
        Looper.loop();
    }
}.start();

具体原理,可以看 Handler 原理和 Toast 原理的相关文章: Android基础回顾(2)——从源码角度理解Handler机制 Android Toast 原理分析

内存泄露

由于非静态内部类默认会持有外部类的引用,所以在 Activity 中创建非静态的自定义 Handler 可能会导致内存泄露,如下代码,会导致内存泄露:

public class MainActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // ...
            }
        }, 1000 * 60 * 10);

        finish();
    }
}

如果自定义的 Handler 中没有使用到外部类,就可以直接静态化避免不必要的引用外部类:

static final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        // ...
    }
}

如果该自定义的 Handler 需要使用外部的 Activity 引用,可以使用弱引用来避免内存泄露:

private static class MyHandler extends Handler {
    private final WeakReference<MainActivity> mActivityReference;

    public MyHandler(MainActivity activity) {
            mActivityReference = new WeakReference<MainActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        MainActivity activity = mActivityReference.get();
        if (activity != null) {
            // ...
        }
    }
}

注意从弱引用中 get 对象后,必须进行非空判断,因为弱引用可能会出现获取的对象为空的情况。

评论