Handler 相关总结

1,076 阅读8分钟

Handler的用途

Handler主要用来做异步消息的处理,主要解决在子线程中无法访问UI的问题。

Handler的初始化

通常有两种使用方法,第一种是直接初始化一个handler,重写其handleMessage方法,第二种是传一个Handler.Callback对象,同样重写Handler.Callback类中的handleMessage方法。

private Handler handler1 = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};

private Handler handler2 = new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        return false;
    }
});

在Handler的方法中:

public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

发送消息

  • handler.sendMessage(Message msg)
  • handler.post(Runnable r) 最终会将runnable对象封装到Message中。

子线程中创建Handler

网上很多地方说子线程不能创建Handler, 其实严格来讲,这种说法是不准确的,如果直接在子线程中调用new Handler() 会报错:

2021-05-31 16:15:52.447 28913-29688/com.fred.app E/AndroidRuntime: FATAL EXCEPTION: Thread-3
    Process: com.fred.app, PID: 28913
    java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-3,5,main] that has not called Looper.prepare()
        at android.os.Handler.<init>(Handler.java:207)
        at android.os.Handler.<init>(Handler.java:119)
        at com.fred.app.MainActivity.lambda$sendMsg$3$MainActivity(MainActivity.java:95)
        at com.fred.app.-$$Lambda$MainActivity$wS6JjKtF31xh-2WedxDQY0yoNJA.run(Unknown Source:2)
        at java.lang.Thread.run(Thread.java:919)

错误信息也很明了,子线程中不能够创建Handler是因为没有调用Looper.prepare()方法。该异常信息是在Handler的构造方法中抛出来的。所以,如果我们在子线程中先调用一次Looper.prepare()方法,再来new Handler()是不会有问题的。

Handler、Looper、MessageQueue

主线程与Looper的绑定

sequenceDiagram
ActivityThread->>Looper: Looper.prepareLooper()
Looper-->>Looper: prepare()
Looper-->> ThreadLocal: set(new Looper(quitAllowed);

ActivityThread的main方法中会调用Looper.prepareMainLooper();, 该方法调用Looper中的prepare()方法,prepare中会调用ThreadLocalset(new Looper(quitAllowed)方法,将当前的线程与将创建的Looper绑定起来, new Looper(quitAllowed)的时候会初始化一个消息队列,于是主线程中对应的LooperMessageQueue就都有了。

Looper和MessageQueue的关系

Looper中会有一个MessageQueue的实例,

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

Handler和Looper

我们来看Handler的构造方法:

public Handler(@Nullable Callback callback, boolean 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;
    mCallback = callback;
    mAsynchronous = async;
}

Looper

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

从Handler的构造方法中可以看出:

设置Handler中的Looper

Handler初始化时,会调用Looper.myLooper()方法去拿一个Looper对象, 最终是会从这个ThreadLocal中去拿,从前面的分析来看,如果是在主线程中调用这个方法时,Looper肯定是已经初始化好了,所以返回值必然不为空。而如果是在子线程,没有提前创建一个Looper对象放到ThreadLocal中,此时调用Looper.myLooper()时必须为null,也就会抛出异常,解决的办法便是在子线程中先调用一次Looper.prepareLooper()

Handler初始化时不能直接new一个Looper, 是因为如果new 一个Looper,就不能保证Looper的唯一性。在Looperprepare方法中,取一个Looper是从ThreadLocal中拿,得到的Looper与当前线程绑定。

设置Handler中的MessageQueue

mQueue = mLooper.mQueue;

消息的发送

调用Handler里面的sendMessage或类似的方法,最终会将消息放到消息队列中

消息的消费

还是以主线程为例,在主线程的main方法中调用了Looper.prepareMainLooper();方法后,会再调用Looper.loop();方法,该方法中有一个死循环,不断地从消息队列中取消息,取到消息后便执行msg.target.dispatchMessage(msg);,我们知道msg.target对应的便是一个Handler对象, 所以该方法最终调用的会是一个HandlerdispatchMessage方法。而dispatchMessage 方法最终会调用HandlerhandleMessage方法,完成消息的分发。

Handler和Message

Handler中sendMessage时,会调用enqueueMessage方法,该方法会将Message和Handler进行绑定。

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
    //msg 的 target 属性指向的便是当前handler
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

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

Handler的内存泄露问题

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    @SuppressLint("HandlerLeak")
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            startActivity(new Intent(MainActivity.this, OrderActivity.class));
            System.out.println("[App Log]-------------------------------->handleMessage" );
        }
    };

    private void initView() {
        findViewById(R.id.tv_id).setOnClickListener(v -> sendMsg());
    }

    private void sendMsg() {
        new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Message msg = new Message();
            msg.what = 1;
            handler.sendMessage(msg);
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeMessages(1);
    }
}

如上代码,点击按钮后,触发sendMsg方法, 该方法会先sleep(5000), 然后再发送一个message, handler在接收到message后会跳转到另一个activity。我们在onDestory中移除掉了发送的那个message。如果我们在点了按钮后,按系统的返回键,此时该activity的onDestory会执行,然后handler.removeMessage(1)这个方法会执行,此时因上上面的sendMsg 里面的handler消息还没有发出去,所以removeMessage方法实际上并没有删除消息。最终handleMessage中的方法还是会执行,造成内存泄露。

解决的办法很简单,只要我们在onDestory里面将handler置为null, 然后在发消息的时候做一个非空判断就可以了。

或者也可以用这种方式,如果我们在sendMsg中这样写

private void sendMsg() {
    new Thread(() -> {
        Message msg = new Message();
        msg.what = 1;
        handler.sendMessageDelayed(msg, 5000);
    }).start();
}

此时msg会压入到handler对应的messgeQueue中,但是在5秒后发出去,但因为我们的onDestory里面又把这个消息从消息队列中给移出去了,所以用这种方式也可以避免。

三种消息类型

同步消息

我们平时的使用,发出的消息都是同步消息。

异步消息

通过msg.setAsynchronous(true)设置的消息为异步消息,该方法是在api level 22(android 5.0之后)才引入。

屏障消息

也称为同步屏障消息,其特点是没有对应的target, 也就是没有对应的handler,需要利用反射才能去调用,属于系统的隐藏api。android对这方面的限制越来越严格,在高版本上不允许反射调用。

插入屏障消息

MessageQueue queue = mHandler.getLooper().getQueue();
Method method = MessageQueue.class.getDeclaredMethod("postSyncBarrier");
method.setAccessible(true);
token = (int)method.invoke(queue);

移除屏障消息

MessageQueue queue = mHandler.getLooper().getQueue();
Method method = MessageQueue.class.getDeclaredMethod("removeSyncBarrier", int.class);
method.setAccessible(true);
method.invoke(queue, token);

当调用postSyncBarrier发出一个屏障消息时,消息队列中的同步消息会被挂起,异步消息正常执行,在调用了removeSyncBarrier后,同步消息会被继续执行。需要注意一点是,在我们插入了屏障消息后,一定要记得移除,否则页面会处于一种挂起状态。比如,页面上的点击事件会无法监听。

HandlerThread

继承自Thread, 是一种可以使用Handler的Thread。其内部会有一个Looper,其run方法开启Looper的消息循环. 它的使用:

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        Log.i(TAG, "thread start");
    }
});
HandlerThread handlerThread = new HandlerThread("Handler Thread");
Handler handler = new Handler(handlerThread.getLooper());
handler.post(t);

IntentService

IntentService继承自Service并且是一个抽象类。IntentService可以用于执行后台耗时的作务,当作务执行后会自动停止,由于它是服务的原因,因此它的优先级比单纯的线程要高很多,也就意味着不容易被系统杀死。IntentService封装了HandlerHandlerThread

  • IntentService的使用场景: 官网的说法是,大多数的Service不需要同时处理多个请求(并法),在这种情况下,我们可以采用IntentService。
  • 工作机制:
    • 创建一个默认的工作线程,将所有的intents传递到onStartCommand
    • 创建一个队列,每次将一个intent传递到onHandleIntent中,所以我们就不用关心多线程的问题
    • 如果所有的request都执行了,就会执行了stopSelf方法
  • IntentService封装了HandlerThreadHandler。在它的onCreate方法中会创建一个HandlerThread对象,然后使用该对象的Looper构造一个Handler, 由这个Handler来处理消息,由于这个Handler是由HandlerThreadLooper构造的,而HandlerThread本质上来讲是一个子线程,因此IntentService是可以执行后台作务.
  • 使用方式:继承自IntentService, 并实现其onHandleIntent 方法

一个Demo

button2.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View v) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            startService(new Intent(LearnThreadActivity.this, MyIntentService.class).putExtra("action", "ACTION_1"));
            startService(new Intent(LearnThreadActivity.this, MyIntentService.class).putExtra("action", "ACTION_2"));
            startService(new Intent(LearnThreadActivity.this, MyIntentService.class).putExtra("action", "ACTION_3"));
            startService(new Intent(LearnThreadActivity.this, MyIntentService.class).putExtra("action", "ACTION_4"));
        }
    }
});
public class MyIntentService extends IntentService {
    private final String TAG = "MyIntentService";


    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getStringExtra("action");
        Log.i(TAG, "task:" + action + "   Thread:" + Thread.currentThread());
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.i(TAG, "task:" + action + "   handled");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "service destory");
    }
}

执行结果

10-22 09:51:10.540 15243-15470/com.fred I/MyIntentService: task:ACTION_1   Thread:Thread[IntentService[MyIntentService],5,main]
10-22 09:51:11.542 15243-15470/com.fred I/MyIntentService: task:ACTION_1   handled
10-22 09:51:11.544 15243-15470/com.fred I/MyIntentService: task:ACTION_2   Thread:Thread[IntentService[MyIntentService],5,main]
10-22 09:51:12.544 15243-15470/com.fred I/MyIntentService: task:ACTION_2   handled
10-22 09:51:12.545 15243-15470/com.fred I/MyIntentService: task:ACTION_3   Thread:Thread[IntentService[MyIntentService],5,main]
10-22 09:51:13.545 15243-15470/com.fred I/MyIntentService: task:ACTION_3   handled
10-22 09:51:13.546 15243-15470/com.fred I/MyIntentService: task:ACTION_4   Thread:Thread[IntentService[MyIntentService],5,main]
10-22 09:51:14.546 15243-15470/com.fred I/MyIntentService: task:ACTION_4   handled
10-22 09:51:14.546 15243-15243/com.fred I/MyIntentService: service destory

由此可以看出IntentService中的任务是依次排队执行。

常见的问题

子线程不能操作UI ?

因为ViewRootImpl里面有checkThread的操作, 如果相应的UI操作中的在checkThread之前就完成,是可以操作UI的,比如在activity的onCreate方法里面去操作UI。

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

为什么looper.loop()不会造成主线程卡死?

  • 主线程中调用了looper.loop()方法,该方法中有一个死循环,这样才能保证主线程会一直执行下去,不会被退出。
  • 在没有消息的时候,会阻塞在loop的queue.next()中的nativePollOnce()方法里, 线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生。所以死循环也不会特别消耗CPU资源。
  • 真正会卡死的操作是在某个消息处理的时候操作时间过长,导致掉帧、ANR,而不是loop方法本身。

参考