彻底弄清楚android 面试经典题:Handler 机制

向来觉得电话面试就觉得肯定黄,总有一种局促和不安,今天接到阿里的电话面,当时在家里。问的问题还是那几个老生常谈的问题。 view 的绘制啊, handler 机制啊,hashmap实现原理啊。效率啊,优化啊啥的。

今天可谓是都碰上了(其实还差那个asyntask 啊 http协议啊 多线程同步啊,jvm 啊,启动流程啊巴拉巴拉),这一关不过,下一关就没门啊,其实这个和项目经验或者一个人能不能做成事情并没有太大关系,最高的智商是把事做成,最高的情商是靠谱。

以前工作没那么多闲情看这个,那今天就带着面试的目的来彻底研究一下 面试第一题 Handler机制。

从应用开发这个角度来看,大家可能都知道 不能在子线程中直接new 一个hander。否则会抛出常见的

[code lang=text]
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
[/code]

然后大家也知道(上面明显提示),在子线程中假如要创建handler 可以先Looper.prepare()然后就可以创建了。

我们写一个常见的用handler 进行线程间通讯例子,子线程发送消息给主线程更新UI

[code lang=java]
mHandler = new Handler() {
@Override
public void dispatchMessage(Message msg) {
super.dispatchMessage(msg);
Log.d(TAG, msg.what + " I am in "+Thread.currentThread());
}
};

Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mHandler.sendEmptyMessage(100);

}
});
thread.start();

[/code]

我们以这个例子为入口 来看看走了哪些流程

首先我在主线程中创建了无参数的handler,我们进入handler构造函数 看看里面有什么

[code lang=java]
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;
}

[/code]

可以看到 Handler 的这个构造函数初始化了两个成员变量 mLooper 和mQueue。 我们发现第二个成员变量 mQueue直接从第一个成员变量中获取的,因此不关心它。只需知道 Handler 中有一个 Looper 的实例这很重要 加粗 ,这就引出了第二个主角 Looper ,那这个myLooper()是个啥玩意? 点进去看看。

[code lang=java]
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
[/code]

嗯看似很简单,是从Looper 类中一个静态变量中取出了一个实例,看名字是一个和什么线程相关的东西。既然一开始用到了get 就一定是在某个地方进行了set了你说不是吗,否则应该是空啊,我再找找。直到找到了这里

[code lang=java]
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));
}
[/code]

明白了吧 所以说 得先prepare 一下。把一个Looper 的实例传递给一个静态sThreadLocal 保存起来,那为什么主线程可以直接new Handler 呢?
其实 在ActivityThread.java 的main 方法中 调用了这个Looper.prepareMainLooper(); 间接再调用了prepare方法。因此我们不需要再主动调用Looper.prepare啦. 调用了反而会报错
Handler 的构造(new) 我们就分析到这里,再来看 实例 ,上面在我们自己的例子中我们用 mHandler.sendMessage了,
我们先来看看Message是个sen魔。
跟踪代码发现我们 sendEmptyMessage 事实上调用的是 sendEmptyMessageDelayed(what, 0)->sendMessageDelayed(msg(这个是自动构造的空内容msg), delayMillis) ->…>sendMessageAtTime(Message msg, long uptimeMillis) 这个是最终函数, uptimeMillis是开机到目前为止的是间。为什么不用SystemClock.currentMillis呢,因为后者是受到系统时间修改影响的。
继续看代码

[code lang=java]
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
[/code]

最终代码调用的是messageQueue 中的 eneueMessage 方法,改方法把传入的Msg 的when 进行链表结构排序(每一个msg都有一个next 引用) messageQueue 在Looper中也是存在的

我们来简化一下这几个类 如下,是不是很明了了?

[code lang=java]
public class Looper {
private static ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();

MessageQueue mQueue;

public static void prepare() {
sThreadLocal.set(new Looper());
}

public static void loop() {
Looper l = myLooper();
for (; ; ) {
Message msg = l.mQueue.next();//这里可能阻塞 看下面的MessageQueue
//这里满足某种条件可以退出循环
msg.target.dispatchMessage(msg);
}
}

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

private Looper() {
mQueue = new MessageQueue();
}
}

public class Handler {
private Looper mLooper;
private MessageQueue mQueue;

public Handler() {
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue;
}

public boolean sendMessage(Message msg) {
msg.target = this;
// SystemClock.uptimeMillis() 计算出一个时间 放入下面的msg中去 参数是when 这里忽略
return mQueue.enqueueMessage(msg);
}

protected void dispatchMessage(Message msg) {
}
}

public class Message {
Message next;
Handler target;
}
public class MessageQueue {
private Message messages;//持有最近一个message

boolean enqueueMessage(Message msg) {
//这里把msg 列队排序
if (messages == null) {
messages = msg;
} else {
//这里其实有一个for循环去比较when 。然后将msg 放入指定位置,这里假如msg 就是要最新执行的简单版本处理
msg.next = messages;
messages = msg;
}
return true;
}
Message next(){
//这里有一个native的阻塞过程,
// nativePollOnce(ptr, nextPollTimeoutMillis);就是 这个方法,
return messages;
}

}

[/code]

那我们再来看最初 的那个例子 如果在子线程字节 new Handler 会是什么个情况,我们可以知道myLooper返回的值一定是空的,所以不行,什么情况下不为空? 那就是调用了Looper.prepare之后,因此,在new Handler 的时候一定会需要prepare 的,那么在主线程中 是什么时候帮我们做了这个事情呢,那就是在activityThread.java 的main 方法里面 去帮我们prepare了MainLooper。

ThreadLocal 是有一个特性,那就是set 后只有在同一个线程总才能访问到,也就是你新开线程你没办法拿到主线程帮我们set的looper。因此我们在子线程中必须再次prepare一次

————-坑爹的分割线,近日tx 的面试官最后问了一个问题,ThreadLocal原理是什么??? WFC 又一次折戟沉沙———–
来 继续分析源码:

在looper 中 ThreadLocal 是一个静态的

[code lang=text]
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
[/code]

保存了一个Looper 实例,那为什么 在不同的线程中 sThreadLocal.get()不一样呢?
我先看set 方法:

[code lang=java]
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
[/code]

每一个线程都有一个 ThreadLocalMap对象,因此 即使是同一个 sThreadLocal 对象,在set 方法中,其实是取到了线程的 ThradLocalMap对象,每一个线程都是不一样的。而把当前ThreadLocal 对象作为Key 存在了ThreadLocalMap中,
包含关系如下:

Thread -> ThreadLocalMap-> (ThreadLocal,Looper)

ThreadLocalMap 又是一个什么东西呢?
查看代码其并没有继承自Map接口。 里面有一个table 数组为成员变量

[code lang=java]
private Entry[] table;

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
[/code]

Entry 是 继承自弱引用的。因此这个Key 可以随时被销毁,
上面代码意味着不像hashmap 那样是一个数组再加上一个链表去查询,那如何解决hash冲突呢?
看Set 方法

[code lang=java]
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

[/code]

可以看到,如果找到冲突的hash 而且key 不相等,就会往数组后移,找到一个Entry空key并替换。 如果找到Entry为空,则新增一个Entry到当前数组位置。

Leave a Reply

Your email address will not be published. Required fields are marked *