iOS圖片瀏覽器(功能強大/效能優越)

sas???發表於2018-04-24
7877747-8274114218cec224

支援cocopods,功能完善,效能不錯,程式碼質量尚可,喜歡的朋友可以給個小星星。

為了適應元件的自定義需求,程式碼和邏輯有點多,所以儘量不要修改原始碼。

寫在前面

本文講解YBImageBrowser的元件設計思路和部分技術實現原理,對本框架有興趣的朋友可以看看 。行文的重點是筆者的框架設計理念、程式碼及體驗優化的思考、關鍵技術點的實現,希望不管是老鳥還是新手看完之後都能有所收穫和感悟。

歡迎大家交流探討,當然,筆者水平有限,若有大佬指教不勝感激。

索引:(簡書不支援頁內跳轉很尷尬)

一、元件框架整體設計

二、元件中如何隱藏屬性和方法

三、拖拽動效的演算法優化

四、分頁間距的演算法優化

五、記憶體的優化

六、預下載和任務同步

七、螢幕旋轉UI適配

一、元件框架整體設計

其實對於圖片瀏覽器,開源專案也有不少,不管是程式碼上還是功能上沒有一個能完整的滿足筆者的需求。所以筆者索性做了一個,力圖將粒度做小,功能做全,當然這需要一個漫長的過程,空閒時間筆者會持續迭代和優化。

目前採用的是UIViewController做為底,上層是一個橫向滾動的UICollectionView,在UICollectionViewCell上面是UIScrollView,當然還包括主要顯示圖片、動畫圖片、裁剪顯示前景圖片等。

使用UICollectionView是為了利用蘋果為我們做的複用機制,不需要專門去實現,不然邏輯程式碼太多,得不償失;而縮放的效果依託於UIScrollView;採用UIViewController為底是為了更好的控制旋轉螢幕時的UI適配,之前也是考慮更輕一點的UIView,但是它會受父檢視的旋轉影響,可能適配難度會翻幾倍,而且使用UIViewController能更方便和優雅的實現圖片瀏覽器的入場和出場動畫。

二、元件中如何隱藏屬性和方法

在做一個元件的時候,我們往往思考著向使用者隱藏某些細節實現,一方面是為了避免使用者的無意更改,一方面是為了簡化API使其看起來更清爽。

對於屬性,若想讓使用者只讀不可寫,可以在.h中對屬性使用readonly修飾符;若根本不想要使用者看到,可以直接將該屬性建立在需要使用的目標類的.m檔案內。

不過這樣並不優雅,意味著我們很多程式碼和類必須搞到同一檔案,才能達到外部無法直接訪問,而內部可以訪問的目的。若我們想分離多個檔案好管理程式碼和實現更優秀的架構時,不得不將屬性寫到.h裡面讓其他檔案可以訪問。

那麼,何不換一種思路?儘管我們將屬性寫在.m中隔離外部訪問,實際上使用者仍然可以用KVC的方式讀寫,那麼我們框架元件內部為何不使用KVC進行讀寫?

於是,在元件的YBImageBrowserModel的.h.m檔案中你可以看到這樣的程式碼:


.h 中

FOUNDATION_EXTERN NSString * const YBImageBrowserModel_KVCKey_isLoading;

FOUNDATION_EXTERN NSString * const YBImageBrowserModel_KVCKey_isLoadFailed;

.m 中

NSString * const YBImageBrowserModel_KVCKey_isLoading = @"isLoading";

NSString * const YBImageBrowserModel_KVCKey_isLoadFailed = @"isLoadFailed";

這裡使用字串常量存放KVC的鍵,元件內部就使用valueForKey:和setValue:forKey:通過這些常量來優雅的讀寫例項變數了。

對於方法的隱藏,元件中不將方法暴露在.h裡面,只寫在.m裡面,然後元件其他檔案通過下的objc_msgSend方法處理,比如隨便擷取一段程式碼:

YBImageBrowserModelScaleImageSuccessBlock successBlock = ^(YBImageBrowserModel *backModel) {

       ...

    };

    ((void(*)(id, SEL, CGRect, YBImageBrowserModelScaleImageSuccessBlock)) objc_msgSend)(model, sel_registerName(YBImageBrowserModel_SELName_scaleImage), imageFrame, successBlock);

或者使用NSInvocation作為私有屬性,外部也用KVC讀寫。

三、拖拽動效的演算法優化

7877747-b3a35541d1a1aad8

拖拽動效是目前很流行的圖片瀏覽器出場效果,筆者看了好幾個知名APP,“新浪微博”,“今日頭條”,“QQ”,“QQ瀏覽器”,“微信”等都做了類似的動效,但是除了“微信”的效果人性化一點,其它的都有些不盡人意的地方。

這個效果咋一看比較簡單,無非就是根據移動的距離,以某種數學關係移動圖片並且縮小圖片,實現可以直接計算frame或者使用CATransform3D等。

但是,有個容易忽略的問題,在拖動的時候我們希望看到的效果是圖片跟隨手指移動並且縮小,上圖左右兩種狀態下的箭頭指向的正是手指拖動觸控的點(理想狀態),若寫一個移動和縮放比例變化之間是線性的動畫,手指觸控的點會是這種理想狀態麼?

答案是否定的,若移動的時候不縮放,是能達到理想狀態,若縮放了狀態二必然會是如下圖所示:

7877747-99588e9895b0039c

拖動動效存在問題

處理方式:若是使用的動畫相關的類庫,可以考慮使用錨點來處理。本元件是使用frame的方式處理,通過一張圖解釋如何處理這個邏輯:

7877747-a8173f8394b48794

處理方式

實際上程式碼邏輯比看起來的複雜一些,有興趣的可以看程式碼,這裡只提出思路。

四、分頁間距的演算法優化

說起分頁,幾乎所有iOS工程師都會說.pagingEnabled屬性,又說分頁間距,稍有經驗的工程師都會說重寫UICollectionView的layout,既建立一個UICollectionViewFlowLayout類重寫約束。現在這裡不浪費篇幅討論API的用法,你只需要知道在重寫的layout裡面,幾乎每一幀的介面都可以靠重寫layoutAttributesForElementsInRect等方法重新計算。

按照常規的邏輯思路,最好想到的方案是:若當前是第n頁時,所有的Cell都向左移動(n-1) * 間距

確實,這種演算法邏輯咋一看好像能解決問題,但當你滑到下圖的情況下時,會發生奇怪的現象:

7877747-f2d51e07b461605d

blog_pic3.png

你會發現在滑動到第n頁第n+1頁之間的臨界點時,介面會突然向左或者向右跳動一段距離,因為這裡就是上面所說方式判斷移動的觸發點,顯然這不夠平滑。

於是元件中筆者的做法是,在每次重寫佈局時,都移動一個距離:當前偏移量 / 最大偏移量 * 總共頁間距

其實做法很簡單,但這種思維方式卻非常實用,在我們做很多需要平滑過渡的邏輯時(不侷限於介面),都可以以這種思維做出“平滑”的效果。

五、記憶體的優化

由於如今的APP做的越來越複雜,作為一個合格的移動端程式設計師,我們需要時刻關注記憶體問題,雖然這並不是剛需。

本地圖片的讀取

在讀取本地圖片時,使用[UIImage imageNamed:]方式時系統會快取該圖片,而釋放快取的時機很微妙。所以在使用比較大、呼叫頻率低的圖片時,儘量使用讀取檔案的方式做:

[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:fileName ofType:fileType]]

超大圖的處理

這樣雖然能減少累加的記憶體,但若一張圖片就非常大呢?系統將它解壓過後將會佔用比你想象中更大的記憶體,APP可能變得非常卡頓甚至崩潰。

於是,元件中設定了一個pt的界限,當圖片超過這個界限,元件會自動非同步壓縮到當前螢幕最大顯示pt數量,當使用者拖動或縮放放大圖片時,元件會自動非同步裁剪可視區域的圖片,通過一張前景圖片顯示出來(當然裁剪也是有最大限度的)。

思路就兩句話,實際邏輯結合其他功能會比較複雜,有興趣可以看看程式碼,這裡不過多闡述。

下載任務的釋放

元件內部是利用SDWebImage做的下載和快取,在每一個model釋放的時候,都會將對應的下載任務取消已節約網路和記憶體開銷。

六、預下載和任務同步

為了提高使用者體驗,在配置圖片瀏覽器圖片對應的model的時候,可以通過 API 設定非同步預下載,當網路狀況不錯的時候,可能使用者開啟瀏覽器圖片就下載好了,畢竟圖片瀏覽器是有很短的建立時間和較長的入場時間的。

其實這也是一種提升效率的思維,我們要習慣性的去思考利用程式的空閒預先做一些任務,才能編寫出高效的程式碼。

這裡有一個點需要注意,若我們執行了預下載,而在圖片瀏覽器開啟的時候,圖片仍未預下載完成,而此刻又會執行正式的下載,它們之間如何資訊同步?

哈哈,其實很簡單,就是將同一類的任務放到同一個地方統一管理,比如本元件就是將圖片下載、圖片快取、圖片壓縮、圖片裁剪等都放到圖片資料模型YBImageBrowserModel中處理,其它地方就用方法排程這些任務,雖然可能會造成看起來比較多的方法呼叫,但是對穩定性、容錯率的提高不容小覷。

這種思維很重要,可以不嚴密的理解為AOP,功能分類集中管理。

七、螢幕旋轉UI適配

找到元件必然支援的方向

元件支援了旋轉功能,由於採用的是UIViewController作為底類,理所當然的是讓元件內部子控制元件跟隨UIViewController的旋轉而旋轉,目前不支援強制旋轉,因為可能會有些麻煩,後期迭代考慮增加。

UIViewController的旋轉會直接受到工程general -> deployment info -> Device Orientation處的影響,所以,在判斷元件支援的旋轉方向的時候,需要取一個交集:

- (void)configSupportAutorotateTypes {

    UIApplication *application = [UIApplication sharedApplication];

    UIInterfaceOrientationMask keyWindowSupport = [application supportedInterfaceOrientationsForWindow:window];

    UIInterfaceOrientationMask selfSupport = ![self shouldAutorotate] ? UIInterfaceOrientationMaskPortrait : [self supportedInterfaceOrientations];

    supportAutorotateTypes = keyWindowSupport & selfSupport;

}

然後這個交集就是UIViewController可能旋轉的方向,也就是元件可能旋轉的方向。

佈局更新時機優化

大家很容易就想到,當裝置旋轉過後,若元件支援該方向,就通知所有子介面重新整理佈局(可能有人會說用autolayout,但是考慮到效率和可控性方面的問題,本元件都採用frame處理)。

其實若你是這樣做,已經滿足了需求,剩下了可能就是繁雜的佈局執行流。

然而我會說還能優化。試想一下,手機的兩種豎屏狀態(home在上,home在下),兩種橫屏狀態(home在左,home在右),它們的frame是不是一樣?

所以,這裡需要加入一個標識,用來儲存此時當前UIView顯示的frame型別是“豎屏”還是“橫屏”,而不是每一種螢幕狀態變化都去做所有的佈局更新,理論上提高了一倍的佈局開銷。

引入代理規範佈局流程

由於通知子檢視更新佈局、儲存當前檢視分別在“豎屏”和“橫屏”下的frame、儲存當前適配的螢幕方向等資訊是每一個檢視幾乎都會做的工作(雖然細節有些差異,但我們稍巨集觀的看這個問題)。

於是,元件做了一個代理:

@protocol YBImageBrowserScreenOrientationProtocol @required

// 當前檢視UI適配的螢幕方向

@property (nonatomic, assign) YBImageBrowserScreenOrientation so_screenOrientation;

// 當前檢視在豎直螢幕的frame

@property (nonatomic, assign) CGRect so_frameOfVertical;

// 當前檢視在橫向螢幕的frame

@property (nonatomic, assign) CGRect so_frameOfHorizontal;

// 更新約束是否完成

@property (nonatomic, assign) BOOL so_isUpdateUICompletely;

- (void)so_setFrameInfoWithSuperViewScreenOrientation:(YBImageBrowserScreenOrientation)screenOrientation superViewSize:(CGSize)size;

- (void)so_updateFrameWithScreenOrientation:(YBImageBrowserScreenOrientation)screenOrientation;

@end

需要跟隨螢幕旋轉更新佈局的UIView都實現這個代理,達到標準控制的目的,值得注意的是代理裡面的屬性需要自己在實現檔案關聯一個例項變數,類似於

@synthesize so_frameOfVertical = _so_frameOfVertical;

@synthesize so_frameOfHorizontal = _so_frameOfHorizontal;

其實吧,這個地方筆者感覺設計得比較雞肋,容筆者有更好的想法的時候更新元件。

寫在後面

看到這裡可能有的朋友有些蒙,這通篇都說些什麼,沒一句完整的程式碼。哈哈,實際上這就是元件的核心,是我花了許多時間做的一些思考和總結,科普基礎知識挺費勁的,百度就是一大篇一大篇的,我相信本文的價值還是有的。

越來越覺得有位朋友的話很有道理:程式設計是靠思維的東西。

希望大家共勉~

相關文章