handler看这一篇就够了(代码片段)

劲火星空 劲火星空     2022-12-11     224

关键词:

Handler使用

首先来熟悉一下Handler的四种使用方式,如果比较熟悉可以直接跳过:

  1. 通过sendMessage消息机制来发送

sendEmptyMessage(int);//发送一个空的消息
sendMessage(Message);//发送消息,消息中可以携带参数
sendMessageAtTime(Message, long);//未来某一时间点发送消息
sendMessageDelayed(Message, long);//延时Nms发送消息

接收数据

mHandler = new Handler()
   @Override
   public void handleMessage(Message msg) 
       super.handleMessage(msg);
       switch (msg.what) 
           case 0:
               String data = (String) msg.obj;
               textView.setText("接收数据:" + data);
               break;
           default:
               break;
       
   
;

发送数据

 new Thread(new Runnable() 
     @Override
     public void run() 
         Message msg = new Message();
         msg.what = 0;
         msg.obj = "测试数据";
         mHandler.sendMessage(msg);
     
 ).start();
  1. 使用Post方式;

post(Runnable);//提交计划任务马上执行
postAtTime(Runnable, long);//提交计划任务在未来的时间点执行
postDelayed(Runnable, long);//提交计划任务延时Nms执行

new Thread(new Runnable() 
    @Override
    public void run() 
        mHandler.postDelayed(new Runnable() 
            @Override
            public void run() 
                tvContent.setText("接收数据:" + str);
            
        , 3000);
    
).start();

其实传递Runnable也是最终转换成message通过sendMessage来发送,将Runnable赋给message的callback,
这个callback在Handler取消息的时候会先进行判断,如果不为空就直接调用了,如果为空才会调用handMessage方法

private static Message getPostMessage(Runnable r) 
    Message m = Message.obtain();
    m.callback = r;
    return m;

  1. 使用runOnUiThread方式;

runOnUiThread其实是调用的post

new Thread(new Runnable() 
    @Override
    public void run() 
        runOnUiThread(new Runnable() 
            @Override
            public void run() 
                tvContent.setText("接收数据:" + str);
            
        );
    
).start();
  1. 通过View的Post方式;

也是调用的Handler的post方法

new Thread(new Runnable() 
    @Override
    public void run() 
        tvContent.post(new Runnable() 
            @Override
            public void run() 
                tvContent.setText("接收数据:" + str);
            
        );
    
).start();

Handler组成

Handler的四大成员:

发送方式功能介绍
Message主要功能是进行消息的封装,同时可以指定消息的操作形式
Looper消息循环泵,用来为一个线程跑一个消息循环,每一个线程最多只可以拥有一个
MessageQueue就是一个消息队列,存放消息的地方每一个线程最多只可以拥有一个
Handler消息的处理者,handler 负责将需要传递的信息封装成Message,发送给Looper,继而由Looper将Message放入MessageQueue中。当Looper对象看到MessageQueue中含有Message,就将其广播出去。该handler 对象收到该消息后,调用相应的handler 对象的handleMessage()方法对其进行处理

需要注意的点是每个线程只有一个Looper和一个MessageQueue,一个线程中可以有多个Handler,每个Handler会处理各自的消息,互相之间没有影响,也就是多个Handler的发送和接收之间值多对多的关系。

Handler原理分析

首先来看下主线程和子线程创建Handler的不同

  1. 主线程
mHandler = new Handler() 
    @Override
    public void handleMessage(Message msg) 
        super.handleMessage(msg);
    
;
  1. 子线程
new Thread(new Runnable() 
    @Override
    public void run() 
        Looper.prepare();
        new Handler() 
            @Override
            public void handleMessage(Message msg) 
                super.handleMessage(msg);
            
        ;
        Looper.loop();
    
).start();

发现主线程直接建立即可,而子线程需要使用Looper.prepare()和Looper.loop方法,这个为啥?带着疑问,接着往下看。

从Looper.prepare()开始
当Looper.prepare()被调用时,发生了什么?

public static void prepare() 
        prepare(true);  //最终其实执行的是私有方法prepare(boolean quitAllowed)中的逻辑
    

    private static void prepare(boolean quitAllowed) 
        if (sThreadLocal.get() != null)    //先尝试获取是否已经存在一个Looper在当前线程中,如果有就抛个异常。
        //这就是为什么我们不能在一个Thread中调用两次Looper.prepare()的原因。
            throw new RuntimeException("Only one Looper may be created per thread");
        
        sThreadLocal.set(new Looper(quitAllowed));  //首次调用的话,就创建一个新的Looper。
    
    
    //Looper的私有构造函数
    private Looper(boolean quitAllowed) 
        mQueue = new MessageQueue(quitAllowed);   //创建新的MessageQueue,稍后在来扒它。
        mThread = Thread.currentThread();         //把当前的线程赋值给mThread。
    
 

可以看到首次调用prepare的时候会创建一个Looper实例放进去,创建Looper实例的时候会创建一个与之关联的MessageQueue,那么这个sThreadLoacal是什么呢? sThreadLocal是个静态的ThreadLocal 实例(在Android中ThreadLocal固定为Looper)。那么,Looper.prepare()既然是个静态方法,Looper是如何确定现在应该和哪一个线程建立绑定关系的呢?
来看看ThreadLocal的get()、set()方法。

public void set(T value) 
        Thread t = Thread.currentThread();  //同样先获取到当前的线程
        ThreadLocalMap map = getMap(t);     //获取线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);           //储存键值对
        else
            createMap(t, value);
    

public T get() 
        Thread t = Thread.currentThread();   //重点啊!获取到了当前运行的线程。
        ThreadLocalMap map = getMap(t);      //取出当前线程的ThreadLocalMap。这个东西是个重点,前面已经提到过。忘了的同学在前面再看看。
        if (map != null) 
            ThreadLocalMap.Entry e = map.getEntry(this);  
            //可以看出,每条线程的ThreadLocalMap中都有一个<ThreadLocal,Looper>键值对。绑定关系就是通过这个键值对建立的。
            if (e != null)
                return (T)e.value;
        
        return setInitialValue();

重点来了:每个线程有一个与之关联的Looper,这个Looper是用ThreadLoacaMap来存储的,ThreadLoacaMap存储的就是ThreadLocal对象,而Android中的ThreadLocal就是Looper,所以存的就是Looper,并且ThreadLocalMap是在每线程中都私有的,这就保证的并发时的线程安全,这是典型的以空间来换时间,而我们熟悉的synchronized是以时间换空间。

创建Handler
平时我们都使用new Handler()来在一个线程中创建Handler实例,但是它是如何知道自己应该处理那个线程的任务呢

public Handler() 
        this(null, false); 

    
public Handler(Callback callback, boolean async)       //可以看到,最终调用了这个方法。
        if (FIND_POTENTIAL_LEAKS) 
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) 
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            
        

        mLooper = Looper.myLooper();                    //重点啊!在这里Handler和当前Thread的Looper绑定了。Looper.myLooper()就是从ThreadLocale中取出当前线程的Looper。
        if (mLooper == null) 
            //如果子线程中new Handler()之前没有调用Looper.prepare(),那么当前线程的Looper就还没创建。就会抛出这个异常。
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        
        mQueue = mLooper.mQueue;  //赋值Looper的MessageQueue给Handler。
        mCallback = callback;
        mAsynchronous = async;

上面代码中有mLooper = Looper.myLooper(),这就是Handler和Looper进行了绑定,然后Handler就可以给Looper发消息了,我们调用Handler的sendMessage方法也就是往Looper的MessageQueue里边发消息

Looper.loop()
我们都知道,在Handler创建之后,还需要调用一下Looper.loop(),这样才能循环起来,那Looper是怎样把消息准确的送到Handler中处理

public static void loop() 
        final Looper me = myLooper();   //这个方法前面已经提到过了,就是获取到当前线程中的Looper对象。
        if (me == null)  
            //没有Looper.prepare()是要报错的!
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        
        final MessageQueue queue = me.mQueue;       //获取到Looper的MessageQueue成员变量,这是在Looper创建的时候new的。

        //这是个Native方法,作用就是检测一下当前线程是否属于当前进程。并且会持续跟踪其真实的身份。
        //在IPC机制中,这个方法用来清除IPCThreadState的pid和uid信息。并且返回一个身份,便于使用restoreCallingIdentity()来恢复。
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;)   //重点(敲黑板)!这里是个死循环,一直等待抽取消息、发送消息。
            Message msg = queue.next(); //  从MessageQueue中抽取一条消息。至于怎么取的,我们稍后再看。
            if (msg == null) 
                // No message indicates that the message queue is quitting.
                return;
            
            final long traceTag = me.mTraceTag;   //取得MessageQueue的跟踪标记
            if (traceTag != 0) 
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));  //开始跟踪本线程的MessageQueue中的当前消息,是Native的方法。
            
            try 
                msg.target.dispatchMessage(msg);   //尝试分派消息到和Message绑定的Handler中
             finally 
                if (traceTag != 0) 
                    Trace.traceEnd(traceTag);      //这个和Trace.traceBegin()配套使用。
                
            

            final long newIdent = Binder.clearCallingIdentity();   //what?又调用这个Native方法了。这里主要是为了再次验证,线程所在的进程是否发生改变。
            msg.recycleUnchecked();   //回收释放消息。
        
    

从上面的分析可以知道,当调用了Looper.loop()之后,线程就就会被一个for(;;)死循环阻塞,每次等待MessageQueue的next()方法取出一条Message才开始往下继续执行。然后通过Message获取到相应的Handler (就是target成员变量),Handler再通过dispatchMessage()方法,把Message派发到handleMessage()中处理。

MessageQueue
MessageQueue是一个用单链的数据结构来维护消息列表,现在又产生一个疑问,MessageQueue的next()方法是如何阻塞住线程的呢?

Message next() 
        //检查loop是否已经为退出状态。mPrt是Native层的MessageQueue的地址。通过这个地址可以和Native层的MessageQueue互动。
        final long ptr = mPtr;
        if (ptr == 0) 
            return null;
        
        int pendingIdleHandlerCount = -1;
        int nextPollTimeoutMillis = 0;      //时间标记,当且仅当第一次获取消息时才为0。因为它在死循环外面啊!

        for (;;) 
           //这是一个Native的方法。
           nativePollOnce(ptr, nextPollTimeoutMillis);
           synchronized (this)        //锁住MessageQueue
                //获取当前的系统时间,用于后面和msg.when进行比较。
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;        //获得当前MessageQueue中的第一条消息
                if (msg != null && msg.target == null) 
                
                    do 
                        prevMsg = msg;
                        msg = msg.next;
                     while (msg != null && !msg.isAsynchronous());
                
                if (msg != null) 
                    if (now < msg.when)   //这个判断的意义在于只有到了Message应该被发送的时刻才去发送,否则继续循环。
                        //计算下一条消息的时间。注意最大就是Integer.MAX_VALUE。
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                     else   //应该发送一条消息了。
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) 
                            prevMsg.next = msg.next;
                         else 
                            mMessages = msg.next;
                        
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();   //转换消息标记为使用过的
                        return msg;         //返回一条消息给Looper。
                    
                 else 
                    // 如果取到的Message为null,将时间标记设置为-1。
                    nextPollTimeoutMillis = -1;
                
        
    

可以看到。MessageQueue在取消息(调用next())时,会进入一个死循环,直到取出一条Message返回。这就是为什么Looper.loop()会在queue.next()处等待的原因
上面方法中出现了一个nativePollOnce(ptr, nextPollTimeoutMillis);函数的调用。线程会被阻塞在这个地方,实际上是阻塞在了底层的Looper的epoll_wait()这个地方等待唤醒呢。nativeWake(),通过这个来进行唤醒唤醒,这个方法是在enqueueMessage中调用的
下面看下enqueueMessage的代码:

boolean enqueueMessage(Message msg, long when) 
        if (msg.target == null)    //没Handler调用是会抛异常的啊
            throw new IllegalArgumentException("Message must have a target.");
        
        synchronized (this)        //锁住MessageQueue再往里添加消息。
            msg.markInUse();        //切换Message的使用状态为未使用。
            msg.when = when;        //我们设置的延迟发送的时间。
            //经过下面的逻辑,Message将会被“储存”在MessageQueue中。实际上,Message在MessageQueue中的储存方式,
            //是使用Message.next逐个向后指向的单链表结构来储存的。比如:A.next = B, B.next = C...
            Message p = mMessages;  //尝试获取当前Message
            boolean needWake;
            if (p == null || when == 0 || when < p.when) 
                // 如果为null,说明是第一条。
                msg.next = p;   
                mMessages = msg;    //设置当前的Message为传入的Message,也就是作为第一条。
                needWake = mBlocked;
             else 
            
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                //不满足作为第一条Message的条件时,通过下面的逐步变换,将它放在最后面。这样便把Message“储存”到MessageQueue中了。
                for (;;) 
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) 
                        break;
                    
                    if (needWake && p.isAsynchronous()) 
                        needWake = false;
                    
                
                msg.next = p; 
                prev.next = msg;
            

          
            if (needWake) 
                nativeWake(mPtr);
            
        
        return true;
    

为什么主线程创建Handler不用Looper
因为主线程在ActivityThread,也就是主线程初始化的时候通过Looper.prepareMainLooper()建立好了对应的Looper
注意ActivityThread并没有继承Thread,它的Handler是继承Handler的私有内部类H.class。在H.class的handleMessage()中,它接收并执行主线程中的各种生命周期状态消息。UI的16ms的绘制也是通过Handler来实现的。也就是说,主线程中的所有操作都是在Looper.prepareMainLooper()和Looper.loop()之间进行的。

ThreadLocal说明

这个在上面已经说过了,因为是非常的重要,所以在这里再强调一遍。Android中ThreadLocal的表现形式就是Looper,我们在Looper.prepare()中会创建一个Looper实例,并将其存入ThreadLocalMap中,而ThreadLoacalMap是属于特定线程的,这样不仅保证的线程安全,同时也将Looper和对应的线程关联起来了,Looper创建的时候也会创建一个对应的MessageQueue

HandlerThread

先来看一个HandlerThread使用的例子:

HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start();
mHandler = new Handler(thread.getLooper());
mHandler.post(new Runnable()...);

再看下HandlerThread的源码

public class HandlerThread extends Thread 
    int mPriority;
    int mTid = -1;
    Looper mLooper;

    public HandlerThread(String name) 
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    
    protected void onLooperPrepared() 
    

    @Override
    public void run() 
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) 
            mLooper = Looper.myLooper();
            notifyAll();
        
        Process.setThreadPriority(mPriority查看详情  

最全排序算法及优化,看这一篇就够了(代码片段)

最全排序算法总结看这一篇就够了没有经过总结的知识是沉重的,无用的瞧一瞧~博健的LeetCode题解:Gitbook版本传送门博健的LeetCode题解:CSDN传送门有趣的CSS:Gitbook传送门前端进阶笔记:Gitbook传送门目录... 查看详情

css渐变背景看这一篇就够了(代码片段)

CSS渐变背景看这一篇就够了在我们自己设计网页的时候,为了好看美观,颜色可谓是最让人头疼的一部分。尤其是在配色上又找不到一些好看的网站。今天我就来记录一些好看的渐变式背景,和一些常用的颜色网站。... 查看详情

☀️javanio?看这一篇就够了!!☀️(代码片段)

文章目录一、NIO简介1.1NIO概述1.2NIO&IO分析1.2.1IO操作流程1.2.2面向流与面向缓冲区1.2.3阻塞与非阻塞1.2.4.同步与异步二、Buffer基本应用2.1Buffer概述2.2Buffer基本应用三、Channel基本应用3.1Channel概述3.2FileChannel基本应用3.3SocketChanel基... 查看详情

☀️javanio?看这一篇就够了!!☀️(代码片段)

文章目录一、NIO简介1.1NIO概述1.2NIO&IO分析1.2.1IO操作流程1.2.2面向流与面向缓冲区1.2.3阻塞与非阻塞1.2.4.同步与异步二、Buffer基本应用2.1Buffer概述2.2Buffer基本应用三、Channel基本应用3.1Channel概述3.2FileChannel基本应用3.3SocketChanel基... 查看详情

想要弄懂groupby看这一篇就够了(代码片段)

一、前言groupby关键字,不管是工作中还是面试都会经常被用到,所以弄懂它是非常有必要的。要弄懂groupby那我们就得联合着:聚合函数、groupby、having一块讲解。讲之前我们先准备一张表:二、聚合函数为了讲好groupby我们必须... 查看详情

逆转单向链表看这一篇就够了java(代码片段)

逆转单向链表逆转前:1->2->3->4->5->null逆转后:5->4->3->2->1->null个人博客地址:逆转单向链表方法一、循环迭代publicNodereverse(Nodehead)if(head==null||head.next==null)returnhead;//取前面节点Nodepre=head;//取后面 查看详情

系统性的学会pandas看这一篇就够了(代码片段)

作者:MaSizhou https://blog.csdn.net/weixin_45901519/article/details/1129808221、Pandas数据结构2008年WesMcKinney开发出的库专门用于数据挖掘的开源python库以Numpy为基础,借力Numpy模块在计算方面性能高的优势基于matplotlib,能够简便的... 查看详情

jquery框架超详细dom操作看这一篇就够了!(代码片段)

目录写在前面一、内容操作1.html()2.text()3.val()二、属性操作(1)通用属性操作1.attr():2.removeAttr()3.prop()4.removeProp()5.attr和prop区别(2)对class属性操作1.addClass()2.removeClass()3.toggleClass()4.css()三、 查看详情

redux从入门到进阶,看这一篇就够了!(代码片段)

Redux,带你从入门到进阶🌂序言☂️一、基础知识1、Redux概念简述2、Redux的工作流程🎃二、使用Antd实现TodoList页面布局1、在项目中使用Antd2、使用Antd实现TodoList的基本布局3、创建redux中的store(1)创建storeÿ... 查看详情

redux从入门到进阶,看这一篇就够了!(代码片段)

Redux,带你从入门到进阶🌂序言☂️一、基础知识1、Redux概念简述2、Redux的工作流程🎃二、使用Antd实现TodoList页面布局1、在项目中使用Antd2、使用Antd实现TodoList的基本布局3、创建redux中的store(1)创建storeÿ... 查看详情

架构整洁之道,看这一篇就够了!(代码片段)

阿里妹导读:程序的世界飞速发展,今天所掌握的技能可能明年就过时了,但有些知识历久弥新,掌握了它们,你在程序的海洋中就不会迷路,架构思想就是这样的知识。本文是《架构整洁之道》的读书心得,作者将书中内容拆... 查看详情

关于el-upload看这一篇就够了(代码片段)

下述源码分析基于Elementv2.15.9版本前提在解析源码之前,先阐述其重点使用的两个基础内容:<inputtype="file">使用type=“file”的元素使得用户可以选择一个或多个元素以提交表单的方式上传到服务器上,... 查看详情

关于el-upload看这一篇就够了(代码片段)

下述源码分析基于Elementv2.15.9版本前提在解析源码之前,先阐述其重点使用的两个基础内容:<inputtype="file">使用type=“file”的元素使得用户可以选择一个或多个元素以提交表单的方式上传到服务器上,... 查看详情

mybatis基础应用,看这一篇就够了!(代码片段)

文字分享希望现在的你无论有明确具体的目标还是没有,都能重视自己的需求和目标,并且常常回顾,或许可以找一个你习惯的方式写出来,挂在哪里,电脑或日记本都好。当你疲惫或迷茫的时候拿出来看一... 查看详情

mix2适配看这一篇就够了,18:9只需一行代码(代码片段)

今天同事已经拿到MIX2了(F码通道,想要的可以找我哦),但是app的状态栏适配不够好,空出很大一块出来。直接上代码<applicationandroid:resizeableActivity="true"></application>AndroidManifest.xml只需在AndroidManifest 查看详情

cas自旋锁,看这一篇就够了(代码片段)

前序时隔多年,杰伦终于出了新专辑,《最伟大的作品》让我们穿越到1920年,见到了马格利特的绿苹果、大利的超现实、常玉画的大腿、莫奈的睡莲、徐志摩的诗…他说“最伟大的作品”并不是自己的歌,而是这... 查看详情

什么是谓词下推,看这一篇就够了(代码片段)

文章目录1.什么是谓词2.什么是下推3.什么是谓词下推4.一些常见的应用4.1传统数据库应用4.2Hive中的谓词下推4.3列式存储中的谓词下推今天有个小伙伴问我,什么是谓词下推,然后我就开启巴拉巴拉模式,说了好长一段... 查看详情