轉場過程解析
UINavigationController對于translation動畫做了一定的封裝, 同時持有fromAnimateView與toAnimateView, 在進行translation動畫時將對應的VC的view掛載到對應的AnimateView上, 動畫視圖AnimateView又掛載到容器視圖wrapperView, UINavigationController只需控制容器中的AnimateView實現(xiàn)相應translation動畫, translation動畫完成后, 移除動畫視圖并掛載棧頂?shù)囊晥D, 實現(xiàn)navigationController對外部進行了動畫隔離.
Can't add self as subview 復現(xiàn)
模擬車禍:
pushNoAnimate(@"A");
pushAnimate(@“B");
pushAnimate(@“C”);
同時執(zhí)行完以上操作(即上一個還沒執(zhí)行完畢就同步執(zhí)行后續(xù)操作), 之后的pop退場操作會導致車禍
車禍現(xiàn)場:
轉場動畫中toAnimateView加載到WrapperView這一步驟
車禍前現(xiàn)象:
pushC, C成功入棧, 但是視圖沒有加載到容器中, 實際顯示的還是B的vc與view, 但是棧頂是C的vc
車禍分析:
- 第一次點返回時(實際應該C的vc出棧), 當前視圖(B的view)被先后加載到fromAnimateView與toAnimateView上, 原本視圖在出棧完成后應該被釋放, 但是容器棧內還存在B的vc, 故保留了
- 第二次點返回時(實際應該B的vc出棧), A的view加載到toAnimateView上, 隨后toAnimateView需要加載到wrapperView進行transition動畫, 但wrapperView通過棧頂元素view.superview取值, 而棧頂元素B的view由于上一次錯誤的轉場, 并未在transition動畫完成后掛載到wrapperView, 還保留在的臨時的動畫視圖toAnimateView上, 所以使toAnimateView加載到WrapperView的操作變成了動畫視圖toAnimateView加載到自己上
時序分析:
A push B
A.view -> From Animation View
B.view -> To Animation View
A.view -> Wrap View
B.view -> Wrap View
A.view -> Nil
B pop A
A.view -> To Animation View
B.view -> From Animation View
B.view -> Wrap View
A.view -> Wrap View
B.view -> Nil
- A Push B No animation
- B Push C animation
- C Push D animation
由于B是無動畫的,使C嘴秸、D的視圖動畫沒按原有的隊列執(zhí)行是钥,一起執(zhí)行而導致沖突,只完成C的動畫对省,并觸發(fā)警告“nested push animation can result in a corrupted navigation bar
Attempting to begin a transition on navigation bar while a transition is in progress”。 - D Pop C
C.view -> To Animation View,而D的view由于上述動畫沖突不在視圖棧中,也使Pop動畫終止狂票。 - C Pop B
B.view -> To Animation View,隨后To Animation View需要加到Wrap View中熙暴,而Wrap View的獲取通過棧頂view.superview獲取闺属,即C.view.superview(To Animation View),觸發(fā)了To Animation View -> To Animation View周霉。
-
思路一:
使用delegate
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController(UINavigationController *)navigationController animationControllerForOperation (UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC
自定義所有轉場動畫, 規(guī)避系統(tǒng)進行轉場動畫時的錯誤視圖加載.
問題:
當錯誤case產生時, transitioningController中取到的containerView取值為nil, 只是跳過了這一次轉場動畫, 而實際的錯誤轉場現(xiàn)象并未解決, 如以上車禍模擬, pushC成功入棧, 但是視圖沒有加載到容器中, 依舊展示的是B的vc與view.
重新定位問題:
轉場動畫時的錯誤視圖加載的根本原因是連續(xù)非正常的轉場, class-dump出UINavigationController有相應的defer transition的屬性與API, navigationController對連續(xù)轉場做了一定流程的控制
連續(xù)轉場的時序圖如下, 后續(xù)兩個transition都被defer, 后續(xù)統(tǒng)一觸發(fā)
UINavigationController暴露給外部調用的push/pop方法實際只是一個“轉場請求”, 對于連續(xù)轉場navigationController會統(tǒng)一調度這些“請求”
系統(tǒng)Bug:
無動畫的轉場也需要完成一些切換vc, 重新掛載view等操作, 而在執(zhí)行這些操作的同時, 后續(xù)觸發(fā)的“轉場請求”會根據(jù)當前正在執(zhí)行的轉場判斷是否需要被加到deffer的隊列中, 所以無動畫的轉場的后續(xù)轉場操作會同步執(zhí)行, 從而導致轉場異常.
-
思路二:
模擬車禍的的路徑中, 都有無動畫的轉場, 在私有方法中根據(jù)transition參數(shù), 判斷是否為有動畫的轉場, 對于無動畫的轉場強制立刻執(zhí)行, 使它不影響后續(xù)的defer transition. (transition: 1為有動畫push, 2為有動畫pop, 0 為無動畫)
- (void)_pushViewController:(id)arg1 transition:(int)arg2 forceImmediate:(_Bool)arg3
問題:
在低端機(iOS8)上, 連續(xù)push三次也會導致轉場異常.
-
思路三:
導致轉場異常的根本原因是上一個次操作還沒執(zhí)行結束就開始執(zhí)行下一個操作, 同步執(zhí)行了多個轉場操作, 根據(jù)私有屬性wasLastOperationAnimated判斷上一個操作是否還在動畫中, 對于上一個次操作還沒執(zhí)行結束就開始執(zhí)行下一個操作的case, 直接clear之前的轉場操作, 但clear操作不能在發(fā)送“轉場請求”時執(zhí)行, 時機太早UINavigationController還沒進行defer transition的處理, 這里需要在UINavigationController進行defer transition的處理失敗后并在觸發(fā)轉場動畫前進行clear(vc已入棧, 只clear轉場的動畫), 即思路二中函數(shù)調用的時機, 在其中進行非正常轉場的clear操作.
問題:
clear操作后, 異常轉場之前還未執(zhí)行或正常執(zhí)行的轉場動畫會被取消, 直接展示最后棧頂元素.
結論:
hook私有API 獲取觸發(fā)轉場動畫前的時機, 在每次觸發(fā)轉場動畫前判斷上一次是否完成, 對于異常情況進行_clearLastOperation操作
取消之前的轉場過程保護, 保證業(yè)務邏輯正常跳轉
- (void)ac_pushViewController:(id)viewController transition:(int)transition forceImmediate:(_Bool)force {
BOOL needClear = [self ac_checkTransition];
if (needClear) {
[self ac_clearOperation];
}
[self ac_pushViewController:viewController transition:transition forceImmediate:force];
}
- (id)ac_popViewControllerWithTransition:(int)transition allowPoppingLast:(_Bool)allowPoppingLast {
BOOL needClear = [self ac_checkTransition];
id value = [self ac_popViewControllerWithTransition:transition allowPoppingLast:allowPoppingLast];
if (needClear) {
[self ac_clearOperation];
}
return value;
}
- (id)ac_popToViewController:(id)viewController transition:(int)transition {
BOOL needClear = [self ac_checkTransition];
id value = [self ac_popToViewController:viewController transition:transition];
if (needClear) {
[self ac_clearOperation];
}
return value;
}
- (BOOL)ac_checkTransition {
bool lastOperationAnimated = NO;
//獲取last opertaion 是否還在轉場動畫中
SEL lastOperationSEL = NSSelectorFromString(@"wasLastOperationAnimated");
if ([self respondsToSelector:lastOperationSEL]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
lastOperationAnimated = [self performSelector:lastOperationSEL];
#pragma clang diagnostic pop
}
return lastOperationAnimated;
}
- (void)ac_clearOperation {
//只是clear轉場動畫, navigation堆棧依舊保持原樣
SEL clearLastOperationSEL = NSSelectorFromString(@"_clearLastOperation");
if ([self respondsToSelector:clearLastOperationSEL]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:clearLastOperationSEL];
#pragma clang diagnostic pop
}
}
風險:
hook私有API 3個, 調用私有API 2個