Handler,Looper,Message是如何运作的

原文地址

Android Handler Internals

为了一个Android应用的响应能力,我们一般需要尽量避免操作阻塞UI线程,当把一些阻塞操作或大规模运算转移到其他工作线程中时,整个应用的响应能力就自然的提升了,但是我们经常需要去更新UI组件,而这些操作必须在主线程才能进行,所以线程间的通信机制十分的重要,为了解决这个问题,Android提供了一个消息通知机制,这就是Handler。Handler提供了一种非阻塞的机制,可以保证生产者和消费者线程在消息通信时不至于阻塞。

Handler

Handler 是线程间进行消息传递的直接接口。生产者和消费者线程通过以下几个操作来使用Handler进行交互。

  • 在消息队列中创建,发送,移除消息
  • 在消费者线程中处理消息
The android.os.Handler component

每一个Handler都关联一个Looper和一个Message Queue。有两种方式创建一个Handler:

  • 通过默认的构造函数,关联当前所在的线程的Looper
  • 明确指定使用的Looper

一个Handler如果没有一个Looper那么就无法运行,因为不能put消息到消息队列中,同样也就不能去接受消息进行处理。

public Handler(Callback callback, boolean async) {
    // code removed for simplicity
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException( “Can’t create handler inside thread that has not called Looper.prepare()”);
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

上面的这个片段展示了new一个Handler时所做的逻辑,首先这个Handler会检测当前线程是否有一个有效的Looper对象,如果没有,那么就抛一个异常。然后这个Handler接收一个Looper中的消息队列的引用。
**提示: ** 许多个Handlers关联到同一个线程时共享同一个消息队列,因为它们共享同一个Looper。
这个Callback是一个可选的参数,如果提供了,它会通过Looper产生一个消息的分发。

Message

这个 Message 表示一个任意数据的容器,这个生产者线程发送Messages到handler上,这样会将数据插入到Message Queue中。这个Message提供了三种额外的信息,这些信息用来让 Handler 和 Message Queue 处理这个消息。

  • what—这个参数是一个标识,用来让Handler区分不同的消息,进而差异处理
  • time—通知消息队列什么时候去处理这个消息
  • target—表明哪一个Handler应该处理这个消息
The android.os.Message component

消息的创建通常采取下面的其中一种方式:

public final Message obtainMessage()
public final Message obtainMessage(int what)
public final Message obtainMessage(int what, Object obj)
public final Message obtainMessage(int what, int arg1, int arg2)
public final Message obtainMessage(int what, int arg1, int arg2, Object obj)

这个消息是从一个消息池中取出来的,这些提供的参数设置了这个消息的属性,同时Handler也可以直接设置给这个消息的target,可以允许我们链式的进行调用就像这样

mHandler.obtainMessage(MSG_SHOW_IMAGE, mBitmap).sendToTarget();

这个消息池是一个消息对象组成的一个LinkedList,池子的对象最大数是50,当Handler处理了这个消息之后,这个 Message Queue 将这个对象重新放到池子中,并且reset所有的field,这样能得到节约内存的效果。
当发送一个Runnable给Handler通过 post(Runnable r) ,这时这个Handler实际上隐式的构造了一个新的消息,通过callback 属性来持有这个 Runnable 对象。

Message m = Message.obtain();
m.callback = r;
Interaction of Producer Thread sending message to a Handler

从这里我们能能看到一个生产者线程和一个Handler的交互,这个生产者创建了一个消息,并且将它发送给了一个Handler,然后这个Handler将这个消息入队到一个消息队列中,这样在消费者线程中Handler从拿到这个消息并且处理,这样就实现了通信。

Message Queue

这个 Message Queue 是一个不限制长度的消息对象的LinkedList 。它按照时间顺序进行插入消息,时间最早的最先被分发。

The android.os.MessageQueue component

MessageQueue 会根据当前的SystemClock.uptimeMillis 来进行一个时间上的调度,消息的时间戳小鱼这个值时,这个消息就会被分发,进而被Handler处理。

public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean sendMessageAtFrontOfQueue(Message msg)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)

发送一个带有延时的消息会将这个消息的 time 字段设置为 SystemClock.uptimeMillis() + delayMillis
Sending a message with a delay sets the Message’s time field as SystemClock.uptimeMillis() + delayMillis.如果将消息发送到队列的前端,time属性会被设置为0,在下一次loop时,消息就会被处理。使用这个方法需要注意可能造成一些排序的问题。
Handler一般适合UI组件绑定在一起,并且引用一个Activity对象,这个引用可能会潜在的导致内存泄露,比如以下这个场景:

public class MainActivity extends AppCompatActivity {
    private static final String IMAGE_URL = "https://www.android.com/static/img/android.png";

    private static final int MSG_SHOW_PROGRESS = 1;
    private static final int MSG_SHOW_IMAGE = 2;

    private ProgressBar progressIndicator;
    private ImageView imageView;
    private Handler handler;

    class ImageFetcher implements Runnable {
        final String imageUrl;

        ImageFetcher(String imageUrl) {
            this.imageUrl = imageUrl;
        }

        @Override
        public void run() {
            handler.obtainMessage(MSG_SHOW_PROGRESS).sendToTarget();
            InputStream is = null;
            try {
                // Download image over the network
                URL url = new URL(imageUrl);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();

                conn.setRequestMethod("GET");
                conn.setDoInput(true);
                conn.connect();
                is = conn.getInputStream();

                // Decode the byte payload into a bitmap
                final Bitmap bitmap = BitmapFactory.decodeStream(is);
                handler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget();
            } catch (IOException ignore) {
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException ignore) {
                    }
                }
            }
        }
    }

    class UIHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SHOW_PROGRESS: {
                    imageView.setVisibility(View.GONE);
                    progressIndicator.setVisibility(View.VISIBLE);
                    break;
                }
                case MSG_SHOW_IMAGE: {
                    progressIndicator.setVisibility(View.GONE);
                    imageView.setVisibility(View.VISIBLE);
                    imageView.setImageBitmap((Bitmap) msg.obj);
                    break;
                }
            }
        }
    }

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

        progressIndicator = (ProgressBar) findViewById(R.id.progress);
        imageView = (ImageView) findViewById(R.id.image);

        handler = new UIHandler();

        final Thread workerThread = new Thread(new ImageFetcher(IMAGE_URL));
        workerThread.start();
    }
}

这个例子中,这个Activity开启了一个新的工作线程来下载图片,在工作线程完成之前销毁这个Activity就会导致内存泄漏。这个代码里有两个强引用,一个是工作线程和UIHandler,另一个是UIHandler和views。这个引用会导致在GC时不能正常的回收内存。
现在我们在看一下另一个例子:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Ping";

    private Handler handler;

    class PingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.d(TAG, "Ping message received");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler = new PingHandler();
        
        final Message msg = handler.obtainMessage();
        handler.sendEmptyMessageDelayed(0, TimeUnit.MINUTES.toMillis(1));
    }
}

事件发生的顺序是这样的:

  • 一个PingHandler被创建
  • 这个Activity发送了一个延时消息给Handler,这个消息被插入到消息队列中。
  • 这个Activity在消息分发之前被销毁
  • 这个消息被分发并且被PingHandler处理,打印一个log

虽然这并不是瞬间显示,但同样的这个Activity发生了泄露,当Activity被销毁后,这个Handler引用需要对GC可见,但是,这里依然保留了一个Handler的引用。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

这个片段表明所有的消息发送到Handler最后都会调用 enqueueMessage 方法,这个Handler的引用显示地指定给了 msg.target ,这也告诉了Looper当从 MessageQueue 中拿到消息的时候,应该哪个Handler处理这个消息,这个消息被加入到了MessageQueue,这样MessageQueue也会持有一个这个Message的引用,除此之外,MessageQueue 也和一个Looper关联在一起,一个显式的Looper一直存在直到被终止,而主的Looper的生命周期和整个应用一样长,这个Handler的引用一直存在只要这个Message不被MessageQueue回收,一旦被回收,这个消息所有的属性包括 target 引用都会被清除。
在这个例子中,存在一个非静态成员类隐式的引用外面的类,准确的说,就是这个PingHandler 并不是被定义成一个静态类,所以存在一个Activity的隐式引用
使用一个WeakReference 或者是静态类可以避免这个Handler的泄露,当Activity被销毁时,这个WeakReference 会使得GC回收这个对象,同样的静态的内部类也能防止一个隐式的引用到外面的类中。
所以这里我们修改一下例子中的UIHandler :

static class UIHandler extends Handler {
    private final WeakReference<ImageFetcherActivity> mActivityRef;
    
    UIHandler(ImageFetcherActivity activity) {
        mActivityRef = new WeakReference(activity);
    }
    
    @Override
    public void handleMessage(Message msg) {
        final ImageFetcherActivity activity = mActivityRef.get();
        if (activity == null) {
            return
        }
        
        switch (msg.what) {
            case MSG_SHOW_LOADER: {
                activity.progressIndicator.setVisibility(View.VISIBLE);
                break;
            }
            case MSG_HIDE_LOADER: {
                activity.progressIndicator.setVisibility(View.GONE);
                break;
            }
            case MSG_SHOW_IMAGE: {
                activity.progressIndicator.setVisibility(View.GONE);
                activity.imageView.setImageBitmap((Bitmap) msg.obj);
                break;
            }
        }
    }
}

现在这个UIHandler 的构造函数中持有这个被包装在WeakReference中的Activity,这样在Activity销毁的时候回收这个引用。当和Activity中的UI组件交互时,我们需要一个activity的强引用,既然这里使用了弱引用,那么就要注意访问Activity,因为极有可能被GC回收掉,所以需要先检查一下是否这个activity引用还存在,如果已经被回收,那么这个消息就需要被忽略。
除了引用被回收的情况,还有一种是activity已经被销毁了,但是引用依然存在,这种情况也有可能导致应用crash,为了解决这个问题,我们还需要检测这个activity当前的状态,所以上面的逻辑还需要修改一下:

static class UIHandler extends Handler {
    private final WeakReference<ImageFetcherActivity> mActivityRef;
    
    UIHandler(ImageFetcherActivity activity) {
        mActivityRef = new WeakReference(activity);
    }
    
    @Override
    public void handleMessage(Message msg) {
        final ImageFetcherActivity activity = mActivityRef.get();
        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
            removeCallbacksAndMessages(null);
            return
        }
        
        switch (msg.what) {
            case MSG_SHOW_LOADER: {
                activity.progressIndicator.setVisibility(View.VISIBLE);
                break;
            }
            case MSG_HIDE_LOADER: {
                activity.progressIndicator.setVisibility(View.GONE);
                break;
            }
            case MSG_SHOW_IMAGE: {
                activity.progressIndicator.setVisibility(View.GONE);
                activity.imageView.setImageBitmap((Bitmap) msg.obj);
                break;
            }
        }
    }
}

现在我们可以概括一下MessageQueue,Handler,和生产者线程之间的关系

Interaction between MessageQueue, Handlers, and Producer Threads

上面这张图中,多个线程发送消息给不同的Handlers,但是每个Handler都关联着同一个Looper,所以所有的消息都被发送到同一个MessageQueue中,这十分重要,因为Android自身就是创建了许多的Handlers 绑定在主Looper上:

  • The Choreographer: 发送垂直同步和帧更新的消息
  • The ViewRoot:发送输入和窗口事件,配置更改等
  • The InputMethodManager:发送键盘触摸指令时间
  • 一些其他的消息

Debugging Tips:
你可以 debug/dump所有的消息分发通过给一个Looper上加一个LogPrinter

final Looper looper = getMainLooper();
looper.setMessageLogging(new LogPrinter(Log.DEBUG, "Looper"));

同样地,你也可以 debug/dump 所有在MessageQueue 中的消息,通过在Handler上加一个LogPrinter

handler.dump(new LogPrinter(Log.*DEBUG*, "Handler"), "");

Looper

Looper 从Message Queue中拿消息,并且分发给对应target的Handler,一旦一个消息通过了分发的栅栏,那么它就能被Looper在下一次loop中拿到,当没有符合条件的消息的时候,Looper会阻塞住,当存在消息有效时,Looper又被被唤醒。
一个Looper只能绑定一个线程,将别的Looper强行绑定在一起会造成异常,Looper中的静态的ThreadLocal 的对象保证了一个Looper只能attach到一个线程上。
调用 Looper.quit 会立刻终止,并且会丢弃掉Message Queue中已经通过栅栏的所有消息。调用 Looper.quitSafely 会在丢弃延迟消息之前让所有已经准备分发的消息被处理。

Overall flow of Handler interacting with MessageQueue and Looper

Looper会在线程的run方法中设置,其中有一个静态方法 Looper.prepare() 会检查是否已经存在一个Looper对象关联这个线程,这里实际上是通过Looper内部的 ThreadLocal 对象来检测是否已经存在,如果发现没有,那么就自然的创建一个Looper对象,以及一个MessageQueue 。

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));
}

这里默认传进来一个true,有了Looper之后,现在Handler就能接收消息,并且添加到消息队列中,执行一个静态方法 Looper.loop () 会开始从队列中取消息。每一次 loop 循环会获取下一个消息,然后分发给target的Handler,最后将消息回收到池子中。Looper.loop 会一直循环直到这个Looper被结束。

public static void loop() {
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();
    }
}

在日常使用中,我们并不需要创建一个线程并绑定一个Looper在上面,Android提供了一个方便的类 --- HandlerThread,这个类继承于Thread,并且内部有一个Loope。先看一个例子

private final Handler handler;
private final HandlerThread handlerThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate();
    handlerThread = new HandlerThread("HandlerDemo");
    handlerThread.start();
    handler = new CustomHandler(handlerThread.getLooper());
}
@Override
protected void onDestroy() {
    super.onDestroy();
    handlerThread.quit();
}

这个 onCreate() 方法构造了一个新的HandlerThread,当这个HandlerThread 开始的时候,它调用了 Looper的perpare方法,将一个Looper对象关联起来,然后整套流程就开始正常的运作。
在activity 被销毁的时候,我们需要去关掉这个HandlerThread,这也会让内部的Looper结束。

后记

在Android的整个生命周期中,Handler无疑是一个重要角色。它创造了一种半同步半异步的一种模式,减少了线程间的通信消耗,保证了主线程的稳定性。深入的理解整个Handler及其组件是如何工作的可以极大的方便我们解决困难的问题。我们一般用Handler只是用来更新UI,但是实际上Handler在其他地方发挥着巨大的作用,比如 IntentServiceCamera2等API,在这些API中,更着眼于多线程的操作。

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容