美文网首页Android开发经验谈Android开发Android开发
可能是全网最简单透彻的安卓子线程更新 UI 解析

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

作者: markRao | 来源:发表于2019-04-24 12:04 被阅读3次

    相信下面的代码大家看过很多遍了,在 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();
    

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/zgcbgqtx.html