Handler 机制

708 阅读19分钟

Handler内部原理

Handler 是 Android 中引入的一种让开发者参与处理线程中消息循环的机制。

消息机制(Handler)主要包含:

Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;

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

Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);

Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。

使用方式

class LooperThread extends Thread {
    public Handler mHandler;

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

        mHandler = new Handler() {
            public void handleMessage(Message msg) {
                
            }
        };

        Looper.loop();
    }
}

Looper

用于为线程执行消息循环的类。线程默认没有关联的消息循环,如果要创建一个,可以在执行消息循环的线程里面调用 prepare() 方法,然后调用 loop() 处理消息,直到循环停止。

Looper 是用来使线程中的消息循环起来的。默认情况下当我们创建一个新的线程的时候,这个线程里面是没有消息队列 MessageQueue 的。为了让线程能够绑定一个消息队列,我们需要借助于 Looper :首先我们要调用 Looper 的 prepare() 方法,然后调用 Looper 的 Loop() 方法。

image.png

ThreadLocal 是一个 本地线程副本变量工具类 。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。

image.png

从上面的结构图,我们已经窥见 ThreadLocal 的 核心机制:

  1. 每个 Thread 线程内部都有一个 Map。
  2. Map 里面存储线程本地对象(key)和线程的变量副本(value)
  3. Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

ThreadLocal.set()

image.png

ThreadLocal.get()

image.png

Loop.loop()

image.png

image.png

MessageQueue

每个线程内部都维护了一个消息队列 —— MessageQueue。消息队列 MessageQueue,顾名思义,就是存放消息的队列。那队列中存储的消息是什么呢?

我们假设一个场景:在 UI 界面上单击了某个按钮,而此时程序又恰好收到了某个广播事件,那我们如何处理这两件事呢?因为一个线程在某一时刻只能处理一件事情,不能同时处理多件事情,所以我们不能同时处理按钮的单击事件和广播事件,我们只能挨个对其进行处理,只要挨个处理就要有处理的先后顺序。

为此 Android 把 UI 界面上单击按钮的事件封装成了一个 Message ,将其放入到 MessageQueue 里面去,即将单击按钮事件的 Message 入栈到消息队列中,然后再将广播事件的封装成 Message ,也将其入栈到消息队列中。

也就是说一个 Message 对象表示的是线程需要处理的一件事情,消息队列就是一堆需要处理的 Message 的池。线程 Thread 会依次取出消息队列中的消息,依次对其进行处理。

MessageQueue 中有两个比较重要的方法,一个是 enqueueMessage() 方法,一个是 next() 方法。

enqueueMessage() 方法用于将一个 Message 放入到消息队列 MessageQueue 中,next() 方法是从消息队列 MessageQueue 中阻塞式地取出一个 Message。

MessageQueue.next()

image.png

image.png

image.png

image.png

image.png

image.png

nativePollOnce 是阻塞操作,其中 nextPollTimeoutMillis 代表下一个消息到来前,还需要等待的时长;当 nextPollTimeoutMillis = -1 时,表示消息队列中无消息,会一直等待下去。

当处于空闲时,往往会执行 IdleHandler 中的方法。当 nativePollOnce() 返回后,next() 从 mMessages 中提取一个消息。

MessageQueue.enqueueMessage()

image.png

image.png

image.png

image.png

MessageQueue 是按照 Message 触发时间的先后顺序排列的,队头的消息是将要最早触发的消息。当有消息需要加入消息队列时,会从队列头开始遍历,直到找到消息应该插入的合适位置,以保证所有消息的时间顺序。

MessageQueue.removeMessages()

image.png

image.png

这个移除消息的方法,采用了两个 while 循环,第一个循环是从队头开始,移除符合条件的消息,第二个循环是从头部移除完连续的满足条件的消息之后,再从队列后面继续查询是否有满足条件的消息需要被移除。

Handler

image.png

image.png

image.png

image.png

首先会判断 msg.callback 存不存在,msg.callback 是 Runnable 类型,如果 msg.callback 存在,那么说明该 Message 是通过执行 Handler 的 post() 系列方法将 Message 放入到消息队列中的,这种情况下会执行 handleCallback()。

如果我们不是通过 post() 系列方法将 Message 放入到消息队列中的,那么 msg.callback 就是 null ,代码继续往下执行。

接着我们会判断 Handler 的成员字段 mCallback 存不存在。mCallback 是 Hanlder.Callback 类型的,我们在上面提到过,在 Handler 的构造函数中我们可以传递 Hanlder.Callback 类型的对象,该对象需要实现 handleMessage() 方法,如果我们在构造函数中传递了该 Callback 对象,那么我们就会让 Callback 的 handleMessage() 方法来处理 Message。

如果我们在构造函数中没有传入 Callback 类型的对象,那么 mCallback 就为 null ,那么我们会调用 Handler 自身的 hanldeMessage() 方法,该方法默认是个空方法,我们需要自己重写实现该方法。

综上所述,我们可以看到 Handler 提供了三种途径处理 Message ,而且处理有前后优先级之分:首先尝试让 post() 中传递的 Runnable 执行,其次尝试让 Handler 构造函数中传入的 Callback 的 handleMessage() 方法处理,最后才是让 Handler 自身的 handleMessage() 方法处理Message。

Handler 与 Looper

public class Handler {
    // ...
    
    /**
     * ...
     * @hide
     */
    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;
        // ...
    }

    /**
     * ...
     * @hide
     */
    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = mLooper.mQueue;
        // ...
    }

    // ...

    final Looper mLooper;
    final MessageQueue mQueue;
    // ...
}

Handler 对象里有 final Looper 成员,所以一个 Handler 只会对应一个固定的 Looper 对象。构造 Handler 对象的时候如果不传 Looper 参数,会默认使用当前线程关联的 Looper,如果当前线程没有关联 Looper,会抛出异常。

那么能不能绑定多个 Handler 到同一个 Looper 呢?答案是可以的。例如以下例子,就绑定了两个 Handler 到主线程的 Looper 上,并都能正常使用(日志 receive msg: 1 和 receive msg: 2 能依次输出)。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private Handler mHandler1;
    private Handler mHandler2;

    private Handler.Callback mCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            Log.v(TAG, "receive msg: " + msg.what);
            return false;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mHandler1 = new Handler(mCallback);
        mHandler2 = new Handler(mCallback);

        mHandler1.sendEmptyMessage(1);
        mHandler2.sendEmptyMessage(2);
    }
}

小结: Handler 与 Looper 是多对一的关系,创建 Handler 实例时要么提供一个 Looper 实例,要么当前线程有关联的 Looper。

能否创建关联到其它线程的 Handler

创建 Handler 时会关联到一个 Looper,而 Looper 是与线程一一绑定的,所以理论上讲,如果能得到要关联的线程的 Looper 实例,这是可以实现的。

public final class Looper {
    // ...
    private static Looper sMainLooper;  // guarded by Looper.class
    // ...
    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
}

可见获取主线程的 Looper 是能实现的,平时写代码过程中,如果要从子线程向主线程添加一段执行逻辑,也经常这么干,这是可行的:

// 从子线程创建关联到主线程 Looper 的 Handler
Handler mHandler = new Handler(Looper.getMainLooper());

mHandler.post(() -> {
        // ...
        });

从子线程创建关联到其它子线程的 Looper :

new Thread() {
    @Override
    public void run() {
        setName("thread-one");
        Looper.prepare();

        final Looper threadOneLooper = Looper.myLooper();

        new Thread() {
            @Override
            public void run() {
                setName("thread-two");
                Handler handler = new Handler(threadOneLooper);

                handler.post(() -> {
                        Log.v("test", Thread.currentThread().getName());
                        });
            }
        }.start();

        Looper.loop();
    }
}.start();

执行后日志输出为 thread-one。

小结: 可以从一个线程创建关联到另一个线程 Looper 的 Handler,只要能拿到对应线程的 Looper 实例。

消息可以插队吗

答案是可以的,使用 Handler.sendMessageAtFrontOfQueue 和 Handler.postAtFrontOfQueue 这两个方法,它们会分别将 Message 和 Runnable(封装后)插入到消息队列的队首。

小结: 消息可以插队,使用 Handler.xxxAtFrontOfQueue 方法。

消息可以撤回吗

可以用 Handler.hasXXX 系列方法判断关联的消息队列里是否有等待中的符合条件的 Message 和 Runnable,用 Handler.removeXXX 系列方法从消息队列里移除等待中的符合条件的 Message 和 Runnable。

小结: 尚未分发的消息是可以撤回的,处理过的就没法了。

MessageQueue.next()

Message next() {
    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        // 调用JNI函数Poll消息。nextPollTimeoutMillis是消息队列中没消息时的等待时间。
        // (01) nextPollTimeoutMillis = 0,不等待。
        // (02) nextPollTimeoutMillis = -1,无限等待。
        nativePollOnce(mPtr, nextPollTimeoutMillis);

        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 如果当前消息非空,但是当前消息的目标是空;则获取下一则消息。
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                if (now < msg.when) {
                    // 如果消息队列中有消息,并且当前时间小于于消息中的执行时间,
                    // 则设置消息的等待时间
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 如果消息队列中有消息,并且当前时间大于/等于消息中的执行时间,
                    // 则将该消息返回给Looper。
                    mBlocked = false;
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    if (false) Log.v("MessageQueue", "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // 如果消息队列中无消息,则设置nextPollTimeoutMillis=-1;
                // 下次调用nativePollOnce()时,则会进入无穷等待状态。
                nextPollTimeoutMillis = -1;
            }

            // 如主线程调用的quit()函数,则退出消息循环。
            if (mQuitting) {
                dispose();
                return null;
            }

            // 查看空闲等待(不是忙等待)对应的pendingIdleHandlerCount数量。
            // 如果pendingIdleHandlerCount=0,则继续下一次循环。
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }

            if (mPendingIdleHandlers == null) {
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            // 将mIdleHandlers转换位数组
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }

        // 执行mPendingIdleHandlers中每一个IdleHandler的queueIdle(),
        // 即,进行空闲等待。
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler

            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
                Log.wtf("MessageQueue", "IdleHandler threw exception", t);
            }

            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }

        // Reset the idle handler count to 0 so we do not run them again.
        pendingIdleHandlerCount = 0;

        // While calling an idle handler, a new message could have been delivered
        // so go back and look again for a pending message without waiting.
        nextPollTimeoutMillis = 0;
    }
}

next()的作用是获取消息队列的下一条待处理消息。方法中使用了 SystemClock.uptimeMillis() 方法获取了当前的时间。

Handler.postDelayed

public final boolean postDelayed(Runnable r, long delayMillis)
{
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

将 Runnable r 添加到消息队列中,r 将在 delayMillis 事件后被执行。r 将在 Handler 绑定的线程执行任务。delayMillis 是基于 SystemClock#uptimeMillis 做增加的。如果之后发生深度睡眠将导致 r 延后执行。

子线程 真的不能更新UI ?

 1android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
 2        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8820)
 3        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1530)
 4        at android.view.View.requestLayout(View.java:24648)
 5        at android.widget.TextView.checkForRelayout(TextView.java:9752)
 6        at android.widget.TextView.setText(TextView.java:6326)
 7        at android.widget.TextView.setText(TextView.java:6154)
 8        at android.widget.TextView.setText(TextView.java:6106)
 9        at com.hfy.demo01.MainActivity$9.run(MainActivity.java:414)
10        at android.os.Handler.handleCallback(Handler.java:888)
11        at android.os.Handler.dispatchMessage(Handler.java:100)
12        at android.os.Looper.loop(Looper.java:213)
13        at android.app.ActivityThread.main(ActivityThread.java:8147)
14        at java.lang.reflect.Method.invoke(Native Method)
15        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
16        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)

一般情况,我们在子线程直接操作UI,没有用handler切到主线程,就会报这个错。

那如果我说,我这里的这个错误就发生在 主线程,你信吗?

下面是具体代码,handleAddWindow()按在MainActivity 的onCreate中执行。

 1    private void handleAddWindow() {
 2
 3        //子线程创建window,只能由这个子线程访问 window的view
 4        Button button = new Button(MainActivity.this);
 5        button.setText("添加到window中的button");
 6        button.setOnClickListener(new View.OnClickListener() {
 7            @Override
 8            public void onClick(View view) {
 9                MyToast.showMsg(MainActivity.this, "点了button");
10            }
11        });
12
13        new Thread(new Runnable() {
14            @Override
15            public void run() {
16                //因为添加window是IPC操作,回调回来时,需要handler切换线程,所以需要Looper
17                Looper.prepare();
18
19                addWindow(button);
20
21                new Handler().postDelayed(new Runnable() {
22                    @Override
23                    public void run() {
24                        button.setText("文字变了!!!");
25                    }
26                },3000);
27
28                //开启looper,循环取消息。
29                Looper.loop();
30            }
31        }).start();
32
33        //这里执行就会报错:Only the original thread that created a view hierarchy can touch its views.
34        new Handler().postDelayed(new Runnable() {
35            @Override
36            public void run() {
37                button.setText("文字 you 变了!!!");
38            }
39        },4000);
40    }
41
42    private void addWindow(Button view) {
43        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
44                WindowManager.LayoutParams.WRAP_CONTENT,
45                WindowManager.LayoutParams.WRAP_CONTENT,
46                0, 0,
47                PixelFormat.TRANSPARENT
48        );
49        // flag 设置 Window 属性
50        layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
51        // type 设置 Window 类别(层级)
52        layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
53        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
54            layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
55        }
56
57        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
58        layoutParams.x = 100;
59        layoutParams.y = 100;
60
61        WindowManager windowManager = getWindowManager();
62        windowManager.addView(view, layoutParams);
63    }

主要是:开了个子线程,然后添加了一个系统window,window中只有一个button。然后3秒后在子线程中直接改变Button的文字,然后又过一秒,在主线程中再改变button文字。

首先,我们看报错原因的描述:Only the original thread that created a view hierarchy can touch its views. 翻译就是说 只有创建了view树的线程,才能访问它的子view。并没有说子线程一定不能访问UI。那可以猜想到,button的确实是在子线程被添加到window中的,子线程确实可以直接访问,而主线程访问确实会抛出异常。看来可以解释这个错误的原因了。

错误的发生在ViewRootImpl的checkThread方法中,且UI的更新都会走到这个方法:

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

通过window的相关知识,我们知道,调用windowManager.addView添加window时会给这个window创建一个ViewRootImpl实例:

 1    public void addView(View view, ViewGroup.LayoutParams params,
 2            Display display, Window parentWindow) {
 3        ...
 4
 5            ViewRootImpl root;
 6            View panelParentView = null;
 7        ...
 8            root = new ViewRootImpl(view.getContext(), display);
 9            view.setLayoutParams(wparams);
10            mViews.add(view);
11            mRoots.add(root);
12            mParams.add(wparams);
13...
14        }
15    }

然后ViewRootImpl构造方法中会拿到当前的线程,

1    public ViewRootImpl(Context context, Display display) {
2        mContext = context;
3        ...
4        mThread = Thread.currentThread();
5        ...
6    }

所以在ViewRootImpl的checkThread()中,确实是 拿 当前想要更新UI的线程 和 添加window时的线程作比较,不是同一个线程机会报错。

通过window的相关知识,我们还知道,Activity也是一个window,window的添加是在ActivityThread的handleResumeActivity()。ActivityThread就是主线程,所以Activity的view访问只能在主线程中。

一般情况,UI就是指Activity的view,这也是我们通常称主线程为UI线程的原因,其实严谨叫法应该是activity的UI线程。而我们这个例子中,这个子线程也可以称为button的UI线程。

那为啥要一定需要checkThread呢?根据handler的相关知识:

因为UI控件不是线程安全的那为啥不加锁呢?一是加锁会让UI访问变得复杂;二是加锁会降低UI访问效率,会阻塞一些线程访问UI所以干脆使用单线程模型处理UI操作,使用时用Handler切换即可

Toast可以在子线程show吗?答案是可以的

 1        new Thread(new Runnable() {
 2            @Override
 3            public void run() {
 4                //因为添加window是IPC操作,回调回来时,需要handler切换线程,所以需要Looper
 5                Looper.prepare();
 6
 7                addWindow(button);
 8
 9                new Handler().postDelayed(new Runnable() {
10                    @Override
11                    public void run() {
12                        button.setText("文字变了!!!");
13                    }
14                },3000);
15
16                Toast.makeText(MainActivity.this, "子线程showToast", Toast.LENGTH_SHORT).show();
17
18                //开启looper,循环取消息。
19                Looper.loop();
20            }
21        }).start();

在上面的例子,线程中showToast,运行发现确实可以的。因为根据window的相关知识,知道Toast也是window,show的过程就是添加Window的过程。

另外注意1,这个线程中Looper.prepare()和Looper.loop(),这是必要的。 因为添加window的过程是和WindowManagerService进行IPC的过程,IPC回来时是执行在binder线程池的,而ViewRootImpl中是默认有Handler实例的,这个handler就是用来切换binder线程池的消息到当前线程。另外Toast还与NotificationMamagerService进行IPC,也是需要Handler实例。既然需要handler,那所以线程是需要looper的。另另外Activity还与ActivityManagerService进行IPC交互,而主线程是默认有Looper的。 扩展开,想在子线程show Toast、Dialog、popupWindow、自定义window,只要在前后调Looper.prepare()和Looper.loop()即可。

另外注意2,在activity的onCreate到首次onResume的时期,创建子线程在其中更新UI也是可以的。这不是违背上面的结论了吗?其实没有,上面说了,因为Activity的window添加在首次onResume之后执行的的,那ViewRootImpl的创建也是在这之后,所以也就无法checkThread了。实际上这个时期也不checkThread,因为View根本还没有显示出来。

Android 中的时间

System.nanoTime():单位:纳秒。android系统开机到当前的时间。

System.currentTimeMillis():单位:毫秒。从1970.1.1 UTC 零点开始到当前的时间。

SystemClock.uptimeMillis():单位:毫秒。系统重启后将从0开始计算,android系统进入深度睡眠后将会暂停计时。

SystemClock.elapsedRealtime():单位:毫秒。系统重启后将重新开始计算,android系统进入深度睡眠,不会停止计时。

SystemClock.currentThreadTimeMillis:单位:毫秒。线程running的时间,线程Sleep的时间不会计入。

小结

如果想要避免用户修改时间,网络校准时间对时间间隔统计的影响,使用SystemClock类相关的方法就可以了,至于选择upTimeMillis()还是elapsedRealtime()就要根据自己的需求确定了。 系统还提供了几个时间控制相关的工具:

  • 标准方法Thread.sleep(long millis) 和 Object.wait(long millis)是基于SystemClock.upTimeMillis()的。所以在系统休眠之后它们的回调也会延期,直到系统被唤醒才继续计时。并且这两个同步方法会响应InterruptException,所以在使用它们的时候必须要处理InterruptException异常。
  • SystemClock.sleep(long millis) 与 Thread.sleep(long millis) 方法是类似的,只不过SystemClock.sleep(long millis) 不响应InterruptException异常。
  • Handler类的 postDelay()方法也是基于SystemClock.upTimeMillis()方法的。
  • AlarmManager可以定时发送消息,即使在系统睡眠、应用停止的状态下也可以发送。我们在创建定时事件的时候有两个参数可以选择RTC和ELAPSED_REALTIME,它们对应的方法就是System.currentTimeMillis() ~ RTC,SystemClock.elapsedRealtime() ~ ELAPSED_REALTIME。这样一对应,它们的区别也就非常明显了。

Handler同步屏障

其实同步屏障对于我们的日常使用的话其实是没有多大用处。因为设置同步屏障和创建异步Handler的方法都是标志为hide,说明谷歌不想要我们去使用他。

同步屏障机制是一套为了让某些特殊的消息得以更快被执行的机制。

注意这里我在同步屏障之后加上了机制二字,原因是单纯的同步屏障并不起作用,他需要和其他的Handler组件配合才能发挥作用。

这里我们假设一个场景:我们向主线程发送了一个UI绘制操作Message,而此时消息队列中的消息非常多,那么这个Message的处理可能会得到延迟,绘制不及时造成界面卡顿。同步屏障机制的作用,是让这个绘制消息得以越过其他的消息,优先被执行。

MessageQueue中的Message,有一个变量isAsynchronous,他标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。同时还有另一个变量target,标志了这个Message最终由哪个Handler处理。

我们知道每一个Message在被插入到MessageQueue中的时候,会强制其target属性不能为null,如下代码:

MessageQueue.class

boolean enqueueMessage(Message msg, long when) {
  // Hanlder不允许为空
  if (msg.target == null) {
      throw new IllegalArgumentException("Message must have a target.");
  }
  ...
}

而android提供了另外一个方法来插入一个特殊的消息,强行让target==null:

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        // 把当前需要执行的Message全部执行
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 插入同步屏障
        if (prev != null) { 
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

代码有点长,重点在于:没有给Message赋值target属性,且插入到Message队列头部。当然源码中还涉及到延迟消息,我们暂时不关心。这个target==null的特殊Message就是同步屏障.(ps: 如果同步屏障设置了延迟,并不是直接插入到链表头,而是会先处理前面的同步消息。)

MessageQueue在获取下一个Message的时候,如果碰到了同步屏障,那么不会取出这个同步屏障,而是会遍历后续的Message,找到第一个异步消息取出并返回。这里跳过了所有的同步消息,直接执行异步消息。为什么叫同步屏障?因为它可以屏蔽掉同步消息,优先执行异步消息。

我们来看看源码是怎么实现的:

Message next() {
    ···
    if (msg != null && msg.target == null) {
        // 同步屏障,找到下一个异步消息
        do {
            prevMsg = msg;
            msg = msg.next;
        } while (msg != null && !msg.isAsynchronous());
    }
    ···
}

如果遇到同步屏障,那么会循环遍历整个链表找到标记为异步消息的Message,即isAsynchronous返回true,其他的消息会直接忽视,那么这样异步消息,就会提前被执行了。 注意,同步屏障不会自动移除,使用完成之后需要手动进行移除,不然会造成同步消息无法被处理。我们可以看一下源码:

Message next() {
    ...
    // 阻塞时间
    int nextPollTimeoutMillis = 0;
    for (;;) {
        // 阻塞对应时间 
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                // 同步屏障,找到下一个异步消息
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            // 如果上面有同步屏障,但却没找到异步消息,
            // 那么msg会循环到链表尾,也就是msg==null
            if (msg != null) {
                ···
            } else {
                // 没有消息,进入阻塞状态
                nextPollTimeoutMillis = -1;
            }
            ···
        }
    }
}

可以看到如果没有即时移除同步屏障,他会一直存在且不会执行同步消息。因此使用完成之后必须即时移除。

上面我们了解到了同步屏障的作用,但是会发现postSyncBarrier方法被标记为@hide,也就是我们无法调用这个方法。但我们可以发异步消息啊。在系统添加同步屏障的时候,不就可以趁机上车了,是吧。

添加异步消息有两种办法:

使用异步类型的Handler发送的全部Message都是异步的 给Message标志异步

给Message标记异步是比较简单的,通过setAsynchronous方法即可。

Handler有一系列带Boolean类型的参数的构造器,这个参数就是决定是否是异步Handler:

public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    // 这里赋值
    mAsynchronous = async;
}

在发送消息的时候就会给Message赋值:

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();
	// 赋值
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

但是异步类型的Handler构造器是标记为hide,我们无法使用,但在api28之后添加了两个重要的方法:

public static Handler createAsync(@NonNull Looper looper) {
    if (looper == null) throw new NullPointerException("looper must not be null");
    return new Handler(looper, null, true);
}

    
public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
    if (looper == null) throw new NullPointerException("looper must not be null");
    if (callback == null) throw new NullPointerException("callback must not be null");
    return new Handler(looper, callback, true);
}

通过这两个api就可以创建异步Handler了,而异步Handler发出来的消息则全是异步的。

public void setAsynchronous(boolean async) {
    if (async) {
        flags |= FLAG_ASYNCHRONOUS;
    } else {
        flags &= ~FLAG_ASYNCHRONOUS;
    }
}

异步消息需要同步屏障的辅助,但同步屏障我们无法手动添加,因此了解系统何时添加和删除同步屏障是非常必要的。只有这样,才能更好地运用异步消息这个功能,知道为什么要用和如何用。

了解同步屏障需要简单了解一点屏幕刷新机制的内容。放心,只需要了解一丢丢就可以了。

我们的手机屏幕刷新频率有不同的类型,60Hz、120Hz等。60Hz表示屏幕在一秒内刷新60次,也就是每隔16.6ms刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMesure()、onLayout()、onDraw()这些方法。好了,大概了解这么多就可以了。

了解过 view 绘制原理的读者应该知道,view绘制的起点是在 viewRootImpl.requestLayout() 方法开始,这个方法会去执行上面的三大绘制任务,就是测量布局绘制。但是,重点来了:

调用requestLayout()方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置 VSYNC 信号监听。 当 VSYNC 信号的到来,会发送一个异步消息到主线程Handler,执行我们上一步设置的绘制监听任务,并移除同步屏障

这里我们只需要明确一个情况:调用requestLayout()方法之后会设置一个同步屏障,直到VSYNC信号到来才会执行绘制任务并移除同步屏障。

那,这样在等待VSYNC信号的时候主线程什么事都没干?是的。这样的好处是:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿。但这样也带来了相对应的代价:

  • 我们的同步消息最多可能被延迟一帧的时间,也就是16ms,才会被执行
  • 主线程Looper造成过大的压力,在VSYNC信号到来之时,才集中处理所有消息
ps:【在等待VSYNC信号的时候主线程什么事都没干?是的。】插入同步屏障之后会阻止同步消息执行,导致即使有同步消息在队列中,也不会进行处理,也就是“什么都没干”;但如果前面的同步任务的执行延缓了绘制请求任务的执行、或者队列中有异步消息,此时主线程并不会“什么都没干”。

改善这个问题办法就是:使用异步消息。当我们发送异步消息到MessageQueue中时,在等待VSYNC期间也可以执行我们的任务,让我们设置的任务可以更快得被执行且减少主线程Looper的压力。

可能有读者会觉得,异步消息机制本身就是为了避免界面卡顿,那我们直接使用异步消息,会不会有隐患?这里我们需要思考一下,什么情况的异步消息会造成界面卡顿:异步消息任务执行过长、异步消息海量。

如果异步消息执行时间太长,那即时是同步任务,也会造成界面卡顿,这点应该都很好理解。其次,若异步消息海量到达影响界面绘制,那么即使是同步任务,也是会导致界面卡顿的;原因是MessageQueue是一个链表结构,海量的消息会导致遍历速度下降,也会影响异步消息的执行效率。所以我们应该注意的一点是:

不可在主线程执行重量级任务,无论异步还是同步。

阻塞唤醒原理

有时我们 MessageQueue 中的消息都不是当下就需要执行的,而是要过一段时间,此时如果 Looper 仍然不断进行循环肯定是一种对于资源的浪费。因此 Handler 设计了这样一种阻塞唤醒机制使得在当下没有需要执行的消息时,就将 Looper 的 loop 过程阻塞,直到下一个任务的执行时间到达或者一些特殊情况下再将其唤醒,从而避免了上述的资源浪费。

epoll

这个阻塞唤醒机制是基于 Linux 的 I/O 多路复用机制 epoll 实现的,它可以同时监控多个文件描述符,当某个文件描述符就绪时,会通知对应程序进行读/写操作。

epoll 主要有三个方法,分别是 epoll_create、epoll_ctl、epoll_wait。

epoll_create

其功能主要是创建一个 epoll 句柄并返回,传入的 size 代表监听的描述符个数(仅仅是初次分配的 fd 个数)

epoll_ctl

其功能是对 epoll 事件进行注册,会对该 fd 执行指定的 op 操作.

epoll_wait

其功能是等待事件的上报.

当调用了该方法后,会进入阻塞状态,等待 epfd 上的 IO 事件,若 epfd 监听的某个文件描述符发生前面指定的 event 时,就会进行回调,从而使得 epoll 被唤醒并返回需要处理的事件个数。若超过了设定的超时时间,同样也会被唤醒并返回 0 避免一直阻塞。

native 阻塞实现

我们看看阻塞,它的实现就在我们之前看到的 MessageQueue::next 中,当发现要返回的消息将来才会执行,则会计算出当下距离其将要执行的时间还差多少毫秒,并调用 nativePollOnce 方法将返回的过程阻塞到指定的时间。

nativePollOnce 很显然是一个 native 方法,它最后调用到了 Looper 这个 native 层类的 pollOnce 方法。

int Looper::pollInner(int timeoutMillis) {
    // ...
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;
    mPolling = true; 
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
	// 1
	int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

	// ...
    return result;
}

可以发现,这里在 1 处调用了 epoll_wait 方法,并传入了我们之前在 natviePollOnce 方法传入的当前时间距下个任务执行时间的差值。这就是我们的阻塞功能的核心实现了,调用该方法后,会一直阻塞,直到到达我们设定的时间或之前我们在 epoll 的 fd 中注册的几个 fd 发生了 IO。其实到了这里我们就可以猜到,nativeWake 方法就是通过对注册的 mWakeEventFd 进行操作从而实现的唤醒。

native 唤醒

nativeWake 方法最后通过 NativeMessageQueue 的 wake 方法调用到了 Native 下 Looper 的 wake 方法:

void Looper::wake() {
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
    if (nWrite != sizeof(uint64_t)) {
        if (errno != EAGAIN) {
            ALOGW("Could not write wake signal, errno=%d", errno);
        }
    }
}

这里其实就是调用了 write 方法,对 mWakeEventFd 中写入了 1,从而使得监听该 fd 的 pollOnce 方法被唤醒,从而使得 Java 中的 next 方法继续执行。

那我们再回去看看,在什么情况下,Java 层会调用 natvieWake 方法进行唤醒呢? MessageQueue 类中调用 nativeWake 方法主要有下列几个时机:

  • 调用 MessageQueue 的 quit 方法进行退出时,会进行唤醒
  • 消息入队时,若插入的消息在链表最前端(最早将执行)或者有同步屏障时插入的是最前端的异步消息(最早被执行的异步消息)
  • 移除同步屏障时,若消息列表为空或者同步屏障后面不是异步消息时

可以发现,主要是在可能不再需要阻塞的情况下进行唤醒。(比如加入了一个更早的任务,那继续阻塞显然会影响这个任务的执行)

卡顿与内存优化

UI卡顿通常产生的原因如下:

  • 系统CPU资源紧张,分配给APP主线程(UI线程)的CPU时间片减少。
  • UI线程中执行了大量的耗时任务,导致了UI线程视图刷新工作的阻塞。
  • Android虚拟机频繁执行GC操作导致的卡顿。由于GC会占用大量的系统资源,同时GC过程中会产生UI线程停顿,从而产生卡顿。
  • 过度绘制产生卡顿。过度绘制会导致GPU执行时间变长,从而产生丢帧现象。

Android中的应用程序是消息驱动的,也就是UI线程执行的所有操作,通常都会经过消息机制来进行传递(也就是Handler通信机制)。

Handler的handleMessage负责在UI线程中处理UI相关逻辑,如果我们能在handleMessage执行之前和handleMessage执行之后,分别插入一段我们的日志代码,不就可以实现UI任务执行时间的监控了吗?

public static void loop() {
    ……
    for (;;) {
        ……
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        ……
        消息处理相关逻辑
        ……
        msg.target.dispatchMessage(msg);
        ……

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        ……
    }
}

loop方法中有一个Printer类型的logging,它会在消息执行之前和消息执行之后,输出一行日志,用于标记消息执行的开始和结束。

我们只要记录开始日志和结束日志的时间差,就可以计算出该任务在UI线程的执行时间了,如果执行时间很长,则必然产生了卡顿。

那么,问题来了,我们如何监控这个Printer类型的日志呢?

private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

我们发现mLogging这个对象可以通过一个public方法进行设置!这简直太好了!我们可以通过setMessageLogging方法设置我们自己的Printer对象就可以实现卡顿的监控了!

public class HandlerBlockTask {
    private final static String TAG = "budaye";
    public final int BLOCK_TMME = 1000;
    private HandlerThread mBlockThread = new HandlerThread("blockThread");
    private Handler mHandler;

    private Runnable mBlockRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString() + "\n");
            }
            Log.d(TAG, sb.toString());
        }
    };
    public void startWork(){
        mBlockThread.start();
        mHandler = new Handler(mBlockThread.getLooper());
        Looper.getMainLooper().setMessageLogging(new Printer() {
            private static final String START = ">>>>> Dispatching";
            private static final String END = "<<<<< Finished";

            @Override
            public void println(String x) {
                if (x.startsWith(START)) {
                    startMonitor();
                }
                if (x.startsWith(END)) {
                    removeMonitor();
                }
            }
        });
    }

    private void startMonitor() {
        mHandler.postDelayed(mBlockRunnable, BLOCK_TMME);
    }
    private void removeMonitor() {
        mHandler.removeCallbacks(mBlockRunnable);
    }
}
  • Demo中,我们使用了一个工作线程mBlockThread来监控UI线程的卡顿。
  • 每次Looper的loop方法对消息进行处理之前,我们添加一个定时监控器。
  • 如果UI线程中的消息处理时间小于我们设定的阈值BLOCK_TMME,则取消已添加的定时器。
  • 当UI线程执行耗时任务,超过我们设定的阈值时,就会执行mBlockRunnable这个Rnnable,在它的run方法中,打印出主线程卡顿时的代码堆栈。
  • 我们把堆栈日志收集起来,进行归类分析,就可以定位到产生卡顿问题的具体代码行号了。

基于WatchDog原理的方案及代码实现

由于在Android 5.0及以上系统中,默认启动了SELinux机制,所以我们的App在线上场景中,不能获得ANR的系统日志。

我们想要监控的是UI线程的卡顿,如果卡顿超过了5s,系统就会ANR,那么我们可以设置一个阈值,比如4s,超过阈值的卡顿,我们把UI线程的运行堆栈上传到我们的分析后台。

那么,如何监控UI线程的长时间卡顿呢?

参考系统的WatchDog原理,我们启动一个卡顿检测线程,该线程定期的向UI线程发送一条延迟消息,执行一个标志位加1的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。

public class WatchDog {
    private final static String TAG = "budaye";
    //一个标志
    private static final int TICK_INIT_VALUE = 0;
    private volatile int mTick = TICK_INIT_VALUE;
    //任务执行间隔
    public final int DELAY_TIME = 4000;
    //UI线程Handler对象
    private Handler mHandler = new Handler(Looper.getMainLooper());
    //性能监控线程
    private HandlerThread mWatchDogThread = new HandlerThread("WatchDogThread");
    //性能监控线程Handler对象
    private Handler mWatchDogHandler;

    //定期执行的任务
    private Runnable mDogRunnable = new Runnable() {
        @Override
        public void run() {
            if (null == mHandler) {
                Log.e(TAG, "handler is null");
                return;
            }
            mHandler.post(new Runnable() {
                @Override
                public void run() {//UI线程中执行
                    mTick++;
                }
            });
            try {
                //线程休眠时间为检测任务的时间间隔
                Thread.sleep(DELAY_TIME);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //当mTick没有自增时,表示产生了卡顿,这时打印UI线程的堆栈
            if (TICK_INIT_VALUE == mTick) {
                StringBuilder sb = new StringBuilder();
                StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
                for (StackTraceElement s : stackTrace) {
                    sb.append(s.toString() + "\n");
                }
                Log.d(TAG, sb.toString());
            } else {
                mTick = TICK_INIT_VALUE;
            }
            mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
        }
    };

    /**
     * 卡顿监控工作start方法
     */
    public void startWork(){
        mWatchDogThread.start();
        mWatchDogHandler = new Handler(mWatchDogThread.getLooper());
        mWatchDogHandler.postDelayed(mDogRunnable, DELAY_TIME);
    }
}

帧率检测

Android在设计的时候,把帧频限定在了每秒60帧,当我们的APP的帧频60fps时,画面就会非常的流程。但是通常由于各种各样的原因,帧频很可能会小于60fps,这样就会出现丢帧现象,用户端表现为可感知的卡顿等现象。那面我们的帧频可以高于60fps吗,答案是否定的,这是因为界面刷新渲染依赖底层的VSYNC信号,VSYNC信号以每秒60次的频率发送给上层,并且高于60fps的帧频也是没有必要的,因为人眼与大脑之间的协作无法感知超过60fps的画面更新。

Android系统从4.1(API 16)开始加入 Choreographer 这个类来协调动画(animations)、输入(input)、绘制(drawing)三个UI相关的操作。

Choreographer 中文翻译过来是”编舞者“,字面上的意思就是优雅地指挥以上三个UI操作一起跳一支舞。Choreographer 从显示子系统接收定时脉冲(例如垂直同步——VSYNC 信号),然后安排工作以渲染下一个显示帧。

每个线程都有自己的 Choreographer,其他线程也可以发布回调以在 Choreographer 上运行,但它们是运行在 Choreographer 所属的 Looper 上。

通过 Choreographer 可以注册5种类型的回调:CALLBACK_INPUT(输入回调)、CALLBACK_ANIMATION(动画回调)、CALLBACK_INSETS_ANIMATION(inset updates 相关的 Animation回调)、CALLBACK_TRAVERSAL(遍历回调)和 CALLBACK_COMMIT(提交回调)。

Choreographer 提供了单例调用模式,系统源码中或者我们在开发中都是通过单例调用来使用 Choreographer 的。

FrameCallback 是和Choreographer 交互,在下一个 frame 被渲染时触发的接口类。开发者可以使用 Choreographer#postFrameCallback 设置自己的callback 与 Choreographer 交互,你设置的 callCack 会在下一个 frame 被渲染时触发。

ViewRootImpl 在开始绘制时会调用 Choreographer 的 postCallback 传递一个任务,Choreographer 同步完 VSYNC 信号后再执行这个任务完成绘制。

FrameDisplayEventReceiver(父类: DisplayEventReceiver) 的 scheduleVsync 调用了 native 层方法 nativeScheduleVsync 来实现 VSYNC 信号的请求(向 SurfaceFlinger 服务请求 Vsync 信号)。

下一次 VSYNC 信号接收后会调用 DisplayEventReceiver 的 dispatchVsync 方法。

最终,doFrame 方法顺序执行所有的事件回调。

想要实现线上用户的帧率监控,我们可以通过 Choreographer.FrameCallback 回调来实现帧率监控。

public class FpsTest implements Choreographer.FrameCallback {
    private long mLastFrameTimeNanos = 0; //最后一次时间
    private long mFrameTimeNanos = 0; //本次的当前时间
    private int mFpsCount = 0;

    public void startFps() {
        mLastFrameTimeNanos = System.nanoTime();
        Choreographer.getInstance().postFrameCallback(this);
        AsyncThreadTask.executeDelayed(runnable, 1000);
    }

    //定时任务
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            calculateFPS();
            AsyncThreadTask.executeDelayed(runnable, 1000);
        }
    };

    private void calculateFPS() {
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = mFrameTimeNanos;
            return;
        }
        float costTime = (float) (mFrameTimeNanos - mLastFrameTimeNanos) / 1000000000.0F;//纳秒转成毫秒。
        if (mFpsCount <= 0 && costTime <= 0.0F) {
            return;
        }
        int fpsResult = (int) (mFpsCount / costTime);
        mLastFrameTimeNanos = mFrameTimeNanos;
        mFpsCount = 0;
        Log.d("budaye", "当前帧率为:" + fpsResult);
    }

    @Override
    public void doFrame(long frameTimeNanos) {
        mFpsCount++;
        mFrameTimeNanos = frameTimeNanos;
        //注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
}
  1. 使用线程,执行一个异步定时任务,每1000ms执行一次,用于统计1秒内的帧率。

  2. 使用 Choreographer.getInstance().postFrameCallback(this) 注册 VSYNC 信号回调监听,当 VSYNC 信号返回时,会执行 doFrame 回调函数。

  3. 在 doFrame 方法中,我们统计每秒内的执行次数,以及记录当前帧的时间,并注册一下次监听。