本文来自于腾讯Bugly公众号(weixinBugly),做者:sparrowchen,未经做者赞成,请勿转载,原文地址:
http://mp.weixin.qq.com/s/hBgvPBP12IQ1s65ru-paWwjava
Page是企鹅FM研发的分页组件,包括支持分页非交互切换(经过方法调用导航切换)和交互切换(屏幕的手势滑动),多个分页Controller和View的管理。git
为何弃用UIPageViewController,首先介绍一下UIPageViewController,这是系统为开发者定制的分页组件,提供了两种分页切换的效果,一是滑动 二是翻页。且提供了先后切换的回调。github
a) UIPageViewController在iOS8如下的系统运行是有问题的,能够参考stackFlow上的症状描述https://stackoverflow.com/questions/12939280/uipageviewcontroller-navigates-to-wrong-page-with-scroll-transition-style/12939384#12939384缓存
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.app
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.测试
大意是说使用.Scroll的时候,UIPageViewController作了内部缓存的排序,当调用动画
setViewControllers:direction:animated:completion:
时 它认为本身知道了前一个的分页存在,当调用前一个页面的时候,就不会去调用dataSource的方法。
b) UIPageViewController的DataSource和Delegate的接口过于简单,对于比较复杂的状况(好比除了分页之外还有其余View的状况下)没法处理。参照下面的例图,我有一个tab下面有小黄条,跟着手势横向滑动的同时也横向滑动,这里系统的UIPageViewController没法支持。其外,我还须要子页面纵向滑动时候去修改Cover和Tab的frame。因此UIPageViewController没法知足比较复杂的需求。
c) 低配的机器会产生卡顿问题,由于系统的UIPageViewController,在快速切换的时候,会释放掉不用的页面,因此在快速回切的时候会形成卡顿,能够参考下面的性能测试。
综上所述,弃用了系统的UIPageViewController。
使用很是简单,继承组件的类,实现相应的delegate和datasourc就能够了。
Page的例图以下:
页面层次关系以下:
图中由一个图片,3个栏目 (详情,节目,评论)和一个List组成。能够分为三个层次,Cover,Tab和Page。
Page组件层次关系以下,
图中的ShowListController是节目分页,AlbumListController是专辑分页.
类图以下:
简要说明下各个协议的做用:
FMPageDataSource, 提供子页面,子页面的个数,子页面展现的frame给PageController。
FMPageDelegate, 提供页面交互切换和非交互切换的回调给上层以及页面的纵向滑动和横向滑动的contentoffset给上层。
FMTabDataSource, 提供TabView的具体展现效果。
FMTabDelegate, 提供TabView的点击响应给上层。
FMCoverController, 提供CoverView给CoverController.
其中,FMTabController默认遵循FMTabDataSource,FMTabDelegateSource,FMPageDataSource,FMPageDelegate协议。FMCoverController遵循FMCoverDatasource协议。
接口遵循高内聚和低耦合的特性,只把Delegate和DataSource开放给上层,同时作接口分离,把Page,Tab,Cover特性的分离。 代码以下:
@interface FMTabController : FMBusinessViewController <FMPageControllerDataSource, FMPageControllerDelegate, FMTabDataSource, FMTabDelegate> @interface FMCoverController : FMTabController <FMCoverDataSource>
1.UIScrollView支持分页效果,手势处理及交互操做多个回调方法能够实现页面的切换效果。
2.生命周期管理有两种方式 a.频繁地add/remove ChildController b.使用下面的代码实现生命周期的管理:
1)shouldAutomaticallyForwardAppearanceMethods 2)beginAppearanceTransition: animated: 3)endAppearanceTransition
a.会产生一个重大缺陷,就是频繁切换的卡顿问题。
b.不须要频繁地去调用add/remove,1)方法避免了 add/remove产生的生命周期,2)和3)保证了开发者能够本身控制ChildController的生命周期。
Page的生命周期图以下:
初次或者reloadPage
交互切换和非交互切换
如下经过Iphone5 模拟器 10.3系统,与UIPageViewController作了性能上的对比。
UIPageViewController 快速切换内存占用状况
UIPageViewController 快速切换GPU占用状况
Page组件快速切换内存占用状况
Page组件快速切换GPU占用状况
从上图中内存占用图标的波动状况能够看出UIPageViewController在快速切换的时,会尽量快地释放掉不用的controller及其view(主要是view)以保证内存占用较小,因此图标指标先才会频繁的波动,与UIPageViewController做对比,Page组件用空间换时间的策略避免页面卡顿。
3.技术实现的难点
从技术上看,能够分为如下四个点
3.1 接口的设计。
接口的设计,是整个架构的核心,若是开始设计很差,会致使后续的扩展就是加属性和加方法,致使代码愈来愈庞大,以至没法维护,因此尽可能保证简洁,职能单一,可扩展。
起初为了让delegate和datasource能够从Controller分离出去,把delegate和datasource都暴露了出去,但这样至关于多了5个属性,对于上层来讲并不便于理解这些接口,仿照UITableViewController,由继承的方式实现这些协议,让接口更加简洁。
3.2 页面纵向滑动跟随Tab和Cover一块儿滑动。
经过上面的动态图,能够知道,Page组件有这样一个功能,子页面纵向滑动会跟随Tab和Cover一块儿向上滑动,其中cover的滑动的实现是监听ChildController的ScrollView的contentOffset,修改Tab的height或y。Scrollview的滑动有一个难点,怎样保证ScrollView的向下滑动的反弹处紧贴Tab,而Scrollview又能够向上滑动到导航栏。
首先Scrollview的可见范围是整屏的,也就是设置frame为整屏,Scrollview滑动的范围,就由ContentInset,ContentOffset 共同决定。由于咱们知道UIScrollView的滑动范围会紧贴scrollView的bounds。因此首先,修改ContentInset的Top为-tabH-tabY,能够保证向下滑动到Tab的下边缘处反弹,又因为frame是整屏的,向上滑动时候就能够滑动导航栏,代码以下:
scrollView.contentInset = UIEdgeInsetsMake([self.dataSource pageTop], contentInset.left, contentInset.bottom, contentInset.right); scrollView.frame = CGRectMake(0,0,Screen_Width,Screen_Height)
其中的pageTop就是tab的下边缘处。
不相邻页面的非交互切换会闪过中间的页面,产生很差的用户体验,本组件的解决方法是
非交互切换,模拟切换的动画,这里须要考虑的一个复杂状况是第一次动画还未结束就开始第二次,这时候须要提早结束第一次动画。修改后的效果图以下,
由于Page要管理多个controller和view,若是子页面到1000,甚至10000个怎样去处理。好比微信阅读的一本书就可能有10000页。因此这里若是所有都保存就可能产生一个问题,内存会不会过大。
观察UIPageViewController,它到必定的内存限制,会主动去释放好久没翻过的页面。因此这里,可使用LRUCache的机制,只保存必定数量的页面。因为本应用并不涉及到过多的子页面,考虑的时间花销和内存,所有保存了全部页面。
demo地址:https://github.com/xichen744/SPPage
本文来自于腾讯Bugly公众号(weixinBugly),未经做者赞成,请勿转载,原文地址:
http://mp.weixin.qq.com/s/hBgvPBP12IQ1s65ru-paWw