Handler源码分析 - Java层

Handler最常见的使用场景就是下载回调,为了不影响用户体验Android不支持在主线程中进行耗时时操作,长时间的耗时操作会产生ANR异常,而下载无疑是耗时操作,所以我们会在子线程中进行下载。但,下载完毕进行UI操作却会发生异常,原来谷歌为了不让UI的操作出现冲突(线程的不可确定性),所以规定只能在主线程中进行UI操作,可这就尴尬了...即不让在主线程中进行联网操作,又不让在子线程中进行UI操作,我们如何将告诉主线程我们已经下载完毕了呢?这时就要用到Handler了.

Handler的简单使用

test

下面是最简单的使用.

// 1.创建Handler对象, 重写方法.
mTestHandler = new TestHandler(this);

-----------------------------点击模拟下载---------------------------------
public void download(View view) {
text.setText("开始下载...");
new Thread(){
  @Override
  public void run() {
    // 2.创建消息对象
    Message msg = mTestHandler.obtainMessage();
    msg.obj = "下载完毕";
    msg.arg1 = 1;
    // 3.发送延时消息
    mTestHandler.sendMessageDelayed(msg, 2000);
    }
  }.start();
}
---------------------------创建一个类继承Handler--------------------------------
// * 用软引用的方法持有Activity对象防止内存泄露
private WeakReference<MainActivity> activity ;
public TestHandler(MainActivity activity){
  this.activity = new WeakReference<>(activity);
}

// 回调方法.
@Override
public void handleMessage(Message msg) {
  switch (msg.arg1){
    case 1:
      activity.get().text.setText((String) msg.obj);
    break;
  }
}

继承Handler对象并重写handleMessage(),然后创建Handler对象,调用obtainMessagee()方法获取Message对象,将数据赋予Message,并发送出去,而发送的消息会回调给HandlerhandleMessage()方法.

在子线程中创建Handler对象.

刚才我们在主线程中创建了Handler对象,在子线程中调用HandlersendMessageDelayed()方法将Message带到主线程间完成了线程中的通信,那我们能在子线程中创建Handler吗 ? 答案是不可以。在子线程中创建Handler对象会抛出如下异常:

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

异常说的很明白它需要调用Looper.prepare()。我们来看看源码,为什么在主线和中可以创建Handler对象呢?点击new Handler(),在两个参数的构造方法中我们发现了刚才抛出异常的代码.

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;

可以看到异常抛出的原因是因为mLooper这个对象为null,我们来看看myLooper里面做了什么?
-----------可我们并没有在主线程中调用这个方法啊?那是因为该方法在ActivityThread类里调用了,我们这里暂时不看它. 继续查看Looper.myLooper()里面的内容.

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

myLooper()实现调用了ThreadLocal身上的get()方法,它返回一个Looper,既然有get()必然在一个地方set()了,于是在Looper中发现以下代码

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

ThreadLocal

我们把Looper对象存放在了ThreadLocal中,当需要Looper时也是从ThreadLocal中取的,为什么要将Looper存放在ThreadLocal中呢?下面是它的get()方法.

public T get() {
  Thread t = Thread.currentThread();
  ThreadLocalMap map = getMap(t);
  if (map != null) {
  ThreadLocalMap.Entry e = map.getEntry(this);
  if (e != null)
    return (T)e.value;
  }
  return setInitialValue();
}

ThreadLocal内部有一个Map集合,根据它所在的线程名取Looper,而ThreadLocal是在创建Looper时创建的所以LooperThreadLocal是同一线程的,而我们创建handler时会调用myLooper()方法,该方法调用get()方法时会根据handler所有线程名取Looper对象,所以我们要保证Looper.prepare()Handler在同一线程中.

现在让我们来理一下思路,首先创建Handler需要Looper,而Looper是在prepare()方法中创建的,也就是说如果我们想在子线程中创建Handler只需要在之前调用

test

Looper.prepare()即可,下面是我们测试的代码.

public void run() {
   Looper.prepare();
   mTestHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
      Log.d("MainActivity", Thread.currentThread().getName());
    }
  };
  mTestHandler.sendMessage(mTestHandler.obtainMessage());
}

MesageQuene

可是问题又来了,虽然没有抛出异常,handleMessage()方法却一直接收不到消息.既然是收不到消息,那我们来看看Handler内部是怎么发送消息的吧,我们继续查看sendMessage()方法.

// 这里我们看到了前面的发送延时消息方法,延时+当时时间 并调用下一方法
sendMessage(msg) -> sendMessageDelayed(msg, 0)
    -> sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)

----------------------------------------------------------------------
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
  MessageQueue queue = mQueue;
  if (queue == null) {
     ...
    return false;
  }
  return enqueueMessage(queue, msg, uptimeMillis);
}

----------------------------------------------------------------------
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
  msg.target = this; // this = handler; 把消息与handler进行绑定.
  if (mAsynchronous) {
    msg.setAsynchronous(true);
  }
  return queue.enqueueMessage(msg, uptimeMillis);
}

最后调用了MesageQueneenqueueMessage()方法,即入队列,那MesageQuene对象是在什么时候创建的呢?是在创建Looper对象时创建的。

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

继续来看MesageQueneenqueueMessage()方法.

...
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;
...

enqueueMessage()就是入队列的意思,Message 是单链表结构,它持有下一个Message 对象的引用,因为MesageQuene中可能存有多个未处理的消息,所以需要判断MesageQuene中有多少消息,若有多个则将当前消息放置在最后,可以看成集合的add方法.

Looper

sendMessage()方法只是将消息加入消息队列中,那消息是如何取出并发给handler的呢?我们来看下Looper.loop()

...
for (;;) {
  Message msg = queue.next(); // might block
...
  try {
    msg.target.dispatchMessage(msg);
  } finally {
    if (traceTag != 0) {
    Trace.traceEnd(traceTag);
  }
}
...

这里的target就是handler(在handler的enqueueMessage()方法里,我们将消息与handler进行了绑定),所以调用了handler身上的dispatchMessage()方法,而该方法最终又调用了handleMessage()方法,并将消息传递进去.

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

上面就是handler发送消息的大致流程最后我们总结一下.
总结:创建Handler对象时会调用Looper.prepare()方法,该方法会用从ThreadLocal对象中取出looper, 所以如果ThreadLocal中没有先存Looper或不在同一线程中则取不到对象,Handler就会抛出异常.

  • 问题1.:主线程在哪里调用了prepare()方法的?见底部代码.

用handler发送消息方法时,会调用到MesageQuene的方法, 而mQuene是在创建Looper对象时创建的, Looper对象是在调用prepare()方法时创建的, 也就是说mQuene与Looper是在同一线程.

我们发送消息时会将所发送的消息加入消息队列,而后调用Looper.loop()方法才能将消息取出并传送给handler,如果不调用Looper.loop()则消息无法取出

  • 问题2:为什么在主线中创建可以接收消息?见底部代码.

最后在子线程中创建handler那handleMessage是在哪个线程回调的呢?
这个问题在上面就已经说了,handleMessage在loop()方法调用,而loop()与Looper是在同一线程,也就是说最终会在子线程回调.那如何让回调在主线程呢?调用prepareMainLooper()方法,而不是prepare()就可让回调在主线程中运行.
下面代码很好回答了第1和第2问题.

public static final void main(String[] args) {
....
// 1.主线程创建Looper 
Looper.prepareMainLooper();
if (sMainThreadHandler == null) {
  sMainThreadHandler = new Handler();
}
        
ActivityThread thread = new ActivityThread();
thread.attach(false);
    
if (false) {
  Looper.myLooper().setMessageLogging(new
  LogPrinter(Log.DEBUG, "ActivityThread"));
}
        
Looper.loop();
...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容