本文來自於騰訊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