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.