blow your mind

The BYM series aims to share not only technology but also ideas, not just being a porter of code.

1. Background

In the process of reforming the existing recording function, add Interface and Activity forms to the original DialogFragment, and transform it into the communication between Activity and DialogFragment through ViewModel.

Click the button for the first time and a DialogFragment is displayed. The recording process is normal. When you click the button for the second time and DialogFragment is displayed again, LogCat reports the following error. It is very simple to resolve the repeated triggering of the ViewModel. You can directly determine the isVisible of DialogFragment. But this also raises a problem, which may be a knowledge blind spot for me. What is it in the mechanics of the ViewModel that causes it to fire repeatedly?

Those of you who find it difficult to analyze can click on the menu on the right and see the conclusion directly.

java.lang.RuntimeException: stop failed.
    at android.media.MediaRecorder.stop(Native Method)
    at xxx.TapeDialogFragment.stopRecord(TapeDialogFragment.kt:218)
    at xxx.TapeDialogFragment.access$stopRecord(TapeDialogFragment.kt:28)
    at xxx.TapeDialogFragment$onCreate$4.onChanged(TapeDialogFragment.kt:94)
    at xxx.TapeDialogFragment$onCreate$4.onChanged(TapeDialogFragment.kt:28)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:144)
    at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:443)
    at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:395)
    at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
    at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:300)
    at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:339)
    at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:145)
    at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:131)
    at androidx.fragment.app.Fragment.performStart(Fragment.java:2735)
    at androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:355)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1192)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1354)
    at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1432)
    at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1495)
    at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:447)
    at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2167)
    at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1990)
    at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1945)
    at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1847)
    at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:207)
    at android.app.ActivityThread.main(ActivityThread.java:6878)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:876)
Copy the code

2. Look at the source code to understand why

Before problem: every click on the button by TapeDialogFragment newInstance () method to create the instance.

DialogFragment.show

// Handle framgent friend FragmentTransaction.
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }
Copy the code

Whereas FragmentTransaction is implemented as a BackStackRecord, the COMMIT method is essentially a commitInternal called, as shown below

int commitInternal(boolean allowStateLoss) {...// This transaction is submitted to the queue
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
    }
Copy the code

MManager is an instance of FragmentManagerImpl. Then see enqueueAction

public void enqueueAction(OpGenerator action, boolean allowStateLoss) {...synchronized (this) {...// Add this action to the list
            if (mPendingActions == null) {
                mPendingActions = new ArrayList<>();
            }
            mPendingActions.add(action);
            scheduleCommit();// Task submission}}// Continue reading
    / * * * this method will be the first call # enqueueAction (OpGenerator, Boolean) method calls or by startPostponedEnterTransition () * / when the startup latency task
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void scheduleCommit(a) {
        synchronized (this) {
            booleanpostponeReady = mPostponedTransactions ! =null && !mPostponedTransactions.isEmpty();// The delay queue is ready
            booleanpendingReady = mPendingActions ! =null && mPendingActions.size() == 1;// The execution queue is ready
            if (postponeReady || pendingReady) {
                // Remove the old runnable and commit the new runnable
                mHost.getHandler().removeCallbacks(mExecCommit);
                mHost.getHandler().post(mExecCommit);
            }
        }
    }
    
    Runnable mExecCommit = new Runnable() {
        @Override
        public void run(a) { execPendingActions(); }};Copy the code

Look back to the us. This line of abnormal androidx fragments. App. FragmentManager. ExecPendingActions (FragmentManager. Java: 1847) thought that’s right, continue to push to walk.

 boolean execPendingActions(boolean allowStateLoss) {
        ensureExecReady(allowStateLoss);

        boolean didSomething = false;
        // The while loop is deleted from the list of temporarily stored records
        while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
            mExecutingActions = true;
            try {
           // Delete redundant BackStackRecord operations and execute them. This method merges operations that allow reordering of approximate records.
           // For example, if one transaction is added to the back stack and another transaction pops off that stack, the back stack records will be optimized to eliminate unnecessary operations.
           // Also, two committed transactions executed simultaneously will be optimized to remove redundant operations and two POP operations executed simultaneously.
                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
            } finally {
                cleanupExec();
            }
            didSomething = true;
        }

        updateOnBackPressedCallbackEnabled();
        doPendingDeferredStart();
        mFragmentStore.burpActive();

        return didSomething;
    }
Copy the code
// Here we are fetching each item in the BackStackRecord list
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1990)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:447)
at androidx.fragment.app.FragmentManager.executeOps(FragmentManager.java:2167)
Copy the code

When the above statement is executed to BackStackRecord. ExecuteOps method

void executeOps(a) {...if(! mReorderingAllowed) {// Add the newly added Framgent to the end as initialized
            mManager.moveToState(mManager.mCurState={Fragment.INITIALIZING}, true); }}Copy the code

Then see FragmentManager moveToState method, the description of the method is: will FragmentManager status changes to {@ code newState because}. If FragmentManager changes state or {@code always} is {@code true}, then the state of any fragment within it will be updated.

 void moveToState(int newState, boolean always) {...// Must add them in the proper order. mActive fragments may be out of order
        // Must be added in the correct order. Active Framgenets can be abnormal
        for (Fragment f : mFragmentStore.getFragments()) {
            // Change the Fragment to the expected final state or FragmentManager state, depending on whether the FragmentManager state is properly promoted.moveFragmentToExpectedState(f); }}void moveFragmentToExpectedState(@NonNull Fragment f) {
    moveToState(f);// Change the fragment state
}
void moveToState(@NonNull Fragment f, int newState) {...switch (f.mState) {
         case Fragment.ACTIVITY_CREATED:
             if (newState > Fragment.ACTIVITY_CREATED) {
                fragmentStateManager.start();// The Fragment state manager is started
            }
     }
}
# FragmentStateManager.java
 void start(a) {
        // The corresponding Framgent is ready to start
        mFragment.performStart();
        // Notify that the fragment is started
        FragmentLifecycleCallbacksDispatcher.dispatchOnFragmentStarted(mFragment, false);
}
# Fragment.java
 void performStart(a) {
        mChildFragmentManager.noteStateNotSaved();
        mChildFragmentManager.execPendingActions(true);
        mState = STARTED;
        mCalled = false;
        onStart();
        if(! mCalled) {throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onStart()");
        }
        //Lifecycle Sends events during the Lifecycle
        mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
        if(mView ! =null) {
            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START);
        }
        mChildFragmentManager.dispatchStart();
    }

Copy the code

LifeCycle is finally relevant to our ViewModel. We all know that LiveData internally listens for callbacks through Lifecycle implementation to know about Fragment and Activity Lifecycle changes.

androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:131)
androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:145)
androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:339)
androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:300)
androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
// Execute to this point
void dispatchEvent(LifecycleOwner owner, Event event) {
            State newState = getStateAfter(event);
            mState = min(mState, newState);
            // Notify the observer that the state has changed
            mLifecycleObserver.onStateChanged(owner, event);
            mState = newState;
        }

# LiveData.java
public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {... Trigger state change activeStateChanged(true);
        }

void activeStateChanged(boolean newActive) {...if (mActive) {
                // Notify the observer that the value has changed
                dispatchingValue(this); }}void dispatchingValue(@Nullable ObserverWrapper initiator) {...do{...if(initiator ! =null) {
                // Send notifications
                considerNotify(initiator);
                initiator = null;
            } else{...// If the observer is empty, loop through the observer collection and notify}}while(mDispatchInvalidated); . }private void considerNotify(ObserverWrapper observer) {... observer.mObserver.onChanged((T) mData);//mData is modified by volatile. And because fragments and activities share the same ViewModel.
        // The mData value of the number of times is the previous postValue value.
    }
Copy the code

conclusion

As you can see, when we rebuild the Fragment, the shared ViewModel will trigger the onChange method when Framgent is in the START state, making Framgent invisible. But still will carry out the ViewModel. XxxLiveData. Observe. This leads to exceptions.

When a Fragment is shared with an Activity, rebuilding the Fragment needs to be vigilant against triggering LiveData.