AndroidDevAndroid技术知识Android知识

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

2017-02-19  本文已影响1029人  acemurder

写过Android 代码的同学应该都听过Android不能在子线程更新UI,只能在主线程即UI线程处理视图。

public class MainActivity extends AppCompatActivity {
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(tv);
        new Thread(){
            @Override
            public void run() {
                super.run();
                textView.setText("not from UI thread!!!");
            }
        }.start();
    }
}

猜一下运行结果呢? 抛出CalledFromWrongThreadException吗? No!No!No!

不信的话可以试下。
这是为啥?说好的不能子线程更新UI呢! 这个我们等下再说

CalledFromWrongThreadException

首先我们测试一下:

public class MainActivity extends AppCompatActivity {
    private Button button;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.btn);
        textView = (TextView) findViewById(R.id.tv);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        textView.setText("not from UI Thread");
                    }
                }.start();
            }
        });
    }
}

我们在点击button时,给textView 设置文字,运行App,点击button时我们会看到这个异常

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
说的很清楚,只有创建视图层次结构的原始线程可以访问它的视图。

通过查看Android是源码,我们发现这个异常是在 android.view.ViewRootImpl 抛出来的

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

当Activity对象被创建完毕后,会创建ViewRootImpl对象,mThread 是在ViewRootImpl 的构造方法里这样初始化的
所以mThread被赋值成主线程。

public ViewRootImpl(Context context, Display display) {
    mContext = context;
    ······
    mThread = Thread.currentThread();
    ......
}

checkThread() 方法是怎样被调用的呢

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                      at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
                      at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
                      at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
                      at android.view.View.invalidateInternal(View.java:13656)
                      at android.view.View.invalidate(View.java:13620)
                      at android.view.View.invalidate(View.java:13604)
                      at android.widget.TextView.checkForRelayout(TextView.java:7347)
                      at android.widget.TextView.setText(TextView.java:4480)
                      at android.widget.TextView.setText(TextView.java:4337)
                      at android.widget.TextView.setText(TextView.java:4312)
                      at com.acemurder.test.MainActivity$1$1.run(MainActivity.java:27)

这样更直白一点

com.acemurder.test.MainActivity$1$1.run

  -> android.widget.TextView.setText

    -> android.widget.TextView.checkForRelayout

      -> android.view.View.invalidate

        -> android.view.ViewGroup.invalidateChild

          -> android.view.ViewRootImpl.invalidateChildInParent

            -> android.view.ViewRootImpl.invalidateChild

              -> android.view.ViewRootImpl.checkThread

当运行到``checkThread()时候,Thread.currentThread()我们在OnCreate()`方法里创建的一个子线程,所以抛出来了异常。

通过创建ViewRootImpl在子线程更新UI

通过方才的分析,我们发现异常的原因是TextViewViewRootImpl是在我们创建Activity的时候创建的,所以我们能不能通过给一个View单独的一个ViewRootImpl呢?

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(){
                    @Override
                    public void run() {
                        super.run();
                        Looper.prepare();
                        TextView tv = new TextView(MainActivity.this);
                        tv.setText("not from UI Thread");
                        WindowManager windowManager = MainActivity.this.getWindowManager();
                        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                                200, 200, 200, 200, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                                WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                        windowManager.addView(tv, params);
                        Looper.loop();
                    }
                }.start();
            }
        });

看结果


等等,不是说好的单独创建一个ViewRootImpl吗?别急,我们来理一下
通过看源码发现,WindowManager是一个抽象类,我们看他的子类WindowManagerImpl

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

addView方法最终调用了mGlobaladdView方法这里的mGlobalprivate final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();这样得到的,我们看下它的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.
            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);
        }

看见了吧,在这里创建了一个ViewRootImpl对象,所以在子线程更新UI成功。

不创建ViewRootImpl在子线程更新UI

逼逼了这么久,终于说到文章开始那个问题了。
我们OnCreate()里直接开启一个子线程去更新UI,并没有创建单独的ViewRootImpl对象啊?
原因就在于ViewRootImpl的建立时间,它是在ActivityThread.Javafinal void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward)里创建的。

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ······      
        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;

           
            boolean willBeVisible = !a.mStartedActivity;
            if (!willBeVisible) {
                try {
                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
                            a.getActivityToken());
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
            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 (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }
             }
                ······
 }

原因就是在Activity的onResume之前ViewRootImpl实例没有建立,所以没有ViewRootImpl.checkThread检查。而textView.setText时设定的文本却保留了下来,所以当ViewRootImpl真正去刷新界面时,就把"not from UI Thread"刷了出来!

上一篇下一篇

猜你喜欢

热点阅读