Handler 源码解析(二)—— 正确创建 Handler 对象

Handler 源码解析系列文章:

  1. Handler 源码解析(一)------ Handler 的工作流程
  2. Handler 源码解析(二)------ 正确创建 Handler 对象

1. 创建 Handler 对象的相关问题

1.1 为什么直接在子线程中创建 Handler 对象会抛出异常?

子线程中直接创建Handler对象:

java 复制代码
// 本文代码基于Android API 34
new Thread(new Runnable() {
        @Override
        public void run() {
                handler2 = new Handler();
        }
}).start();

运行上述程序,会崩溃:

该异常提示你不能在没有调用过 Looper.prepare() 的线程中创建 Handler 对象,如果运行下述代码,运行就不会报错:

java 复制代码
new Thread(new Runnable() {
        @Override
        public void run() {
                Looper.prepare();
                handler2 = new Handler();
        }
}).start();

我们可以在 Handler 的构造函数中找到抛出异常的地方:

java 复制代码
// Handler.java
public Handler(@Nullable Callback callback, boolean async) {  
    ...
    mLooper = Looper.myLooper();  
    if (mLooper == null) {  
        throw new RuntimeException(  
        "Can't create handler inside thread " + Thread.currentThread()  
        + " that has not called Looper.prepare()");  
    }  
    mQueue = mLooper.mQueue;  
    mCallback = callback;  
    mAsynchronous = async;  
    mIsShared = false;  
}


// Looper.java
public static @Nullable Looper myLooper() {  
    return sThreadLocal.get();  
}

第5~9行,如果通过 sThreadLocal.get() 获取当前线程的 Looper 对象为 null,就会抛出异常。

从上一篇文章我们可以知道,Looper 在 Handler 机制中起到了非常关键的作用,每个线程必须有一个 Looper 对象去维护一个消息循环。

根据这个我们可以猜测出 Looper.prepare() 可能与 Looper 对象的创建有关,查看 Looper.prepare() 代码:

果然,在 prepare() 方法中创建了 Looper 对象,并将其设置到 sThreadLocal 中。

该方法为单例模式,每个线程只能有一个 Looper 对象,否则会抛出异常。

并且在创建 Looper 的时候,创建了 MessageQueue 对象:

mQueue 是使用 final 关键字进行修饰的,赋值后不能改变。

一个线程对应着一个 Looper,一个 Looper 对应着一个 MessageQueue。Looper 是实现消息循环的核心,使(主)线程能够不断地接收、分发和处理消息。

所以必须在创建 Handler 对象前,保证该线程有一个 Looper。

1.2 主线程为什么不需要主动调用 Looper.prepare()

那为什么主线程中没有调用 Looper.prepare() 却没有报错呢?

这是由于在程序启动的时候,系统已经帮我们自动调用了 Looper.prepare() 方法。查看 ActivityThread 中 main() 方法:

java 复制代码
// ActivityThread。java
Looper.prepareMainLooper();

prepareMainLooper() 方法中,其实也是调用了 prepare() 方法。由此可见,每个线程都对应着一个 Looper。

1.3 Looper 对象为什么要存放到 sThreadLocal 中

这就要说到 ThreadLocal 线程间隔离机制

java 复制代码
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

prepare() 方法中第5行,new 出一个 Looper 对象并调用了 ThreadLocal 的 set 方法:

java 复制代码
// ThreadLocal.java
public void set(T value) {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        map.set(this, value);  
    } else {  
        createMap(t, value);  
    }  
}

// ThreadLocal.java
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}

// Thread,java
ThreadLocal.ThreadLocalMap threadLocals = null;

通过 set 方法可以发现,Looper 对象实际上是放到了 ThreadLocalMap 中,而第4行的 ThreadLocalMap 对象,是通过当前线程获得的。当前 thread 持有 ThreadLocalMap 对象。

每个 Thread 的内部都会有一个 ThreadLocalMap 对象,用来存储 Looper。所以,在 sThreadLocal.set(new Looper(quitAllowed)) 时,其实是将 Looper 对象存储到每个 Thread 内部的 ThreadLocalMap 对象中,从而保证每个线程只能访问自己的 Looper 对象,确保了线程之间的隔离性。

2. 在子线程中正确创建 Handler 对象

2.1 正确创建一个 LooperThread

根据上述分析,核心问题就是,一个线程必须对应一个 Looper,我们可以如下在子线程中创建 Handler 对象:

java 复制代码
class LooperThread extends Thread {
       public Handler mHandler;
 
       public void run() {
           Looper.prepare();
 
           mHandler = new Handler(Looper.myLooper()) {
               public void handleMessage(Message msg) {
                   // process incoming messages here
               }
           };
 
           Looper.loop();
       }
   }

分别在第5行和第13行加入 Looper.prepare()Looper.loop()

2.2 上述方法的局限性

但这种写法仍存在一个问题,如果我们想向这个线程中发送消息,只能用 mHandler 去发送消息。不能创建使用这个线程其他 Handler 对象去发送消息。

很容易想到,将子线程中的 Looper 暴露出去:

java 复制代码
class LooperThread extends Thread {
    Looper looper;
    public LooperThread(@NonNull String name) {
        super(name);
    }

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

    @Override
    public synchronized void start() {
        super.start();
    }

    public Looper getLooper(){
        return looper;
    }
   }

然后就可以取到子线程 Looper 对象,并创建 Handler 对象。

java 复制代码
LooperThread looperThread = new LooperThread("thread");
looperThread.start();
Handler handler1 = new Handler(looperThread.getLooper());
Handler handler2 = new Handler(looperThread.getLooper());

2.3 并发同步问题

这样就没有问题了吗?

其实还存在并发同步问题

java 复制代码
LooperThread looperThread = new LooperThread("thread");
looperThread.start();
Handler handler1 = new Handler(looperThread.getLooper());
Handler handler2 = new Handler(looperThread.getLooper());

由于 looperThread 是子线程,而第3、4行运行在主线程中,在主线程中运行不一定保证可以拿到子线程中运行的结果,所以 looperThread.getLooper() 取到的Looper 对象很有可能为空。

也就是说,第3、4行的运行,必须等待子线程运行的结果。所以存在着并发同步问题。

怎么解决这个问题,在 looperThread.start() 将主线程 sleep,等待子线程运行完成?这样会大大降低系统性能,不是并发问题的正确解决方式。

3. HandlerThread

其实在 Android 已经提供了这样的一个线程类 HandlerThread,可以完美解决此问题。

HandlerThread 的源码并不复杂:

java 复制代码
public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private @Nullable Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    
    
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
   
    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;
        }

        boolean wasInterrupted = false;

        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {
            while (isAlive() && mLooper == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    wasInterrupted = true;
                }
            }
        }

        if (wasInterrupted) {
            Thread.currentThread().interrupt();
        }

        return mLooper;
    }

    @NonNull
    public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }

    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }

    public int getThreadId() {
        return mTid;
    }
}

HandlerThread 继承了 Thread,并封装了 Handler。

java 复制代码
public void run() {  
    ...
    Looper.prepare();  
    synchronized (this) {  
        mLooper = Looper.myLooper();  
        notifyAll();  
    }  
    ...
}

public Looper getLooper() {  
    ...
    // If the thread has been started, wait until the looper has been created.  
    synchronized (this) {  
        while (isAlive() && mLooper == null) {  
            try {  
                wait();  
            } catch (InterruptedException e) {  
                wasInterrupted = true;  
            }  
        }  
    }  
    ...
    return mLooper;  
}

在 run 方法和 getLooper 方法中都使用了 synchronized (this) 进行上锁。同一个对象调用这两个方法为互斥访问。

java 复制代码
HandlerThread handlerThread = new HandlerThread("thread");
handlerThread.start();
Handler handler1 = new Handler(handlerThread.getLooper());
Handler handler2 = new Handler(handlerThread.getLooper());

所以每当使用 handlerThread 调用 getLooper() 方法时,一定能拿到不为空的 Looper 对象。

如果 getLooper() 方法先拿到了锁,这时还没有运行 Looper.prepare(),则会执行第17行 wait() 方法,进入阻塞状态,并释放锁。稍后 run 方法会拿到了锁,并且给 mLoopeer 进行了赋值,然后调用 notifyAll() 方法进行唤醒了,这样就可以拿到 mLooper 了。

执行 notifyAll() 后,会等到 synchronized 代码块中所有代码都执行完毕才会去执行其它代码。所以 notifyAll() 在 synchronized 代码块中的位置无关紧要,他也可以放到 mLooper = Looper.myLooper() 前执行。

HandlerThread 对象被创建出来之后,不执行 start 方法直接去 getLooper ,线程会一直被挂起吗?

getLooper() 会通过 isAlive() 去判断线程是否在运行中,如果线程还未 start,会直接返回 null,会报空指针异常。不会调用到 wait() 方法,线程自然不会被挂起。

相关推荐
长亭外的少年7 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
周三有雨8 小时前
【面试题系列Vue07】Vuex是什么?使用Vuex的好处有哪些?
前端·vue.js·面试·typescript
爱米的前端小笔记9 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
好学近乎知o9 小时前
解决sql字符串
面试
建群新人小猿9 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神10 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛10 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法11 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter12 小时前
Android吸顶效果,并有着ViewPager左右切换
android
我明天再来学Web渗透13 小时前
【SQL50】day 2
开发语言·数据结构·leetcode·面试