非UI线程可不可以更新UI

Posted coderzdz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了非UI线程可不可以更新UI相关的知识,希望对你有一定的参考价值。

子线程中不可以更新UI,大多数android开发者都是这么认为的。Android官方这样描述the Android UI toolkit is not thread-safe and must always be manipulated on the UI thread。那么非UI线程到底能不能更新UI?请看下面一段代码:

public class MainActivity extends Activity 

    Button btn_test;
    TextView tv_content;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn_test = (Button) findViewById(R.id.btn_test);
        tv_content = (TextView) findViewById(R.id.tv_content);
        new Thread(new Runnable() 
            @Override
            public void run() 
                tv_content.setText("Not UIThread");
            
        ).start();
    

你会发现这段代码正常运行。

再看下面这段代码

public class MainActivity extends Activity 

    Button btn_test;
    TextView tv_content;
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn_test = (Button) findViewById(R.id.btn_test);
        tv_content = (TextView) findViewById(R.id.tv_content);
        new Thread(new Runnable() 
            @Override
            public void run() 
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e) 
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                
                tv_content.setText("Not UIThread");
            
        ).start();
    

程序崩溃:Only the original thread that created a view hierarchy can touch its views.

从logcat上面可以看到ViewRootImpl.checkThread方法报错。通过查看源码checkThread方法源码

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

mThread在ViewRootImpl的构造方法中

public ViewRootImpl(Context context, Display display) 
        ///省略代码

        mThread = Thread.currentThread();
        mLocation = new WindowLeaked(null);
        mLocation.fillInStackTrace();
        mWidth = -1;
        mHeight = -1;
        mDirty = new Rect();
        mTempRect = new Rect();
        mVisRect = new Rect();
        mWinFrame = new Rect();
        mWindow = new W(this);
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        mViewVisibility = View.GONE;
        mTransparentRegion = new Region();

        ///省略部分代码
        loadSystemProperties();
    

从checkThread方法中可以看到ViewRootImpl会检查当前线程是不是创建它的线程,如果不是则会抛出上面的异常。
那为什么开始的那段代码没有出问题呢,但是等待了500毫秒后程序崩溃了呢?
只能是checkThread方法没有执行。 查看ViewRootImpl源码可以看出,只要是和UI相关的,都会调用checkThread。所以只能猜测ViewRootImpl 对象还未创建。
在Activity启动过程中,ViewRootImpl 创建在ActivityThread类中的handleResumeActivity方法中的 r.activity.makeVisible();方法中创建

public final class ActivityThread 

    //只列出有关代码

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) 
        // If we are getting ready to gc after going to the background, well
        // we are back active so skip it.
        unscheduleGcIdler();
        mSomeActivitiesChanged = true;

        // TODO Push resumeArgs into the activity for consideration
        ActivityClientRecord r = performResumeActivity(token, clearHide);

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

            if (localLOGV) Slog.v(
                TAG, "Resume " + r + " started activity: " +
                a.mStartedActivity + ", hideForNow: " + r.hideForNow
                + ", finished: " + a.mFinished);

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

            // If the window hasn't yet been added to the window manager,
            // and this guy didn't finish itself or start another activity,
            // then go ahead and add the window.
            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);
                

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
             else if (!willBeVisible) 
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
            

            // Get rid of anything left hanging around.
            cleanUpPendingRemoveWindows(r);

            // The window is now visible if it has been added, we are not
            // simply finishing, and we are not starting another activity.
            if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) 
                if (r.newConfig != null) 
                    r.tmpConfig.setTo(r.newConfig);
                    if (r.overrideConfig != null) 
                        r.tmpConfig.updateFrom(r.overrideConfig);
                    
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Resuming activity "
                            + r.activityInfo.name + " with newConfig " + r.tmpConfig);
                    performConfigurationChanged(r.activity, r.tmpConfig);
                    freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig));
                    r.newConfig = null;
                
                if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward="
                        + isForward);
                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;
                if (localLOGV) Slog.v(
                    TAG, "Scheduling idle handler for " + r);
                Looper.myQueue().addIdleHandler(new Idler());
            
            r.onlyLocalRequest = false;

            // Tell the activity manager we have resumed.
            if (reallyResume) 
                try 
                    ActivityManagerNative.getDefault().activityResumed(token);
                 catch (RemoteException ex) 
                
            

         else 
            // If an exception was thrown when trying to resume, then
            // just end this activity.
            try 
                ActivityManagerNative.getDefault()
                    .finishActivity(token, Activity.RESULT_CANCELED, null, false);
             catch (RemoteException ex) 
            
        
    

makeVisible方法实现在Activity.java中实现

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

通过对getWindowManager()方法追踪,最后可以发现addView方法的实现在WindowManagerGlobal.java文件中

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) 
        if (view == null) 
            throw new IllegalArgumentException("view must not be null");
        
        if (display == null) 
            throw new IllegalArgumentException("display must not be null");
        
        if (!(params instanceof WindowManager.LayoutParams)) 
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        if (parentWindow != null) 
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) 
            // Start watching for system property changes.
            if (mSystemPropertyUpdater == null) 
                mSystemPropertyUpdater = new Runnable() 
                    @Override public void run() 
                        synchronized (mLock) 
                            for (int i = mRoots.size() - 1; i >= 0; --i) 
                                mRoots.get(i).loadSystemProperties();
                            
                        
                    
                ;
                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
            

            int index = findViewLocked(view, false);
            if (index >= 0) 
                if (mDyingViews.contains(view)) 
                    // Don't wait for MSG_DIE to make it's way through root's queue.
                    mRoots.get(index).doDie();
                 else 
                    throw new IllegalStateException("View " + view
                            + " has already been added to the window manager.");
                
                // The previous removeView() had not completed executing. Now it has.
            

            // If this is a panel window, then find the window it is being
            // attached to for future reference.
            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) 
                final int count = mViews.size();
                for (int i = 0; i < count; i++) 
                    if (mRoots.get(i).mWindow.asBinder() == wparams.token) 
                        panelParentView = mViews.get(i);
                    
                
            

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

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        

        // do this last because it fires off messages to start doing things
        try 
            root.setView(view, wparams, panelParentView);
         catch (RuntimeException e) 
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) 
                final int index = findViewLocked(view, false);
                if (index >= 0) 
                    removeViewLocked(index, true);
                
            
            throw e;
        
    

root = new ViewRootImpl(view.getContext(), display);
ViewRootImpl对象在addview时候创建。那么ViewRootImpl 创建是在performResumeActivity方法之后调用的,即是在Activity的onResume方法调用之后创建的。

所以当你无论在Activity第一次启动时,你将下面这段代码放到onCreate OnStart 或者 onResume中都不会报异常,因为此时ViewRootImpl还未创建(注意是Activity第一次启动时)。如果放在OnStart 或者 onResume中在activity创建完成后,将应用在前后台切换,程序还是会崩溃。因为此时ViewRootImpl已经创建完成。在线程中等待500毫秒也是为了等待ViewRootImpl创建。

new Thread(new Runnable() 
            @Override
            public void run() 
                tv_content.setText("Not UIThread");
            
        ).start();

以上是关于非UI线程可不可以更新UI的主要内容,如果未能解决你的问题,请参考以下文章

非UI线程可不可以更新UI

非UI线程可不可以更新UI

使用 DirectX11 更新非 UI 线程中的顶点缓冲区

在多线程中,子线程更新主线程ui都有哪些方法及注意点

Android 异步更新UI-线程池-Future-Handler实例分析

多线程学习之--真的不能在子线程里更新UI吗?