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

Posted 碎格子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程学习之--真的不能在子线程里更新UI吗?相关的知识,希望对你有一定的参考价值。

在我们学习多线程的路上,都会听到这样一句话:

不能在子线程里更新UI,UI更新必须在UI线程中

why?为什么不能在子线程中更新UI?如果在子线程中更新UI会怎样?
为了模拟在子线程中更新UI的场景,简单地写了几行代码:

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final Thread thread = new Thread(new Runnable() 
            @Override
            public void run() 
            ((TextView)findViewById(R.id.sv_view)).setText("子线程");
            
        );
        saButton.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                thread.start();
            
        );
    

运行,“理所当然”地崩溃了。打印错误日志如下:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                                                                     at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6357)
                                                                     at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:874)

崩溃的原因是:“Only the original thread that created a view hierarchy can touch its views.”意思是只有创建这个View布局层次的原始线程才可以改变这个View,看起来好像也并没有解释为什么子线程中不能更新UI。
而我们能看到产生异常崩溃的代码在ViewRootImpl这个类的checkThread方法,所以我们找到这个类:
ViewRootImpl.java#checkThread

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

异常抛出的条件是mThread != Thread.currentThread()那么这个mThread在哪里初始化的呢?接着看。

public ViewRootImpl(Context context, Display display) 
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();

        mDisplayAdjustments = display.getDisplayAdjustments();

        mThread = Thread.currentThread();//此处初始化
        ...
        

在ViewRootImpl的构造方法里可以看到mThread指向当前线程的引用,意思是只要在子线程中创建ViewRootImpl的实例我们就可以避免抛异常了吗?于是楼主尝试在子线程中创建ViewRootImpl的实例可是发现并不能找到ViewRootImpl这个类。换个角度,如果不能在子线程更新UI,那主线程刷新UI是不是也要实例化这个类呢?而我们启动Activity绘制UI的方法在onResume方法里,所以我们找到Activity的线程ActivityThread类。
ActivityThread.java#handleResumeActivity

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) 
            ...
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;
            
            ...

wm.addView(decor, l);是他进行的View的加载,我们去看看他的实现方法,在WindowManager的实现类WindowManagerImpl里:

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) 
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    

发现他是调用WindowManagerGlobal的方法实现的,最后我们找到了最终实现addView的方法:
WindowManagerGlobal.java#addView

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) 
        ...

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) 
            // Start watching for system property changes.
            ...
            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;
        
    

果然在这里,View的加载最后就是在这里实现的,而ViewRootImpl的实例化也在这里。所以如果我们在子线程中调用WindowManager的addView方法,是不是就可以成功更新UI呢?所以我修改了代码:

@Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (Button) findViewById(R.id.text);
        final Thread thread = new Thread(new Runnable() 
            @Override
            public void run() 
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子线程");
                tx.setBackgroundColor(Color.WHITE);
                ViewManager viewManager = MainActivity.this.getWindowManager();
                WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
                        200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                viewManager.addView(view,layoutParams);
            
        );
        ...
        

运行,程序崩溃了,来看看错误日志:

 java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

错误原因是没有启动Looper。原来是因为在ViewRootImpl类里新建了ViewRootHandler的实例mHandler,而mHandler要启动Looper才能处理相关信息。所以我们在代码里加入两行:

...
public void run() 
                Looper.prepare();
                TextView tx = new TextView(MainActivity.this);
                tx.setText("子线程");
                ...
                windowManager.addView(tx, params);
                Looper.loop();
            
            ...

再次运行,成功了!

所以其实是可以在子线程中更新UI的,只要实例化ViewRootImpl。而为什么Android设计只能在UI线程中更新UI呢?大概是因为如果子线程更新UI可能导致线程之间抢夺资源和死锁等线程安全问题而不允许在子线程中更新UI。

以上是关于多线程学习之--真的不能在子线程里更新UI吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android中子线程真的不能更新UI吗?

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

iOS学习之GCD

Android 子线程更新UI那些事儿

Android学习之AsyncTask

AndroidUI进阶-为什么不能在子线程更新UI