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

写过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"刷了出来!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容