全面理解Handler第一步:理解消息队列,手写消息队列

前言

Handler机制这个话题,算是烂大街的内容。但是为什么偏偏重拿出来“炒一波冷饭”呢?因为自己发现这“冷饭”好像吃的不是很明白。最近在思考几个问题,发现以之前对Handler机制的了解是在过于浅显。什么问题?

  • Handler机制存在的意义是什么?能否用其他方式替换?
  • Looper.loop();是一个死循环,为什么没有阻塞主线程?用什么样的方式解决死循环的问题?

如果透彻的了解Handler,以及线程的知识。是肯定不会有这些疑问的,因为以上问题本身就存在问题。

就这俩个小问题,就发现自己在学习道路上的不扎实,所以这段时间重新理解了一下Handler。先预告一小下下,关于Handler的内容将是一个系列文章,今天这一篇内容重点在于Handler的理解,以及对消息队列的思考。

正文

1、Handler机制为了什么?

我们都知道,在Android开发中,无法在子线程中更新UI。

我们先思考一个问题?为什么不能在子线程更新UI。如果看过View绘制的源码,我们都知道不能在子线程更新UI的原因是:ViewRootImpl中有这么一个方法:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views.");
    }
}

很明显这是人为限制的一个操作。那我们在思考,为什么谷歌开发Android系统时要这么限制?

其实不难推测出来。对于线程来说,我们都知道线程与线程之间是内存共享的。所以如果某一时刻多个子线程同时去更新UI,那么对于绘制UI来说便成为了一个不安全的操作。为了保证UI绘制的正确性,此时势必要增加锁,以同步的方式去控制这个问题。

然而加锁的方式显然是一种牺牲性能的方式。

那么还有没有其他方案呢?很显然,最终谷歌选择了只能在主线程更新UI,应运而生的Handler机制被创造出来了。但是它也不是什么新概念,说白了就是消息队列。实现原理也很简单:只允许一个线程(主线程)去更新UI,子线程将消息放到消息队列中,由主线程去轮询消息队列,拿出消息并执行。

这也就是我们的Handler机制。

2、消息队列

这种单线程 + 消息队列的模型其实应用很广。比如在Web前端之中,对于JavaScript来说,被设计时就决定了单线程模型。假设如果 Javascript 被设计为多线程的程序,那么操作 DOM 必然会涉及到资源的竞争。此时只能加锁,那么在 Client 端中跑这么一门语言的程序,资源消耗和性能都将是不乐观的。但是如果设计成单线程,并辅以完善的异步队列来实现,那么运行成本就会比多线程的设计要小很多了。

所以我们可以看到,Handler机制的思路可以说是一个颇为常见的设计。

既然本质是消息队列,是不是我们自己也可以写一套消息队列来感受一下Handler的设计思路呢?没错,接下来让我们一起实现一套简单的消息队列:

3、手写消息队列

我们先来捋一捋思路:

Looper中创建了MessageQueue,Handler之中又通过ThreadLocal拿到主线程new出来的Looper,因此Handler就持有了MessageQueue,又因此线程间是内存共享的,所以子线程可以通过Handler去往MessageQueue之中发送Message。

Looper.loop()死循环轮询MessageQueue,拿到Message就回调其对应的方法。

这样整个Handler机制就运转起来了。接下来我们就依靠这个思路,实现自己的消息队列,为了代码更简洁,以及和Handler机制产生区别,我这里省略一些操作比如ThreadLocal之类的。

3.1、代码实现

代码结束后有解释

public class MainMQ {
    private MyMessageQueue mMQ;

    public static void main(String[] args) {
        new MainMQ().fun();
    }

    public void fun() {
        mMQ = new MyMessageQueue();
        System.out.println("当前线程id:" + Thread.currentThread().getId());
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 省略try-catch
                Thread.sleep(3000);
                mMQ.post(new MyMessage(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("执行此条消息的线程id:" + Thread.currentThread().getId());
                    }
                }));
            }
        }).start();

        loop();
        System.out.println("死循环了,我永远也不被执行~");
    }

    public void loop() {
        while (true) {
            // 省略try-catch
            MyMessage next = mMQ.next();
            if (next == null) {
                continue;
            }
            Runnable runnable = next.getRunnable();
            if (runnable != null) {
                runnable.run();
            
        }
    }
}

这里没有使用Looper这种思想,因为Looper本质就是使用ThreadLocal创建一个和主线程唯一关联的Looper实例,并以此保证MessageQueue的唯一性。

知道这个原理之后,这个demo。直接在主线程中new MessageQueue(),是同样的道理,然后调用loop()方法死循环轮询MessageQueue中的Message,不为null则执行。

main()方法中start了一个子线程,然后sleep3秒后,往MessageQueue中post Message。效果很简单,我猜很多小伙伴已经猜到了:

image

贴一下MessageQueue和Message

public class MyMessageQueue {
    private final Queue<MyMessage> mQueue = new ArrayDeque<>();

    public void post(MyMessage message) {
        synchronized (this) {
            notify();
            mQueue.add(message);
        }
    }

    public MyMessage next() {
        while (true) {
            synchronized (this) {
                // 省略try-catch
                if (!mQueue.isEmpty()) {
                    return mQueue.poll();
                }
                wait(); 
            }
        }
    }
}
public class MyMessage {
    private Runnable mRunnable;

    public MyMessage(Runnable runnable) {
        mRunnable = runnable;
    }

    public Runnable getRunnable() {
        return mRunnable;
    }
}

3.2、思考存在的问题

细心的小伙伴,可能有留意到loop()方法执行后有这么一行代码,然后效果图中并没有被打印:

System.out.println("死循环了,我永远也不被执行~");

当然这是必然的,毕竟我们的loop()是一个死循环,后边的代码是不可能被执行的。其实我们ActivityThread中调用了Looper.loop()之后,也没有任何代码了。

这里可能有小伙伴有疑问了。loop()死循环了,那么我们在主线程中的生命周期回调怎么办?岂不也不被执行了?其实不然,通过上述的消息队列,我们就能看出:我们在手写的这个demo中,loop启动前start了一个子线程,由子线程发送Message交由loop去执行。保证了消息的流畅性。

那是不是我们Android中的loop也是这种思路?没错,main中的loop启动前,的确会起一个子线程......

不要着急,关于这个问题,让我们下篇文章再展开~

结尾

今天这篇文章是全面理解Handler机制的第一篇,内容大多并没有直切到Handler机制本身,而是从外部去思考Handler的设计。而接下来的内容则是对Handler内部源码进行剖析了。

希望可以对小伙伴们有所帮助,如果感觉有收获,欢迎点赞,收藏,关注呦~

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,以及我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

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

推荐阅读更多精彩内容