handle 学习笔记

作者: 前行的乌龟 | 来源:发表于2018-05-16 15:30 被阅读51次

    经典勿用践踏,太经典了,打击都是跟着经典来学习的,我就不抄了,下面是经典资料:

    为啥要有 handle


    首先 android UI 线程的类型是 ActivityThread,这可能在这里没什么用,凑凑字数吧......

    1. android 的 UI 控件不是线程安全的, 多线程并发访问 UI 控件时可能会产生问题。
    2. 为什么不给 UI 控件加锁,一是加锁会复杂很多,二是加锁会阻塞其他访问 UI 的线程,有可能造成别的线程占用 UI 而把 UI 线程阻塞了,这就肯定会造成卡顿问题了。所以才采用了单线程更新 UI 的模式,使用 handle 来切换线程。

    上面的解释是看:开发艺术探索 总结的......

    handle 中几个角色:


    • ThreadLocal
      每个线程中用来保存私有变量的容器
    • Looper
      消息队列的管理容器,也可以叫轮询器
    • MessageQueue
      消息队列
    • Message
      消息本身
    • handle
      消息发送器,和消息消费者

    说下过程:


    1. Looper.prepare();
      looper 的初始化创建,looper 会创建自己,每个 Looper 对象的创建都会伴随创建一个消息队列 MessageQueue,并把自己保存在当前线程的 ThreadLocal 中,保证每个线程中 looper 的唯一性。
    2. Looper.loop();
      looper 开始一个无限循环,从内部的 MessageQueue 消息队列中循环取出数据挨个执行,消息队列没有数据了就会挂起。
    3. message.getTarget().dispatchMessage(message);
      消息最终就是这么被执行的, message.getTarget() 的返回的对象就是 handle ,所以这里由 handle 会出现内存泄露。
    4. handler.sendMessage();
      handle 发送数据,就是把自己传递给这个待处理的消息 message 中,然后添加到 MessageQueue 消息队列里面去。
    5. Handler handler2 = new Handler(Looper.getMainLooper());
      Looper里面有个静态的 looper ,就是当前进程中 UI 线程的 looper,通过这个 UI 的线程的 looper ,我们可以创建一个 handle 出来,然后添加消息到主进程中去执行。

    为啥 looper 可以切换线程


    looper 的都是要求我们在 Looper.prepare() looper 的初始化之后马上 Looper.loop() 让 looper 跑起来的,lopper 本身是一个无限循环,会一直在 looper 所在线程中执行,所以我们通过不同的 looper 对象,创建 handle 对象发送消失时都是把这个消息发送到了对应 looper 所以在的 MessageQueue 消息队列中去,这个消息队列自然的会在所诉的那个 looper 循环中执行,looper 在哪个线程,那么就是在哪个线程运行。所以线程切换就是这么做的。

    looper 的无限循环


    这是 looper 的循环大致代码 :

    public static final void loop() {
    ...
        //死循环
        while (true) {
            //每次从队列中去取下一个数据(消息),要看什么时候给队列赋值,(在Handler创建的时候,就已经给它设置了队列)
            Message msg = queue.next(); // might block,取消息,如果没有消息,会阻塞
            //if (!me.mRun) {
            //    break;
            //}
            if (msg != null) {
                if (msg.target == null) {
                    // No target is a magic identifier for the quit message.
                    return;
                }
                if (me.mLogging!= null) me.mLogging.println(
                        ">>>>> Dispatching to " + msg.target + " "
                        + msg.callback + ": " + msg.what
                        );
                //msg.target其实就是一个Handler,Handler就会调用dispatchMessage方法。
                msg.target.dispatchMessage(msg);
                ...
            }
        }
    }
    

    looper 循环有必要说道说道,详细探究一下

    怎么实现线程阻塞的

    首先我们看上面的代码可以看到 looper 本身就是一个 while(true) 的无限循环,去 MessageQueue 里面不停的取数据,然后执行消息处理,处理完了再去 MessageQueue 消息队列里面获取下一个消息。但是当 MessageQueue 消息队列没有消息时,looper 的无限循环就会阻塞,阻塞点在哪,Message msg = queue.next() 获取下一条消息这里,那么就是说 queue.next() 方法本身是阻塞的才能阻塞 looper 的无限循环,那么这就带着我们继续去看 MessageQueue 消息队列了,线程阻塞的根本在于 MessageQueue

    MessageQueue 的 next() 方法内部会调用 nativePollOnce() 方法,该方法会阻塞线程。该方法的作用简单说,就是当消息队列中没消息时,阻塞掉当前执行的线程.避免过度的cpu消耗。

    我们知道了队列如何阻塞的当前线程,那么当前线程又是如何唤醒的呢

    怎么实现线程唤醒的

    MessageQueue 的队列使用了使用管道(Pipe)技术

    管道是Linux中的一种进程间通信的方式。管道的原理:使用了特殊的文件,文件里面有两个文件描述符(一个是读取,一个是写入)

    应用场景:当Linux下有两个进程需要通信时,主进程拿到读取的描述符等待读取,没有内容就阻塞,然后另一个进程拿到写入描述符去写内容,唤醒主进程,主进程拿到读取描述符读取到的内容,继续执行。

    管道在Handler的应用场景:Handler在主线程中创建,Looper会在死循环里等待取消息,一种是没取到消息,就阻塞;一种是主线程一旦被子线程唤醒,取到消息,就把Message交给Handler去处理。子线程是用Handler去发送消息,拿写入描述符去写消息,写完之后就唤醒主线程。

    大致流程如下:

    在主线程创建Handler的时候,就可以拿到主线程的Looper,主线程创建完Looper之后,就会执行Looper中的loop方法,loop方法就会从MessageQueue消息队列中一个一个去拿Message消息,它是一个死循环。死循环里面使用了管道的通信方式,管道里面就会拿到文件描述符,一个是往里面存,一个是往里面写。存的时候,是通过上层的Handler,去sendMessage发送消息,调用MessageQueue的enqueueMessage方法,这样就拿到写入的描述符往里面写了,在loop方法里面有queue.next()方法,这个方法会拿到读取的描述符,当它没有读到消息时就阻塞,读到有消息的时候就调用dispatchMessage方法,dispatchMessage方法就会调用handleMessage方法处理消息

    线程唤醒的这部分资料,来源于Android的Handle消息机制|SquirrelNote

    可以看到,不管是线程阻塞还是线程唤醒,使用的方法都是 native 本地的 C 的方法,此时我感觉学习的路啊是那么长啊,任重而道远啊......

        // 线程唤醒
        private native static void nativeWake(long ptr);
        // 线程阻塞
        private native static boolean nativeIsPolling(long ptr);
    

    handle.post 干啥了


    为什么特别说下 handle 的 post 方法,因为有的人面试会问,其实这个很简单,post 方法会生成一个 message 对象,然后把我们 post 方法里面的 runnable 对象存到 message 的 callback 里面去,message 在执行时,会判断有没有 callback 值,有的话直接执行消费本地信息了,没有的话才会把这个 message 交给 handle 去执行。

    private static Message getPostMessage(Runnable r) {
                            // 1. 创建1个消息对象(Message)
                            Message m = Message.obtain();
                                // 注:创建Message对象可用关键字new 或 Message.obtain()
                                // 建议:使用Message.obtain()创建,
                                // 原因:因为Message内部维护了1个Message池,用于Message的复用,使用obtain()直接从池内获取,从而避免使用new重新分配内存
    
                            // 2. 将 Runable对象 赋值给消息对象(message)的callback属性
                            m.callback = r;
                            
                            // 3. 返回该消息对象
                            return m;
                        } // 回到调用原处
    

    对于 looper 的阻塞测试


    其实看了上面对于 looper 阻塞的解释后,我被这个 Pipe 管道 hold 住了,原来用到了这么高大上的技术啊,此时我非常觉得我得做点什么才行,于是不管怎样样总得干点,干脆咱来看看这个阻塞是不是真的,我是不是有点失心疯啊...........

    所以我做了个测试明确下思路:

    • 起2个 thread 线程

    • thread1 里面跑一个 looper ,然后把这个 looper 抛出去,在 looper 启动之后加一个 打印循环

    • thread2 拿到 thread1 抛出的 looper 构建 handle 发送消息

    • 最后看看这个在 looper 之后的打印循环有没有执行机会。

    • thread1

        public class Thread1 extends Thread {
    
            private Looper looper;
    
            @Override
            public void run() {
                super.run();
    
                Looper.prepare();
                looper = Looper.myLooper();
                Looper.loop();
    
                int num = 1;
                while (num <= 50) {
                    Log.d("AAA", Thread.currentThread().getName() + "_执行次数 / " + num);
                    num++;
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
    
            }
    
            public Looper getLooper() {
                return looper;
            }
        }
    
    • thread2
        public class Thread2 extends Thread {
    
            private Handler handler;
    
            public void setHandler(Handler handler) {
                this.handler = handler;
            }
    
            @Override
            public void run() {
    
                int num = 1;
    
                while (num <= 3) {
                    try {
                        java.lang.Thread.sleep(1000);
                        if (handler != null) {
                            Message message = new Message();
                            message.what = 2;
                            handler.sendMessage(message);
                            Log.d("AAA", Thread.currentThread().getName() + "发送消息 / " + num);
                        }
                        num++;
    
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    
    • UI 线程启动这 2 个测试线程
            Thread1 t1 = new Thread1();
            Thread2 t2 = new Thread2();
    
            t1.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            Handler handler = new Handler(t1.getLooper()) {
    
                int num = 0;
    
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    if (msg.what == 2) {
                        num++;
                        Log.d("AAA", Thread.currentThread().getName() + "接受到消息 / " + num);
                    }
                }
            };
            t2.setHandler(handler);
    
            t2.start();
    
        }
    

    UI 线程中中间 sleep 1秒,因为 looper 初始化需要一点时间,handle 中传入的 looper 要是 null 的话会抛异常。

    测试结果

    测试结果是果然 thread1 后面的打印循环是出不来的,looper 的确是阻塞了当前线程的,looper 之后的代码都是没机会执行的

    • 首先 looper 里面的队列是无限循环的,这个循环出不去后面的代码肯定不会执行
    • 再次 looper 的阻塞会阻塞这个无限循环,进而阻塞当前线程。

    我想说这个测试有点凑字数的嫌疑,但是我的确在 looper 无限循环,阻塞,唤醒的问题上找了好多资料,折腾了好几回,非常不爽啊

    ThreadLocal


    ThreadLocal 是一个存储器,作用域在单个线程对象内部,多线程间是无法共享的,相当于线程对象私有的变量存储器。这个存储器只能存一个数据,那么要是想存多个数据的话,就需要多个 ThreadLocal 对象了。

    • threadLocal.set("BB") -- > 存数据
    • threadLocal.get() -- > 取数据
      通过这 2 个方法就知道了吧,只能存一个数据进去

    一般我们认为,你在一个类里面声明的一个对象,这个对象的作用域肯定就是你这个类的对象啊。但是注意 ThreadLocal 的特别之处在于即使你在3个线程之内,操作同一个ThreadLocal 对象里面的值,那么你会发现 3个线程所属的 ThreadLocal 里面对应的值都是不同的。

    下面是一个测试,在 UI 线程启动2个 thread ,这3个线程一齐修改同一个 ThreadLocal 里面的数据

            ThreadLocal threadLocal = new ThreadLocal();
            threadLocal.set("AA");
    
            Thread t1 = new Thread() {
    
                @Override
                public void run() {
                    super.run();
                    threadLocal.set("BB");
                    Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
                }
            };
    
            Thread t2 = new Thread() {
    
                @Override
                public void run() {
                    super.run();
                    threadLocal.set("CC");
                    Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
                }
            };
    
            t1.start();
            t2.start();
    
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Log.d("AA", Thread.currentThread().getName() + " / ThreadLocal : value = " + threadLocal.get());
    
        }
    
    测试结果

    ThreadLocal 的使用场景并不多,基本都是用这个作用域范围,像 Looper,ActivityThread,AMS 这些。

    趴趴源码实现

    为啥 ThreadLocal 这么屌呢,其实也没啥,主要我们尝试看源码,就会发现很多东西其实也是很简单的,就是设计很灵活,很 Nice

    1. looper 对象里有一个 static 的 ThreadLocal 对象,声明对象时就初始化了
        static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    1. ThreadLocal 的 ge() 干了什么
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    获取当前线程对象,然后从拿到线程对象里面的参数 ThreadLocalMap ,ThreadLocal 的 map 集合,然后以 ThreadLocal 对象自己为 key 获取到 value 值,也就是我们储存的数据

    1. 我们来看看这个map 里面的 Entry 的声明
            static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    很明显的可以看到 key 是 ThreadLocal 对象本身,value 是我们储存的值。每个 thread 对象内部都有一个 map 集合的话,通过同一个 key 我们当然会可以存不同的值进去了。

    其他人的解释

    ThreadLocal是一种保存变量的线程安全的类.
    在多线程环境下能安全使用变量,最好的方式是在每个线程中定义一个local变量.
    这样对线程中的local变量做修改就只会影响当前线程中的该变量,因此变量也就是线程安全的.
    这种保证变量线程安全的思想,实际上就是ThreadLocal的实现.

    Threadlocal在调用threadLocal.get方法时,会获取当前thread的threadLocalMap.
    threadLocalMap是thread中的一个属性.
    第一次调用ThreadLocal.get()方法时,会先判断当前线程对应的threadlocalMap是否被创建了,
    如果没创建则会创建ThreadLocalMap,并把该对象赋值给thread.sThreadLocal对象.后续再获取当前thread的threadLocalMap时,就会取该赋值对象.
    ThreadLocalMap就是用来保存线程中需要保存的变量的对象了.

    因为threadLocalMap是赋值给当前thread的,属于thread的内部变量,
    所以每个线程的threadlocalMap就都是不同的对象,也就是上面说的threadlocal是线程安全的原因了.

    ThreadLocalMap内部实际上是一个Entry[],用来保存Entry对象的数组.
    Entry对象是继承weakReference的,其中Entry的key为ThreadLocal对象,value为threadLocal需要保存的变量值.

    调用ThreadLocal.set方法时,会向threadLocalMap中添加一个Entry对象.
    调用get方法时,是通过将调用的threadLocal对象本身作为key,来遍历threadLocalMap数组.
    当threadLocal等于Entry[]中的key时,则返回该Entry中的value.

    最后

    ThreadLocal 的源码看过后是我的理解,另外我看的源码是 API 23 的,我看了其他一些人的解释感觉有好几个说法啊,也许是源码变动挺大的原因吧。

    最后,ThreadLocal 的内容目前就看了这么多,看了我的解释有疑问的欢迎去看看别人的文章,因为多线程这块概念很多,不一定都理解的到位,错误欢迎指正。

    相关文章

      网友评论

        本文标题:handle 学习笔记

        本文链接:https://www.haomeiwen.com/subject/swipdftx.html