谈谈Hanlder消息机制

1、什么是Handler,为什么要有Handler?

Android中主线程也叫UI线程,主线程主要是用来创建、更新UI的。而其他耗时操作,比如网络访问、文件处理、多媒体处理等都需要在子线程中操作,之所以在子线程中操作是为了保证UI的流畅程度,手机显示的刷新频率是60Hz,也就是一秒钟刷新60次,每16.67毫秒刷新一次,为了不丢失帧,那么主线程处理代码最好不要超过16毫秒。当子线程处理完数据后,为了防止UI处理逻辑的混乱,Android只允许主线程修改UI,那么这时候就需要Handler来充当子线程和主线程之间的桥梁了。

2、Handler的使用方法

post(runnable)
sendMessage(message)
其实post(runnable)和sendMessage(message)最终底层都是调用了sendMessageAtTime方法。

3、Handler消息机制中涉及到哪些类,各自的功能是什么?

Handler主要用于跨线程通信。涉及MessageQueue、Message、Looper、Handler这4个类。

  • Message:消息对象。对象的内部实现是单列链表,最大长度是50,用于缓存消息对象,达到重复利用消息对象的目的,以减少消息对象的创建。

  • MessageQueue:消息队列。主要功能是向消息池投递信息(MessageQueue.enqueueMessage)和取走消息池的信息(MessageQueue.next) 。

  • Handler:消息对象的发送者和处理者。负责向消息池中发送消息(Handler.enqueueMessage)和处理消息(Handler.handleMessage) 。

  • Looper:消息队列的处理者。用于轮询消息队列里面的消息对象,不断从MessageQueue队列中轮询取出Message,交由handler的dispatchMessage()方法进行消息的分发,dispatchMessage方法再调用handleMessage()方法进行消息回调。

它们之间的类关系:Looper里面有一个MessageQueue消息队列,MessageQueue有一组待处理的Message,Message中有一个用于处理消息的Handler,Handler中有Looper和MessageQueue。

4、Handler消息机制的原理

在应用启动时,ActivityThread类的main方法里面初始化调用Looper.preperMainLooper方法,调用该方法的目的是在Looper中创建MessageQueue成员变量并把Looper对象绑定到当前线程中(ThreadLocal)。当调用Handler的sendMessage方法的时候,就将Message对象添加到了Looper创建的MessageQueue队列中,同时给Message指定了target对象,其实这个target对象就是Handler对象。由于在main方法里面初始化时执行了Looper.loop()方法,该方法从Looper的成员变量MessageQueue队列中不断轮询取出Message,交由handler的dispatchMessage()方法进行消息的分发,dispatchMessage方法再调用handleMessage()方法进行消息回调,这样就完成了整个消息机制。

5、Handler线程安全问题

对于子线程访问主线程的Handler对象,你可能会问,多个子线程都访问主线程的Handler对象,发送消息和处理消息的过程中会不会出现数据的不一致呢?答案是不会出现数据的不一致问题。因为Handler对象通过ThreadLocal管理的Looper对象是线程安全的,不管是添加消息到消息队列还是从消息队列中读取消息都是通过synchronized同步保护的,所以不会出现数据不一致现象。

6、Handler引起的内存泄漏以及解决办法

原因:
非静态内部类持有外部类的强引用,导致外部Activity无法释放。

解决办法:
1、handler内部持有外部activity的弱引用
2、把handler改为静态内部类
3、mHandler.removeCallbacksAndMessages

//代码示例
public class MainActivity extends AppCompatActivity {
 
    //创建静态内部类
    private static class MyHandler extends Handler{
        //持有弱引用MainActivity,GC回收时会被回收掉.
        private final WeakReference<MainActivity> mAct;
        public MyHandler(MainActivity mainActivity) {
            mAct = new WeakReference<MainActivity>(mainActivity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity mainAct = mAct.get();
            super.handleMessage(msg);
            if(mainAct != null){
                //执行业务逻辑
            }
        }
    }

    private static final Runnable myRunnable = new Runnable() {
        @Override
        public void run() {
            //执行业务逻辑
        }
    };
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyHandler myHandler=new MyHandler(this);
        //通过postDelayed延迟发送
        //myHandler.postDelayed(myRunnable, 1000 * 5);
        //通过sendMessageDelayed延迟发送
        Message message = Message.obtain();
        message.what = 1;
        myHandler.sendMessageDelayed(message, 1000 * 5);
    }
}

7、一个线程可以有几个Looper、几个MessageQueue和几个Handler?

在Android中,Looper类利用了ThreadLocal的特性,保证了每个线程只存在一个Looper对象。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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));
}

Looper构造函数中创建了MessageQueue 对象,因此一个线程只有一个MessageQueue。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

可以有多个Handler。
Handler在创建时将Looper和MessageQueue关联起来:

public Handler(Callback callback, boolean async) {
    ...
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    ...
}

8、可以在子线程直接创建一个Handler吗?会出现什么问题,那该怎么做?

不能在子线程直接new一个Handler。因为Handler的工作依赖于Looper,而Looper又是属于某一个线程的,其他线程不能访问,所以在线程中使用Handler时必须要保证当前线程中Looper对象创建并且启动循环。不然会抛出异常throw new RuntimeException("Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()");

正确做法是:

class LooperThread extends Thread {
    public Handler mHandler;
    public void run() {
        Looper.prepare();//为线程创建Looper对象
        mHandler = new Handler() {  
            public void handleMessage(Message msg) {
               
            }
        };
        Looper.loop();//启动消息循环
    }
}

9、既然线程中创建Handler时需要Looper对象,为什么主线程不用调用Looper.prepare()创建Looper对象?

在应用启动时,ActivityThread类的main方法里面调用了Looper.prepareMainLooper()方法,Looper.prepareMainLooper()方法里面调用了Looper.prepare()方法,方法里面将Looper对象set到sThreadLocal对象当中。

10、Looper死循环为什么不会导致应用卡死,会消耗大量资源吗?

对于线程执行一段可执行代码,当可执行代码执行完成后,线程生命周期便终止了,线程退出。而对于主线程,我们是绝不希望运行一段时间后,自己就退出了,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,Binder线程也是采用死循环的方法,通过循环方式与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,Looper.loop()本身不会导致应用卡死。

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Looper的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

11、MessageQueue是队列吗?它是什么数据结构?

MessageQueue不是队列,它内部使用一个Message链表来实现消息的存和取。 链表的排列依据是Message.when,表示Message期望被分发的时间,该值是SystemClock. uptimeMillis()与delayMillis之和。

12、handler.postDelayed()函数延时执行计时是否准确?

当上一个消息存在耗时任务的时候,会占用延时任务执行的时机,实际延迟时间可能会超过预设延时时间,这时候就不准确了。

13、handler发送延时消息是怎么处理的?

根据消息队列入队规制,如果队列中没消息,那么不管要入队的消息有没有延时,都放到队列头。如果队列中有消息,那么要跟队列头的消息比较一下延时,如果要入队的消息延时短,则放队列头,否则,放到队列中去,需要移动链表。

入队规制的好处是,延时越长的消息在队列越后面,所以next方法取到一个延时消息时,如果判断时间没有到,就进行阻塞,不用管后面的消息,因为队列后面的消息延迟时间更长。

14、你了解HandlerThread吗?

HandlerThread继承自Thread,它是一种可以使用Handler的Thread,它的实现也很简单,在run方法中也是通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环(与我们手动创建方法基本一致),这样在实际的使用中就允许在HandlerThread中创建Handler了。

public class HandlerThread extends Thread {
    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
}

由于HandlerThread的run方法是一个无限循环,因此当不需要使用的时候通过quit或者quitSafely方法来终止线程的执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容