(Source)HandlerThread

本文介绍了HandlerThread类,它继承于Thread,拥有Looper,可用于构造基于子线程的handler消息循环。文中讲解了其构造函数、Looper初始化、getLooper()方法,还通过两个场景分析了quit()和quitSafe()的区别,指出实例需调用start()后才能getLooper(),且getLooper()可能阻塞线程。

类注释

/**
 * Handy class for starting a new thread that has a looper. The looper can then be
 * used to create handler classes. Note that start() must still be called.
 */
复制代码

HandlerThread是拥有Looper的一个线程类,它继承于Thread,当它调用 start() 方法后,通过getLooper()可以传递Looper给Handler,作为它初始化的参数,这样就完成了子线程的一个消息循环机制。

构造函数

public HandlerThread(String name, int priority) {
    super(name);
    mPriority = priority;
}
复制代码

传入线程的名字,和线程的优先级。

Looper

看到变量里有looper,

 int mPriority;
 int mTid = -1;
 Looper mLooper;
复制代码

再查到继承于Thread的run()方法:

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}
复制代码

先记录线程的TID,然后Looper.prepare,初始化好Looper,Looper内部初始化好MessageQueue,然后 notifyAll(),唤醒所有等待中的线程?啥,为什么呢?先搁置,看下去,接着设置线程优先级,然后,调用一个方法:onLooperPrepared() :

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

从注释看到,这个方法通常用于重写,在looper开始消息循环前进行一些初始化操作。回到上面的代码,最后就是开启 looper.loop() 开启消息循环。

getLooper()

从类注释看到,HandlerThread 是包含了Looper的子线程,不需要操作Looper就可以使用Handler机制,我们看到它的 getLooper() 方法:

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;
}
复制代码

如果线程因为某些原因,isAlive 返回 false,则返回 null 的looper。如果线程已经 start了,但是呢还未初始化好 looper,那么会一直阻塞:while (isAlive() && mLooper == null),细心的同学看到里面,有 wait() ,休眠线程,那么在什么时候被唤醒呢?噢噢,恍然大悟,就是上面的 run() 方法里面的 notifyAll() 啊,原来它唤醒的线程就是在这里,然后就接着返回正确的 looper啊,哈哈,SogaSoga。

quit

quit结束Looper的消息循环,有两个方法,safe quit 和 not safe quit,其都是调用 looper 的对应方法:

 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;
}
复制代码

那么这两个方法有什么区别呢?我们还是看看 (Source)MessageQueue 源码里面,这里举个例子吧:

public class HandlerThreadActivity extends AppCompatActivity {

    private HandlerThread handlerThread;
    private String TAG = "HandlerThreadTest";
    private Handler handler;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_thread);
        Log.i(TAG,"onCreate");
        handlerThread = new HandlerThread(TAG) {
            @Override
            protected void onLooperPrepared() {
                super.onLooperPrepared();
                Log.i(TAG,"onLooperPrepared, "+Thread.currentThread().getName());
            }
        };
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case 0x01:
                        Log.i(TAG,"handleMessage, 0x01, start");
                        try {
                            Thread.sleep(10*1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Log.i(TAG,"handleMessage, 0x01, done");
                        break;
                    case 0x02:
                        Log.i(TAG,"handleMessage, 0x02, done");
                        break;
                }
            }
        };
        findViewById(R.id.btn_1).setOnClickListener(v -> {
            handler.sendEmptyMessage(0x01);
        });
        findViewById(R.id.btn_2).setOnClickListener(v -> {
            handler.sendEmptyMessage(0x02);
        });
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG,"onDestroy");
        //handlerThread.quit();
        handlerThread.quitSafely();
    }
}
复制代码

很简单的页面,点击按钮分别发送不同的消息,一条是阻塞线程的消息。在 onDestory() 中分别使用 quit() quitSafely() 观察log:

场景1,点击两次btn1,发送两个Message消息A1,A2,然后立即点击 btn2,发送一个Message消息B,立即退出这个activity
  • quit():
I/HandlerThreadTest: onCreate
I/HandlerThreadTest: onLooperPrepared, HandlerThreadTest
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: onDestroy
I/HandlerThreadTest: handleMessage, 0x01, done
复制代码

结合源码解析,quit() 直接退出,并remove所有在等待处理的消息,就是说,后面发送的A2和B被直接扔掉,而正在处理的 A1 就等待处理完成。

  • quitSafe():
I/HandlerThreadTest: onCreate
I/HandlerThreadTest: onLooperPrepared, HandlerThreadTest
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: onDestroy
I/HandlerThreadTest: handleMessage, 0x01, done
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: handleMessage, 0x01, done
I/HandlerThreadTest: handleMessage, 0x02, done
复制代码

有点不同,MessageQueue 的消息按照处理的时间 time 做了一个从小到大的排序,调用quitSafe的时刻假设为 currentTime,那么从MessageQueue的队列头开始寻找,直到有一个message.when>currentTime,那么就从这个message开始remove。也就是说,quitSafe时刻,的未来处理的消息全部会被remove调,而之前的消息会一直等待处理。我们来验证一下:

场景2:btn2的消息delay 10 秒,同样地点击两次btn1,发送两个Message消息A1,A2,然后立即点击 btn2,发送一个Message消息B,立即退出这个activity
findViewById(R.id.btn_2).setOnClickListener(v -> {
    handler.sendEmptyMessageDelayed(0x02,10*1000);
});
复制代码
  • quit() :
I/HandlerThreadTest: onCreate
I/HandlerThreadTest: onLooperPrepared, HandlerThreadTest
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: onDestroy
I/HandlerThreadTest: handleMessage, 0x01, done
复制代码

跟场景1一样的log,quit() 这么残暴地直接remove所有的消息。

  • quitSafe():
I/HandlerThreadTest: onCreate
I/HandlerThreadTest: onLooperPrepared, HandlerThreadTest
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: onDestroy
I/HandlerThreadTest: handleMessage, 0x01, done
I/HandlerThreadTest: handleMessage, 0x01, start
I/HandlerThreadTest: handleMessage, 0x01, done
复制代码

和上面解释符合。btn2发送的消息,在quitSafe的时刻相对来说是未来的事件,因而只有它被remove。

总结

  • HandlerThread 继承于 Thread,拥有 Looper,用于构造基于子线程的 handler 消息循环。
  • HandlerThread 的实例要 调用 start() 后才能 getLooper()
  • 清楚知道 quit() 和 quitSafe() 的区别
  • HandlerThread.getLooper() 有可能会阻塞线程

转载于:https://juejin.im/post/5c8d0d69f265da2dd168b29b

07-12 11:50:05.649 09318 09318 E EventLogger: playerFailed [eventTime=58.21, mediaPos=-6.00, window=0, period=0, errorCode=ERROR_CODE_BEHIND_LIVE_WINDOW androidx.media3.exoplayer.ExoPlaybackException: Source error at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException(SourceFile:17) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(SourceFile:376) at android.os.Handler.dispatchMessage(Handler.java:108) at android.os.Looper.loopOnce(Looper.java:288) at android.os.Looper.loop(Looper.java:393) at android.os.HandlerThread.run(HandlerThread.java:85) Caused by: androidx.media3.exoplayer.source.BehindLiveWindowException at com.setplex.media_ui.players.content_steering.media_source.dash.DefaultDashChunkSource.getNextChunk(SourceFile:696) at androidx.media3.exoplayer.source.chunk.ChunkSampleStream.continueLoading(SourceFile:54) at androidx.media3.exoplayer.source.CompositeSequenceableLoader$SequenceableLoaderWithTrackTypes.continueLoading(SourceFile:3) at androidx.media3.exoplayer.source.CompositeSequenceableLoader.continueLoading(SourceFile:59) at com.setplex.media_ui.players.content_steering.media_source.dash.DashMediaPeriod.continueLoading(SourceFile:3) at androidx.media3.exoplayer.source.MaskingMediaPeriod.continueLoading(SourceFile:5) at androidx.media3.exoplayer.ExoPlayerImplInternal.maybeContinueLoading(SourceFile:264) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleContinueLoadingRequested(SourceFile:37) at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(SourceFile:265) ... 4 more
07-23
PackageName:com.sankuai.meituan versionName:1200470212 versionCode:1200470212 Foreground:true time:1763695418277 statcktrace:java.lang.IllegalArgumentException: You must call this method on the main thread at com.bumptech.glide.util.i.a(SourceFile:100010) at com.bumptech.glide.e.l(SourceFile:230010) at com.squareup.picasso.RequestCreator.P(SourceFile:150216) at com.sankuai.meituan.mbc.business.item.dynamic.ImageLoaderImpl.loadImage(SourceFile:280120) at com.sankuai.litho.LithoImageLoader.loadImageInner(SourceFile:440055) at com.sankuai.litho.LithoImageLoader.loadImage(SourceFile:370051) at com.sankuai.litho.drawable.GlideDelegateDrawable.loadDrawableForPreload(Unknown Source:31) at com.sankuai.litho.drawable.GlideDelegateDrawable.preloadDrawable(SourceFile:220045) at com.sankuai.litho.component.ForwardingImageSpec.onBoundsDefined(SourceFile:370050) at com.sankuai.litho.component.ForwardingImage.onBoundsDefined(SourceFile:170010) at com.facebook.litho.LayoutState.collectResults(SourceFile:44) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.collectResults(SourceFile:79) at com.facebook.litho.LayoutState.calculate(SourceFile:44) at com.facebook.litho.ComponentTree.calculateLayoutState(SourceFile:14) at com.facebook.litho.ComponentTree.calculateLayout(SourceFile:420156) at com.facebook.litho.ComponentTree$CalculateLayoutRunnable.run(Unknown Source:5) at android.os.Handler.handleCallback(Handler.java:1027) at android.os.Handler.dispatchMessage(Handler.java:108) at android.os.Looper.loopOnce(Looper.java:298) at android.os.Looper.loop(Looper.java:408) at android.os.HandlerThread.run(HandlerThread.java:85)
11-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值