Android Handler消息机制(五)-HandlerThread完全解析

Android 消息机制Handler完全解析(一)

Android 消息机制Handler完全解析(二)

Android Handler消息机制-消息屏障(三)

Android Handler消息机制完全解析(四)-IdleHandler和epoll机制

Android Handler消息机制(五)-HandlerThread完全解析

关于Handler的相关文章我们已经学习了四篇了,这一篇我们来学习下另一个Handler相关的知识HandlerThread,从名字来看它有Handler和Thread应该跟这两者有关,同样我们还是带着问题来学习今天的内容

  • 为什么要有HandlerThread
  • HandlerThread解决了什么问题

首先Handler是可以跨线程进行通讯的,大家想象一个场景:有两个Thread,ThreadA和ThreadB,如何在ThreadB中声明一个Handler对象且ThreadB中的Handler发送消息会发送到ThreadA中处理?通过前面的学习我们知道在Handler有一个传递Looper对象的构造方法

java 复制代码
public Handler(@NonNull Looper looper) {
    this(looper, null, false);
}

因此我们可以在ThreadB中获取到ThreadA中的looper对象,然后声明Handler的时候将此looper传递给Handler对象,这样就可以达到在ThreadB中声明的Handler发送的消息会发送到ThreadA中处理。来看一段代码

java 复制代码
Thread threadA = new Thread(new Runnable() {
    Looper looper;
    @Override
    public void run() {
        Looper.prepare();
        looper = Looper.myLooper();
        Looper.loop();
    }

    public Looper getLooper() {
        return looper;
    }
});
threadA.start();

Handler workHandler = new Handler(threadA.getLooper()) {
    @Override
     public void handleMessage(@NonNull Message msg) {
         // dosomething
     }
 };

如上所示第16行在主线程声明一个Handler的时候传递了threadA的Looper,这样通过workHandler发送的消息就能在threadA中收到,但是这样有一个问题就是上述threadA.getLooper()这个方法获取不到,因为在实例化handler的时候的Runnable是一个匿名内部类,而外部类不能直接访问匿名内部类的属性和方法,那么怎么办呢?我们可以将将Thread封装一下

java 复制代码
public class MyHandlerThread extends Thread {
    Looper looper;

    MyHandlerThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        super.run();
        Looper.prepare();
        looper = Looper.myLooper();
        Looper.loop();
    }

    public Looper getLooper() {
        return looper;
    }
}

于是我们调用就可以像下面这样调用

java 复制代码
MyHandlerThread threadA = new MyHandlerThread("threadTest");
threadA.start();
Handler workHandler = new Handler(threadA.getLooper());

这样封装之后似乎在线程中声明一个Handler简洁了很多,因为我们把Looper的一些操作封装在run方法里。但是这样有并发的问题,在第二行调用threadA.start之后会运行MyHandlerThread的run方法并对looper进行赋值(这个过程是在子线程中执行的),而主线程的代码运行到第三行时会调用threadA.getLooper方法,此时有可能子线程还未对其进行赋值,也就是说此时threadA.getLooper可能为空。怎么解决这种问题呢?鉴于上述一系列问题所以谷歌给我们封装了一个HandlerThread,我们来看看它是怎么解决这个并发问题的呢?也就是说它是怎么确保执行threadA.getLooper时looper一定是有值的。要想彻底弄明白这个问题,需要对多线程并发有一定的了解,首先要理解synchronized关键字,我在这里写两段伪代码

java 复制代码
public class Test {
    public void test1() {
        synchronized (this) {
            code1
        }
    }

    public void test2() {
        synchronized (this) {
            code2
        }
    }
}

问题来了,test1中的code1和test2中的code2是互斥访问的吗?答案是的,因为者两者持有的是同一把锁所以同一时刻只能执行一个。如果是下面这样就不会互斥

java 复制代码
public class Test {
    public void test1() {
        synchronized (obj1) {
            code1
        }
    }

    public void test2() {
        synchronized (obj2) {
            code2
        }
    }
}

了解synchronized还不够,还需要对多线程的wait和notify有所了解,这里我直接上一个实例,这个实例就是利用多线程的wait和notify方法,来达到等待、唤醒的一个操作

java 复制代码
public class ThreadA extends Thread {
    private Object obj;
    public ThreadA(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        synchronized (obj) {
            try {
                System.out.println("wait之前的时间:" + TimeUtils.getDate(System.currentTimeMillis()));
                obj.wait();
                System.out.println("wait之后的时间:" + TimeUtils.getDate(System.currentTimeMillis()));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

首先定义一个ThreadA它继承自Thread在构造方法中接收一个Object对象用来加锁,并重写run方法,在run方法中调用wait

java 复制代码
public class ThreadB extends Thread {
    private Object obj;
    public ThreadB(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        synchronized (obj) {
            System.out.println("notify之前的时间:" + TimeUtils.getDate(System.currentTimeMillis()));
            obj.notify();
            System.out.println("notify之后的时间:" + TimeUtils.getDate(System.currentTimeMillis()));
        }
    }
}

然后定义一个ThreadB与ThreadA类似,只不过它在run方法里调用notify方法,然后再main函数调用

java 复制代码
Object object = new Object();
ThreadA threadA = new ThreadA(object);
threadA.start();

Thread.sleep(3000);

ThreadB threadB = new ThreadB(object);
threadB.start();

大家先想一想如何打印,打印结果

java 复制代码
wait之前的时间:2024-10-29 17:16:09
notify之前的时间:2024-10-29 17:16:12
notify之后的时间:2024-10-29 17:16:12
wait之后的时间:2024-10-29 17:16:12

发现没有在threadA执行到wait方法后就释放了锁并进入了等待状态,后面的代码暂时不执行,等threadB拿到锁并调用notify方法并且将代码块执行完后将锁释放,此时ThreadA中的wait收到了notify的通知,继续后面的代码执行。这种场景在并发编程中也是经常用到,大家要记住这种场景的处理方式即加锁+wait+notify的方式。

回想一下我们刚才遇到的问题即如何确保threadA.getLooper()方法一定有不为null的返回值。这里是不是提供了一个思路:可以对getLooper方法加锁并进行判断如果looper为空则调用wait等待,run方法也加锁当完成了对mLooper的赋值之后notify一下,此时getLooper方法被唤醒并且looper已经完成了赋值。

说了这么多好像HandlerThread还没正式登场,上面我们讲的内容其实就是HandlerThread的主要原理,HandlerThread的源码并不多总共还不到200行,我精简一下

java 复制代码
public class HandlerThread extends Thread {
    ...
    Looper mLooper;
    private @Nullable Handler mHandler;

    /**
     * Call back method that can be explicitly overridden if needed to execute some
     * setup before Looper loops.
     */
    protected void onLooperPrepared() {
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
    
    public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }
    ...
}

可以看到首先HandlerThread是一个线程类它继承自Thread,并且对其进行了封装,在run方法里调用了Looper.prepare()和Looper.loop()方法,第17行和33行的锁是不是就跟我们上面讲的实例是一样,在调用getLooper方法时会判断mLooper是否为null,如果mLooper为null说明run方法里还未对mLooper赋值,此时调用wait方法进入等待状态且getLooper方法会将锁释放,run方法拿到锁并对mLooper赋值之后调用notifyAll方法此时getLooper收到这个通知,此时会再次判断mLooper是否为空,由于此时在run方法里已经对mLooper进行了赋值因此此时mLooper是不为空的,所以会执行return mLooper,这样getLooper就确保了mLooper一定有值。

关于HandlerThread的源码以及封装的过程就讲到这里,这也是关于Handler的第五篇文章了,相信这五篇文章的内容足以解决你工作和面试中的问题。

如果大家有疑问或者发现错误,欢迎在下方留言,我会在第一时间回答!!
如果觉得文章对你有帮助就帮忙点个赞吧。

相关推荐
阿甘知识库35 分钟前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道1 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
居居飒2 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He5 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗5 小时前
Android笔试面试题AI答之Android基础(1)
android
qq_397562317 小时前
android studio更改应用图片,和应用名字。
android·ide·android studio
峥嵘life7 小时前
Android Studio版本升级那些事
android·ide·android studio
新手上路狂踩坑7 小时前
Android Studio的笔记--BusyBox相关
android·linux·笔记·android studio·busybox
TroubleMaker9 小时前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西11 小时前
Android Studio IDE环境配置
android·ide·android studio