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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

明白了吧 所以说 得先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呢,因为后者是受到系统时间修改影响的。
继续看代码

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

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

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

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

}


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

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

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

在looper 中 ThreadLocal 是一个静态的

   static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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

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

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

Thread -> ThreadLocalMap-> (ThreadLocal,Looper)

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

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

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

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

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

Leave a Reply

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