前文回顧
文接上一篇UIPageViewController缺陷。上篇中總結(jié)了UIPageViewController的幾個(gè)不可接受缺陷:1.在Scroll style下UIPageViewController的setViewControllers方法調(diào)用導(dǎo)致緩存設(shè)置不正確的缺陷以及針對(duì)這個(gè)缺陷改進(jìn)方案引發(fā)的另一個(gè)快速連續(xù)切換問題;2.在低配設(shè)備上的性能缺陷。
針對(duì)這些問題本文通過自定義GYPageViewController的方式來解決。并記錄分享在模擬替換過程中遇到的麻煩及解決方案。文中及代碼中如有任何形式的錯(cuò)誤、疑問歡迎在留言區(qū)提出戒悠。
名詞解釋
交互切換:用戶通過屏幕的手勢(shì)操作切換令哟,例如滑動(dòng)笤休。
非交互切換:通過方法調(diào)用來導(dǎo)航切換,例如選擇segment選項(xiàng)來觸發(fā)切換方法筛圆。
交互動(dòng)畫:用戶通過屏幕手勢(shì)參與的切換動(dòng)畫,動(dòng)畫無固定時(shí)長(zhǎng)隨用戶交互變化六孵。
非交互動(dòng)畫:通過方法調(diào)用的切換纬黎,所帶有的固定時(shí)長(zhǎng)動(dòng)畫。
GitHub上的替代方案
Git上流行的替代方案:TYPagerController劫窒、WMPageController本今。這兩個(gè)都是基于scroll view做的定制。適配了很多需求使用場(chǎng)景主巍。但是它們有兩個(gè)關(guān)鍵點(diǎn)沒有考慮:生命周期管理冠息、** 非交互動(dòng)畫**。
這兩個(gè)解決方案在child controllers生命周期管理時(shí)孕索,不能以交互切換逛艰、非交互切換的起止點(diǎn)來區(qū)分willAppear/didAppear,willDisappear/didDisppear等方法調(diào)用順序與時(shí)間間隔搞旭;動(dòng)畫方面以禁止非交互切換動(dòng)畫的方式來規(guī)避非相鄰index切換時(shí)動(dòng)畫突兀的問題(非相鄰index切換散怖,會(huì)快速scroll過中間間隔的page,且有些page可能并未添加到scrollView上)肄渗。
本文中涉及的業(yè)務(wù)模塊基于UIPageViewController構(gòu)建镇眷,只有保證生命周期管理順序上完全一致才能使項(xiàng)目的修改最小翎嫡;另外欠动,scroll view原生offset動(dòng)畫在非相鄰index切換時(shí)會(huì)很突兀;也為了滿足需求的前提下最大限度模擬UIPageViewController的功能惑申,最終采取自定義控件的方式來解決問題具伍。
Step1:問題分解
功能:
- 管理多個(gè)controller和view。
- 支持頁面導(dǎo)航切換圈驼,包括交互切換沿猜,非交互切換(例segment bar)
- 保證與UIPageViewController導(dǎo)航時(shí)child controller的生命周期方法調(diào)用順序完全相同(包括交互、非交互切換)
- 模擬UIPageViewController的平滑切換碗脊,在非相鄰index切換時(shí)仍與相鄰頁面切換效果相同。
性能:
- 在低配設(shè)備(iPhone4橄妆、4s等)child controller初始化之后能在大部分情況下保證切換速度衙伶、避免頁面卡頓。
標(biāo)黑 部分問題是替換方案的關(guān)鍵害碾,將在Step 3 趟坑時(shí)中細(xì)述
Step2:初步方案
- UIScrollView提供了分頁效果矢劲、手勢(shì)處理以及交互操作中多個(gè)時(shí)機(jī)的代理回調(diào)方法。從這些代理方法入手可以獲得交互切換慌随、非交互切換的時(shí)機(jī)與起止點(diǎn)芬沉。管理child controller方面躺同,UIViewController即可勝任。
- child controller生命周期的切換與模擬需要花一些功夫丸逸。下文中會(huì)詳細(xì)描述生命周期模擬過程蹋艺。
- 動(dòng)畫問題,scroll view的setContentOffset:animated:方法效果上在不相鄰index切換回跨過中間頁面黄刚,很突兀不可接受捎谨。本文將通過自定義動(dòng)畫模擬來解決。
- 為了保證切換速度避免卡頓憔维。編碼時(shí)盡量避免頻繁地add/remove/transitionFrom child view controller涛救。只在第一次用到child view controller的時(shí)候?qū)ζ溥M(jìn)行addChildViewController操作。這樣在下次切換的時(shí)候只需要關(guān)注child controllers的生命周期調(diào)用即可业扒。
Step3:開始趟坑
1號(hào)坑:生命周期管理方式選用
一般情況下检吆,child controller的生命周期調(diào)用是通過parent controller傳遞的。而parent controller生命周期方法首次傳遞是通過對(duì)child controller的添加/刪除/切換操作來實(shí)現(xiàn)的程储,在此之后child controller的生命周期則隨parent controller的生命周期一起調(diào)用蹭沛。
顯然這種中規(guī)中矩的方式遇到了瓶頸,因?yàn)閏hild controller的添加/刪除/切換操作具有事務(wù)性虱肄。所以最終改成了一種直接控制child controller生命周期順序的方式致板。下文瓶頸 中詳述。
Note:這里可以復(fù)習(xí)一下控制器的管理的相關(guān)知識(shí)
瓶頸:
子控制器生命周期方法的首次被調(diào)用依賴于添加/刪除/切換操作咏窿。而這些操作都必須將對(duì)view的操作包夾在操作過程中(例:添加/刪除/切換源碼)斟或,即對(duì)child controller的添加/刪除/切換這三種操作均具有事務(wù)性(其中包括controller操作add/remove、view操作)集嵌。曾經(jīng)嘗試過將view的操作分離出來單獨(dú)add/remove萝挤,但這將直接導(dǎo)致生命周期調(diào)用順序錯(cuò)誤或者不調(diào)用,因?yàn)橹钡絭iew顯示在頁面上整個(gè)添加操作才算完成根欧。
- 如果通過頻繁地添加(willAppear怜珍、didAppear)/刪除(willDisappear、did Disappear)/切換(前兩個(gè)操作綜合)child controllers的方式來模擬生命周期調(diào)用凤粗。即當(dāng)非交互切換頁面時(shí)(點(diǎn)擊segment通過代碼showPageAtIndex:)酥泛,效果毫無問題。但是嫌拣,頻繁的添加刪除controller這樣帶來的卡頓問題是這種方案的缺陷1柔袁。
- 當(dāng)用戶通過滑動(dòng)scroll view來切換頁面時(shí),在交互切換未完成時(shí)添加新child controller异逐。將會(huì)造成一種現(xiàn)象:用戶取消滑動(dòng)操作或者來回多次滑向相鄰的index時(shí)捶索,這種設(shè)計(jì)將會(huì)更加頻繁地調(diào)用child controller的添加刪除。缺陷2
- 缺陷2其實(shí)可以通過scroll view的代理方法在scroll view徹底切換到新的index時(shí)再調(diào)用child controller操作灰瞻,就可以避免無謂的調(diào)用而引起卡頓腥例。但是辅甥,因?yàn)関iew的操作和child controller的操作是綁定的事務(wù)性的。因?yàn)橹钡絪croll view滑動(dòng)到新的index才會(huì)添加新view燎竖。這期間是看不到新view的缺陷3
- 考慮過璃弄,可以預(yù)加載的方式來解決相鄰頁面交互切換滑動(dòng)的問題,但是無論預(yù)加載的是view還是controller底瓣,要么會(huì)引起卡頓問題谢揪、要么會(huì)引起生命周期錯(cuò)誤調(diào)用問題。
解決方法:
這三個(gè)方法可以解決這個(gè)瓶頸:
-(BOOL) shouldAutomaticallyForwardAppearanceMethods
在UIViewController的子類中可以重寫這個(gè)方法捐凭,return YES將會(huì)把生命周期自動(dòng)傳遞給childControllers拨扶,NO將不會(huì)自動(dòng)傳遞生命周期。
-(void) beginAppearanceTransition:animated:
當(dāng)實(shí)現(xiàn)一個(gè)container controller時(shí)茁肠,使用這些方法來通知child合適調(diào)用appear患民、disappear方法。而不是直接調(diào)用垦梆。第一個(gè)參數(shù)YES表示要顯示頁面調(diào)用willAppear方法匹颤,NO表示要讓頁面小時(shí)調(diào)用willDisappear方法。
-(void) endAppearanceTransition
這個(gè)方法要與上一個(gè)方法成對(duì)出現(xiàn)托猩。上一個(gè)方法的效果會(huì)調(diào)用willXXX操作印蓖,這個(gè)方法會(huì)調(diào)用didXXX操作。不成對(duì)會(huì)導(dǎo)致生命周期調(diào)用錯(cuò)誤京腥。
就是說可以重寫shouldAutomaticallyForwardAppearanceMethods方法以return NO的方式規(guī)避添加/刪除/切換的不合適的生命周期調(diào)用赦肃。并通過beginAppearanceTransition和endAppearanceTransition方法在合適時(shí)機(jī)管理生命周期。這樣既解決了缺陷1公浪、 缺陷2他宛、 缺陷3又給模擬UIPageViewController的生命周期調(diào)用順序提供了一種新方式。
代碼:
func scrollViewDidScroll(scrollView: UIScrollView) {
......
if lastGuessIndex != guessToIndex &&
guessToIndex != self.currentPageIndex &&
self.guessToIndex >= 0 &&
self.guessToIndex < maxCount
{
self.gy_pageViewControllerWillShow(self.guessToIndex, toIndex: self.currentPageIndex, animated: true)
self.delegate?.gy_pageViewController?(self, willTransitonFrom: self.pageControllers[self.guessToIndex],
toViewController: self.pageControllers[self.currentPageIndex])
self.addVisibleViewContorllerWith(self.guessToIndex)
self.pageControllers[self.guessToIndex].beginAppearanceTransition(true, animated: true)
/**
* Solve problem: When scroll with interaction, scroll page from one direction to the other for more than one time, the beginAppearanceTransition() method will invoke more than once but only one time endAppearanceTransition() invoked, so that the life cycle methods not correct.
* When lastGuessIndex = self.currentPageIndex is the first time which need to invoke beginAppearanceTransition().
*/
if lastGuessIndex == self.currentPageIndex {
self.pageControllers[self.currentPageIndex].beginAppearanceTransition(false, animated: true)
}
if lastGuessIndex != self.currentPageIndex &&
lastGuessIndex >= 0 &&
lastGuessIndex < maxCount{
self.pageControllers[lastGuessIndex].beginAppearanceTransition(false, animated: true)
self.pageControllers[lastGuessIndex].endAppearanceTransition()
}
}
}
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let newIndex = self.calcIndexWithOffset(Float(scrollView.contentOffset.x),
width: Float(scrollView.frame.size.width))
let oldIndex = self.currentPageIndex
self.currentPageIndex = newIndex
if newIndex == oldIndex {//最終確定的位置與其實(shí)位置相同時(shí)欠气,需要重新顯示其實(shí)位置的視圖厅各,以及消失最近一次猜測(cè)的位置的視圖。
if self.guessToIndex >= 0 && self.guessToIndex < self.pageControllers.count {
self.pageControllers[oldIndex].beginAppearanceTransition(true, animated: true)
self.pageControllers[oldIndex].endAppearanceTransition()
self.pageControllers[self.guessToIndex].beginAppearanceTransition(false, animated: true)
self.pageControllers[self.guessToIndex].endAppearanceTransition()
}
} else {
self.pageControllers[newIndex].endAppearanceTransition()
self.pageControllers[oldIndex].endAppearanceTransition()
}
//Reset for calculation in next interaction
self.originOffset = Double(scrollView.contentOffset.x)
self.guessToIndex = self.currentPageIndex
self.gy_pageViewControllerDidShow(self.guessToIndex, toIndex: self.currentPageIndex, finished:true)
self.delegate?.gy_pageViewController?(self, didTransitonFrom: self.pageControllers[self.guessToIndex],
toViewController: self.pageControllers[self.currentPageIndex])
}
2號(hào)坑:UIPageViewController生命周期順序模擬
UIPageViewController生命周期規(guī)律
UIPageViewController生命周期方法調(diào)用場(chǎng)景预柒,可以根據(jù)用戶的交互方式加以區(qū)別:**交互切換 **队塘、 非交互切換。先看看它的生命周期管理順序宜鸯。
-
非交互切換:
這種導(dǎo)航交互比較簡(jiǎn)單人灼,動(dòng)畫具有固定時(shí)間大概是0.3秒左右。而且顾翼,在非交互的導(dǎo)航切換中,index是否相鄰順序都是一樣的奈泪。child controllers的生命周期調(diào)用順序是:
** Will Appear 0
** Will Disappear 1
//這里大概會(huì)有0.3秒的動(dòng)畫時(shí)間
** Did Appear 0
** Did Disppear 1
-
交互切換:
有交互切換情況會(huì)復(fù)雜很多适贸,切換動(dòng)畫是根據(jù)用戶的操作來決定的灸芳。而且涉及到用戶的取消操作即來回滑動(dòng)。不過一般只能切換到相鄰的index拜姿,連續(xù)切換(第一次滑動(dòng)松手Decelerate動(dòng)畫未結(jié)束時(shí)烙样,馬上交互開始下一次滑動(dòng))的情況也會(huì)進(jìn)行特殊處理
1. 滑動(dòng)切換到相鄰index:
//交互開始剛進(jìn)入到新index立即執(zhí)行
** Will Appear 0
** Will Disappear 1
//間隔時(shí)間:由交互時(shí)間+松手后Decelerate動(dòng)畫時(shí)間決定
** Did Appear 0
** Did Disppear 1
2. 從index1滑向index2緊接著反向滑動(dòng)到index0(取消一次):
//從index1滑向index2
** Will Appear 2
** Will Disappear 1
//取消滑向index2并離開,反向滑向index0
** Will Appear 0
** Will Disppear 2
** Did Disppear 2
//松手完成交互蕊肥,Decelerate動(dòng)畫完成剩余offset的偏移谒获。
** Did Appear 0
** Did Disppear 1
3. 從index1滑向index2緊接著反向滑動(dòng)到index0再反向滑向index2(取消兩次):
//第一次滑向index2
** Will Appear 2
** Will Disappear 1
//第一次取消并反向滑向index0
** Will Appear 0
** Will Disappear 2
** Did Disappear 2
//第二次取消并反向滑向index2
** Will Appear 2
** Will Disappear 0
** Did Disppear 0
//松手完成交互,Decelerate動(dòng)畫完成剩余offset的偏移壁却。
** Did Appear 2
** Did Disppear 1
4. 連續(xù)切換(第一次滑動(dòng)松手Decelerate動(dòng)畫未結(jié)束時(shí)批狱,馬上交互開始下一次滑動(dòng)):
** Will Appear 2
** Will Disppear 3
** Will Appear 1
** Will Disppear 2
** Did Disppear 2
** Will Appear 0
** Will Disappear 1
** Did Disppear 1
** Did Appear 0
事實(shí)上,每次操作的調(diào)用順序都是不穩(wěn)定的展东,雖然結(jié)果取決于操作的速度和滑動(dòng)時(shí)機(jī)銜接赔硫,但這已經(jīng)毫無規(guī)律可言⊙嗡啵可見在這點(diǎn)上UIPageViewController也并沒有做處理爪膊。我們暫且忽略這種情況。
生命周期一般規(guī)律:
- 一個(gè)Will Appear可以沒有Did Appear與之對(duì)應(yīng)砸王;
- 一個(gè)Will Disappear一定有一個(gè)Did Disappear與之對(duì)應(yīng)推盛;
- 只有最終確定導(dǎo)航到的index才會(huì)調(diào)用Did Appear,即整個(gè)交互過程中Did Appear只調(diào)用一次谦铃;
解決方法:
-
非交互切換:
模擬scroll view的導(dǎo)航動(dòng)畫耘成,并在動(dòng)畫之前調(diào)用beginAppearanceTransition:animated:,在動(dòng)畫結(jié)束回調(diào)中調(diào)用endAppearanceTransition即可。代碼:
// Aciton closure before simulated scroll animation
let scrollBeginAnimation = { () -> Void in
self.pageControllers[self.currentPageIndex].beginAppearanceTransition(true, animated: animated)
if self.currentPageIndex != self.lastSelectedIndex {
self.pageControllers[self.lastSelectedIndex].beginAppearanceTransition(false, animated: animated)
}
}
/* Scroll closure invoke setContentOffset with animation false. Because the scroll animation is customed.
*
* Simulate scroll animation among oldSelectView, lastView and currentView.
* After simulated animation the scrollAnimation closure is invoked
*/
let scrollAnimation = { () -> Void in
self.scrollView.setContentOffset(self.calcOffsetWithIndex(
self.currentPageIndex,
width:Float(self.scrollView.frame.size.width),
maxWidth:Float(self.scrollView.contentSize.width)), animated: false)
}
// Action closure after simulated scroll animation
let scrollEndAnimation = { () -> Void in
self.pageControllers[self.currentPageIndex].endAppearanceTransition()
if self.currentPageIndex != self.lastSelectedIndex {
self.pageControllers[self.lastSelectedIndex].endAppearanceTransition()
}
self.gy_pageViewControllerDidShow(self.lastSelectedIndex, toIndex: self.currentPageIndex, finished: animated)
self.delegate?.gy_pageViewController?(self, didLeaveViewController: self.pageControllers[self.lastSelectedIndex],
toViewController: self.pageControllers[self.currentPageIndex],
finished:animated)
}
-
交互切換:
有交互切換較為復(fù)雜的地方就是要考慮到取消的情況荷辕,而這些情況在生命周期方法中并沒有很好地給與區(qū)分凿跳。我們只能根據(jù)交互過程中scroll view的property以及緩存的一些參數(shù)來判斷用戶的行為。
首先疮方,交互切換的結(jié)束代理方法是固定的:scrollViewDidEndDecelerating控嗜。
其次,我們要通過對(duì)方法scrollViewDidScroll的調(diào)用情況加以區(qū)分骡显,區(qū)別用戶的交互動(dòng)作的開始和變化等行為疆栏。
func scrollViewDidScroll(scrollView: UIScrollView) {
//首先scrollView在dragging狀態(tài)下確定其為交互切換。
if scrollView.dragging == true && scrollView == self.scrollView {
let offset = scrollView.contentOffset.x
let width = CGRectGetWidth(scrollView.frame)
//上一次操作猜測(cè)的用戶將要滑向的lastGuessIndex
let lastGuessIndex = self.guessToIndex < 0 ? self.currentPageIndex : self.guessToIndex
//計(jì)算本次用戶將要去往的index:并緩存到變量guessToIndex
if self.originOffset < Double(offset) {
self.guessToIndex = Int(ceil((offset)/width))
} else if (self.originOffset > Double(offset)) {
self.guessToIndex = Int(floor((offset)/width))
} else {}
let maxCount = self.pageControllers.count
//如果上一次猜測(cè)的和本次猜測(cè)的有變化惫谤,即用戶取消了上一次操作做了一次反向滑動(dòng)壁顶。當(dāng)然,所有猜測(cè)的index應(yīng)該在安全范圍內(nèi)且所有猜測(cè)的頁面不應(yīng)該是當(dāng)前顯示的currentPageIndex溜歪。
if lastGuessIndex != guessToIndex &&
guessToIndex != self.currentPageIndex &&
self.guessToIndex >= 0 &&
self.guessToIndex < maxCount
{
self.gy_pageViewControllerWillShow(self.guessToIndex, toIndex: self.currentPageIndex, animated: true)
self.delegate?.gy_pageViewController?(self, willTransitonFrom: self.pageControllers[self.guessToIndex],
toViewController: self.pageControllers[self.currentPageIndex])
//如果guessToIndex對(duì)應(yīng)child controller還未添加則添加若专。然后調(diào)用生IM那個(gè)周期will appear方法。
self.addVisibleViewContorllerWith(self.guessToIndex)
self.pageControllers[self.guessToIndex].beginAppearanceTransition(true, animated: true)
/**
* Solve problem: When scroll with interaction, scroll page from one direction to the other for more than one time, the beginAppearanceTransition() method will invoke more than once but only one time endAppearanceTransition() invoked, so that the life cycle methods not correct.
* When lastGuessIndex = self.currentPageIndex is the first time which need to invoke beginAppearanceTransition().
*/
//只有交互初始化的時(shí)候lastGuessIndex 才會(huì)等于 self.currentPageIndex也只有這一次才需要調(diào)用當(dāng)前頁面的will disappear方法蝴猪。
if lastGuessIndex == self.currentPageIndex {
self.pageControllers[self.currentPageIndex].beginAppearanceTransition(false, animated: true)
}
//如果lastGuessIndex和當(dāng)前的頁面不是一個(gè)頁面调衰,就要調(diào)用其will disappear方法和did disappear方法膊爪。
if lastGuessIndex != self.currentPageIndex &&
lastGuessIndex >= 0 &&
lastGuessIndex < maxCount{
self.pageControllers[lastGuessIndex].beginAppearanceTransition(false, animated: true)
self.pageControllers[lastGuessIndex].endAppearanceTransition()
}
}
}
}
至此,經(jīng)過能夠完全模擬UIPageViewController的child controller 生命周期方法調(diào)用嚎莉。
3號(hào)坑: 交互動(dòng)畫效果
前文提到:在非相鄰index導(dǎo)航切換的動(dòng)畫會(huì)很突兀米酬。有交互的導(dǎo)航,是不需要考慮這個(gè)問題的趋箩,因?yàn)橹荒軌蚯袚Q到相鄰index赃额。在無交互情況下,才需要去模擬切換動(dòng)畫叫确。
這里跳芳,需要考慮的復(fù)雜情況主要是:在一次導(dǎo)航切換動(dòng)畫沒有完成的時(shí)候,馬上又開啟下一次的導(dǎo)航切換启妹。前文說過UIPageViewController在做這一操作的時(shí)候筛严,偶爾會(huì)出現(xiàn)最終頁面錯(cuò)亂的問題。而本文代碼在導(dǎo)航結(jié)果上不會(huì)出現(xiàn)問題饶米,但是動(dòng)畫上需要處理桨啃。
考慮在第一次動(dòng)畫未執(zhí)行結(jié)束就開啟第二次動(dòng)畫的時(shí)候,提前結(jié)束第一次動(dòng)畫:
Step4:性能對(duì)比
下面的性能測(cè)試對(duì)比都是基于iPhone6 Plus/iOS9.3:
在靜止?fàn)顟B(tài)下檬输,新老控件的CPU占用率很小很小甚至不到1% 照瘾。當(dāng)交互、非交互快速切換時(shí)丧慈,我們可以看到(圖4.1 - 4.4)GYPageViewController的CPU占用率明顯優(yōu)于UIPageViewController:
新老控件的內(nèi)存(Memory)占用方面也存在很大差異:
從圖4.5中內(nèi)存占用圖標(biāo)的波動(dòng)情況可以看出UIPageViewController在快速切換的時(shí)析命,會(huì)盡可能快地釋放掉不用的controller及其view(主要是view)以保證內(nèi)存占用較小,所以圖標(biāo)指標(biāo)先才會(huì)頻繁的波動(dòng)逃默。(這并不是引起低配設(shè)備卡頓的直接原因鹃愤。)
這和UIPageViewController缺陷文章開頭關(guān)于UIPageViewController設(shè)計(jì)的介紹是一致的。
從圖4.6 中可以看到起內(nèi)存占用在所有頁面都緩存之后是趨于穩(wěn)定的完域。這樣是以空間換時(shí)間软吐,來換取切換child controller時(shí)的低CPU占用。在實(shí)際測(cè)試中(包括iPhone4s/iOS7吟税、iPhone6 Plus/iOS9.3)這樣的犧牲在頁面卡頓方面有了明顯改善凹耙。
在兩個(gè)測(cè)試?yán)赢?dāng)中,主要的內(nèi)存差異主要來源于view的內(nèi)存占用肠仪。
Step5:總結(jié)
新的解決方案在需求上能夠很好地解決UIPageViewController功能方面的缺陷(緩存設(shè)置不正確等)肖抱;在性能上也優(yōu)化了在低配設(shè)備上由于切換child controller及其view而引發(fā)的卡頓;而且彌補(bǔ)了網(wǎng)上現(xiàn)行方案生命周期管理順序不正確异旧、非相鄰index切換動(dòng)畫等的問題意述。
但新方案并非在所有方面都優(yōu)于UIPageViewController,至少在內(nèi)存占用上是有劣勢(shì)的。(一個(gè)相當(dāng)復(fù)雜的child controller及其view內(nèi)存占用大概在(3~4M)荤崇,簡(jiǎn)單頁面大概在0.5M以內(nèi)镐依,就這點(diǎn)看維護(hù)十幾個(gè)以內(nèi)的頁面還不成問題。)所有的取舍應(yīng)該以自己的需求為準(zhǔn)天试。
GYPageViewController解決的問題 2016-7-28 更新
1.從設(shè)計(jì)思路上避免了UIPageViewController切換頁面時(shí)的緩存Bug
2.生命周期與UIPageViewController完全一致且解決了其連續(xù)快速切換生命周期調(diào)用錯(cuò)亂的問題。
3.用緩存控件換取切換child conroller的時(shí)間然低,避免瞬間CPU使用率飆升問題喜每。
4.通過緩存限額節(jié)省內(nèi)存,并且處理內(nèi)存警告雳攘。使內(nèi)存占用量可控且做了一場(chǎng)處理带兜。
5.解決UIPageViewController連續(xù)快速切換的頁面閃白問題(頁面在使用時(shí)未及時(shí)添加或提前移除)
Step6:TO DO
在整個(gè)替代方案中,仍有一些地方值得繼續(xù)優(yōu)化:
-
維護(hù)緩存池吨灭,移除長(zhǎng)期不使用的child controller及其頁面以節(jié)省內(nèi)存刚照。Step7:更新中已解決 - 非交互動(dòng)畫,在前一次動(dòng)畫未結(jié)束時(shí)開始新一次動(dòng)畫喧兄,不強(qiáng)制結(jié)束上次移動(dòng)而是在當(dāng)前時(shí)刻暫停并以這個(gè)狀態(tài)為起點(diǎn)无畔,開始新動(dòng)畫。
-
在快速連續(xù)交互切換時(shí)吠冤,生命周期規(guī)則重新整理并實(shí)現(xiàn)有規(guī)律調(diào)用浑彰。Step7:更新中已解決
Step7:更新2016-7-28
緩存策略:
內(nèi)存占用方面,主要是container controller的childViewControllers持有過多不必要的child controller所導(dǎo)致拯辙。之前的想法是用空間換取切換(add/remove)child controller的時(shí)間郭变。但綜合考慮內(nèi)存等因素對(duì)早前方案做一個(gè)折衷:緩存一定數(shù)量的常用child controllers并且在適當(dāng)時(shí)機(jī)清理多余的controllers。
Memory Cache的職責(zé):
1.避免多次調(diào)用dataSource方法涯保,即避免頻繁通過delegate創(chuàng)建常用的child controller帶來的性能損耗诉濒。
2.提供緩存child controllers限額
3.提供淘汰策略與時(shí)機(jī)
childViewControllers職責(zé)與限制:
1.存儲(chǔ)具體child controllers指針∠Υ海空間換時(shí)間的功臣未荒。
2.需要淘汰時(shí)機(jī),穩(wěn)定狀態(tài)下數(shù)量與Memory Cache一致撇他。
因?yàn)榇娴氖莄hild controllers的指針茄猫,兩份存儲(chǔ)并不會(huì)導(dǎo)致內(nèi)存double。難點(diǎn)在于兩者的同步問題:
1.當(dāng)element從Memory Cache淘汰的同時(shí)檢查是否為當(dāng)前操作頁面的相關(guān)交互頁面困肩。(非交互切換:原始頁面划纽、目的頁面為相關(guān)交互頁面;交互切換:當(dāng)前猜的的目的頁面及其左右兩個(gè)頁面為相關(guān)交互頁面)如果檢查非相關(guān)交互頁面則直接清理锌畸,否則加入到延遲清理集合并在最終的穩(wěn)定態(tài)開始時(shí)清理(切換過程徹底結(jié)束時(shí))勇劣。
2.過濾延遲切換隊(duì)列:當(dāng)頻繁切換時(shí),延遲隊(duì)列里的element可能是最終切換停止時(shí)的page。如果不過濾比默,將根據(jù)交互情況出現(xiàn)最終頁面被錯(cuò)誤移除的情況幻捏。需要在延遲清理集合的時(shí)候,過濾掉最終的目標(biāo)頁面命咐。
3.在連續(xù)交互切換時(shí)篡九,很長(zhǎng)時(shí)間不能達(dá)到一個(gè)穩(wěn)定態(tài)。即很多根據(jù)穩(wěn)定態(tài)計(jì)算的臨時(shí)數(shù)據(jù)是有偏差的醋奠。需要在連續(xù)切換過程中隨時(shí)糾偏以保證緩存的正確清理榛臼。
快速連續(xù)切換生命周期規(guī)律調(diào)用:
這個(gè)問題和緩存同步問題的第三點(diǎn)的問題原因是一樣的。在連續(xù)切換交互中窜司,數(shù)據(jù)計(jì)算有偏差會(huì)導(dǎo)致猜測(cè)的目標(biāo)頁面不準(zhǔn)確沛善,生命周期調(diào)用錯(cuò)亂。在達(dá)到穩(wěn)定態(tài)之前的任何交互節(jié)點(diǎn)(例如手指離開屏幕)都需要對(duì)數(shù)據(jù)計(jì)算糾偏塞祈。糾偏主要是不斷調(diào)試和優(yōu)化計(jì)算的工作金刁,這里有興趣可以參看下Git上的代碼。