本文來自於騰訊Bugly公眾號(weixinBugly),作者:sparrowchen,未經作者同意,請勿轉載,原文地址:
http://mp.weixin.qq.com/s/hBgvPBP12IQ1s65ru-paWw
1.元件介紹
Page是企鵝FM研發的分頁元件,包括支援分頁非互動切換(通過方法呼叫導航切換)和互動切換(螢幕的手勢滑動),多個分頁Controller和View的管理。
1.1需求背景
為什麼棄用UIPageViewController,首先介紹一下UIPageViewController,這是系統為開發者定製的分頁元件,提供了兩種分頁切換的效果,一是滑動 二是翻頁。且提供了前後切換的回撥。
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.
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。
1.2使用說明
使用非常簡單,繼承元件的類,實現相應的delegate和datasourc就可以了。
Page的例圖如下:
頁面層次關係如下:
圖中由一個圖片,3個欄目 (詳情,節目,評論)和一個List組成。可以分為三個層次,Cover,Tab和Page。
Page元件層次關係如下,
圖中的ShowListController是節目分頁,AlbumListController是專輯分頁.
2.元件架構設計
2.1 架構介紹
類圖如下:
簡要說明下各個協議的作用:
FMPageDataSource, 提供子頁面,子頁面的個數,子頁面展示的frame給PageController。
FMPageDelegate, 提供頁面互動切換和非互動切換的回撥給上層以及頁面的縱向滑動和橫向滑動的contentoffset給上層。
FMTabDataSource, 提供TabView的具體展示效果。
FMTabDelegate, 提供TabView的點選響應給上層。
FMCoverController, 提供CoverView給CoverController.
其中,FMTabController預設遵循FMTabDataSource,FMTabDelegateSource,FMPageDataSource,FMPageDelegate協議。FMCoverController遵循FMCoverDatasource協議。
2.2 介面設計
介面遵循高內聚和低耦合的特性,只把Delegate和DataSource開放給上層,同時做介面分離,把Page,Tab,Cover特性的分離。 程式碼如下:
@interface FMTabController : FMBusinessViewController <FMPageControllerDataSource, FMPageControllerDelegate, FMTabDataSource, FMTabDelegate>
@interface FMCoverController : FMTabController <FMCoverDataSource>
2.3 Child頁面的生命週期管理和切換。
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
互動切換和非互動切換
2.4 效能問題擴充套件
以下通過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的下邊緣處。
3.3不相鄰頁面切換的問題
不相鄰頁面的非互動切換會閃過中間的頁面,產生不好的使用者體驗,本元件的解決方法是
非互動切換,模擬切換的動畫,這裡需要考慮的一個複雜情況是第一次動畫還未結束就開始第二次,這時候需要提前結束第一次動畫。修改後的效果圖如下,
3.4平衡效能的問題。
因為Page要管理多個controller和view,如果子頁面到1000,甚至10000個怎樣去處理。比如微信閱讀的一本書就可能有10000頁。所以這裡如果全部都儲存就可能產生一個問題,記憶體會不會過大。
觀察UIPageViewController,它到一定的記憶體限制,會主動去釋放很久沒翻過的頁面。所以這裡,可以使用LRUCache的機制,只儲存一定數量的頁面。由於本應用並不涉及到過多的子頁面,考慮的時間花銷和記憶體,全部儲存了所有頁面。
demo地址:https://github.com/xichen744/SPPage
本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:
http://mp.weixin.qq.com/s/hBgvPBP12IQ1s65ru-paWw