UIPageViewController缺陷

写在前面

本文整理了UIPageViewController在使用中的一些缺陷和bug,结合网上的一些资料提供一个相对全面的总结。文章及代码中如有任何形式的错误、疑问欢迎在留言区提出。

文中提到的Crash类问题使用(iOS7)设备进行测试,在iOS8、9上UIPageViewController的crash类问题已经得到了改进。但仍然存在不同程度的缺陷。

UIPageViewController

UIPageViewController是App中常用的控制器。它提供了一种分页效果来显示其childController的View。用户可以通过手势像翻书一样切换页面。
切换页面时看起来是连续的,但静止状态下UIPageViewController同时只有一个childViewController。它的导航效果是通过替换childController实现的。
这是一种非常有效的设计:无论是三两个还上千个页面,用户的翻页与导航处理都是无差别的。因为这些页面都是即时创建的,每个页面只有当你浏览它的时候才会存在。这种设计显然苹果更多地考虑了内存的问题和通用性。
UIPageViewController替我们解决了页面导航、childController生命周期管理平滑过渡动画(index不相邻页面切换时)等问题。但是存在一些不足和bug。

为什么弃用UIPageViewController?

问题1:

设置UIPageViewController为UIPageViewControllerTransitionStyleScroll且调用setViewControllers:direction:animated:completion:传递参数animated YES时,会引发一系列症状,例如:

  1. 缓存页面导航设置不正确,Page View Controller会导航到错误的页面
  2. 删除上一页view controller(已经切换完成后)失败,仍然可以scroll回到一个空白页

这儿这些症状的根源被描述地很详细:

This is actually a bug in UIPageViewController. It occurs only with the scroll style (.Scroll) and only after calling setViewControllers:direction:animated:completion: with animated:YES. Thus there are two workarounds:

Don't use UIPageViewControllerTransitionStyleScroll.

Or, if you call setViewControllers:direction:animated:completion:, use animated:NO.

To see the bug clearly, call setViewControllers:direction:animated:completion: and then, in the interface (as user), navigate left (back) to the preceding page manually. You will navigate back to the wrong page: not the preceding page at all, but the page you were on when setViewControllers:direction:animated:completion: was called.

The reason for the bug appears to be that, when using the scroll style, UIPageViewController does some sort of internal caching. Thus, after the call to setViewControllers:direction:animated:completion:, it fails to clear its internal cache. It thinks it knows what the preceding page is. Thus, when the user navigates leftward to the preceding page, UIPageViewController fails to call the dataSource method pageViewController:viewControllerBeforeViewController:, or calls it with the wrong current view controller.

大意是这是UIPageViewController的bug,当且仅当UIPageViewController为.Scroll时,调用setViewControllers:direction:animated:completion: 且方法参数animated YES才可能出现。
错误的原因是,当使用.Scroll风格时,UIPageViewController做了一些内部的缓存排序,当调用setViewControllers:direction:animated:completion:时,它没有清空其内部缓存。它认为它已经知道前一个页面的存在,当它调用前一个页面的时候,就不会去调用dataSource方法或者调用错误。
根据这个描述,其实可以对之前的错误代码做相应的处理,通过再次调用setViewControllers方法强制重新调用dataSource方法以更新缓存。例如:

@weakify(self)
 [self.pageViewController setViewControllers:@[targetViewController]
                                   direction:direction
                                    animated:animated
                                  completion:^(BOOL finished)
 {
     @strongify(self)
     if (finished) {
         [self.pageViewController setViewControllers:@[self.pages[index]] direction:direction animated:NO completion:NULL];               
         // bug fix for uipageview controller
     }
 }];

很不幸这种做法引发了崩溃(iOS7/8/9):

Assertion failure in
[_UIQueuingScrollView_replaceViews:updatingContents:adjustContentInsets:animated:], /SourceCache/UIKit_Sim/UIKit-3318.16.14/_UIQueuingScrollView.m:383**

分析后发现我们忽略的一个重点,setViewControllers这个方法是更新页面的操作应该在主线程中调用。修改代码如下:

 @weakify(self)
 [self.pageViewController setViewControllers:@[targetViewController]
                                   direction:direction
                                    animated:animated
                                  completion:^(BOOL finished)
 {
     @strongify(self)
     if (finished) {
         dispatch_async(dispatch_get_main_queue(), ^
                        {
                            if (self) {
                                [self.pageViewController setViewControllers:@[self.pages[index]] direction:direction animated:NO completion:NULL];               
                                // bug fix for uipageview controller
                            }
                        });
     }
 }];

代码看起来虽然不怎么优雅,但好在这样可以继续使用UIPageViewController。直到我们发现了Fabric上的崩溃和上段解决方案代码引发的新问题:

Fabric崩溃:
UIPageViewController_Fabric.jpg

这个崩溃在iOS7/8/9上都有出现。显然这和Apple Developer Forums中提到的问题现象是一样的:No view controller managing visible view:

但Apple Developer Forums中的问题,是因为没有在主线程中执行第二次setViewControllers方法导致的。我们的代码里已经做了相应处理,但仍然有小规模数量的崩溃。很不幸,暂时还没找到Fabric崩溃的具体复现方式。

上图说明,上段代码并没有完全解决page view controller页面缓存不正确的bug。只是不会那么频繁地出现。

引发新问题:

此外还发现:当通过上段代码导航切换index的时候,如果第一次导航(setViewControllers)动画没有结束时,马上开始第二次导航(setViewControllers)。很容易复现问题:页面没有停在最终的那个index上。这是因为第二次主动调用的带动画的setViewControllers执行在第一次调用的回调的无动画的setViewControllers之前导致的。

问题2:

切换childViewController引起的卡顿问题很严重。在iPhone4、4s、5、5c、甚至5s上都会有不同程度的卡顿问题:
一般情况下,page view controller切换页面的资源消耗至少相当于调用一次transitionFromViewController:toViewController:duration:options:animations:completion:。在硬件配置较低的iPhone4、4s等手机上就会有明显卡顿,如果child view controller的生命周期方法中再做一些消耗资源的操作,App甚至会因为切换导致资源占用过多、内存警告,最终引起Crash。这种情况给低配手机性能优化带来了很大障碍。

其性能问题主要体现在,切换childController时候的CPU占用升高、以及切换时的内存频繁波动。
静止状态下,其CPU占用率位置在很低的水品甚至不到1%,当child congroller切换时,其CPU占用率如图所示(iPhone6 Plus/iOS9.3):

UIPageViewController快速非交互切换_CPU.png
UIPageViewController快速交互切换_CPU.png

文章开头介绍UIPageViewController时提到过,UIPageViewController的设计更多的考虑了少占用内存,从下图中内存的波动曲线也可以看到。当频繁切换child controller时,UIPageViewController尽可能快的清理内存。快速切换时,其内存波动如下图所示(iPhone6 Plus/iOS9.3):


UIPageViewController快速切换_Memory.png

总结:

经过多个设备、系统版本的测试和网上资料的整理。问题1的处理方案是现在最主流的一种解决方案,但这样的方案仍然引发了Fabric崩溃和新的缺陷问题,从保证App质量的角度出发,这个解决方案是不可取的。问题2的性能问题虽然在高配手机上并不明显,但考虑的所有用户的体验这个缺陷也很值得去优化。
为了彻底解决这些缺陷问题,作者计划重新开发一个Page view controller控件来彻底解决UIPageViewController带来的缺陷问题。替代方案将会尽快整理并发出,待续...

传送门:UIPageViewController替换方案

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

推荐阅读更多精彩内容