handler机制

139 阅读13分钟

一、Handler流程

  • Android是基于消息驱动的,当进程创建后,ActivityThread的main方法会创建Looper并调用loop()方法开启消息循环;

    子线程创建Handler,必须调用:

    Looper.prepare();
    Looper.loop();
    

    对于只处理一次耗时任务的线程来说,一直采用looper()循环无疑是消耗性能的。

    所以在所有的任务执行完成后应该调用quit方法来终止消息循环。

    Looper.quit();
    

    Android内部已经有一个帮我们封装好的HandlerThread

    HandlerThread handlerThread = new HandlerThread("handler-Thread");
    handlerThread.start();
    Handler childHandler = new Handler(handlerThread.getLooper()){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            //处理其他线程发送的消息
            //运行在子线程
        }
    };
    
  • 当handler调用sendMessage或者post方法传入runable组装成message后,同步message将存储到MessageQuene中;

    handler的post()和sendMessage()的区别?

    没区别。post方法只是多了个将Runnable封装成Message的过程。

  • Looper轮询并处理MessageQuene中的message。

    Looper是ThreadLocal修饰的,所以Looper是线程私有的,所以Handler线程通信必须拿到对面线程的Handler才能将消息发送到对面线程。

二、quit和quitSafely的区别

void quit(boolean safe) {
    if (!mQuitAllowed) {
        throw new IllegalStateException("Main thread not allowed to quit.");
    }

    synchronized (this) {
        if (mQuitting) {
            return;
        }
        mQuitting = true;

        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }

        // We can assume mPtr != 0 because mQuitting was previously false.
        nativeWake(mPtr);
    }
}
  • 当我们调用了quit方法时,其实最终是调用了MessageQueueremoveAllFutureMessagesLocked方法,该方法是清空MessageQueue消息池中的所有消息,包括延迟消息和非延迟消息。
  • 当我们调用了quitSafely方法时,其实是调用了removeAllMessagesLocked方法,该方法会清空MessageQueue消息池中的全部延迟消息,同时将消息池中全部的非延迟消息派发出去让Handler处理。

quitSafely相比较quit方法,会派发全部的非延迟消息。

三、为什么子线程不能更新UI?

首先先说结论:

子线程也能更新UI,只是不能跨线程更新UI。

因为多线程操作同一资源会存在并发问题,Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,为了解决原子性、有序性及可见性等问题,就要加锁,那么为什么系统不对UI控件的访问加上锁机制呢?

  • 首先加上锁机制会让UI访问的逻辑变得复杂。
  • 锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

所以最简单且高效的方法就是采用单线程模型来处理UI操作。

之所以会有 “子线程不能更新UI” 这个说法,是因为Android中几乎所有UI创建都是主线程。

ViewRootImplrequestLayout()方法中有个checkThread()方法,其规定,如果当前操作UI的线程和创建View的线程不是同一个线程,则会抛出异常。

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
            "Only the original thread that created a view hierarchy can touch its views.");
    }
}

反例:通过这种方式来修改界面就可以有自己的View树,也就不受限于主线程中修改UI了。

new Thread() {
     public void run(){
     	Looper.prepare();

		    view=new View(getApplicationContext());
		    view.setBackgroundResource(R.drawable.ic_launcher); mWindowManager=(WindowManager)
		        getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
		    WindowManager.LayoutParams param=new WindowManager.LayoutParams();
		    param.type=WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
		    param.format=1;
		    param.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
		    param.flags = param.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
		    param.flags = param.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
		    param.alpha = 1.0f;
		    param.width=200;
		    param.height=200;
		    mWindowManager.addView(view, param);
     }
 }.start();

ViewRootImpl是在子线程创建的,需要准备Looper,所以DecorView是在子线程,所以也可以在子线程中更新View。

3.1、onCreate里开启子线程更新UI会报错吗?

//代码1
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tv = (TextView)findViewById(R.id.tv);
    new Thread() {
        public void run(){
            tv.setText("change text in non-UI Thread");
        }
    }.start();
}

//代码2
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tv = (TextView)findViewById(R.id.tv);
    new Thread() {
        public void run(){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tv.setText("change text in non-UI Thread");
        }
    }.start();
}

比较一下这两个代码,发现:

上面的代码可以正常的运行,而下面的代码就会报异常。为什么呢?

View树是在onResume后才创建的,CheckThread也是在onResume后才执行,那么在此之前修改UI,由于当前的View树还不存在,因此暂时不会绘制界面,只会保存设置的状态,在下次请求绘制UI时会再次刷新UI。所以代码1正常运行。

由于睡眠两秒钟后才更新UI,这段时间内早已完成了前期的初始化,onResume也已经执行完成,有了自己的View树,当更新View状态时会进行线程检查的。所以代码2会报错。

3.2、子线程中怎么Toast和showDialog

先说结论:准备子线程的Looper即可。

   new Thread(new Runnable() {
        @Override
        public void run() {

            Looper.prepare();
            Toast.makeText(MainActivity.this, "run on thread", Toast.LENGTH_SHORT).show();
            Looper.loop();

        }
    }).start();

Toast本质是通过window显示和绘制的(操作的是window),而上面提到的主线程不能更新UI,是因为ViewRootImplcheckThread方法在Activity维护的View树的行为。

Toast中TN类使用Handler是为了用队列和时间控制排队显示Toast,所以为了防止在创建TN时抛出异常,需要在子线程中使用Looper.prepare();和Looper.loop();

但是不建议这么做,因为它会使线程无法执行结束,导致内存泄露。在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。Looper.myLooper().quit();

五、Handler注意事项

注意内存泄漏。

message持有当前的handler对象,handler又属于当前的Looper,一个线程只有一个Looper和MessageQueue,而我们的主线程通常都是以Activity的形式展示在前台供用户交互。

如果Handler为非静态内部类,默认持有Activity对象,当Activity销毁,MessageQueue中还有消息未处理,将导致已经被关闭的Activity无法被正常释放,从而导致内存泄露。

解决方案:

  • Activity销毁时移除所有消息;

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }
    
  • 弱引用 + 静态内部类

    private static class MyHandler extends Handler {
        private WeakReference<FirstMainActivity> weakReference;
    
        //弱引用的方式,在gc时,activity可被回收
        public MyHandler(WeakReference<FirstMainActivity> weakReference) {
            this.weakReference = weakReference;
        }
    
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
        }
    }
    

六、同步屏障机制

Android消息队列MessageQueue中加入的消息分成同步消息和异步消息,在平常开发中接触到的消息基本上都是同步消息,同步消息会被放到消息队列的队尾,Looper在消息循环时从队列头部不断取出同步消息执行。

如果我们想发送异步消息,那么在创建Handler时使用以下构造函数中的其中一种(async传true)

public Handler(boolean async);
public Handler(Callback callback, boolean async);
public Handler(Looper looper, Callback callback, boolean async);

为什么需要异步消息?

在Android系统中存在一个VSync消息,它主要负责每16ms更新一次屏幕展示。如果用户同步消息在16ms内没有执行完成,那么VSync消息的更新操作就无法执行在用户看来就出现了掉帧或卡顿的情况,为此Android开发要求每个消息的执行需要限制在16ms之内完成。但是消息队列中可能会包含多个同步消息,假如当前主线程消息队列有10个同步消息,每个同步消息要执行10ms,总共也就需要执行100ms,这段时间内就会有近7帧无法正常刷新展示,应用执行过程中遇到这种情况还是很普遍的。

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;
    private VsyncEventData mLastVsyncEventData = new VsyncEventData();

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource, 0);
    }

    // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
    // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
    // for the internal display implicitly.
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
            VsyncEventData vsyncEventData) {
        try {
            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW,
                        "Choreographer#onVsync " + vsyncEventData.id);
            }
            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame;
            mLastVsyncEventData = vsyncEventData;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        //在run方法里会从mCallbackQueue中取出消息并按照时间戳顺序调用mTraversalRunnable的run函数.
        doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
    }
}

Android系统设计时自然也会考虑到这种情况,同步消息会导致延迟,主要原因在于排队等候,如果消息发送后不必排队等待直接就执行就能够解决消息延迟问题。

Android系统中的异步消息就是专门解决消息处理延迟的问题,它需要配合同步屏障(SyncBarrier)一起工作,在发送异步消息的时候向消息队列投放同步屏障对象,消息队列会返回同步屏障的token,此时消息队列中的同步消息都会被暂停处理,优先执行异步消息处理,等异步消息处理完成再通过消息队列移除token对应的同步屏障,消息队列继续之前暂停的同步消息处理。MessageQueue中同步屏障处理的方法都是隐藏API,需要通过反射方法来调用。

// 反射执行投递同步屏障,省略try..catch
public void postSyncBarrier() {
    Method method = MessageQueue.class.getDeclaredMethod("postSyncBarrier");
    token = (int) method.invoke(Looper.getMainLooper().getQueue());
}

// 反射执行移除同步屏障,省略try..catch
public void removeSyncBarrier() {
    Method method = MessageQueue.class.getDeclaredMethod("removeSyncBarrier", int.class);
    method.invoke(Looper.getMainLooper().getQueue(), token);
}

同步屏障的应用: Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障。

MessageQueue之所以将同步屏障的接口都变成隐藏接口是不想普通的开发者向主线程队列投递同步屏障影响VSync消息的正常执行,开发过程中尽量不要使用异步消息和同步屏障。

6.1、丢帧

既然定时定频率的刷新,怎么还会丢帧呢?

造成丢帧一般有两种原因

  • View的绘制方法超过了16.6ms,下一帧的屏幕刷新信号已经来了,可是此时绘制方法还没有执行结束。

    我们的布局嵌套或者绘制时间过长,需要我们去优化布局。

  • 主线程一直在处理其他耗时的操作,导致无法读到同步屏障,,法读到这个异步消息。

    避免在主线程中执行耗时操作。

七、IdleHandler

之前的同步屏障我们提到了如何提高消息队列中消息的优先级,那有些消息可能就比较懂事了。

他们知道轮询的消息机一直很忙,又提出了一个需求:大哥,我知道你很忙,你先处理你要干的事,我这个活吧,优先级别不是特别高,能不能不忙的时候帮我干一下我的活?

其实Android内部已经提供了一个IdleHandler的接口,帮我们去做这个逻辑判断了。

在MessageQueue中可以看到这么一个接口:

public static interface IdleHandler {
    /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    //如果返回false,用完就会移除这个接口,相当与使用一次,后面这个消息就从队列移除了。
	//返回true就会保留,在下次Looper空闲时继续处理。比如在主线程return true,就会出现一直轮询。
    boolean queueIdle();
}

当MessageQueue为空,没有消息或者MessageQueue中最近需要处理的消息是延迟消息时,此时都会尝试执行IdleHandler。

使用:

Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                /*dosometing*/
                return false;
            }
        });

7.1、使用场景

  • gc机制。
  • LeakCanary也是使用了IdleHandler判断内存泄露的。它对内存的dump分析过程,就是在IdleHandler中处理的,从而避免对主线程的影响。
  • 或者让开发者头疼的App启动优化,我们有些优先级别较低的缓存加载策略,就可以使用IdleHandler。

八、Message的new方法和obtain方法的区别?

  • new方法 每次需要Message对象的时候都创建一个新的对象,每次都要去堆内存开辟对象存储空间,对象使用完后,jvm又要去对这个废弃的对象进行垃圾回收

  • obtain方法 采用单链表,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率

原理图解:

20210415151728583.png

咱们对着源码来看: 假设该链表初始状态如下

image.png

执行Message m = sPool;就变成下图

image.png

继续sPool = m.next;

image.png

然后m.next = null;

image.png

接下来m.flags=0;sPoolSize–;return m;便是表示m指向的对象已经从链表中取出并返回了。

再看回收recycle():

然后再看看sPoolSize是什么时候自增的。按图索骥便可找到recycle()方法和recycleUnchecked()方法。前者供开发者调用进行回收,后者执行回收操作。来看看回收操作都干了啥:

void recycleUnchecked() {    // Mark the message as in use while it remains in the recycled object pool.    // Clear out all other details.
    flags = FLAG_IN_USE;
    what = 0;
    arg1 = 0;
    arg2 = 0;
    obj = null;
    replyTo = null;
    sendingUid = -1;    when = 0;
    target = null;
    callback = null;
    data = null;

synchronized (sPoolSync) {       if (sPoolSize < MAX_POOL_SIZE) {
        next = sPool;
        sPool = this;
        sPoolSize++;
    }
}

}

前半段不必多说,显然是“重置”改对象的个个字段。后半段又是一个同步代码段,同样用图来解释一下(假设当前代码为message.recycle(),则需要被回收的则是message对象)。 假设当前链表如下:

image.png

执行next=sPool;

image.png

执行sPool=this;

image.png

现在可以很清楚的看到,Message类本身就组织了一个栈结构的缓冲池。并使用obtain()方法和recycler()方法来取出和放入。

九、Looper.loop分析

 /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;
        /*
         * 删除了一些无关代码
         */
        for (;;) {
            // 这里循环从queue中获取Message消息,如果没有消息的话这里会阻塞
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            try {
                //msg.target 对象是发送Message消息的Handler对象,通过Handler的dispatchMessage进行消息处理
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
             /*
              * 删除了一些无关代码
              */
            // 消息处理完后,将Message放入对象池中,这样Message.obtain()获取Message时候可以减少对象的创建
            msg.recycleUnchecked();
        }
    }
  • loop死循环会不会使主线程阻塞?

    会。

    对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环阻塞便能保证不会被退出。

  • loop死循环会不会卡死?会不会ANR?

    不会。

    ANR 和 Looper.loop()的阻塞 是两个不同的概念。

    • Looper.loop()阻塞是消息队列为空,在等待新的消息,然后进行处理。
    • ANR 是消息队列不为空的时候,程序在处理某一次的Message时,系统检测耗时太久,提示的ANR。
  • 主线程既然阻塞,会不会消耗性能?

    不会。

    MessageQueue底层采用了epoll进行阻塞。简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态;直到下个消息到达或者有事务发生,通过nativeWake唤醒之前的线程,继续执行queue.next()。

  • loop死循环又如何去处理其他事务?

    Activity的生命周期是怎么实现在死循环体外能够执行起来的?

    在代码ActivityThread.main()中, 通过thread.attach(false);,在进入死循环之前创建了新binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程.

    image.png

    结合图说说Activity生命周期,比如暂停Activity,流程如下:

    • 线程1的AMS中调用线程2的ATP;(由于同一个进程的线程间资源共享,可以相互直接调用,但需要注意多线程并发问题)

    • 线程2通过binder传输到App进程的线程4;

    • 线程4通过handler消息机制,将暂停Activity的消息发送给主线程;

    • 主线程在looper.loop()中循环遍历消息,当收到暂停Activity的消息时,便将消息分发给ActivityThread.H.handleMessage()方法,再经过方法的调用,最后便会调用到Activity.onPause(),当onPause()处理完后,继续循环loop下去。