当前位置:网站首页>Source code analysis the problem that fragments cannot be displayed in the custom ViewGroup

Source code analysis the problem that fragments cannot be displayed in the custom ViewGroup

2022-06-25 00:26:00 a10615

One 、 background

I received a question from my classmate yesterday : Used other people's custom sideslip menu control , This control inherits from ViewGroup, I want to use the list in the menu on the left , Change the content displayed on the right interface , The content passes Fragment To display . The problem is coming. :

  1. When you click on the list ,replace A new Fragment, Unable to display
  2. stay onCreate() In the direct replace, Can be displayed

Two 、 Pit filling process

This is a completely self mocking process , You can skip .

Because after debugging several times , I think we should find the reason from the source code , Take a brief look at , No breakthrough has been found , Continue debugging again . The final reason is found through the source code , But the lesson has to be recorded .

His code doesn't look normal , look for bug The idea of is sometimes very funny , I doubt all my logic without reservation ,O(∩_∩)O ha-ha ~. Mainly from these aspects :

  1. First , See if the time click event arrives . result : normal
  2. Check Fragment Life cycle , Whether to start , That is, whether or not onCreateView(). result : Started
  3. Check Fragment Life cycle , Is it closed for some reason , That is, whether or not onDestroyView(). result : Not closed
  4. You can also check other lifecycles :onAttach()、onCreate()、onActivityCreated()、onStart()、onResume(), It must be the same : normal
  5. here , Found :Fragment It is created normally , But it just didn't show up
  6. I even suspect that other places have been tampered with , Just customize ViewGroup outside , Create a similar function , One button After clicking , Display a Fragment. result : normal
  7. Description is to customize ViewGroup Internal problems . Look at the code , Not modified contentView( Used for display Fragment Of FrameLayout) The content of
  8. Didn't you redraw ? stay ViewGroup Add a TextView,button After clicking , Except to show Fragment, At the same time TextView Modify the contents of , Use counter , What is displayed like this , It's different every time . result :TextView The content of has been modified normally , but Fragment Still not shown
  9. halfway , And the sky is free 、 To suspect illogically : The constructor is missing a three parameter , without doubt , Make up for nothing , Otherwise, it would have been wrong ; wide 、 High is based on the screen as the standard , Change to fixed value , result : Or it doesn't work .
  10. The student said that after clicking, there would be no , And it rewrites onInterceptTouchEvent()、onTouchEvent(), Is it related to this . Success led me to look it up , Even kill it both . result : Invalid
  11. Finally, the code peels the cocoon , There are only three structures left 、onMeasure()、onLayout(). Still not
  12. Don't customize ViewGroup Words , Directly in xml Use... In layout FrameLayout or LinearLayout, Certainly . Just change the inheritance to FrameLayout,onMeasure() and onLayout() Get rid of . result : normal
  13. Inherit FrameLayout, Original onMeasure() And onLayout() Retain . result : No display
  14. Last , hold onMeasure() And onLayout() in , Two switches added for performance are turned off . result : normal

I'm obsessive-compulsive , Finally, the problem was solved . but , I don't know why , It must be hard , When I got home, I analyzed another wave Fragment Source code ( It's hard to see for yourself , Later, I followed the reference blog to analyze ), Main view replace() and commit() What did you do after .

3、 ... and 、 Cause analysis

3.1 Problem recovery

Found a problem , Reverse analysis of causes . When you know why , Let's make a positive and concrete analysis , It will be clear at a glance . With a simple demo To reproduce :

/** *  Customize 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) {
            //  Place all horizontally 
            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

//  Omit other code 
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 effect :
 Picture description here

Click button replace after , There was no reaction in the back area . adopt log journal , You can see every click , There will be log output ( The current mobile phone system is 4.4.2, And another one. 6.0 Of did not print , It should be right onMeasure and onLayout It has been optimized ):
 Picture description here

3.2 Source code analysis

We all know , Only when the layout changes , Will be re measured .Fragment Of replace() Will the layout change ? You don't have to think about it ( I must have thought ), Otherwise Fragment How can I show the interface in . Now look for the source code
getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
This code , What did you do , So that the interface changes . Here is only a quick and simple analysis , Refer to the blog below for details . In addition, give the author a compliment , For the first time, I saw the blog with the class name on the source code , This makes it easy to view and understand , To study the .

The following source code version :27.1.1

3.2.1 getSupportFragmentManager()

getSupportFragmentManager() What you get ?

// 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();

//  class FragmentManagerImpl stay FragmentManager.java In the file , But not an inner class , But at the same level :
final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 

Conclusion :getSupportFragmentManager() What you get is a FragmentManagerImpl object

3.2.2 beginTransaction()

Look again beginTransaction() How to start a transaction :

// FragmentManager: This is an abstract method in an abstract class :
public abstract FragmentTransaction beginTransaction();

// FragmentManagerImpl: This method is implemented by this implementation class :
@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;
    }
}

therefore ,beginTransaction() Is to get a BackStackRecord object

3.2.3 replace()

replace() How to put us xml Inside R.id.fl_content Replace with fragment Of ?

// BackStackRecord: The following code is in this class , Some non core code will be omitted 
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
    return replace(containerViewId, fragment, null);
}

@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
    //  Pay attention to this operation command :OP_REPLACE
    doAddOp(containerViewId, fragment, tag, OP_REPLACE);
    return this;
}

/** *  Preparation before adding operation  */
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
    if (containerViewId != 0) {
        ...
        //  The same Fragment, Cannot add to two different containerViewId in 
        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));
}

/** *  Add operation  */
void addOp(Op op) {
    mOps.add(op);
    op.enterAnim = mEnterAnim;
    op.exitAnim = mExitAnim;
    op.popEnterAnim = mPopEnterAnim;
    op.popExitAnim = mPopExitAnim;
}

// mOps It's a ArrayList aggregate 
ArrayList<Op> mOps = new ArrayList<>()

/** * Op Is a static inner class , It is used to store the operation to be performed  */
static final class Op {
    int cmd;
    Fragment fragment;
    //  In and out of animation resources id
    int enterAnim;
    int exitAnim;
    //  Animation resources in and out again id.popEnterAnim The explanation of :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, Here we are , You can see replace() Is to add a replacement operation to be executed Op, The operation command is saved inside 、 What to replace Fragment、 And in and out of the animation . Customize the way in and out of animation :setCustomAnimations()

replace That's it ? you 're right , The best part is later

3.2.4 commit()

// BackStateRecord: The following code is in this class 
@Override
public int commit() {
    return commitInternal(false);
}

int commitInternal(boolean allowStateLoss) {
    mManager.enqueueAction(this, allowStateLoss);
}
/** * FragmentManagerImpl: The following code is in this class  *  Add the current object to the queue ready for execution  */
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
    ...
    scheduleCommit();
}

/** *  Here comes the first point : Scheduling execution , adopt Handler To execute  */
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() It can only be Runnable */
Runnable mExecCommit = new Runnable() {
    @Override
    public void run() {
        execPendingActions();
    }
};

/** *  Be careful : This is different from the reference blog  * Only call from main thread! */
public boolean execPendingActions() {
    ...
    removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
    ...
    doPendingDeferredStart();
}

First analysis removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);

/** * FragmentManagerImpl: The following code is in this class  *  remove mTmpRecords(ArrayList<BackStackRecord>) Superfluous operations in , And do these things  */
private void removeRedundantOperationsAndExecute(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop){
    executeOpsTogether(records, isRecordPop, startIndex, recordNum);
}

/** *  Perform operations together  */
private void executeOpsTogether(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
    ...
    record.expandOps(mTmpAddedFragments, oldPrimaryNav);
}

/** * BackStackRecord: *  The second point : Here is OP_REPLACE The only place where commands are processed , But I didn't understand , Personally, I think it mainly guarantees replace Uniqueness  */
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;
            }
        }
    }

Look again. doPendingDeferredStart();

/** * FragmengManager: The following code is in this class  */
void doPendingDeferredStart() {
    startPendingDeferredFragments();
}

void startPendingDeferredFragments() {
    performPendingDeferredStart(f);
}

public void performPendingDeferredStart(Fragment f) {
    moveToState(f, mCurState, 0, 0, false);
}

/** *  Third core : Here the code has been filtered , But there are more , Mainly to illustrate : * -  there case, Match the life cycle , The order of a  * -  be-all case None break, So will fall through Continue down  * -  Different states come in , The starting point of execution is also different  * -  You can see the life cycle method calls we often use  *  Back to the core , our R.id.fl_content, Convert here to ViewGroup container, then Fragment The layout of is through performCreateView() Fill in View after , Re pass container.addView() Went in.  */
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:  here performCreateView What is called is in the life cycle onCreateView(), Other performXXX It's the same thing 
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    // ...
    return onCreateView(inflater, container, savedInstanceState);
}

commit() It's finally over .

3.2.5 Process recheck

Review the whole process :

FragmentActivity.getSupportFragmentManager(): 
    <- FragmentController.getSupportFragmentManager()
    <- FragmentHostCallback.getFragmentManagerImpl()
    <- new FragmentManagerImpl()
FragmentManager.beginTranscation()
    <- new BackStackRecord(this):this  finger  FragmentManagerImpl  object 

FragmentTranscation.replace(): In the implementation class BackStackRecord in , hold OP_REPLACE The command is put into the set of operations to be executed

BackStackRecord.commit(): Submit the operation to FragmentManagerImpl, And then through Handler Come on post Runnable object mExecCommit, You can guess from this replace Fragment It can be executed in a child thread , After testing , That's all right. :

new Thread(new Runnable() {
    @Override
    public void run() {
        getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
    }
}).start();

mExecCommit There are two main things that I have done in :

  • hold replace command , stay BackStackRecord Switch to the next command in , The core approach :expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav)
  • stay FragmentManager in , According to the life cycle , Switch step by step Fragment Current state , At the same time call Fragment Corresponding life cycle . The core approach :void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

episode :
Midway analysis moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
in f.performCreate(f.mSavedFragmentState); When , Enter the cycle processing mechanism of the life cycle , But it made me dizzy , Then I skipped . Interested friends can study the following , It probably includes these categories :LifecycleRegistry、Lifecycle、LifecycleOwner、LifecycleObserver、ObserverWithState、GenericLifecycleObserver And its implementation class

3.2.6 addView(View)

addView(View) When was the measurement triggered ?

// 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() That is the Fragment The interface of is added to container In the container , Let's just container.requestLayout() measurement , Is it effective ?
Or look at the source code :

// 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;
}

You can see ,container Of requestLayout(), Must be to let its parent control measure , Pass it up one level at a time , until Activity The root layout of DecorView. Then, the root layout is measured from one level to the next onMeasure(), And layout onLayout()

and TestViewGroup The measurement has been turned off in the , So even if you call container.requestLayout() It doesn't work

3.2.8 resolvent

When you know why , There are many solutions :

The most direct solution is to get rid of isMeasured,onLayout() Inside changed The judgment is also removed

If it is necessary to avoid repeated measurement and layout , Improve performance , That can be done when requesting a re measurement , Reset the measurement mark and layout mark :

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;
    }
}

Let's suppose another situation : If it is already encapsulated , Then use the universal launch modification isMeasured; And rewrite onLayout(), Force to give super Of changed Pass on true

Four 、 summary

  1. To find problems, we should start with the source code , To find the root cause , It's the only way
  2. You need to look at the source code , To grasp the core point faster
  3. The ability of language organization needs to be improved , It took three nights to finish it

5、 ... and 、 Reference resources

《 Through source code analysis Fragment The boot process 》

原网站

版权声明
本文为[a10615]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/02/202202210547079313.html