安全性iOS学习iOS 开发每天分享优质文章

Can't add self as subview解析

2017-09-26  本文已影响705人  Archerlly

转场过程解析

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

A push to B (transition) A push to B (complete) 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

车祸分析:

使用delegate

- (nullable id <UIViewControllerAnimatedTransitioning>)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会统一调度这些“请求”


连续转场的时序图

系统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:

上一篇下一篇

猜你喜欢

热点阅读