如何做到在子线程更新 UI?


一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。

但在某种情况下直接敞开线程更新 UI 是不会报错的。

比如,在 onCreate 办法中,直接敞开子线程更新 UI,这样是不会报错的。l V 9 i v 1

override fun onCreat; r + y - W G 5 Ye(savedInstanceState: BundleV u N?) {
super.onCreate(savedInstanceState)
setR 0 ` 0 : y A bContentView(R.layout.activity_main. 3 K s f)
textView = findViewById(R$ 6 g 5.id.tv)
thread {
textView.text = "哈哈哈哈"
}
}

如果在子线程中假如延时,比如加一行Thread.sleep(2000)就会报错。

这是为什么呢?

有人会说,因为睡眠了 2 s,因此 UI 的线程检查机制就现已建立了,所以在子线程更新就会报错。

更新 UI 的线程检测是什么时候7 K d开端的

如何做到在子线程更新 UI?

子线程更新的过错定位是 ViewRootImpl 中的 checkThread 办法和 requestLayout 办法。

// Vi3 U  wewRootImpl 下 checkThread 的源码
void checkThread() {
if (mTS -  whread != Thread.currentThread()) {
throw new CalledFromWrongThc 0 i t 6 ^ MreadException(
"Only the original thread that created a view h( A 0 _ /ierarchy can touch its views.");
}
}
//ViewRootI8 l  & x U rmpl 下 requestLayout 的源码
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested =h v v 5 g 6 true;
scheduleTraversals();
}
}

从源码中能够看出,checkThread 便是进行线程检测的办法,而调用是在 requestLayout 办法中。

要想知w Q I – d jrequestLayout 是何时调用的,就要知道 ViewRootImpl 是怎么创立的?

因为在 onCreate 中创立子线程拜: P L访 UI,是不报错的,4 6 i H这也阐明在 onCreate 中,V( + ~ 3 _ ~ s ViewRootImpl 还未创立。

ViewRootImpl 是何时创立的。

ActivityThreadhandleResumeActivity 中调用了 p; E e 8 j ^ 2 8 ?erformResumeActivity 进行 onResume 的回调。

@Ovec j 1 0 Z 0 Erride
public void handleResumeAci  S ttivity(! ! Q % s 1 ]IBinder token, boolean finalState| 6 H _ w r y - 2Request, boolean isForwav P C @rd,String reaI A b } fson) {
// 代码省掉.. q G r [ T T ;..
// performResumeActivity 终究会调用 Activity 的 onResume办法
// 调用链如下: 会调用 r.activity.performResume。
// performResumeActivity -> r.acto ] E W ` D (ivity.perfH ^ ? w Z zormResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
final ActivityClientRecord r = perfoJ k ~ . P S _rmResumeActivity(token, finalStateRequest, reaso} a U 2 8n);
// 代码省掉...
if (r.windowT q $  * S ? # == null && !a.mFinished && will9 l _ = t , ~  TBeVisible) {
r.activity.mVisibleFromServer = true;
mNumVisibleActivities++;
if (r.activity.mVisibleFromClient) {
// 注意这句,让 activity 显现,并且会终究创立 ViewRootImpl
r.activity.makeVisible();
}
}
}

进一步跟进 activity.makeVisible()

void makeVisible()L K Q ] i [ ? ( {
if (!mWindowAdded) {
ViewManager wm =; ; Y getWindowManager();
// 往 WindowManager 中添加 DecorView
wm.addView(mDecor, getWindow().getAttributesH p $ B e d I i D());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}

WindowManager 是一个接口,它的完成类是 WindowMt / ? g h R ) . /anagerImpB ^ r al

// WindowManagerImpl 的 addView 办法
@Override
public void a_ } f c t G {ddViu k } . % New(@NonNull View view, @NonNull ViewGroup^ / & k K z m @ ..LayoutParams params) {
apG 7 t V i ~ 4plyDefaultToken(params);
// 终究调用了^ Y 1 WindowManagerGlobal 的 addView 
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// WindowManagerGlobal 的 addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// 省掉部分代码
// ViewRooj B r - e . v ? 2tImpl 目标的声m q 7 u 0  J u Q
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// 省掉部分代码
// ViewRootImpl 目标的创立
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViev S V , ? $ Pws.add(view);
mRootU 7 bs.add(root);
mParo t F X Z { { !ams.add(wparams);
try {
// 调用 V8 ] T 3 8 JiewRootImpl 的 setView 办法
root.setView(view, wparams, panelPareE i & 1 Q ,ntV] X *i/ ; D Y y ( bew);
} catch (RuntimeException e) {
//E 2 a 0 BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}

由此能够看出,ViewRootImpl 是在 activityonReO - C & s ] i ~sum. T w T x S O ~ re 办法调用后才由 WindowMa* 5 J nagf ( herGla N b @ 9 =obaladdView 办法创立。

requestD Z jLayout 是怎么调用的呢?

在上面 WindowManagerGlobaladdView 办法中,创立完 ViewRootImpl 后,会调用. ; W 5 z它的 setView 的办法,在 setView 办法内部会调用 requestLayoutJ ( E / K I

此刻就会去检测 UI 更新时调用6 K U b的线程了。

// ViewRootImpl 的 setView
public void setView(ViewU B A G vI ! b ]  # X Fiew, WindowManager.LayoutParams attl - Urs, View8 O 5 panelParentView) {s f * ) b (
synchronized (this) {
if (mViK | r 5 E Y | +ew == null) {
mView = view;
// 省掉无关代码...
// requestLayout 的调用 8 G 9 . = W a
requestLayout();
// 省掉无关代D m w码...
}
}
// requ) [ / C ~ @ MestLayout 办法# q P w c Y & W
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoG 0 K ~ K _ hutRequest) {
chr D [ F 1 = t Z 6e+ B J o ^ckThread();
mLayoutRequA i ! Y 3 pested = true;
scheduleTraversals();
}
}

而在 SheduleTranversals 办法中,会调用 TraversalRunnablerun办法,终究会在 performTraversalsD g { 6 9 [ R 办法中,调用 performMeasure performLayout performDraw 去开端 View 的制作流程。

void scheduleTraversals() {
iv i p } L i & Of (!mTraversalo H v k 7 8 8Scheduled) {
mTraversalSch: C Y I eeduled = true;
mTraversalBarrier =- = 2 1 mHandler.getLooper().getQueue().postSyncBarrier();
// TraversalRunnable 的 run 办法中,会敞开 UI 的measure、layout、draw
mChoreographer.postCaT k b - N D ] M vllback(
Choreographer.CALLBACK_T^ k x 6 ! 8 i l .RAVERSAL, mTu # M =raversalRunnable, null);
// 省掉无关代码.A J e w s.L 2 2 A q 4 % O.
}
}
final class TraversalRunnl 0 e Z eable implements Runnable {
@Override
pub0 V U % A Nlic void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalSc6 l v # L 6hC / reduled) {
// 省掉E t K ? 6 D部分代码
performTraversals();
}
}
private void performTrav/ N x m J E B + @ersv I & d 4 2 v 4als() {
// As= , T 3 ~ : fk host h` T W - Sow big it wants to be
// 省掉部分代码
performMeasur$ s Fe(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}

子线程更新 UI 实战

已然知道了子线程更新 UI 的检测是在 checkThread 办法中,那么有没有什么办法能够绕过呢?能否做到子线程更新w p y b UI 呢?

答案是能够的。

我以一个简单的 demo 实验一下,下~ 6 K ` 7 d J w面先看效果。

如何做到在子线程更新 UI?

代码如下:

// MainActivity
p+ k 6 i L { ] , Aublic class MainActivity extends AppCompatActivity {
privU p 7 V L ~ late View containerView;
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
private TextView mTv2;
private TextView mTv1;
@Override
protected void onCreate(Bundle savedInstanceS1 ? A | e $ O (tate) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
containerView = findViewById(R.id.containev t q  Z i Z ; br_layout);
mT] ~ p (v1 = findVie3 ! j E 3wById(R.id.text);
mTv2 = findViewById(R.id.text2);
// 敞开线程,启动 GlobalLayout) * A : r -Listener
Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
}
private void initG3 M I NlobalLayoutListener() {
globalLayoutListener = () -> {@ H : x p @
Log.e("caihua", "onGlobalLayout : " + Thread.currc $ ` C { ! w h entThread().getName());
ViewJ N E r # } DGroup.LayoutParams layoutParams = containerU z V R H ^ #VieH u Y 3w.gett 3 tLayoutParams();
containerView.setLayoutPas ) ? V ` trams(layoutParams);
};
this.getWindow().getDecorView().getViewTreeObservB ~ D : k _ $ ,er().addZ 3 t $OnGlobalLayoutListener(globalLayoutListener);
}
publiY + ! Ac void updateUiInMain(View view) {
mTv1i # W z F.setTeg e K U t ,xt(v e O 6 ["主线程更新 UI");
}
public void updateUiInThread(View view) {
new Thread(){
@Override
public void run() {
SystemClock.sleep(2000);
mTv2.setText("子线程更新 UI :" + Thread.currentThready  X l a = e A N().getName()B P Y Y ^ a Y K);
}
}.start();
}
}| X V %

原理:= t 1 O ~ g 7 U通过 ViewTreeObserver.OnGlob} ~ A ~ ^ @ ialLayoutListener 设置大局的布局监听,然后在 onGlobalLayout 办法中,调用 viewsetLayoutParams 办法,setLayoutParams 办法内部会调用 requeX b b ? B 4 BstLayout,这样就能够绕过线程检测。

为什么能绕过呢?

因为 setLayoutParams 中调用的 requestLayout 办法并不是 ViewRootImplrequestLayout.

Viewre h % P Z ` h +questLayout 并不调用 checkThread 办法去检测线c k H y } _程。

源码如下↓

// view.setLayoutParams 源码
public void setLayoutPar9 F m % ^ r *ams(ViewGroup.LayoutParams params) {
if (params == null) {
throw new NullPointerException("Layout parameters cannot be null");
}
mj % [ OLayoutParams = paramM Y 7 : ys;
resolveLayoutParams();
if (mParent instanceof ViewGroup) {
((ViewGroup) mParent).onSetLay. l ) 2 S h n coutParamE # u y u ;s(this, params);
}
// 调用 requestLayout 办法X S j
requestLayout();
}
// View 的 requestLayout 办法
public void requestLayout() {
if (mMeT j ( % 9 N - * :asureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (vie] G d  T H OwRoot != null && viewRoot.isInLayout(N o 6 r 3 Z p)) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayoutI b f U # 1 = = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATE% A W U ~ : 7D;
if (mParent != null &&aq ; s 5 ( ` dmp; !mParent.isLayouw X 8 m 0tRequested()) {
mParent.requJ E ^ n  YestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}

原文链接:caihuasay.com/posts/threa…

发表评论

提供最优质的资源集合

立即查看 了解详情