深入了解Android多线程(三)Handler与多线程

前言

【深入了解Android多线程】当前分为三个部分,这三个部分一起阅读,能更好的帮助你理解,Android在多线程方面设计与优化。

Handler并不陌生,在android开发中经常使用它来进行UI线程和子线程间的通信,当然如果你不了解也没有关系,文章中会介绍它的简单用法,你只需要知道它在Android开发中与多线程之间密不可分,既然它如此多线程之间联系的如此密切,那么我们就有必要了解它的运行原理了。

Handler的简单使用

先来看一下handler在Android开发中经典使用场景

        //ui对象
        TextView textView=findViewById(R.id.textView);
        //创建handler
        Handler handler=new Handler();
        //创建一个线程池
        ExecutorService service = Executors.newCachedThreadPool();
        //创建一个后台任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                //模拟耗时操作
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("修改一下");
                    }
                });
            }
        });

上述代码中,开启了一个线程并模拟一段时间的后台任务,在后台任务执行完毕后,使用handler发送(post)了一个任务并将该任务切换到UI线程执行。
需要注意的是,通过handler post或者send的任务并不一定是在UI线程中执行的,这个任务总会在创建handler的线程中执行,上述示例中的handler正好是UI线程中创建,所以post的任务才会在UI线程中执行。

再来看一个handler不在UI线程创建的例子

        //在主线程中开启handler
        Handler handler = new Handler();
        new Thread(new Runnable() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Log.e(TAG, "主线程:" + Thread.currentThread().getName());
                    }
                });
            }
        }).start();

        //创建一个线程池
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Handler handler = new Handler();
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        Log.e(TAG, "子线程:" + Thread.currentThread().getName());
                    }
                });
                Looper.loop();
            }
        });

上面的代码分别实现了在主线程中创建handler,在子线程中通过handler发送一个打印当前线程名字的任务,和在子线程创建一个handler,在子线程中通过handler发送一个打印当前线程名字的任务。运行结果如下


运行结果

运行结果,证实了上面的结论,通过handler post或者send的任务会是在创建handler的线程中执行的。

等等!代码里面的Looper.prepare是什么?
Looper是Handler运行机制的一部分,它负责将handler发送到MessageQuene中任务取出来,在UI线程中使用handler我们不需要创建Looper,是因为UI线程中已经创建好了一个Looper,但是如果在其他线程中创建Handler,我们就需要通过Looper.prepare创建一个Looper。
实际上Looper、Handler以及MessageQuene三者的关系密不可分,它们共同组成了Android的消息机制,下面就将详细讲解。

Android的消息模型

在Android中,消息的处理大致有以下几个过程

  • 在线程A(一般是UI线程)中创建handler,开启线程B用于执行耗时操作。
  • 当线程B需要与线程A进行通信时,在线程B中创建消息(Message或Runnable)。
  • 使用已经创建的handler向消息队列(MessageQueue)中插入(post/send)消息。
  • Looper开始循环,并从消息队列中取出消息,并将消息传给handler,这样这个消息就从线程B中来到了创建handler的线程A中。
  • 根据开发者的要求处理消息。

下面就分别讲解各个部分的工作原理

MessageQueue的工作原理

MessageQueue在Android称之为消息队列,虽然是队列,但实际上它的内部是通过单链表实现的,单链表在数据的插入和删除上比较好的性能优势。
MessageQueue中通过enqueueMessage向链表中插入数据,通过next方法取出数据。
需要注意的是next()是一个阻塞方法,当链表中没有消息时,next会一直阻塞在那里。

Looper的工作原理

Looper在Android的消息机制中主要用于循环消息队列,当它发现消息队列中有新的消息时就会立即处理,否则会一直阻塞在那里。
在上面的例子里我们已经说过了,使用handler必须要先创建一个Looper,创建Looper有两个方法

  • Looper.prepare();
    这个方法用于在子线程中开启一个Looper
  • Looper.prepareMainLooper();
    该方法用于在UI线程中开启一个Looper,因为主线程的Looper比较特殊,所以Looper单独提供了初始化方法。

如果你需要获取主线程的Looper,可以通过Looper.getMainLooper()在任何地方、任何线程中获得主线程的Looper。
创建完Looper后,你还需要开启循环,Looper才可以正常工作。
开启循环的方法主要是

  • Looper.loop()。

loop()的内部是一个死循环,它会不停的从消息队列取出消息,如果消息的队列的next()返回null,则会跳出循环,如下所示

  for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
  ……
  }

因为next()是一个阻塞方法,当消息队列中没有消息时,它并不会返回null,而是一直阻塞在那里。那消息队列什么时候会返回null呢?这个我们暂时不说,接着往下看。
如果msg不为空,Looper就会开始处理这条消息: msg.target.dispatchMessage(msg);msg.target就是发送这条消息的handler对象。这样Handler发送的消息就又交给它的dispatchMessage方法处理了。

Looper也提供了退出循环的方法。

  • quit()
  • quitSafely()

这两个方法区别在于quit会直接退出Looper,而quitSafely只是设定一个退出标记,它会把消息队列中已有的消息全部处理完,才会退出Looper。
Looper退出后,通过handler发送的消息都会失败,handler的send方法会返回false。
子线程中创建了Looper,在所有的事件处理完毕后需要退出Looper,否则该子线程会一直处于阻塞状态,继续占据系统资源,而Looper退出后,该线程就会终止。

Handler的工作原理

Handler的主要工作的就是消息的发送和接收。消息的发送主要使用send的一些方法(post内部也是send的实现)。
handler发送消息的过程仅仅是向消息队列中插入一条,然后消息队列的next就会返回这条消息给Looper,而Looper最终会把消息交给Handler处理,此时消息就转入到了创建handler所在线程中。
handler通过dispatchMessage(Message msg)来处理Message源码如下:

 /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        //handleCallback(msg) 实际上就是 message.callback.run();
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

首先检查Message的callback是否为空,不为空则交给handleCallback处理,其中callback就是一个runnable的对象,实际上就是handler.post(runnable)中runnable对象。
如果callbak为空,进入eles代码块,如果mCallback不为空,就调用mCallback的handleMessage来处理消息。
这里的Callback是一个接口,这个Callback允许我们在使用handler时,不需要派生一个子类,即可创建一个handler对象。
上面例子中我们在创建handler使用的无参构造方法中,默认就将callback设定为null。
如果mCallback为空,则调用handleMessage(msg)处理消息,handleMessage是一个空的方法,需要我们在派生Handler的子类实现,用于进一步对发送的消息做处理。源码如下

    public void handleMessage(Message msg) {
    }

为什么需要这样的消息机制呢?

首先,Android系统规定访问、修改UI只能在主线程中,如果在子线程中访问UI,那么程序就会抛出异常。而在Android中耗时操作是不能在主线程中操作的,所以访问网络、数据库等等这些耗时操作就不得不在子线程中执行了。如果在子线程中执行完毕的操作需要修改UI时?这时就轮到Android的消息机制出场了,再来看一个经典的使用场景:

    private TextView mTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.textview);

        new Thread(new Runnable() {
            @Override
            public void run() {
                double d = 0;
                for (int i = 0; i < 99999; i++) {
                    d += i;    
                }
                Message message = new Message();
                message.what = 1;
                Bundle bundle = new Bundle();
                bundle.putDouble("key", d);
                message.setData(bundle);
                handler.sendMessage(message);
            }
        }).start();

       Handler handler=new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == 1) {
                    mTextView.setText(msg.getData().getDouble("key") + "");
                }
                return true;
            }
        });
    }

上述的代码展示了在一个新的线程中做耗时计算后,将计算结果输出到主线程中并展示的过程。
注意:上面创建handler时使用了我们在Handler的原理中说到的使用Callback的方式创建。
这里延伸一下,Android为什么不允许在子线程中修改UI?原因在于Android中的各种UI控件不是线程安全的,在多线程并发操作时会发生不可预期的状态,那为什么UI控件不加锁呢?因为锁会让UI访问的逻辑变得复杂,同时也会降低UI的访问效率。所以Android选择了性能很高的但线程运行模式。

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

推荐阅读更多精彩内容