記錄一個LifeCycle 多執行緒使用導致的崩潰

四七發表於2020-04-05

關鍵字

  • lifecycle
  • 多執行緒
  • java.lang.IllegalArgumentException
  • bug
  • android
  • androidx

問題描述

在呼叫 getLifecycle().addObserver() 的時候報出這樣的錯誤

java.lang.IllegalArgumentException  
at androidx.lifecycle.LifecycleRegistry.upEvent(SourceFile:279)  
at androidx.lifecycle.LifecycleRegistry.forwardPass(SourceFile:293)  
at androidx.lifecycle.LifecycleRegistry.sync(SourceFile:333)  
at androidx.lifecycle.LifecycleRegistry.addObserver(SourceFile:189)  
複製程式碼

問題定位

問題程式碼出現在這裡 LifecycleRegisty 這個類中,程式碼如下

  private static Event upEvent(State state) {
        switch (state) {
            case INITIALIZED:
            case DESTROYED:
                return ON_CREATE;
            case CREATED:
                return ON_START;
            case STARTED:
                return ON_RESUME;
            case RESUMED:
                throw new IllegalArgumentException();
        }
        throw new IllegalArgumentException("Unexpected state value " + state);
    }
複製程式碼

當傳入的狀態是 RESUMED 的時候可以就會丟擲錯誤,而呼叫這個方法的程式碼如下

 @Override
    public void addObserver(LifecycleObserver observer) {
        State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
        ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
        ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

        if (previous != null) {
            return;
        }

        boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;

        State targetState = calculateTargetState(observer);
        mAddingObserverCounter++;
        while ((statefulObserver.mState.compareTo(targetState) < 0
                && mObserverMap.contains(observer))) {
            pushParentState(statefulObserver.mState);
            // 這裡呼叫
            statefulObserver.dispatchEvent(mLifecycleOwner, upEvent(statefulObserver.mState));
            popParentState();
            // mState / subling may have been changed recalculate
            targetState = calculateTargetState(observer);
        }

        if (!isReentrance) {
            // we do sync only on the top level.
            sync();
        }
        mAddingObserverCounter--;
    }
複製程式碼

因為在 State 是個列舉型別 ,RESUME 排在最後,所以是最大的

public enum State {
        // 刪除了無用註釋
        DESTROYED,
        INITIALIZED,
        CREATED,
        STARTED,
        RESUMED;
    }
複製程式碼

也就是說,以下程式碼中

 while ((statefulObserver.mState.compareTo(targetState) < 0&& mObserverMap.contains(observer))) {
            pushParentState(statefulObserver.mState);
            statefulObserver.dispatchEvent(mLifecycleOwner, upEvent(statefulObserver.mState));
            popParentState();
            // mState / subling may have been changed recalculate
            targetState = calculateTargetState(observer);
        }
}
複製程式碼

如果你想進入迴圈並且滿足呼叫 upEvent() 發生崩潰的 statefulObserver.mState 值是不存在的,因為 upEvent()需要 statefulObserver.mState的值等於RESUMED , 但是 statefulObserver.mState.compareTo(targetState) < 0 這個剛好就不能是這個條件,RESUMED 是最大的 state 值,是不可能存在其他值比較之後小於0的。

當出現這種前後矛盾的時候,大概率就是多執行緒呼叫導致了

這個時候我們就要開始找,還有什麼別的方法會導致 statefulObserver.mState 的改變,通過 IDE 的 find usage 可以輕鬆找到 mState 修改只有下圖中的兩處

6384ff7d357422f23a0885cc902667be
我們忽略構建方法(因為每次構建的物件不一樣,不可能同時改),專心再找 dispatchEvent 的使用
acb9ea9cd1198f73a407ca56509df041
排除 addObserver,重心放在 forwardPassbackwardPass ,他們統一呼叫的方法就是 sync,通過斷點除錯就能發現LifeCycleOwner 生命週期改變的時候會呼叫這個方法。也就是說,如果我使用非UI執行緒呼叫 addOboserver 同時改變生命週期就能達到崩潰的條件

我們在兩個地方設定斷點,分別是

84dfd90a2134d0398b70b44a91bb032f
2d8350fe490a15c1d0238e778d6b351e

然後在 onResume 增加程式碼

override fun onResume(){
    super.onResume()
    Thread { lifecycle.addObserver(new ObserverImp()) }.start()
}
複製程式碼

因為出錯的狀態是 RESUMED, 所以你只要 RESUMED 的時候加入 Oboserver 才能得到生命週期報錯。操作路徑是 App 後臺返回前臺顯示,然後你就會看到

2996f302873fe06a420b6adc9d7286cc

兩個執行緒的顯示不是一開始就有的,需要點多幾下過,因為需要生命週期呼叫 addObserver 之後才會開始新執行緒

這個時候我們只需要操作 UI 執行緒停在 RESUME 即可,如下圖

58424558ab9effce44b3f8bf32b93957

這個時候我們切換到另外一個執行緒, statefulObserver.mState 值就是 RESUME

c58ffa7b0ca6e23941a87d9633190668
這個時候點選下一步就是崩潰了。

修復方案

  1. 增加不是主執行緒 addObserer 檢查(用於防止事情再次發生)
  2. 移動非主執行緒程式碼到主執行緒中

小結

條件矛盾大概率是執行緒問題,剩下就是怎麼構造多執行緒修改條件。

相關文章