问题表现
某自定义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,但会有如下错误:
问题分析
先看看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或者其child 的requestLayout不会递归到ViewRootImpl#requestLayout,便不会触发View树的measure layout流程,从而表现便是不生效。
问题解决方案
避免在binder线程更新UI即可,如:
new Handler(Looper.getMainLooper()).post(
() -> {
group.requestLayout();
}
);