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

2019-05-09 15:31:13 浏览数 (1)

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

代码语言:javascript复制
   @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();
    }

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

代码语言:javascript复制
@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();
    }

很明显,抛出异常闪退

代码语言:javascript复制
 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()方法经过多次跳转进入以下方法

代码语言:javascript复制
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() 方法

代码语言:javascript复制
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 是不为空的,所以才会触发父层的方法。

代码语言:javascript复制
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()

代码语言:javascript复制
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 查看有没有调用该方法

代码语言:javascript复制
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

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

  view.assignParent(this);
}

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

代码语言:javascript复制
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

代码语言:javascript复制
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 线程的实现类去查看

代码语言:javascript复制
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

代码语言:javascript复制
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    }

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

代码语言:javascript复制
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 线程抛出异常。

代码语言:javascript复制
 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();

0 人点赞