Handler, Message, MessageQueue 和 Looper

原文: Medium.com

Handler 是 Android 线程间传递消息的入口。consumer 和 producer 线程都通过与 Handler 的交互执行以下操作:

  • 从 MessageQueue 中创建( creating ), 插入( inserting ), 移除( removing ) Message.
  • 处理 consumer 线程上的 Message.
Handler
  + handleMessage
  + send*
  + post*
  + remove*
  - mLooper
  - mMessageQueue

每个 Handler 都与一个 Looper 和 MessageQueue 相关联,有两种方式创建 Handler:

  • 通过默认的构造方法,使用与当前线程相关联的 Looper
  • 通过明确指定使用哪个 Looper

没有 Looper 的话 Handler 无法工作,因为它不能将 message 放入到 MessageQueue 中,同样也无法接收处理 Message.

public Handler(Callback callback, boolean async) {
  ...
  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;
}

上面的代码简单地演示了创建一个新的 Handler 的逻辑。 Handler 检查当前线程是否有有效的 Looper,如果没有,抛出异常;如果有的话, Handler 接收 Looper 保存的 MessageQueue 的引用。

Note:与多个 Handler 相关联的同一 Thread 共用相同的 MessageQueue,因为它们共用相同的 Looper。

Callback 是可选参数。如果提供,它将会处理 Looper 调度的 messages.

Message

Message 可以作为任意数据的的容器。Handler 在 producer 线程发送 Message,然后被加入到 MessageQueue 队列。Message 提供额外的三个信息,可以被 Handler 和 MessageQueue 用来处理 message:

  • what —— Handler 可以用这个标识符来区分不同的 Message.
  • time —— 通知 MessageQueue 何时去处理 Message.
  • target —— 表示使用哪个 Handler 去处理 Message.
Message
+ what
+ target handler [Handler]
+ callback [Runnable]
+ data [Bundle]
+ obj / arg1 / arg2
+ time (default, at_front, delay, uptime)

Message 的创建通常使用如下 Handler 的方法:

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)

Message 是从 Message Pool 中获取,Handler 提供的 obtain 方法参数填充了 Message 的值域,同时将 Message 的 target 设置为自身。我们可以这样写方法链:

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

Message pool 是一个 Message 对象的链表,最大数量为 50。当 Handler 处理完 Message 之后,MessageQueue 将 Message 对象返回到 pool 中并重置所有值域。

当通过 post(Runnable r) 方法向 Handler 传递一个 Runnable 对象之后,Handler 立即构建一个新的 Message 对象。同样设置 callback 值域来保存 Runnable。

Message m = Message.obtain();
m.callback = r;
producer-thread-send-msg-to-handler.PNG

Producer 线程创建一条 Message 并将它发送至 Handler. Handler 然后将 Message 加入到 MessageQueue. Handler 将在某个时间在 consumer 线程上处理 Message。

Message Queue

MessageQueue 是一个 Message 对象的链表。它按时间顺序插入 Message 对象,数值最低的时间戳将被最先调度( Lowest timestamp dispatch first )。

message-queue.png

MessageQueue 还维护着一个与系统当前时间( SystemClock.uptimeMillis )相关的 dispatch barrier。当 Message 的时间戳低于这个数值,这个 Message 将被 Handler 调度和处理。

Handler 提供三种发送 Message 的方法:

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

发送有延迟时间( delayMillis )的 Message 表示该 Message 的 time 值域被设置为 SystemClock.uptimeMillis() + delayMillis。然而 MessageQueue 中位于链表前段的 Message time 值域会被设置为 0,并在下一个 Message 循环中处理。所以使用这种方法时需要注意,它可能会 starve(饿死) MessageQueue 或者有其它的副作用。

Handler 一般与 UI 组件相关联,通常是 Activity 组件。Handler 中的 Activity 引用可能会有潜在泄露。考虑下面的场景:

public class MyActivity 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 mProgressBar;
  private ImageView mImageView;
  private Handler mHandler;

  class ImageFetcher implements Runnable {
    final String imageUrl;

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

    @Override public void run() {
      mHandler.obtainMessage(MSG_SHOW_PROGRESS).sendToTarget();
      InputStream is = null;

      try {
        URL url = new URL(imageUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        connection.setRequestMethod("GET");
        connection.setDoInput(true);
        connection.connect();

        is = connection.getInputStream();

        final Bitmap bitmap = BitmapFactory.decodeStream(is);
        mHandler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget();
      } catch (IOException e) {
        e.printStackTrace();
      } finally {
        try {
          assert is != null;
          is.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

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

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

    mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
    mImageView = (ImageView) findViewById(R.id.image);

    mHandler = new UIHandler();

    Thread thread = new Thread(new ImageFetcher(IMAGE_URL));
    thread.start();
  }
}

上例中 Activity 开启一个新的工作线程下载 Image 并展示在 ImageView 中。工作线程通过 UIHandler 和 UI 交互,保留对 Views 的引用并更新其状态。

假设在较慢的网络环境下,工作线程需要花费较长时间下载图片。在工作线程完成任务之前销毁 Activity 导致 Activity 泄露。在本例中有两个强引用,一是工作线程和 UIHandler 之间,另一个是 UIHandler 和 Views 之间。这些阻止 GC 收回 Activity 的引用。再来看另外一个例子:

public class MyActivity extends AppCompatActivity {

  private static final String TAG = "Ping"; 
  
  private Handler mHandler;
  
  class PingHandler extends Handler {
    @Override public void handleMessage(Message msg) {
      Log.d(TAG, "Ping message received!");
    }
  }
  
  @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    mHandler = new PingHandler();
    
    final Message msg = mHandler.obtainMessage();
    mHandler.sendEmptyMessageDelayed(0, TimeUnit.MINUTES.toMillis(1));
  }
}

这个例子中,依次发生了以下事件:

  • 一个 PingHandler 被创建
  • Activity 的 Handler 发送了一个延时的 Message,被加入 MessageQueue 中。
  • Activity 在 Message 发生调度前销毁
  • PingHandler 收到 Message 并打印出信息。

虽然没有下载网络图片的例子明显,但这里 Activity 依然发生了泄漏。
在 Activity 销毁之后,Handler 的引用需要被回收。然而,当创建一个 Message 对象之后,它持有一个 Handler 的引用:

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

上面的源码片段表明,所有的 Message 传递到 Handler 后,最终会调用 enqueueMessage(...) 方法。注意 Handler 的引用显式地赋给了 msg.target,这告知 Looper 从 MessageQueue 收回 Message 时,哪个 Handler 负责处理这个 Message。

这条 Message 被添加至 MessageQueue,它持有 Handler 的引用。MessageQueue 同样有一个与之相关联的 Looper。一个普通的 Looper 生命周期会持续到它的 terminated,而 Main Looper 的生命周期将会和 app 一样长。 Handler 的引用将会持续到 MessageQueue 回收这条 Message 之前。一旦被回收,它的值域,包括 target 引用,都被清除。

虽然 Handler 有一条 long living 引用,但并不是很清晰 Activity 是否发生泄漏。为了检查泄露问题,我们还需要确定 Handler 是否持有 Activity 的引用。在这个例子中,是有这种问题的。通常一个非静态内部类成员都会隐式的保留外部类的引用。这个例子中,内部类 PingHandler 是非静态类,因此它隐式地持有 Activity 的引用。

使用 WeakReference 和 static 类标识符相结合可以阻止 Handler 泄露 Activity。当 Activity 被销毁后,WeakReference 允许 GC 回收要保留的对象,static 类标识符可以防止内部类 Handler 隐式持有外部类 Activity 的引用。
修改后的 UIHandler 类:

static class UIHandler extends Handler {

    private final WeakReference<Activity> mActivityRef;

    public UIHandler(Activity activity) {
      mActivityRef = new WeakReference<>(activity);
    }

    @Override public void handleMessage(Message msg) {
      final Activity activity = mActivityRef.get();

      if (activity == null) {
        return;
      }

      switch (msg.what) {
        case MSG_SHOW_LOADER:
          activity.mProgressBar.setVisibility(View.VISIBLE);
          break;
        case MSG_HIDE_LOADER:
          activity.mProgressBar.setVisibility(View.GONE);
          break;
        case MSG_SHOW_IMAGE:
          activity.mProgressBar.setVisibility(View.GONE);
          activity.mImageView.setImageBitmap((Bitmap) msg.obj);
          break;
      }
    }
  }

现在 UIHandler 构造方法接收 Activity,被包裹在 WeakReference 中。这允许垃圾回收器在 activity 被销毁时回收 activity 的引用,为了与 Activity 的 UI 组件进行交互,我们需要来自 mActivityRef.get() 的强引用 Activity。由于使用的是 WeakReference,在访问 Activity 时必须小心谨慎。如果获取
Activity 引用的唯一路径是通过 WeakReference,那么它可能已经被回收。我们需要多一些检查步骤。

static class UIHandler extends Handler {

    private final WeakReference<Activity> mWeakRef;

    public UIHandler(Activity activity) {
      mWeakRef = new WeakReference<>(activity);
    }

    @Override public void handleMessage(Message msg) {
      final Activity activity = mWeakRef.get();

      if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
        removeCallbacksAndMessages(null);
        return;
      }

      switch (msg.what) {
        case MSG_SHOW_LOADER:
          activity.mProgressBar.setVisibility(View.VISIBLE);
          break;
        case MSG_HIDE_LOADER:
          activity.mProgressBar.setVisibility(View.GONE);
          break;
        case MSG_SHOW_IMAGE:
          activity.mProgressBar.setVisibility(View.GONE);
          activity.mImageView.setImageBitmap((Bitmap) msg.obj);
          break;
      }
    }
  }

我们可以概括 MessageQueue, Handler, Producer 线程之间的交互:


Interaction between MessageQueue, Handlers, and Producer Threads.png

上图中多条 producer 线程发送 Messages 到不同的 Handler。然而每条 Handler 公用相同的 Looper,因此所有的 Message 被送往相同的 MessageQueue 中。这很重要,因为 Android 创建了多条与 Main Looper 相关的 Handler:

  • Choreographer :处理 vsyncframe 的更新。
  • ViewRoot:处理输入和窗口事件,系统配置更新等。
  • InputMethodManager:处理软键盘触摸事件。
  • 其他部分。

Tip:确保 producer 线程不会产生多条 Messages,否则它们会 starve 系统生产的 Messages.

Looper

Looper 从 MessageQueue 读取 Message 并执行调度到目标 Handler。一旦 Message 被传递到 dispatch barrier,Looper 可以在下一个消息循环中读取它。当没有 Message 被调度时,Looper 发生阻塞。在 Message 调度时恢复。

只有一个 Looper 可以与线程相关联,将另一个 Looper 附加到一个线程会导致 RuntimeException。Looper 类中的静态 ThreadLocal 对象确保只有一个 Looper 附加到线程上。

调用 Looper.quit 会立即终止 Looper,同时它会忽略 所有
MessageQueue 中传递到 dispatch barrier 的 Message。调用 Looper.quitSafely 会确保所有被调度( dispatch ) 的 Message 得到执行。

Overall flow of Handler interacting with MessageQueue and Looper.png

Looper 会在 Thread 的 run() 方法中被设置。调用静态方法 Looper.prapare() 检查 Thread 上是否有预先设定的 Looper,Looper 内部会通过静态的 ThreadLocal 对象检查是否 Looper 对象已存在。

注意:public 的 Looper.prepare 内部调用 prepare(true)方法。

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

此时 Handler 可以接收 Messages 并将它们添加至 MessageQueue。执行 Looper.loop() 将开始读取 Message 并出列。每次 loop 循环取出下一个 Message,调度至对应的 Handler,然后将其回收至 Message pool。 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 类并管理创建 Looper。下面的 snippet 描述了一个典型的使用场景:

private final Handler mHandler;
private final HandlerThread mHandlerThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate();
  mHandlerThread = new HandlerThread("HandlerThreadDemo");
  mHandlerThread.start();
  mHandler = new UIHandler(mHandlerThread.getLooper());
}
@Override
protected void onDestroy() {
  super.onDestroy();
  mHandlerThread.quit();
}

onCreate(...) 方法中构造了新的 HandlerThread 对象。当 HandlerThread.start() 时,它的内部会创建一个有 Looper 依附的线,然后 Looper 开始在 HandlerThread 上处理 MessageQueue 的消息。

Note:在Activity 被销毁时,有必要终止 HandlerThread,从而终止 Looper.

总结

Android 的 Handler 在 App 的生命周期中起着不可或缺的作用,它为 Half-Sync/Half-Async 架构模型奠定了基础。各种内部和外部资源依赖 Handler 进行异步事件调度,因为它使开销最小化并维护线程的安全性。

更加深入的了解组件的工作原理能帮助我们解决一些难题,它还允许我们能最佳地使用组件 API。我们通常使用 Handler 作为工作线程与 UI 线程通信的机制,事实不仅于此。Handler 还出现在 IntentServiceCamera2 API中。在这些 API 中,Handler 更广泛地被用于任意线程间的通信。

我们可以通过更深入地了解 Handler 来构建更高效,简单和强大的 App。

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

推荐阅读更多精彩内容