首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >可能是全网最简单透彻的安卓子线程更新 UI 解析

可能是全网最简单透彻的安卓子线程更新 UI 解析

作者头像
萬物並作吾以觀復
发布2019-05-09 15:31:13
1.1K0
发布2019-05-09 15:31:13
举报
文章被收录于专栏:指尖下的Android指尖下的Android

相信下面的代码大家看过很多遍了,在 onCreate() 生命周期里开启一个线程来更新 UI ,居然没有闪退和异常( 在大概率情况下是没有问题的 )

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("MyButton", "onCreate");
        new Thread(new Runnable() {
            @Override
            public void run() {
                btn.setText("子线程更新UI");
                Log.e("MyButton", "子线程更新UI");
            }
        }).start();
    }

我们在子线程里睡眠一秒试试看

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("MyButton", "onCreate");
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                btn.setText("子线程更新UI");
                Log.e("MyButton", "子线程更新UI");
            }
        }).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:7512)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1206)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.widget.ScrollView.requestLayout(ScrollView.java:1533)
        at android.view.View.requestLayout(View.java:22029)
        at android.view.View.requestLayout(View.java:22029)
        at android.widget.TextView.checkForRelayout(TextView.java:8538)
        at android.widget.TextView.setText(TextView.java:5401)
        at android.widget.TextView.setText(TextView.java:5257)
        at android.widget.TextView.setText(TextView.java:5214)
        at demo.rzj.com.androiddemo.activity.MainActivity$1.run(MainActivity.java:93)
        at java.lang.Thread.run(Thread.java:764)

这个分享一个解决 Bug 时的小技巧,异常的起点在最下面,最顶上的是抛出异常的方法栈,我们只需从下往上就可以知道方法的调用顺序了,跟着 TextView 的源码从 setText() 里去查看源码,setText()方法经过多次跳转进入以下方法

3561    private void setText(CharSequence text, BufferType type,
3562                         boolean notifyBefore, int oldlen) {

  ....
//过滤掉一些非关键代码

 // 这段代码是核心,当 mLayout 不为空的时候才会触发 checkForRelayout();
3695        if (mLayout != null) {
3696            checkForRelayout();
3697        }
3698
3699        sendOnTextChanged(text, 0, oldlen, textLength);
3700        onTextChanged(text, 0, oldlen, textLength);
3701
3702        if (needEditableForNotification) {
3703            sendAfterTextChanged((Editable) text);
3704        }
3705
3706        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
3707        if (mEditor != null) mEditor.prepareCursorControllers();
3708    }

这个方法是关键,当 mLayout 不为空时才会进入,我们进入 checkForRelayout() 方法

6400    /**
6401     * Check whether entirely new text requires a new view layout
6402     * or merely a new text layout.
6403     */
6404    private void checkForRelayout() {
6405        // If we have a fixed width, we can just swap in a new text layout
6406        // if the text height stays the same or if the view height is fixed.
6407
6408        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
6409                (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
6410                (mHint == null || mHintLayout != null) &&
6411                (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
6412            // Static width, so try making a new text layout.
6413
6414            int oldht = mLayout.getHeight();
6415            int want = mLayout.getWidth();
6416            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
6417
6418            /*
6419             * No need to bring the text into view, since the size is not
6420             * changing (unless we do the requestLayout(), in which case it
6421             * will happen at measure).
6422             */
6423            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
6424                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
6425                          false);
6426
6427            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
6428                // In a fixed-height view, so use our new text layout.
6429                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
6430                    mLayoutParams.height != LayoutParams.MATCH_PARENT) {
6431                    invalidate();
6432                    return;
6433                }
6434
6435                // Dynamic height, but height has stayed the same,
6436                // so use our new text layout.
6437                if (mLayout.getHeight() == oldht &&
6438                    (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
6439                    invalidate();
6440                    return;
6441                }
6442            }
6443
6444            // We lose: the height has changed and we have a dynamic height.
6445            // Request a new view layout using our new text layout.
6446            requestLayout();
6447            invalidate();
6448        } else {
6449            // Dynamic width, so we have no choice but to request a new
6450            // view layout with a new text layout.
6451            nullLayouts();
6452            requestLayout();
6453            invalidate();
6454        }
6455    }

这个方法的核心就是 requestLayout() 以及 invalidate() ,相信大家也都清楚这两个方法的用途,requestLayout() 方法会执行 onMeasure() 和 onLayout() 方法,不会执行 onDraw() 方法,而 invalidate() 只会触发 onDraw() 方法,根据 View 的绘制流程,所以一般都是先调用 requestLayout() 然后 invalidate() ,废话不多说,我们回到那个异常报错继续跟进 View 的 requestLayout(),这个报错说明当我们在子线程睡眠一秒后,mLayout 是不为空的,所以才会触发父层的方法。

15463    /**
15464     * Call this when something has changed which has invalidated the
15465     * layout of this view. This will schedule a layout pass of the view
15466     * tree.
15467     */
15468    public void requestLayout() {
15469        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
15470        mPrivateFlags |= PFLAG_INVALIDATED;
15471
15472        if (mParent != null && !mParent.isLayoutRequested()) {
15473            mParent.requestLayout();
15474        }
15475    }

View 类中的 mParent 是一个 ViewParent 接口类型变量,其实这个是 ViewRootImpl 的实例对象,为什么这么说,下面的代码会有解释,也就是说这个 mParent.requestLayout() 会触发 ViewRootImpl 里的 requestLayout()

11526    /*
11527     * Caller is responsible for calling requestLayout if necessary.
11528     * (This allows addViewInLayout to not request a new layout.)
11529     */
11530    void assignParent(ViewParent parent) {
11531        if (mParent == null) {
11532            mParent = parent;
11533        } else if (parent == null) {
11534            mParent = null;
11535        } else {
11536            throw new RuntimeException("view " + this + " being added, but"
11537                    + " it already has a parent");
11538        }
11539    }

遍寻 View 的源码,只有这个方法里有对 mParent 进行赋值,进入 ViewRootImpl 查看有没有调用该方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

 ....
//过滤掉一些非关键代码

  view.assignParent(this);
}

答案很明显,我们再延伸一下, ViewRootImpl 是通过 WindowManager 实例化的,它的实现类是 WindowManagerImpl,这里分享一个查看源码的小知识点,一个接口或抽象类的实现类往往都是以它本身的类名 + Impl 的命名方式,这里也体现了规范化命名的好处,便于查找。

46    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();

67    @Override
68    public void addView(View view, ViewGroup.LayoutParams params) {
69        mGlobal.addView(view, params, mDisplay, mParentWindow);
70    }

也就是说,这个实例化 ViewRootImpl 是在 WindowManagerGlobal 里的 addView

163    public void addView(View view, ViewGroup.LayoutParams params,
164            Display display, Window parentWindow) {
165        if (view == null) {
166            throw new IllegalArgumentException("view must not be null");
167        }
168        if (display == null) {
169            throw new IllegalArgumentException("display must not be null");
170        }
171        if (!(params instanceof WindowManager.LayoutParams)) {
172            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
173        }
174
175        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
176        if (parentWindow != null) {
177            parentWindow.adjustLayoutParamsForSubWindow(wparams);
178        }
179
180        ViewRootImpl root;
181        View panelParentView = null;
182
183        synchronized (mLock) {
184            // Start watching for system property changes.
185            if (mSystemPropertyUpdater == null) {
186                mSystemPropertyUpdater = new Runnable() {
187                    @Override public void run() {
188                        synchronized (mLock) {
189                            for (ViewRootImpl viewRoot : mRoots) {
190                                viewRoot.loadSystemProperties();
191                            }
192                        }
193                    }
194                };
195                SystemProperties.addChangeCallback(mSystemPropertyUpdater);
196            }
197
198            int index = findViewLocked(view, false);
199            if (index >= 0) {
200                throw new IllegalStateException("View " + view
201                        + " has already been added to the window manager.");
202            }
203
204            // If this is a panel window, then find the window it is being
205            // attached to for future reference.
206            if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
207                    wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
208                final int count = mViews != null ? mViews.length : 0;
209                for (int i=0; i<count; i++) {
210                    if (mRoots[i].mWindow.asBinder() == wparams.token) {
211                        panelParentView = mViews[i];
212                    }
213                }
214            }
215
216            root = new ViewRootImpl(view.getContext(), display);
217
218            view.setLayoutParams(wparams);
219
220            if (mViews == null) {
221                index = 1;
222                mViews = new View[1];
223                mRoots = new ViewRootImpl[1];
224                mParams = new WindowManager.LayoutParams[1];
225            } else {
226                index = mViews.length + 1;
227                Object[] old = mViews;
228                mViews = new View[index];
229                System.arraycopy(old, 0, mViews, 0, index-1);
230                old = mRoots;
231                mRoots = new ViewRootImpl[index];
232                System.arraycopy(old, 0, mRoots, 0, index-1);
233                old = mParams;
234                mParams = new WindowManager.LayoutParams[index];
235                System.arraycopy(old, 0, mParams, 0, index-1);
236            }
237            index--;
238
239            mViews[index] = view;
240            mRoots[index] = root;
241            mParams[index] = wparams;
242        }
243
244        // do this last because it fires off messages to start doing things
245        try {
246            root.setView(view, wparams, panelParentView);
247        } catch (RuntimeException e) {
248            // BadTokenException or InvalidDisplayException, clean up.
249            synchronized (mLock) {
250                final int index = findViewLocked(view, false);
251                if (index >= 0) {
252                    removeViewLocked(index, true);
253                }
254            }
255            throw e;
256        }
257    }

最后我们在看一下 Activity 的 ViewRootImpl 是在哪里实例化的,作为单线程模型,我们可以从 应用的 Java 层入口,ActivityThread 也就是 UI 线程的实现类去查看

1131    private class H extends Handler {
1132        public static final int LAUNCH_ACTIVITY         = 100;
1133        public static final int PAUSE_ACTIVITY          = 101;
1134        public static final int PAUSE_ACTIVITY_FINISHING= 102;
1135        public static final int STOP_ACTIVITY_SHOW      = 103;
1136        public static final int STOP_ACTIVITY_HIDE      = 104;
...
// 省略大量的生命周期状态码

1175        String codeToString(int code) {
1176            if (DEBUG_MESSAGES) {
1177                switch (code) {
...
// 省略大量的 case 判断
1185                    case RESUME_ACTIVITY: return "RESUME_ACTIVITY";
1221                }
1222            }
1223            return Integer.toString(code);
1224        }
1225        public void handleMessage(Message msg) {
1226            if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
1227            switch (msg.what) {
...
// 省略大量的生命周期处理
1274                case RESUME_ACTIVITY:
1275                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume");
1276                    handleResumeActivity((IBinder)msg.obj, true,
1277                            msg.arg1 != 0, true);
1278                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
1279                    break;
        }
1433            if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
1434        }
1461    }

ActivityThread 里的 H Handler实例是核心中的核心,关键中的关键,一句话,我们的所有消息都需要通过它的处理分发,Activity 的生命周期、用户的触碰事件,一切的反馈都是通过这个来交互,如果没有这个,应用就会像一个 Java 程序,运行然后结束,轮询器的阻塞让 ActivityThread 的 main 方法持续处于运行状态,根据代码中的逻辑,非常明显,当 Activity 的 onResume() 方法被触发时会调用 handleResumeActivity()方法,而 handleResumeActivity 方法里实例化了 ViewRootImpl

2765    final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
2766            boolean reallyResume) {
2767        // If we are getting ready to gc after going to the background, well
2768        // we are back active so skip it.
2769        unscheduleGcIdler();
2770
2771        ActivityClientRecord r = performResumeActivity(token, clearHide);
2772
2773        if (r != null) {
2774            final Activity a = r.activity;
2775
2776            if (localLOGV) Slog.v(
2777                TAG, "Resume " + r + " started activity: " +
2778                a.mStartedActivity + ", hideForNow: " + r.hideForNow
2779                + ", finished: " + a.mFinished);
2780
2781            final int forwardBit = isForward ?
2782                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;
2783
2784            // If the window hasn't yet been added to the window manager,
2785            // and this guy didn't finish itself or start another activity,
2786            // then go ahead and add the window.
2787            boolean willBeVisible = !a.mStartedActivity;
2788            if (!willBeVisible) {
2789                try {
2790                    willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
2791                            a.getActivityToken());
2792                } catch (RemoteException e) {
2793                }
2794            }
2795            if (r.window == null && !a.mFinished && willBeVisible) {
2796                r.window = r.activity.getWindow();
2797                View decor = r.window.getDecorView();
2798                decor.setVisibility(View.INVISIBLE);
// 通过Activity 获取 WindowManager 的实例对象
2799                ViewManager wm = a.getWindowManager();
2800                WindowManager.LayoutParams l = r.window.getAttributes();
2801                a.mDecor = decor;
2802                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
2803                l.softInputMode |= forwardBit;
2804                if (a.mVisibleFromClient) {
2805                    a.mWindowAdded = true;
// WindowManager  的 addView 方法,一切的源头
2806                    wm.addView(decor, l);
2807                }
...
// 省略部分无关代码
2880    }

那么我们回到最顶部的报错方法栈

at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:7512)

4744    void checkThread() {
4745        if (mThread != Thread.currentThread()) {
4746            throw new CalledFromWrongThreadException(
// 只有创建视图层次结构的原始线程才能访问它的视图
4747                    "Only the original thread that created a view hierarchy can touch its views.");
4748        }
4749    }

还记得 TextView 里的 setText 方法吗,当 mLayout 不为空时才会进入,而事实上只有 View 在 测量 方法里才会对这个值进行赋值,答案也就很明显了,当我们在子线程里 setText 的时候,其实只是简单的设置了这个控件要显示的值,并不会立即去显示,因为 mLayout 是为空,为什么为空,因为只有在 Activity 的onResume 生命周期里才会去实例化 ViewRootImpl 一个个方法栈的调用最后才会触发 View 的测量。 最后扩展一下,如果就是想在子线程里更新 UI 怎么办呢,在onResume 之前就行,或者把 View 的 ViewRootImpl 实例化放到子线程来进行,这样就不会因为非 UI 线程抛出异常。

 new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Button button = new Button(MainActivity.this);
                WindowManager wm = MainActivity.this.getWindowManager();
                WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
                        WindowManager.LayoutParams.WRAP_CONTENT,0, 0, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
                        WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.OPAQUE);
                wm.addView(button, params);
                button.setTextColor(MainActivity.this.getResources().getColor(R.color.colorPrimaryDark));
                button.setText("子线程更新UI");
                Looper.loop();
                Log.e("MyButton", "子线程更新UI");
            }
        }).start();
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.04.24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档