Can't add self as subview解析

转场过程解析

UINavigationController对于translation动画做了一定的封装, 同时持有fromAnimateView与toAnimateView, 在进行translation动画时将对应的VC的view挂载到对应的AnimateView上, 动画视图AnimateView又挂载到容器视图wrapperView, UINavigationController只需控制容器中的AnimateView实现相应translation动画, translation动画完成后, 移除动画视图并挂载栈顶的视图, 实现navigationController对外部进行了动画隔离.

Can't add self as subview解析_第1张图片
A push to B (transition)
Can't add self as subview解析_第2张图片
A push to B (complete)
Can't add self as subview解析_第3张图片
B pop to A (transition)

Can't add self as subview 复现

模拟车祸:

pushNoAnimate(@"A");
pushAnimate(@“B");
pushAnimate(@“C”);
同时执行完以上操作(即上一个还没执行完毕就同步执行后续操作), 之后的pop退场操作会导致车祸

车祸现场:

转场动画中toAnimateView加载到WrapperView这一步骤

车祸前现象:

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的视图动画没按原有的队列执行,一起执行而导致冲突,只完成C的动画,并触发警告“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),触发了To Animation View -> To Animation View。

  • 思路一:

使用delegate

- (nullable id )navigationController(UINavigationController *)navigationController animationControllerForOperation (UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC

自定义所有转场动画, 规避系统进行转场动画时的错误视图加载.

问题:

当错误case产生时, transitioningController中取到的containerView取值为nil, 只是跳过了这一次转场动画, 而实际的错误转场现象并未解决, 如以上车祸模拟, pushC成功入栈, 但是视图没有加载到容器中, 依旧展示的是B的vc与view.

重新定位问题:

转场动画时的错误视图加载的根本原因是连续非正常的转场, class-dump出UINavigationController有相应的defer transition的属性与API, navigationController对连续转场做了一定流程的控制

连续转场的时序图如下, 后续两个transition都被defer, 后续统一触发
UINavigationController暴露给外部调用的push/pop方法实际只是一个“转场请求”, 对于连续转场navigationController会统一调度这些“请求”


Can't add self as subview解析_第4张图片
连续转场的时序图

系统Bug:
无动画的转场也需要完成一些切换vc, 重新挂载view等操作, 而在执行这些操作的同时, 后续触发的“转场请求”会根据当前正在执行的转场判断是否需要被加到deffer的队列中, 所以无动画的转场的后续转场操作会同步执行, 从而导致转场异常.

  • 思路二:

模拟车祸的的路径中, 都有无动画的转场, 在私有方法中根据transition参数, 判断是否为有动画的转场, 对于无动画的转场强制立刻执行, 使它不影响后续的defer transition. (transition: 1为有动画push, 2为有动画pop, 0 为无动画)

- (void)_pushViewController:(id)arg1 transition:(int)arg2 forceImmediate:(_Bool)arg3
问题:

在低端机(iOS8)上, 连续push三次也会导致转场异常.

  • 思路三:

导致转场异常的根本原因是上一个次操作还没执行结束就开始执行下一个操作, 同步执行了多个转场操作, 根据私有属性wasLastOperationAnimated判断上一个操作是否还在动画中, 对于上一个次操作还没执行结束就开始执行下一个操作的case, 直接clear之前的转场操作, 但clear操作不能在发送“转场请求”时执行, 时机太早UINavigationController还没进行defer transition的处理, 这里需要在UINavigationController进行defer transition的处理失败后并在触发转场动画前进行clear(vc已入栈, 只clear转场的动画), 即思路二中函数调用的时机, 在其中进行非正常转场的clear操作.

问题:

clear操作后, 异常转场之前还未执行或正常执行的转场动画会被取消, 直接展示最后栈顶元素.


结论:

hook私有API 获取触发转场动画前的时机, 在每次触发转场动画前判断上一次是否完成, 对于异常情况进行_clearLastOperation操作
取消之前的转场过程保护, 保证业务逻辑正常跳转

- (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个

Ref:

  • class dump
  • 图控制器转场详解

你可能感兴趣的:(Can't add self as subview解析)