iOS開源:LazyScrollView - 阿里開源iOS高性能異構滾動視圖構建方案

五嘎子 7年前發布 | 10K 次閱讀 IOS iOS開發 移動開發

LazyScroll是什么

LazyScrollView 繼承自ScrollView,目標是解決異構(與TableView的同構對比)滾動視圖的復用回收問題。它可以支持跨View層的復用,用易用方式來生成一個高性能的滾動視圖。此方案最先在天貓iOS客戶端的首頁落地。

為什么要用LazyScrollView

貓客首頁之前首頁的View比較少,不需要復用和回收也有很優秀的性能,但是之后首頁的View數量逐漸膨脹,沒有一套復用回收機制的ScrollView已經影響到性能了,迫切需要處理對ScrollView中View的復用和回收。

使用TableView只能用來解決同類Cell的展示,而在貓客首頁這個ScrollView里面,View的種類太多了。不適合我們的場景。

而UICollectionView本身的布局和復用回收機制不夠靈活,用起來也較為繁瑣。并且本來貓客的首頁就有一套相對成熟的卡片布局方案。所以誕生了LazyScrollView去解決這個問題。

LazyScroll如何用

LazyScrollView的使用和TableView很像,不過多了一個需要實現的方法:返回對應index的View 相對LazyScrollView的絕對坐標。

實現LazyScrollViewDatasource

類似TableView的用法,我們需要使用方實現LazyScrollViewDatasource這個Delegate

@protocol TMMuiLazyScrollViewDataSource <NSObject>
@required
//ScrollView一共展示多少個item

  • (NSUInteger)numberOfItemInScrollView:(TMMuiLazyScrollView *)scrollView; //要求根據index直接返回RectModel
  • (TMMuiRectModel )scrollView:(TMMuiLazyScrollView )scrollView rectModelAtIndex:(NSUInteger)index; //返回下標所對應的view
  • (UIView )scrollView:(TMMuiLazyScrollView )scrollView itemByMuiID:(NSString )muiID; @end </code></pre>

    LazyScrollView的核心是在初始狀態就得知所有View應該顯示的位置。這個Protocol可以讓LazyScrollView獲取到這些信息。

    第一個方法很簡單,獲取LazyScrollView中item的個數。

    第二個方法需要按照Index返回TMMuiRectModel ,它會攜帶對應index的View 相對LazyScrollView的絕對坐標。TMMuiRectModel是這么個東西:

    @interface TMMuiRectModel:NSObject
    //轉換后的絕對值rect
    @property (nonatomic,assign) CGRect absRect;
    //業務下標
    @property (nonatomic,copy) NSString muiID;

@end </code></pre>

absRect是LazyScroll中的View相對LazyScrollView的絕對坐標,muiID是這個View在LazyScrollView中唯一的標識符,可賦值也可不賦值,不賦值的話LazyScroll會處理成轉換為字符串的下標。如果這個標識符在Protocol的第三個方法中會用到。

第三個方法,返回View。首先,我們在UIView之外加了一個Category:

@interface UIView(TMMui)

//索引過的標識,在LazyScrollView范圍內唯一 @property (nonatomic, copy) NSString muiID; //重用的ID @property (nonatomic, copy) NSString reuseIdentifier; </code></pre>

這個category可以讓View攜帶muiID和reuseIdentifier,對于返回的View來說,只需要在乎對View的reuseIdentifier賦值,muiID的賦值會在lazyScrollView中處理掉。reuseIdentifier相同的View會被復用,如果這個View的reuseIdentifier是nil或者空字符串,則不會被復用。

調用核心API

- (void)reloadData;

重新走一遍DataSource的這些方法,等同于TableView中的reloadData

- (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier

根據identifier獲取可以復用的View。和TableView的dequeueReusableCellWithIdentifier:(NSString *)identifier方法意義相同。通常是在LazyScrollViewDatasource第三個方法,返回View的時候使用。先嘗試獲取復用池中的View,如果沒有再去新建。

LazyScrollView的內部實現

這是一個Demo, 被復用的View,標記的backgroundColor會和之前生成的時候有所不同。

STEP 1 根據DataSource獲取所有的TMMuiRectModel

根據DataSource的Delegate,拿到所有的View應該被顯示的位置。這一步,核心是拿到的位置是確定的。

根據Demo,我們觀察從 0/1 - 2/3 之間這些View

這個時候LazyScrollView拿到的Rect如下:

Index 標號(MUIID) Rect
0 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
1 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)
2 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
3 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
4 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
5 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
6 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
7 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
8 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
9 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
10 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)

STEP 2 排序

拿到了這些位置之后,接下來做的事情就是排序。排序生成的索引會有兩個:根據頂邊(y)升序排序的索引和根據底邊(y+height)降序排序的索引。

根據頂邊(y)升序排序的索引

RANK 標號(MUIID) Rect
0 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
1 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)
2 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
3 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
4 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
5 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
6 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
7 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
8 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
9 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
10 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)

根據底邊(y+height)降序排序的索引

RANK 標號(MUIID) Rect
0 2/2 origin = (x = 25, y = 860), size = (width = 325, height = 150)
1 2/1 origin = (x = 25, y = 695), size = (width = 325, height = 150)
2 2/0 origin = (x = 25, y = 530), size = (width = 325, height = 150)
3 1/0 origin = (x = 5, y = 360), size = (width = 177.5, height = 150)
4 1/2 origin = (x = 192.5, y = 360), size = (width = 177.5, height = 56)
5 1/3 origin = (x = 286.5, y = 426), size = (width = 83.5, height = 84)
6 1/1 origin = (x = 192.5, y = 426), size = (width = 84, height = 84)
7 0/2 origin = (x = 25, y = 180), size = (width = 156, height = 150)
8 0/3 origin = (x = 194, y = 180), size = (width = 156, height = 150
9 0/0 origin = (x = 25, y = 15), size = (width = 156, height = 150
10 0/1 origin = (x = 194, y = 15), size = (width = 156, height = 150)

STEP 3 查找

前兩步是在執行完reload,在視圖還沒有生成的時候就開始做了,而接下來的步驟在要生成視圖(初始化或滾動的時候)才會去做。

我們設定了Buffer為上下各20,滾動超過20個像素后才會指定查找視圖并顯示的動作。

接下來就是找哪些View應該被顯示了。舉個例子,如下圖,紅圈是應該顯示的區域。

iOS開源:LazyScrollView - 阿里開源iOS高性能異構滾動視圖構建方案

現在已知的是紅圈頂邊y是242,底邊y是949,加上緩沖區Buffer,應該是找222 - 969 之間的View。我們要做的是,找到底邊y小于969的Model頂邊y大于222的Model,取交集,就是我們要顯示的View

采用的方法為二分查找,在根據頂邊升序排序的索引中找949,找到的index為0(MUIID為2/2),我們使用一個Set,把根據頂邊排序中index >= 0 的元素先放在這里。獲取的Set中包含的muiID為0/0,0/1,0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

根據底邊排序的索引中找222,找到的index為2,我們把index >= 2的元素放在另一個Set,獲取的Set中包含的muiID為0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

兩個Set取交集,得到的就是我們的ResultSet,這里面都是我們要顯示View的Model,它們的muiID是0/2,0/3,1/0,1/1,1/2,1/3,2/0,2/1,2/2

STEP 4 回收、復用、生成

我們知道了應該顯示哪些View,但是我們之后做的第一步是把不需要顯示的View加入到復用池中。

LazyScroll可以取到當前顯示了的View,拿當前顯示的View的muiID和將要顯示view的Model的muiID做對比,可以知道當前顯示的View哪些應該被回收。

LazyScrollView中有一個Dictionary,key是reuseIdentifier,Value是對應reuseIdentifier被回收的View,當LazyScrollView得知這個View不該再出現了,會把View放在這里,并且把這個View hidden掉。

接下來,LazyScrollView會去調用datasource的- (UIView *)scrollView:(TMMuiLazyScrollView *)scrollView itemByMuiID:(NSString *)muiID; 復用還是不復用,是由datasource決定的。如果要復用,需要datasource方法內調用- (UIView *)dequeueReusableItemWithIdentifier:(NSString *)identifier獲取復用的View,這個方法取出來的View就是在上一段所說的Dictionary中拿的。

這樣,我們就完成了一次完整的循環 : 找到所有View將要顯示的位置 – 排序 – 查找應該顯示的View – 回收 – 創建/復用。

##最后

LazyScroll的復用和回收能力是比較強大的,在貓客首頁使用之后,因為View數量而導致內存過多的問題得到了解決。

在這套復用和回收機制的加持之下,我們將LazyScrollView繼續延伸,構造出一套完整的布局解決方案Tangram,它將Native中View的布局方式變得更動態化,敬請期待。

 本文由用戶 五嘎子 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!