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的第五篇文章了,相信这五篇文章的内容足以解决你工作和面试中的问题。

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

相关推荐
00后程序员张18 小时前
iOS 应用程序使用历史记录和耗能记录怎么查?
android·ios·小程序·https·uni-app·iphone·webview
用户693717500138419 小时前
OS级AI Agent:手机操作系统的下一个战场
android·前端·人工智能
私人珍藏库19 小时前
[Android] 亿连车机版V7.0.1
android·app·软件·车机
用户693717500138420 小时前
315曝光AI搜索问题:GEO技术靠内容投喂操控答案,新型营销操作全揭秘
android·前端·人工智能
进击的cc20 小时前
彻底搞懂 Binder:不止是 IPC,更是 Android 的灵魂
android·面试
段娇娇20 小时前
Android jetpack LiveData (三) 粘性数据(数据倒灌)问题分析及解决方案
android·android jetpack
用户20187928316720 小时前
TabLayout被ViewPager2遮盖部分导致Tab难选中
android
法欧特斯卡雷特20 小时前
Kotlin 2.3.20 现已发布,来看看!
android·前端·后端
闻哥21 小时前
深入理解 MySQL InnoDB Buffer Pool 的 LRU 冷热数据机制
android·java·jvm·spring boot·mysql·adb·面试
ii_best21 小时前
安卓/ios开发辅助软件按键精灵小精灵实现简单的UI多配置管理
android·ui·ios·自动化