从SystemUI源码学习子线程更新UI

2020-08-06  本文已影响0人  浪里_个郎

1,原生SystemUI更新状态栏图标


原生SystemUI中,PhoneStatusBarPolicy中通过StatusBarIconList提供的setIcon、removeIcon等方法在主线程直接操作UI,简单直接。
但这里存在一个问题,如果有一些Icon更新的通知来自于子线程,就需要把消息传递到主线程完成UI操作,否则会报错。SystemUI对这套传递机制做了一个封装,我们往下看。

2,其他应用更新状态栏图标

在应用中,我们不能直接访问StatusBarIconList的实例,需要通过StatusBarManager,以Binder的形式向系统服务请求修改状态栏Icon。CommandQueue作为IStatusBar的实现类,向IStatusBarService服务注册了回调,这样服务接收到请求,会把请求发送到CommandQueue。

CommandQueue本身既实现了IStatusBar回调接口,也作为回调的发起者。StatusBarIconList的实现类就实现了CommandQueue.Callbacks接口,并将自己向CommandQueue注册。这样CommandQueue接收到回调后,又向StatusBarIconControllerImpl发送请求,从而实现了其他应用调用StatusBarIconList提供的setIcon、removeIcon等方法操作状态栏的目的。

3,线程什么时候完成了切换?

为什么PhoneStatusBarPolicy更新状态栏Icon需要在主线程,而从StatusBarManager过来的运行在Binder线程的请求可以直接操作状态栏Icon?
细心的人一定看到了,在上图,有一个大大的Handler!没错,CommandQueue接到异步请求后,通过Handler,将消息发往了主线程:

//CommandQueue.java
    private Handler mHandler = new H(Looper.getMainLooper());

    public void setIcon(String slot, StatusBarIcon icon) {
        synchronized (mLock) {
            // don't coalesce these
            mHandler.obtainMessage(MSG_ICON, OP_SET_ICON, 0,
                    new Pair<String, StatusBarIcon>(slot, icon)).sendToTarget();
        }
    }

CommandQueue的setIcon函数中并没有更新UI,而是将消息通过Handler发往了主线程的MessageQueue。自己投的篮自己抢篮板,发消息的mHandler接收到消息后,就可以通知回调真正处理UI了,因为这时来自Binder线程的请求已经洗白,在主线程中执行了。

//CommandQueue.java
    private final class H extends Handler {
        private H(Looper l) {
            super(l);
        }

        public void handleMessage(Message msg) {
            final int what = msg.what & MSG_MASK;
            switch (what) {
                case MSG_ICON: {
                    switch (msg.arg1) {
                        case OP_SET_ICON: {
                            Pair<String, StatusBarIcon> p = (Pair<String, StatusBarIcon>) msg.obj;
                            for (int i = 0; i < mCallbacks.size(); i++) {
                                mCallbacks.get(i).setIcon(p.first, p.second);
                            }
                            break;
                        }
                        case OP_REMOVE_ICON:
                            for (int i = 0; i < mCallbacks.size(); i++) {
                                mCallbacks.get(i).removeIcon((String) msg.obj);
                            }
                            break;
                    }
                    break;
                }

最后总结下其他应用操作状态栏Icon的流程:


4,android中异步更新UI有几种方式?

一般我们会说有4种方式:
1,Handler发送Message
2,AsycTask
3,Activity.runOnUiThread
4,Handler.post(Runnable)
看起来选择很丰富嘛!但实际上,这四种方式本质上都是第一种方式!让我们逐一分析。

4.1,Handler发送消息

这种方法不多说了,原理请阅读Handler源码。前面SystemUI的例子用的就是Handler发送消息实现子线程通知主线程更新UI。

4.2,AsycTask

AsycTask本质是对Handler的封装。使用方法如下。
创建继承自AsycTask的类,并重写以下方法:

//耗时方法,运行在子线程
@WorkerThread
protected abstract Result doInBackground(Params... params);
//在doInBackground之前调用,在UI线程内执行
@MainThread
protected void onPreExecute() {
}
//在执行中,且在调用publishProgress方法时,在UI线程内执行,用于更新进度
@MainThread
protected void onProgressUpdate(Progress... values) {
}
//在doInBackground之后调用,在UI线程内执行
@MainThread
protected void onPostExecute(Result result) {
}

需要关注的,就是子线程转向主线程是如何做到的,可以看到就是通过Handler:

//子线程中通过Handler向主线程发送进度信息,从而在主线程执行onProgressUpdate函数
    @WorkerThread
    protected final void publishProgress(Progress... values) {
        if (!isCancelled()) {
            getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
                    new AsyncTaskResult<Progress>(this, values)).sendToTarget();
        }
    }

其他关于AsycTask的细节就不展开了。

4.3,Activity.runOnUiThread

这个就简单了,直接上代码:

    @Override
    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);  //又是Handler
        } else {
            action.run();
        }
    }

4.4,Handler.post(Runnable)

其实就是把Runnable封装成了Message,本质上还是通过Handler发Message:

    public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

5,深度思考

1,Android为什么不让子线程更新UI
因为android并没有对UI更新做并发处理(没有加锁),强行并发会导致UI显示混乱。

2,Android怎么控制不让子线程更新UI
Activity进行UI操作时,会是用ViewRootImpl做了线程检测:

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

ViewRootImpl会用保存的主线程(mThread )与当前执行线程作比较,如果当前不是主线程,则抛异常。

3,有没有办法在子线程更新UI?
有。Activity中的ViewRootImpl对象是在onResume方法回调之后才创建的,在这之前的onCreate方法,甚至是onResume方法里都可以实现子线程更新UI,因为此时还没有创建ViewRootImpl对象, 可以跳过线程检测的流程。

上一篇下一篇

猜你喜欢

热点阅读