Android子线程真的不能更新UI吗?让我们从源码的角度一探究竟

关于主线程更新UI这已经是个老生常谈的话题,几乎所有人都知道Android要在主线程更新UI。

Android官方文档这样描述:Android UI操作并不是线程安全的,并且这些操作必须在UI线程执行。
那么,子线程到底能否更新UI,如果不能更新UI,又是为什么?先来看一个简单的例子:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_centerInParent="true"
        android:text="Hello World!"/>
</RelativeLayout>
public class MainActivity extends AppCompatActivity {
    private TextView mTv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTv = (TextView) findViewById(R.id.tv);
        new Thread(new Runnable() {
            @Override
            public void run() {
                mTv.setText("Worker Thread Update UI");
            }
        }).start();
    }
}

在activity的onCreate()中创建并启动子线程,在TextView上面简单的写了一句话,运行效果如下图:
这里写图片描述

奇怪,子线程更新UI居然不报错,这是为何?让我们再换一种写法:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTv = (TextView) findViewById(R.id.tv);
        mTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        mTv.setText("Worker Thread Update UI");
                    }
                }).start();
            }
        });

    }

运行如图所示:
这里写图片描述
点击上面的TextView,出现崩溃,找到如下异常信息:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                                                                       at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6365)
                                                                       at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:890)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:360)
                                                                       at android.view.View.requestLayout(View.java:17516)
                                                                       at android.widget.TextView.checkForRelayout(TextView.java:6943)
                                                                       at android.widget.TextView.setText(TextView.java:4100)
                                                                       at android.widget.TextView.setText(TextView.java:3948)
                                                                       at android.widget.TextView.setText(TextView.java:3923)
                                                                       at com.wzw.threadtest.MainActivity$1$1.run(MainActivity.java:22)
                                                                       at java.lang.Thread.run(Thread.java:818)

发现这个异常是在ViewRootImpl的checkThread方法中抛出的,找到这个方法,跟进去看看:

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

代码很简单,就是检查一下当前线程和mThread是否是同一个线程,如果不是,就抛出这个异常。

那么,现在有一个问题,这个mThread是主线程吗?而且,为什么在onCreate中子线程更新UI就不报错,而把更新UI的子线程放到点击事件里就报错了呢?我们可以猜想,是不是onCreate()方法在checkThread()方法之前执行,这样的话,就可以避免这个异常。光猜想没用,要用源码来证实我们的猜想是不是正确的。
好了,接下来开启源码之旅,一步一步跟进,看看系统在这里面到底做了些什么。

首先,先分析View的更新流程,View的更新会调用invalidate()方法,找到这个方法:

public void invalidate() {
        invalidate(true);
    }

一路跟进,发现最终调用到invalidateInternal()方法,点进去,忽略其它细节,只寻找我们需要的地方:

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
...
final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }
...
}

可以找到p.invalidateChild(this, damage)这个方法,这个方法是声明在ViewParent里面的,而ViewParent是个接口,我们找到它的实现类ViewGroup,并且找到这个方法,跟进去瞧瞧,同样,只寻找我们需要的地方:

public final void invalidateChild(View child, final Rect dirty) {
...
    parent = parent.invalidateChildInParent(location, dirty);
...
}

在这里,实现了invalidateChildInParent()这个抽象方法的是ViewRootImpl,找到这个类,寻找这个方法:

@Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }

终于走到checkThread了,View更新一路走来,也就是在这里,会检查当前的Thread,如果和mThread这个成员变量不一样,就会抛出CalledFromWrongThreadException这个异常,那么问题来了,mThread这个变量是在哪里被赋值的呢?一般情况下,构造方法中会初始化很多变量,那我们去构造方法中瞧瞧:

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();

        mDisplayAdjustments = display.getDisplayAdjustments();

        mThread = Thread.currentThread();   //mThread在这里被赋值
        mLocation = new WindowLeaked(null);
        mLocation.fillInStackTrace();
        mWidth = -1;
        mHeight = -1;
        mDirty = new Rect();
        mTempRect = new Rect();
        mVisRect = new Rect();
        mWinFrame = new Rect();
        mWindow = new W(this);
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        mViewVisibility = View.GONE;
        mTransparentRegion = new Region();
        mPreviousTransparentRegion = new Region();
        mFirst = true; // true for the first time the view is added
        mAdded = false;
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
        mAccessibilityManager = AccessibilityManager.getInstance(context);
        mAccessibilityInteractionConnectionManager =
            new AccessibilityInteractionConnectionManager();
        mAccessibilityManager.addAccessibilityStateChangeListener(
                mAccessibilityInteractionConnectionManager);
        mHighContrastTextManager = new HighContrastTextManager();
        mAccessibilityManager.addHighTextContrastStateChangeListener(
                mHighContrastTextManager);
        mViewConfiguration = ViewConfiguration.get(context);
        mDensity = context.getResources().getDisplayMetrics().densityDpi;
        mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
        mFallbackEventHandler = new PhoneFallbackEventHandler(context);
        mChoreographer = Choreographer.getInstance();
        mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
        loadSystemProperties();
    }

我们可以看到,在构造方法中,获取了当前的Thread作为mThread,可是我们还是不能说明mThread就一定是主线程。走到这里,我们已经找到了最开始抛异常的地方,回到最开始我们提出的那个疑问,checkThread这个方法执行的时间是不是比Activity的onCreate()方法执行的要晚呢?那我们只有看一看Activity的生命周期是在哪里被回调的。从Activity的startActivity开始,一步一步跟进源码,这里涉及到Activity的启动流程,由于和本篇文章关系不大,所以也就不在此分析了,我就只找出关键的地方来和大家一起分析:在我们的app启动时,系统会调用ActivityThread的main方法,并且把ActivityThread作为我们app的主线程,在ActivityThread中,找到handleLaunchActivity()这个方法:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
       ...
       Activity a = performLaunchActivity(r, customIntent); 
       if (a != null) {
            r.createdConfig = new Configuration(mConfiguration);
            Bundle oldState = r.state;
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);
       ...
}

直接找到关键地方,一个是performLaunchActivity(),另一个是handleResumeActivity(),先进performLaunchActivity()这个方法:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        if (r.isPersistable()) {
             mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
             mInstrumentation.callActivityOnCreate(activity, r.state);
        }
        ...          
}

功夫不负有心人,终于找到了onCreate被回调的地方,顺着callActivityOnCreate()一路走下去,会调用activity的onCreate()

再看另一个关键地方,handleResumeActivity()方法,跟进里面去瞧瞧:

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
      ...
      ActivityClientRecord r = performResumeActivity(token, clearHide);//这里是activity的onResume被调用的入口
      ...
      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 (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);//这里是将View和window关联的地方
                }
       } else if (!willBeVisible) {
                if (localLOGV) Slog.v(
                    TAG, "Launch " + r + " mStartedActivity set");
                r.hideForNow = true;
       }
      ...
}

这里我们找到了View和Window关联的地方wm.addView(decor, l);由于WindowManager是个接口,我们找到它的实现类WindowManagerImpl,并且找到addView方法:

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

顺着mGlobal.addView(view, params, mDisplay, mParentWindow)这一句,继续跟进:

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;
        ...
        root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        ...  
}

无关的代码我都用省略号表示,我们发现ViewRootImpl 对象在这里被创建,到底为止,文章开始提出的疑问也就解决了,onCreate方法比onResume方法回调的早,而ViewRootImpl 对象是在activity的onResume之后创建,ViewRootImpl 中的mThread变量是在构造器中被赋值,所以,在onCreate更新UI,并不会出现异常,而我们添加点击事件以后,由于activity已经可见并且可交互,处于Resume状态,这时ViewRootImpl中的mThread已经是主线程,所以,我们在启动的分线程中更新UI就会报错。

看了这么长的源码,终于完全清晰的了解了子线程更新UI报错的原理。

可是我依然不满足,还是在想,既然ViewRootImpl的变量mThread获得的是当前的Thread,那如果它获得了当前的Thread和操作UI的Thread相同,是不是就不会报错了呢?那我们如何实现这个想法呢?

我们可以自己手动addView,这样就会让ViewRootImpl初始化一次,在分线程初始化它,在分线程操作UI,是不是不会报错呢?哈哈,这个想法很奇葩,我们来实现一下:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTv = (TextView) findViewById(R.id.tv);
        mTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Looper.prepare();
                        TextView tv = new TextView(MainActivity.this);
                        tv.setTextColor(Color.RED);
                        tv.setGravity(Gravity.CENTER);
                        tv.setText("Worker Thread Update UI");//在子线程中创建一个TextView
                        WindowManager windowManager = MainActivity.this.getWindowManager();
                        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
                        windowManager.addView(tv, params);
                        Looper.loop();
                    }
                }).start();
            }
        });
    }

运行这个demo,如下图所示:
这里写图片描述

OK,成功了,通过这个demo可以看到,说到底子线程是可以更新UI的,只不过需要有它自己的ViewRoot,Android系统在ActivityThread中,也就是主线程中创建了ViewRoot(onResume之后),而在子线程中,需要我们自己手动创建ViewRoot,这样才可以操作UI,但是我们通常并不会这样做。

通过这么长的源码探索之路,我们对文章开篇提出的几个问题也有了深刻的理解,当我们看到问题的表象时,也应当挖掘其内部原理,这样,我们对于Android的理解才能成为一个体系,而不是杂乱无章的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值