记一次View#requestLayout调用后不生效问题

479 阅读2分钟

问题表现

某自定义ViewGroupA,其子View的topMargin在更新之后,会调用View#setLayoutParams,内部会调用View#requestLayout,调用之后,子View仍旧在原位置,即不生效。

问题原因

先说问题原因:在binder线程中更新了topMargin,相当于在binder线程中调用了View#requestLayout

问题Demo

ICallback.aidl

interface ICallback {
    void invoke();
}

IMyAidlInterface

interface IMyAidlInterface {
    void setCallback(ICallback callback);
}

MyService.java

public class MyService extends Service {
    private ICallback callback;
    private IMyAidlInterface binder = new IMyAidlInterface.Stub() {
        @Override
        public void setCallback(ICallback callback) throws RemoteException {
            MyService.this.callback = callback;
        }
    };
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                try {
                    callback.invoke();
                } catch (RemoteException e) {
                    throw new RuntimeException(e);
                }
            }
        }, 2000);
        return binder.asBinder();
    }
}

在binder回调中执行requestLayout:

        bindService(new Intent(this, MyService.class), new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                try {
                    IMyAidlInterface.Stub.asInterface(service).setCallback(new ICallback.Stub() {
                        @Override
                        public void invoke() throws RemoteException {
                            ViewGroup group;
                            group.requestLayout();
                            new Handler(Looper.getMainLooper()).post(
                                    () -> {
                                        // 再调ViewGroup或者child的requestLayout均不会生效了
                                        group.getChildAt(0).requestLayout();
                                    }
                            );
                            requestLayout(tabBar);
                        }
                    });
                } catch (RemoteException e) {
                    throw new RuntimeException(e);
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        }, Context.BIND_AUTO_CREATE);

运行上述代码之后不会Crash,但会有如下错误:

image.png

问题分析

先看看View#requestLayout自身逻辑:

public void requestLayout() {
    // 省略部分代码
    // 设置LayoutFlag,后续Layout流程会使用此Flag
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    // 递归调用,注意,此判断,是问题原因的关键点
    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

上述实质上是个递归调用,最终会调用ViewRootImpl#requestLayout中:

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
private void performTraversals() {
    // 省略部分代码
    final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    // 省略部分代码  
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
    final View host = mView;
    try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            // 省略部分代码
    }

最终调到View#layout中:

public void layout(int l, int t, int r, int b) {
    // 省略部分代码
    // 清除LAYOUT Flag
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    // 省略部分代码
}

在binder线程中调用View#requestLayout之后, 可以看到ViewRootImpl#checkThread会抛出异常,但被binder线程try catch了,并不会导致进程Crash。

但是由于此时终止了requestLayout流程,会导致View中的Flag无法清除,即View#isLayoutRequested一直为true,再次调用出问题的View或者其childrequestLayout不会递归到ViewRootImpl#requestLayout,便不会触发View树的measure layout流程,从而表现便是不生效。

问题解决方案

避免在binder线程更新UI即可,如:

new Handler(Looper.getMainLooper()).post(
     () -> {
          group.requestLayout();
     }
);