再谈子线程-竟然能够在非UI线程中更新UI

咱们经常听到这么一句话:更新UI要在UI线程(或者说主线程)中去更新,不要在子线程中更新UI,而Android官方也建议咱们不要在非UI线程直接更新UI。java

事实是否是如此呢,作一个实验:android

更新以前:web

这里写图片描述

代码:数组

package com.bourne.android_common.ServiceDemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.bourne.android_common.R;

public class ThreadActivity extends AppCompatActivity {

    private Thread thread;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        textView = (TextView) findViewById(R.id.textView);

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("text text text");
            }
        });

        thread.start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

这里在Activity里面新建了一个子线程去更新UI,按理说会报错啊,但是执行结果是并无报错,如图所示:安全

这里写图片描述

接下来让线程休眠一下:多线程

package com.bourne.android_common.ServiceDemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import com.bourne.android_common.R;

public class ThreadActivity extends AppCompatActivity {

    private Thread thread;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        textView = (TextView) findViewById(R.id.textView);

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

                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                textView.setText("text text text");
            }
        });

        thread.start();
    }

}

应用报错,抛出异常:app

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its viewsless

只有建立View层次结构的线程才能修改View,咱们在非UI主线程里面更新了View,因此会报错ide


由于在OnCreate里面睡眠了一下才报错,这是为何呢?svg

Android经过检查咱们当前的线程是否为UI线程从而抛出一个自定义的AndroidRuntimeException来提醒咱们“Only the original thread that created a view hierarchy can touch its views”并强制终止程序运行,具体的实如今ViewRootImpl类的checkThread方法中:

@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})  
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {  
    // 省去海量代码………………………… 

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

    // 省去巨量代码…………………… 
}

这就是Android在4.0后对咱们作出的一个限制。

其实形成这个现象的根本缘由是:

尚未到执行checkThread方法去检查咱们的当前线程那一步。”Android对UI事件的处理须要依赖于Message Queue,当一个Msg被压入MQ处处理这个过程并不是当即的,它须要一段事件,咱们在线程中经过Thread.sleep(200)在等,在等什么呢?在等ViewRootImpl的实例对象被建立。”

ViewRootImpl的实例对象是在OnResume中建立的啊!

看onResume方法的调度,其在ActivityThread中经过handleResumeActivity调度:

public final class ActivityThread {  
    // 省去海量代码…………………………  

    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,  
            boolean reallyResume) {  
        unscheduleGcIdler(); 

        ActivityClientRecord r = performResumeActivity(token, clearHide); 

        if (r != null) {  
            final Activity a = r.activity; 

            // 省去无关代码…………  

            final int forwardBit = isForward ?  
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; 

            boolean willBeVisible = !a.mStartedActivity; 
            if (!willBeVisible) {  
                try {  
                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(  
                            a.getActivityToken()); 
                } catch (RemoteException e) {  
                }  
            }  
            if (r.window == null && !a.mFinished && willBeVisible) {  
                r.window = r.activity.getWindow(); 
                View decor = r.window.getDecorView(); 
                decor.setVisibility(View.INVISIBLE); 
                ViewManager wm = a.getWindowManager(); 
                WindowManager.LayoutParams l = r.window.getAttributes(); 
                a.mDecor = decor; 
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; 
                l.softInputMode |= forwardBit; 
                if (a.mVisibleFromClient) {  
                    a.mWindowAdded = true; 
                    wm.addView(decor, l); 
                }  

            } else if (!willBeVisible) {  
                // 省去无关代码…………  

                r.hideForNow = true; 
            }  

            cleanUpPendingRemoveWindows(r); 

            if (!r.activity.mFinished && willBeVisible  
                    && r.activity.mDecor != null && !r.hideForNow) {  
                if (r.newConfig != null) {  
                    // 省去无关代码…………  

                    performConfigurationChanged(r.activity, r.newConfig); 
                    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.newConfig)); 
                    r.newConfig = null; 
                }  

                // 省去无关代码…………  

                WindowManager.LayoutParams l = r.window.getAttributes(); 
                if ((l.softInputMode  
                        & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)  
                        != forwardBit) {  
                    l.softInputMode = (l.softInputMode  
                            & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))  
                            | forwardBit; 
                    if (r.activity.mVisibleFromClient) {  
                        ViewManager wm = a.getWindowManager(); 
                        View decor = r.window.getDecorView(); 
                        wm.updateViewLayout(decor, l); 
                    }  
                }  
                r.activity.mVisibleFromServer = true; 
                mNumVisibleActivities++; 
                if (r.activity.mVisibleFromClient) {  
                    r.activity.makeVisible(); 
                }  
            }  

            if (!r.onlyLocalRequest) {  
                r.nextIdle = mNewActivities; 
                mNewActivities = r; 

                // 省去无关代码…………  

                Looper.myQueue().addIdleHandler(new Idler()); 
            }  
            r.onlyLocalRequest = false; 

            // 省去与ActivityManager的通讯处理  

        } else {  
            // 省略异常发生时对Activity的处理逻辑  
        }  
    }  

    // 省去巨量代码……………………  
}

handleResumeActivity方法逻辑相对要复杂一些,除了对当前显示Window的逻辑判断以及没建立的初始化等等工做外其在最终会调用Activity的makeVisible方法

public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2 {  
    // 省去海量代码………………………… 

    void makeVisible() {  
        if (!mWindowAdded) {  
            ViewManager wm = getWindowManager();  
            wm.addView(mDecor, getWindow().getAttributes());  
            mWindowAdded = true;  
        }  
        mDecor.setVisibility(View.VISIBLE);  
    }  

    // 省去巨量代码…………………… 
}

在makeVisible方法中逻辑至关简单,获取一个窗口管理器对象并将根视图DecorView添加到其中,addView的具体实如今WindowManagerGlobal中:

public final class WindowManagerGlobal {  
    public void addView(View view, ViewGroup.LayoutParams params,  
            Display display, Window parentWindow) {  
        // 省去不少代码 

        ViewRootImpl root;  

        // 省去一行代码 

        synchronized (mLock) {  
            // 省去无关代码 

            root = new ViewRootImpl(view.getContext(), display);  

            // 省去一行代码 

            // 省去一行代码 

            mRoots.add(root);  

            // 省去一行代码 
        }  

        // 省去部分代码 
    }  
}

在addView生成了一个ViewRootImpl对象并将其保存在了mRoots数组中,每当咱们addView一次,就会生成一个ViewRootImpl对象,其实看到这里咱们还能够扩展一下问题一个APP是否能够拥有多个根视图呢?答案是确定的,由于只要我调用了addView方法,咱们传入的View参数就能够被认为是一个根视图,可是!在framework的默认实现中有且仅有一个根视图,那就是咱们上面makeVisible方法中addView进去的DecorView,因此为何咱们能够说一个APP虽然能够有多个Activity,可是每一个Activity只会有一个Window一个DecorView一个ViewRootImpl,看到这里不少童鞋依然会问,也就是说在onResume方法被执行后咱们的ViewRootImpl才会被生成对吧,可是为何下面的代码依然能够正确运行呢:

package com.bourne.android_common.ServiceDemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

import com.bourne.android_common.R;

public class ThreadActivity extends AppCompatActivity {

    private Thread thread;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_thread);
        textView = (TextView) findViewById(R.id.textView);

        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("text text text");

            }
        });

        thread.start();
    }


    @Override
    protected void onResume() {
        super.onResume();
    }
}

Activity.onResume前,ViewRootImpl实例没有创建,因此没有checkThread检查。可是使用了Thread.sleep(200)的时候,ViewRootImpl已经被建立完毕了,天然checkThread就起做用了,抛出异常瓜熟蒂落。

第一种作法中,虽然是在子线程中setText,可是这时候View还没画出来呢,因此并不会调用以后的invalidate,而至关于只是设置TextView的一个属性,不会invalidate,就没有后面的那些方法调用了,归根结底,就不会调用ViewRootImpl的checkThread,也就不会报错。而第二种方法,调用setText以后,就会引起后面的一系列的方法调用,VIew要刷新界面,ViewGroup要更新布局,计算子View的大小位置,到最后,ViewRootImpl就会checkThread,就崩了。

因此,严格上来讲,第一种方法虽然在子线程了设置View属性,可是不可以归结到”更新View”的范畴,由于还没画出来呢,就没有所谓的更新。

当咱们执行Thread.sleep时候,这时候onStart、onResume都执行了,子线程再调用setText的时候,就会崩溃。

那么说,在onStart()或者onResume()里面执行线程操做UI也是能够的:

@Override
    protected void onStart() {
        super.onStart();
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("text text text");

            }
        });
        thread.start();
    }
@Override
    protected void onResume() {
        super.onResume();
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                textView.setText("text text text");

            }
        });
        thread.start();
    }

注意的是:当你在来回切换界面的时候,onStart()和onResume()是会再执行一遍的,这时候程序就崩溃了!


一、能不能在非UI线程中更新UI呢?
答案:能、固然能够

二、View的运行和Activity的生命周期有什么必然联系吗?
答案:没有、或者隐晦地说没有必然联系

三、除了Handler外是否还有更简便的方式在非UI线程更新UI呢?
答案:有、并且还很多,Activity.runOnUiThread(Runnable)、View.Post(Runnable)、View.PostDelayed(Runnable,long)、AsyncTask、其内部实现原理都是向此View的线程的内部消息队列发送一个Message消息,并传送数据和处理方式,省去了本身再写一个专门的Handler去处理。

四、在子线程里面用Toast也会报错,加上Looper.prepare和Looper.loop就能够了,这里能够这样作吗?
答案固然是不能够。Toast和View本质上是不同的,Toast在子线程报错,是由于Toast的显示须要添加到一个MessageQueue中,而后Looper取出来,发给Handler调用显示,子线程由于没有Looper,因此须要加上Looper.prepare和Looper.loop建立一个Looper,可是实质上,这仍是在子线程调用,因此仍是会报错的!

五、为何Android要求只能在UI主线程中更改View呢
这就要说到Android的单线程模型了,由于若是支持多线程修改View的话,由此产生的线程同步和线程安全问题将是很是繁琐的,因此Android直接就定死了,View的操做必须在UI线程,从而简化了系统设计。


参考文章