UIScrollView的重用機制的了解。大家都知道這個UIScrollView。UItableVIew是繼承UIScrollView的UItableVIew預設裡面有自己的重用機制可以節省記憶體。UIScrollView是可以用來加載很多圖檔,有利用顯示,但是沒有重用機制會在真機上crash。
網上找了些資料:主要有兩種解決的方案
1.圖檔記憶體托管
将scrollview上所有的圖檔指針收集起來,當圖檔占用記憶體到達某個量後用setImage:nil将可視區域之外的圖檔全部删除,這個方法基本上能夠解決圖檔加載過多的crash問題,但是由于承載image的view沒有被删除,長時間滑動也會出現抖動的現象。
2.可重用的UIScrollView
經過了這些分析之後,我們很容易就能找到更進階的解決方案,就是模仿UITableView的實作機制,将scrollview進行重用,當scrollview上的view滑出可視範圍是,就将滑出的view重新設定rect添加到scrollview的最後,這樣的實作方式,保證在scrollview從開始到結束隻有渲染一屏,多屏隻是重新reload資源,進而讓海量圖檔浏覽也能保持高性能。
UIScrollView是iphone中的一個重要的視圖,它提供了一個方法,讓你在一個界面中看到所有的内容,進而不必擔心因為螢幕的大小有限,必須翻到下一頁進行閱覽。确實對于使用者來說是一個很好的體驗。但是又是如何把所有的内容都加入到scrollview,是簡單的addsubView。假如是這樣,豈不是scrollView界面上要放置很多的圖形,圖檔。移動裝置的顯示裝置肯定不如PC,怎麼可能放得下如此多的視圖。是以在使用scrollView中一定要考慮這個問題,當某些視圖滾動出可見範圍的時候,應該怎麼處理,是不管它那,還是進行記憶體回收或者重利用。蘋果公司的UITableView就很好的展示了在UIScrollView中如何重用可視的空間,減少記憶體的開銷。
首先還是看官方API如何解釋UIScrollView,一下是翻譯官方UIScrollView的幫助文檔。
UIScrollView類支援顯示比螢幕更大的應用視窗的内容。它通過揮動手勢,能夠使使用者滾動内容,并且通過捏合手勢縮放部分内容。
UIScrollView是UITableView和UITextView的超類。
UIScrollView的核心理念是,它是一個可以在内容視圖之上,調整自己原點位置的視圖。它根據自身架構的大小,剪切視圖中的内容,通常架構是和應用程式視窗一樣大。一個滾動的視圖可以根據手指的移動,調整原點的位置。展示内容的視圖,根據滾動視圖的原點位置,開始繪制視圖的内容,這個原點位置就是滾動視圖的偏移量。ScrollView本身不能繪制,除非顯示水準和豎直的訓示器。滾動視圖必須知道内容視圖的大小,以便于知道什麼時候停止;一般而言,當滾動出内容的邊界時,它就傳回了。
某些對象是用來管理内容顯示如何繪制的,這些對象應該是管理如何平鋪顯示内容的子視圖,以便于沒有子視圖可以超過螢幕的尺寸。就是當使用者滾動時,這些對象應該恰當的增加或者移除子視圖。
因為滾動視圖沒有滾動條,它必須知道一個觸摸信号是打算滾動還是打算跟蹤裡面的子視圖。為了達到這個目的,它臨時中斷了一個touch-down的事件,通過建立一個定時器,在定時器開始行動之前,看是否觸摸的手指做了任何的移動。假如定時器行動時,沒有任何的大的位置改變,滾動視圖就發送一個跟蹤事件給觸摸的子視圖。如果在定時器消失前,使用者拖動他們的手指足夠的遠,滾動視圖取消子視圖的任何跟蹤事件,滾動它自己。子類可以重載touchesShouldBegin:withEvent:inContentView:,pagingEnabled,和touchesShouldCancelInContentView:方法,進而影響滾動視圖的滾動手勢。
一個滾動視圖也可以控制一個視圖的縮放和平鋪。當使用者做捏合手勢時,滾動視圖調整偏移量和視圖的比例。當手勢結束的時候,管理視圖内容顯示的對象,就應該恰當的更新子視圖的顯示。當手勢在處理的過程中,滾動視圖不能夠給子視圖,發送任何跟蹤的調用。
UIScrollView類有一個delegate,需要适配的協定是UIScrollViewDelegate。為了縮放和平鋪工作,代理必須實作viewForZoomingInScrollView:和scrollViewDidEndZooming:withView:atScale:方法。另外,最大和最小縮放比例應該是不同的。
重要的提示:在UIScrollView對象中,你不應該嵌入任何UIWebView和UITableView。假如這樣做,會出現一些異常情況,因為2個對象的觸摸事件可能被混合,進而錯誤的處理。
這些都是官方API的解釋,重點是了解UIScrollView怎麼來控制手勢的。可以由canCancelContentTouches這個方法的運用來解釋UIScrollView如何控制手勢的。
假如你設定canCancelContentTouches為YES,那麼當你在UIScrollView上面放置任何子視圖的時候,當你在子視圖上移動手指的時候,UIScrollView會給子視圖發送touchCancel的消息。而如果該屬性設定為NO,ScrollView本身不處理這個消息,全部交給子視圖處理。
那麼這裡就有疑問了,既然該屬性設定未來NO了,那麼豈不是UIScrollView不能處理任何事件了,那麼為何在子視圖上快速滾動的時候,UIScrollView還能移動那。這個一定要區分前面所說的UIScrollView中斷touch-Down事件,開啟一個定時器。我們設定的這個cancancelContentTouches屬性為NO時,隻是讓UIScrollView不能發送cancel事件給子視圖。而前面所說的時,中斷touch-down事件,和取消touch事件是倆碼事,是以當快速在子視圖上移動的時候,當然可以滾動。但是如果你慢速的移動的話,就可以區分這個屬性了,假如設定為YES,在子視圖上慢速移動也可以滾動視圖,但是如果為NO 。因為UIScrollView,發送了cancel事件給子視圖處理了,自己當然滾動不了了。
事件處理看過了,就要考慮scrollView如何重用記憶體的,下面寫了一個例子模仿UITableView的重用的思想,這裡隻是模仿,至于蘋果公司怎麼實作這種重用的,他們應該有更好的方法。
這裡的例子是在scrollView上放置4個2排2列的視圖,但是記憶體中隻占用6個視圖的記憶體空間。當scrollView滾動的時候,通過不停的重用之前視圖的記憶體空間,進而達到節省記憶體的效果。重用的方法如下:
1.如果scrollView向下面滾動,一旦一排視圖滾出了可視範圍,就改變滾動出去的那個view在scrollView中的frame,也就是改變位置到達末尾,達到重用的效果。
2.如果scrollView向上面滾動,一旦最末排的視圖view滾出了可視範圍,就改變滾動出去的那個view在scrollView中的frame,移動到最前面。
下面就需要在你建立的視圖控制器中,建立一個重用的視圖數組,用來把這些要顯示的視圖放入記憶體中,這裡雖然界面上顯示的是2排2列的四個視圖,但是當拖動的時候,可能出現前面一排的視圖顯示一部分,末尾一排的視圖顯示一部分的情況,是以重用的數組中要放置6個視圖。下面是定義的一些宏:
#define sMyViewTotal 6
#define sMyViewWidth 150
#define sMyViewHeight 220
#define sMyViewGap 10
_aryViews = [[NSMutableArray alloc] init];
for (int i = 0; i < sMyViewTotal; i++) {
CGFloat x;
if (i%2) {
x = sMyViewWidth + sMyViewGap + sMyViewGap/2;
}
else{
x = sMyViewGap/2;
CGFloat y = (sMyViewHeight + sMyViewGap)*(i/2);
MyView *myView = [[MyView alloc] initWithFrame:CGRectMake(x, y, sMyViewWidth, sMyViewHeight)];
myView.showNumber = i;
[myScrollView addSubview:myView];
[_aryViews addObject:myView];
[myView release];
}
是以這裡的核心方法是,首先要判斷是向上滾動還是向下滾動方法如下:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
BOOL directDown;
if (previousOffSet.y < scrollView.contentOffset.y) {
directDown = YES;
directDown = NO;
previousOffSet.y = scrollView.contentOffset.y;
//防止最開始就向上面拖動的時候,改變數組視圖樹的位置。
if (scrollView.contentOffset.y < 0) {
return;
if (directDown) {
NSLog(@"down");
MyView * subView = [_aryViews objectAtIndex:firstViewIndex];
CGFloat firstViewYOffset = subView.frame.origin.y + subView.frame.size.height + sMyViewGap;
//尋找第一個視圖是否滾動出去
if (firstViewYOffset < scrollView.contentOffset.y) {
//改變數組中第一排可見視圖的位置。
[self moveIndexInViewsWithDirect:YES];
}
NSLog(@"up");
MyView * subView = [_aryViews objectAtIndex:(firstViewIndex + sMyViewTotal - 2)%sMyViewTotal];
CGFloat lastViewYOffset = subView.frame.origin.y - scrollView.bounds.size.height;
if (lastViewYOffset > scrollView.contentOffset.y) {
[self moveIndexInViewsWithDirect:NO];
每次滾動的時候先判斷滾動位置即offset,和先前的比較。如果先前的大就是向下滾動,否則就是向上滾動。
找到了向下滾動了,就該判斷是否子視圖已經離開了可視範圍。方法就是判斷目前offset和視圖的位置進行比較。如果判斷滾到離開了可視範圍,然後就是要改變重用視圖數組中第一個視圖的位置了。這裡用了firstViewIndex來記錄scrollView中第一個可見視圖的位置, 循環使用這6個視圖達到重用的目的。自然firstViewIndex上面的一個視圖就是最後一個視圖的位置(firstViewIndex + sMyViewTotal - 1)%sMyViewTotal。是以這裡需要改變重用視圖中firstViewIndex即第一個可見視圖的位置。代碼如下:
- (void)moveIndexInViewsWithDirect:(BOOL)forward{
[UIView setAnimationsEnabled:NO];
if (forward) {
for (int i = firstViewIndex; i < (firstViewIndex + 2); i++) {
MyView *subView = [_aryViews objectAtIndex:i%sMyViewTotal];
subView.showNumber = subView.showNumber + sMyViewTotal;
subView.frame = CGRectMake(subView.frame.origin.x, subView.frame.origin.y + (sMyViewTotal/2) * (sMyViewHeight + sMyViewGap), subView.frame.size.width, subView.frame.size.height);
firstViewIndex = (firstViewIndex + 2)%sMyViewTotal;
int lastViewIndex = firstViewIndex + sMyViewTotal - 1;
for (int i = lastViewIndex; i > (lastViewIndex - 2); i--) { MyView *subView = [_aryViews objectAtIndex:(firstViewIndex + sMyViewTotal - i)%sMyViewTotal];
subView.showNumber = subView.showNumber - sMyViewTotal;
subView.frame = CGRectMake(subView.frame.origin.x, subView.frame.origin.y - (sMyViewTotal/2) * (sMyViewHeight + sMyViewGap), subView.frame.size.width, subView.frame.size.height);
firstViewIndex = (firstViewIndex + sMyViewTotal - 2)%sMyViewTotal;
[UIView setAnimationsEnabled:YES];
這裡建立的子視圖數字屬性,是用來在視圖上畫數字的,這樣就可以看到視圖重用的效果了,應該是從0開始到無窮多,但是實際上記憶體中就建立了6個視圖。
- (void)drawRect:(CGRect)rect
{
// Drawing code
NSString *text = [NSString stringWithFormat:@"%d",showNumber];
[[UIColor redColor] set];
[text drawInRect:CGRectMake(rect.origin.x, rect.origin.y + rect.size.height/2 - 30, rect.size.width, 30) withFont:[UIFont fontWithName:@"Helvetica" size:20] lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentCenter];