源码分析自定义ViewGroup中Fragment无法显示的问题
一、背景
昨天接到同学的一个问题:用了别人的自定义侧滑菜单控件,这个控件继承自ViewGroup,想通过左侧菜单里的列表,更改右侧界面显示的内容,内容通过Fragment来显示。问题来了:
- 点击列表时,replace一个新的Fragment,无法显示
- 在onCreate()中直接replace,可以显示
二、填坑过程
这里完全是个自嘲过程,可以直接跳过。
因为调试过几次后,觉得应该从源码找原因,简单地看了下,没找到突破口,又继续调试。最后原因还是通过源码找到的,但这个教训还是得记录下来。
他的代码看上去没有正常,找bug的思路有时也挺逗的,毫无保留地怀疑自己的一切逻辑,O(∩_∩)O哈哈~。主要从这几方面分析:
- 首先,看时候点击事件是否抵达。结果:正常
- 检查Fragment生命周期,是否启动,即是否调用了onCreateView()。结果:已启动
- 检查Fragment生命周期,是否因某个原因关闭了,即是否调用了onDestroyView()。结果:未关闭
- 也可以再去检查其它生命周期:onAttach()、onCreate()、onActivityCreated()、onStart()、onResume(),也肯定一样:正常
- 这里,就发现:Fragment正常创建出来了,但就是没显示出来
- 甚至怀疑其他地方动过手脚,就在自定义ViewGroup外面,创建一个类似的功能,一个button点击后,显示一个Fragment。结果:正常
- 说明就是自定义ViewGroup内部的问题。查看代码,并没有修改过contentView(用于显示Fragment的FrameLayout)的内容
- 难道没有重新绘制?在ViewGroup里添加一个TextView,button点击后,除了显示Fragment,同时把TextView的内容修改掉,使用计数器,这样显示的内容,每次都不一样。结果:TextView的内容正常修改了,但Fragment依旧没显示
- 中途,还有天马行空、不合逻辑地怀疑:构造方法少了一个三个参数的,毫无疑问,补上无果,否则早就报错了;宽、高都是以屏幕为标准,改成固定值,结果:还是无效。
- 同学说点击之后就没有,而里面重写了onInterceptTouchEvent()、onTouchEvent(),是否跟这有关。成功引导我去查了一遍,甚至把它两干掉。结果:无效
- 最后代码抽丝剥茧,只剩下三个构造、onMeasure()、onLayout()。依旧不行
- 不自定义ViewGroup的话,直接在xml布局中使用FrameLayout或LinearLayout,肯定可以。就把继承改成FrameLayout,onMeasure()和onLayout()去掉。结果:正常
- 继承FrameLayout,把原来的onMeasure()与onLayout()保留。结果:不显示
- 最后,把onMeasure()与onLayout()里,两个为了性能而加的开关给关掉。结果:正常
强迫症的我,最终还是把问题给解决了。但,原因不知道,也肯定难受,回到家又分析了一波Fragment的源码(自己看比较吃力,后来还是跟着后面的参考博客来分析的),主要查看replace()和commit()后做了哪些事情。
三、原因分析
3.1 问题复原
找到了问题,逆向分析原因。知道原因后,再来正向具体分析,就会一目了然。用一个简单的demo来复现:
/**
* 自定义ViewGroup
*/
public class TestViewGroup extends ViewGroup {
private boolean isMeasured;
public TestViewGroup(Context context) {
super(context);
}
public TestViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
logD("onMeasure: width.mode=%d, width.size=%d, height.mode=%d, height.size=%d",
(widthMeasureSpec & 3 << 30) >> 30, widthMeasureSpec & 0x3FFF,
(heightMeasureSpec & 3 << 30) >> 30, heightMeasureSpec & 0x3FFF
);
if (!isMeasured) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childW = child.getLayoutParams().width;
int childWidthSpec = MeasureSpec.makeMeasureSpec(childW, MeasureSpec.getMode(widthMeasureSpec));
child.measure(childWidthSpec, heightMeasureSpec);
}
isMeasured = true;
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
logD("onLayout: changed=%b, l=%d, t=%d, r=%d, b=%d",
changed, l, t, r, b);
if (!changed) {
// 全部水平摆放
final int count = getChildCount();
int wOffset = 0;
int w, h;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
w = child.getMeasuredWidth();
h = child.getMeasuredHeight();
child.layout(wOffset, 0, wOffset + w, h);
wOffset += w;
}
isLayouted = true;
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="18dp"
tools:context=".MainActivity">
<com.zjun.demo.gradationview.TestViewGroup
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="8dp"
android:background="#c5c6c7">
<TextView
android:id="@+id/tv_hello"
android:layout_width="80dp"
android:layout_height="match_parent"
android:text="hello"
android:background="#cac9aa"/>
<FrameLayout
android:id="@+id/fl_content"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#dbbfb2"/>
</com.zjun.demo.gradationview.TestViewGroup>
<Button
android:id="@+id/btn_replace"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:text="replace"
android:textAllCaps="false"
android:onClick="onClick"
/>
</LinearLayout>
MainActivity.java
// 省略其它的代码
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_replace:
getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
break;
default: break;
}
}
UI效果:
点击按钮replace后,后面那块区域没有任何反应。通过log日志,可以看到每次点击,都会有日志输出(当前手机系统是4.4.2,而另一个6.0的并没有打印,应该是对onMeasure和onLayout做了优化处理):
3.2 源码分析
我们都知道,只有布局变动的情况下,才会重新测量。Fragment的replace()会引起布局的变动吗?不用想就知道肯定会(当时肯定想了),不然Fragment里的界面怎么能展示出来。现在就从源码找找 getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
这句代码,具体做了哪些事情,从而让界面变化的。这里仅做快速简单分析,具体参考底下的博客。另外给这作者点个赞,首次看到源码上面带上了类名的博客,这样便于查看与理解,学习了。
以下源码版本:27.1.1
3.2.1 getSupportFragmentManager()
getSupportFragmentManager()得到的是什么?
// FragmentActvity:
public FragmentManager getSupportFragmentManager() {
return mFragments.getSupportFragmentManager();
}
// FragmentController:
public FragmentManager getSupportFragmentManager() {
return mHost.getFragmentManagerImpl();
}
// FragmentHostCallback:
FragmentManagerImpl getFragmentManagerImpl() {
return mFragmentManager;
}
// FragmentHostCallback:
final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();
// 类FragmentManagerImpl在FragmentManager.java文件内,但不是内部类,而是同级:
final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2
结论:getSupportFragmentManager()得到的是一个FragmentManagerImpl对象
3.2.2 beginTransaction()
再看beginTransaction()又是怎么开启事务的:
// FragmentManager:这是抽象类里的抽象方法:
public abstract FragmentTransaction beginTransaction();
// FragmentManagerImpl:由这个实现类来实现此方法:
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
// BackStackRecord:
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator{
public BackStackRecord(FragmentManagerImpl manager) {
mManager = manager;
}
}
所以,beginTransaction()是获取到了一个BackStackRecord对象
3.2.3 replace()
replace()是如何把我们xml里的R.id.fl_content替换成fragment的?
// BackStackRecord:以下代码都是在这个类中,一些非核心的代码都将省略
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
return replace(containerViewId, fragment, null);
}
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
// 注意这个操作命令:OP_REPLACE
doAddOp(containerViewId, fragment, tag, OP_REPLACE);
return this;
}
/**
* 添加操作前的准备工作
*/
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
if (containerViewId != 0) {
...
// 同一个Fragment,不能同时添加到两个不一样的containerViewId中
if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
throw new IllegalStateException("Can't change container ID of fragment "
+ fragment + ": was " + fragment.mFragmentId
+ " now " + containerViewId);
}
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
addOp(new Op(opcmd, fragment));
}
/**
* 添加操作
*/
void addOp(Op op) {
mOps.add(op);
op.enterAnim = mEnterAnim;
op.exitAnim = mExitAnim;
op.popEnterAnim = mPopEnterAnim;
op.popExitAnim = mPopExitAnim;
}
// mOps是个ArrayList集合
ArrayList<Op> mOps = new ArrayList<>()
/**
* Op是一个静态内部类,用于存放待执行的操作
*/
static final class Op {
int cmd;
Fragment fragment;
// 进出动画资源id
int enterAnim;
int exitAnim;
// 再次进出的动画资源id。popEnterAnim的解释:An animation or animator resource ID used for the enter animation on the view of the fragment being readded or reattached caused by
int popEnterAnim;
int popExitAnim;
Op() {
}
Op(int cmd, Fragment fragment) {
this.cmd = cmd;
this.fragment = fragment;
}
}
OK,到这,可以看到replace()就是添加了一个要待执行的替换操作Op,里面保存了操作命令、要替换的Fragment、和进出动画的信息。自定义进出动画方式:setCustomAnimations()
replace就这样没了?没错,好戏在后头呢
3.2.4 commit()
// BackStateRecord:以下代码都在此类中
@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
mManager.enqueueAction(this, allowStateLoss);
}
/**
* FragmentManagerImpl:以下代码都在此类中
* 把当前对象加入准备执行的队列
*/
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
...
scheduleCommit();
}
/**
* 第一个重点来了:调度执行,通过Handler来执行
*/
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
/**
* Hanlder.post()里面只能是Runnable
*/
Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
/**
* 注意:这里开始跟参考博客不一样
* Only call from main thread!
*/
public boolean execPendingActions() {
...
removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
...
doPendingDeferredStart();
}
先分析removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
/**
* FragmentManagerImpl:以下代码都在此类中
* 移除mTmpRecords(ArrayList<BackStackRecord>)里多余的操作,并执行这些操作
*/
private void removeRedundantOperationsAndExecute(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop){
executeOpsTogether(records, isRecordPop, startIndex, recordNum);
}
/**
* 一起执行操作
*/
private void executeOpsTogether(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
...
record.expandOps(mTmpAddedFragments, oldPrimaryNav);
}
/**
* BackStackRecord:
* 第二个重点:这里是OP_REPLACE命令唯一被处理的地方,但没看懂,个人觉得在这里主要保证了replace的唯一性
*/
Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {
switch (op.cmd) {
case OP_ADD:
case OP_ATTACH:
case OP_REMOVE:
case OP_DETACH:
...
break;
case OP_REPLACE: {
final Fragment f = op.fragment;
final int containerId = f.mContainerId;
boolean alreadyAdded = false;
for (int i = added.size() - 1; i >= 0; i--) {
final Fragment old = added.get(i);
if (old.mContainerId == containerId) {
if (old == f) {
alreadyAdded = true;
} else {
// This is duplicated from above since we only make
// a single pass for expanding ops. Unset any outgoing primary nav.
if (old == oldPrimaryNav) {
mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old));
opNum++;
oldPrimaryNav = null;
}
final Op removeOp = new Op(OP_REMOVE, old);
removeOp.enterAnim = op.enterAnim;
removeOp.popEnterAnim = op.popEnterAnim;
removeOp.exitAnim = op.exitAnim;
removeOp.popExitAnim = op.popExitAnim;
mOps.add(opNum, removeOp);
added.remove(old);
opNum++;
}
}
}
if (alreadyAdded) {
mOps.remove(opNum);
opNum--;
} else {
op.cmd = OP_ADD;
added.add(f);
}
}
break;
case OP_SET_PRIMARY_NAV:
break;
}
}
}
再来看doPendingDeferredStart();
/**
* FragmengManager:以下代码都在此类中
*/
void doPendingDeferredStart() {
startPendingDeferredFragments();
}
void startPendingDeferredFragments() {
performPendingDeferredStart(f);
}
public void performPendingDeferredStart(Fragment f) {
moveToState(f, mCurState, 0, 0, false);
}
/**
* 第三个核心:这里代码筛选过,但也比较多,主要为了说明:
* - 这里的case,跟生命周期匹配,有顺序的
* - 所有的case都没有break,所以会fall through向下继续执行
* - 不同的状态进来,执行的起始点也就不一样
* - 里面能看到我们经常使用到的生命周期方法的调用
* 回到核心,我们的R.id.fl_content,在这里转换成ViewGroup container,然后Fragment的布局通过performCreateView()填充成View后,再通过container.addView()进去了
*/
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
switch (f.mState) {
case Fragment.INITIALIZING:
if (newState > Fragment.INITIALIZING) {
dispatchOnFragmentPreAttached(f, mHost.getContext(), false);
f.onAttach(mHost.getContext());
dispatchOnFragmentAttached(f, mHost.getContext(), false);
if (!f.mIsCreated) {
dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false);
f.performCreate(f.mSavedFragmentState);
dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);
} else {
f.restoreChildFragmentState(f.mSavedFragmentState);
f.mState = Fragment.CREATED;
}
}
// fall through
case Fragment.CREATED:
if (newState > Fragment.CREATED) {
if (!f.mFromLayout) {
ViewGroup container = null;
if (f.mContainerId != 0) {
if (f.mContainerId == View.NO_ID) {
throwException(new IllegalArgumentException(
"Cannot create fragment "
+ f
+ " for a container view with no id"));
}
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
if (container == null && !f.mRestored) {
String resName;
try {
resName = f.getResources().getResourceName(f.mContainerId);
} catch (Resources.NotFoundException e) {
resName = "unknown";
}
throwException(new IllegalArgumentException(
"No view found for id 0x"
+ Integer.toHexString(f.mContainerId) + " ("
+ resName
+ ") for fragment " + f));
}
}
f.mContainer = container;
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (container != null) {
container.addView(f.mView);
}
if (f.mHidden) {
f.mView.setVisibility(View.GONE);
}
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
false);
// Only animate the view if it is visible. This is done after
// dispatchOnFragmentViewCreated in case visibility is changed
f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
&& f.mContainer != null;
} else {
f.mInnerView = null;
}
}
f.performActivityCreated(f.mSavedFragmentState);
dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
if (f.mView != null) {
f.restoreViewState(f.mSavedFragmentState);
}
f.mSavedFragmentState = null;
}
// fall through
case Fragment.ACTIVITY_CREATED:
if (newState > Fragment.ACTIVITY_CREATED) {
f.mState = Fragment.STOPPED;
}
// fall through
case Fragment.STOPPED:
if (newState > Fragment.STOPPED) {
if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
f.performStart();
dispatchOnFragmentStarted(f, false);
}
// fall through
case Fragment.STARTED:
if (newState > Fragment.STARTED) {
if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
f.performResume();
dispatchOnFragmentResumed(f, false);
f.mSavedFragmentState = null;
f.mSavedViewState = null;
}
}
}
// Fragment: 这里performCreateView调用的是生命周期里的onCreateView(),其它performXXX也一样
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// ...
return onCreateView(inflater, container, savedInstanceState);
}
commit()终于告一段落。
3.2.5 流程复盘
回顾一下整个流程:
FragmentActivity.getSupportFragmentManager():
<- FragmentController.getSupportFragmentManager()
<- FragmentHostCallback.getFragmentManagerImpl()
<- new FragmentManagerImpl()
FragmentManager.beginTranscation()
<- new BackStackRecord(this):this 指 FragmentManagerImpl 对象
FragmentTranscation.replace():
在实现类BackStackRecord里,把OP_REPLACE命令放入到待执行的操作集合中
BackStackRecord.commit():
把提交操作交给 FragmentManagerImpl,然后通过 Handler 来 post Runnable 对象 mExecCommit,从这可以猜测 replace Fragment 可以在子线程中执行,经测试,没问题:
new Thread(new Runnable() {
@Override
public void run() {
getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
}
}).start();
mExecCommit里主要做了两件事:
- 把replace命令,在BackStackRecord中切换到下一步命令,核心方法:
expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav)
- 在FragmentManager中,根据生命周期,一步一步切换Fragment的当前状态,同时调用Fragment对应的生命周期。核心方法:
void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
插曲:
中途分析moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
里f.performCreate(f.mSavedFragmentState);
的时候,进入了生命周期的循环处理机制,可把我给绕晕了,后来跳过了。感兴趣的小伙伴可以去研究以下,大概包括了这几个类:LifecycleRegistry、Lifecycle、LifecycleOwner、LifecycleObserver、ObserverWithState、GenericLifecycleObserver及其实现类
3.2.6 addView(View)
addView(View)什么时候触发测量的呢?
// ViewGroup:
public void addView(View child, int index, LayoutParams params) {
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
3.2.7 requestLayout()
replace()也就是把Fragment的界面添加到container容器中,那直接让container.requestLayout()测量,是否有效呢?
还是看下源码:
// View:
public void requestLayout() {
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
可以看到,container的requestLayout(),肯定是让其父控件去测量,一级一级传上去,直到Activity的根布局DecorView。然后再由根布局一级一级向下测量onMeasure(),和布局onLayout()
而TestViewGroup里已经关闭了测量,所以即使调用container.requestLayout()也无效
3.2.8 解决方法
知道原因后,解决办法就很多了:
最直接地解决方法就是去掉isMeasured,onLayout()里的changed判断也去掉
如果硬要避免重复测量与布局,提高性能的话,那可以在请求重新测量的时候,把测量标记和布局标记复位:
private boolean isMeasured;
private boolean isLayouted;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!isMeasured) {
...
isMeasured = true;
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
@Override
public void requestLayout() {
isMeasured = false;
isLayouted = false;
super.requestLayout();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (!isLayouted) {
...
isLayouted = true;
}
}
再假设一种情况:如果已经封装好了,那就用万能的发射修改isMeasured;同时重写onLayout(),强制给给super的changed传true
四、总结
- 找问题还要从源码入手,才能找到根源,才能柳暗花明
- 需要多看源码,才能更快地抓住核心点
- 语言组织能力亟待提高,写了好三个晚上才搞定