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 小时前
Flink CDC 机制精讲(二):Checkpoint Barrier 如何形成一致快照 【面试宝典】
大数据·经验分享·面试·flink
wen_zhufeng10 小时前
用 vLLM 加速 TTS 推理:通用改造指南
android·vllm
长友cy11 小时前
windows 搭建Qt编译Android 应用程序,快速搭建
android·windows
笨笨饿12 小时前
#121_图传中的H.364编码与MP4的联系
linux·运维·前端·单片机·嵌入式硬件·面试·职场和发展
智驭未来掌门人12 小时前
Android Kotlin 开发避坑指南:从语言特性到工程化实践
android
码农coding12 小时前
android12 ViewRootImpl分析
android
stevenzqzq12 小时前
compose项目返回到当前页面闪烁原因分析
android
古法安卓13 小时前
Android-显示流程
android·java·android studio
CircleMouse13 小时前
画一个Android智能手机机器人
android·智能手机·机器人
数据知道14 小时前
PHP 代码审计实战——ThinkPHP、Laravel 历史漏洞模式深度剖析
android·网络·web安全·网络安全·php·laravel