Can't add self as subview解析

转场过程解析

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

车祸分析:
  • 第一次点返回时(实际应该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 <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:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容