iOS圖片瀏覽器(功能強(qiáng)大/性能優(yōu)越)

支持cocopods,功能完善,性能不錯(cuò),代碼質(zhì)量尚可,喜歡的朋友可以給個(gè)小星星。

為了適應(yīng)組件的自定義需求,代碼和邏輯有點(diǎn)多,所以盡量不要修改源碼。

寫在前面

本文講解YBImageBrowser的組件設(shè)計(jì)思路和部分技術(shù)實(shí)現(xiàn)原理,對(duì)本框架有興趣的朋友可以看看?。行文的重點(diǎn)是筆者的框架設(shè)計(jì)理念、代碼及體驗(yàn)優(yōu)化的思考、關(guān)鍵技術(shù)點(diǎn)的實(shí)現(xiàn),希望不管是老鳥還是新手看完之后都能有所收獲和感悟。

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

索引:(簡(jiǎn)書不支持頁(yè)內(nèi)跳轉(zhuǎn)很尷尬)

一、組件框架整體設(shè)計(jì)

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

三、拖拽動(dòng)效的算法優(yōu)化

四、分頁(yè)間距的算法優(yōu)化

五、內(nèi)存的優(yōu)化

六、預(yù)下載和任務(wù)同步

七、屏幕旋轉(zhuǎn)UI適配

一、組件框架整體設(shè)計(jì)

其實(shí)對(duì)于圖片瀏覽器,開(kāi)源項(xiàng)目也有不少,不管是代碼上還是功能上沒(méi)有一個(gè)能完整的滿足筆者的需求。所以筆者索性做了一個(gè),力圖將粒度做小,功能做全,當(dāng)然這需要一個(gè)漫長(zhǎng)的過(guò)程,空閑時(shí)間筆者會(huì)持續(xù)迭代和優(yōu)化。

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

使用UICollectionView是為了利用蘋果為我們做的復(fù)用機(jī)制,不需要專門去實(shí)現(xiàn),不然邏輯代碼太多,得不償失;而縮放的效果依托于UIScrollView;采用UIViewController為底是為了更好的控制旋轉(zhuǎn)屏幕時(shí)的UI適配,之前也是考慮更輕一點(diǎn)的UIView,但是它會(huì)受父視圖的旋轉(zhuǎn)影響,可能適配難度會(huì)翻幾倍,而且使用UIViewController能更方便和優(yōu)雅的實(shí)現(xiàn)圖片瀏覽器的入場(chǎng)和出場(chǎng)動(dòng)畫。

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

在做一個(gè)組件的時(shí)候,我們往往思考著向用戶隱藏某些細(xì)節(jié)實(shí)現(xiàn),一方面是為了避免用戶的無(wú)意更改,一方面是為了簡(jiǎn)化API使其看起來(lái)更清爽。

對(duì)于屬性,若想讓用戶只讀不可寫,可以在.h中對(duì)屬性使用readonly修飾符;若根本不想要用戶看到,可以直接將該屬性創(chuàng)建在需要使用的目標(biāo)類的.m文件內(nèi)。

不過(guò)這樣并不優(yōu)雅,意味著我們很多代碼和類必須搞到同一文件,才能達(dá)到外部無(wú)法直接訪問(wèn),而內(nèi)部可以訪問(wèn)的目的。若我們想分離多個(gè)文件好管理代碼和實(shí)現(xiàn)更優(yōu)秀的架構(gòu)時(shí),不得不將屬性寫到.h里面讓其他文件可以訪問(wèn)。

那么,何不換一種思路?盡管我們將屬性寫在.m中隔離外部訪問(wèn),實(shí)際上用戶仍然可以用KVC的方式讀寫,那么我們框架組件內(nèi)部為何不使用KVC進(jìn)行讀寫?

于是,在組件的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的鍵,組件內(nèi)部就使用valueForKey:和setValue:forKey:通過(guò)這些常量來(lái)優(yōu)雅的讀寫實(shí)例變量了。

對(duì)于方法的隱藏,組件中不將方法暴露在.h里面,只寫在.m里面,然后組件其他文件通過(guò)下的objc_msgSend方法處理,比如隨便截取一段代碼:

YBImageBrowserModelScaleImageSuccessBlock?successBlock?=?^(YBImageBrowserModel?*backModel)?{

???????...

????};

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

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

三、拖拽動(dòng)效的算法優(yōu)化

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

這個(gè)效果咋一看比較簡(jiǎn)單,無(wú)非就是根據(jù)移動(dòng)的距離,以某種數(shù)學(xué)關(guān)系移動(dòng)圖片并且縮小圖片,實(shí)現(xiàn)可以直接計(jì)算frame或者使用CATransform3D等。

但是,有個(gè)容易忽略的問(wèn)題,在拖動(dòng)的時(shí)候我們希望看到的效果是圖片跟隨手指移動(dòng)并且縮小,上圖左右兩種狀態(tài)下的箭頭指向的正是手指拖動(dòng)觸摸的點(diǎn)(理想狀態(tài)),若寫一個(gè)移動(dòng)和縮放比例變化之間是線性的動(dòng)畫,手指觸摸的點(diǎn)會(huì)是這種理想狀態(tài)么?

答案是否定的,若移動(dòng)的時(shí)候不縮放,是能達(dá)到理想狀態(tài),若縮放了狀態(tài)二必然會(huì)是如下圖所示:

拖動(dòng)動(dòng)效存在問(wèn)題

處理方式:若是使用的動(dòng)畫相關(guān)的類庫(kù),可以考慮使用錨點(diǎn)來(lái)處理。本組件是使用frame的方式處理,通過(guò)一張圖解釋如何處理這個(gè)邏輯:

處理方式

實(shí)際上代碼邏輯比看起來(lái)的復(fù)雜一些,有興趣的可以看代碼,這里只提出思路。

四、分頁(yè)間距的算法優(yōu)化

說(shuō)起分頁(yè),幾乎所有iOS工程師都會(huì)說(shuō).pagingEnabled屬性,又說(shuō)分頁(yè)間距,稍有經(jīng)驗(yàn)的工程師都會(huì)說(shuō)重寫UICollectionView的layout,既創(chuàng)建一個(gè)UICollectionViewFlowLayout類重寫約束?,F(xiàn)在這里不浪費(fèi)篇幅討論API的用法,你只需要知道在重寫的layout里面,幾乎每一幀的界面都可以靠重寫layoutAttributesForElementsInRect等方法重新計(jì)算。

按照常規(guī)的邏輯思路,最好想到的方案是:若當(dāng)前是第n頁(yè)時(shí),所有的Cell都向左移動(dòng)(n-1) * 間距。

確實(shí),這種算法邏輯咋一看好像能解決問(wèn)題,但當(dāng)你滑到下圖的情況下時(shí),會(huì)發(fā)生奇怪的現(xiàn)象:

blog_pic3.png

你會(huì)發(fā)現(xiàn)在滑動(dòng)到第n頁(yè)第n+1頁(yè)之間的臨界點(diǎn)時(shí),界面會(huì)突然向左或者向右跳動(dòng)一段距離,因?yàn)檫@里就是上面所說(shuō)方式判斷移動(dòng)的觸發(fā)點(diǎn),顯然這不夠平滑。

于是組件中筆者的做法是,在每次重寫布局時(shí),都移動(dòng)一個(gè)距離:當(dāng)前偏移量 / 最大偏移量 * 總共頁(yè)間距

其實(shí)做法很簡(jiǎn)單,但這種思維方式卻非常實(shí)用,在我們做很多需要平滑過(guò)渡的邏輯時(shí)(不局限于界面),都可以以這種思維做出“平滑”的效果。

五、內(nèi)存的優(yōu)化

由于如今的APP做的越來(lái)越復(fù)雜,作為一個(gè)合格的移動(dòng)端程序員,我們需要時(shí)刻關(guān)注內(nèi)存問(wèn)題,雖然這并不是剛需。

本地圖片的讀取

在讀取本地圖片時(shí),使用[UIImage imageNamed:]方式時(shí)系統(tǒng)會(huì)緩存該圖片,而釋放緩存的時(shí)機(jī)很微妙。所以在使用比較大、調(diào)用頻率低的圖片時(shí),盡量使用讀取文件的方式做:

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

超大圖的處理

這樣雖然能減少累加的內(nèi)存,但若一張圖片就非常大呢?系統(tǒng)將它解壓過(guò)后將會(huì)占用比你想象中更大的內(nèi)存,APP可能變得非??D甚至崩潰。

于是,組件中設(shè)置了一個(gè)pt的界限,當(dāng)圖片超過(guò)這個(gè)界限,組件會(huì)自動(dòng)異步壓縮到當(dāng)前屏幕最大顯示pt數(shù)量,當(dāng)用戶拖動(dòng)或縮放放大圖片時(shí),組件會(huì)自動(dòng)異步裁剪可視區(qū)域的圖片,通過(guò)一張前景圖片顯示出來(lái)(當(dāng)然裁剪也是有最大限度的)。

思路就兩句話,實(shí)際邏輯結(jié)合其他功能會(huì)比較復(fù)雜,有興趣可以看看代碼,這里不過(guò)多闡述。

下載任務(wù)的釋放

組件內(nèi)部是利用SDWebImage做的下載和緩存,在每一個(gè)model釋放的時(shí)候,都會(huì)將對(duì)應(yīng)的下載任務(wù)取消已節(jié)約網(wǎng)絡(luò)和內(nèi)存開(kāi)銷。

六、預(yù)下載和任務(wù)同步

為了提高用戶體驗(yàn),在配置圖片瀏覽器圖片對(duì)應(yīng)的model的時(shí)候,可以通過(guò) API 設(shè)置異步預(yù)下載,當(dāng)網(wǎng)絡(luò)狀況不錯(cuò)的時(shí)候,可能用戶打開(kāi)瀏覽器圖片就下載好了,畢竟圖片瀏覽器是有很短的創(chuàng)建時(shí)間和較長(zhǎng)的入場(chǎng)時(shí)間的。

其實(shí)這也是一種提升效率的思維,我們要習(xí)慣性的去思考利用程序的空閑預(yù)先做一些任務(wù),才能編寫出高效的代碼。

這里有一個(gè)點(diǎn)需要注意,若我們執(zhí)行了預(yù)下載,而在圖片瀏覽器打開(kāi)的時(shí)候,圖片仍未預(yù)下載完成,而此刻又會(huì)執(zhí)行正式的下載,它們之間如何信息同步?

哈哈,其實(shí)很簡(jiǎn)單,就是將同一類的任務(wù)放到同一個(gè)地方統(tǒng)一管理,比如本組件就是將圖片下載、圖片緩存、圖片壓縮、圖片裁剪等都放到圖片數(shù)據(jù)模型YBImageBrowserModel中處理,其它地方就用方法調(diào)度這些任務(wù),雖然可能會(huì)造成看起來(lái)比較多的方法調(diào)用,但是對(duì)穩(wěn)定性、容錯(cuò)率的提高不容小覷。

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

七、屏幕旋轉(zhuǎn)UI適配

找到組件必然支持的方向

組件支持了旋轉(zhuǎn)功能,由于采用的是UIViewController作為底類,理所當(dāng)然的是讓組件內(nèi)部子控件跟隨UIViewController的旋轉(zhuǎn)而旋轉(zhuǎn),目前不支持強(qiáng)制旋轉(zhuǎn),因?yàn)榭赡軙?huì)有些麻煩,后期迭代考慮增加。

UIViewController的旋轉(zhuǎn)會(huì)直接受到工程general -> deployment info -> Device Orientation處的影響,所以,在判斷組件支持的旋轉(zhuǎn)方向的時(shí)候,需要取一個(gè)交集:

-?(void)configSupportAutorotateTypes?{

????UIApplication?*application?=?[UIApplication?sharedApplication];

????UIInterfaceOrientationMask?keyWindowSupport?=?[application?supportedInterfaceOrientationsForWindow:window];

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

????supportAutorotateTypes?=?keyWindowSupport?&?selfSupport;

}

然后這個(gè)交集就是UIViewController可能旋轉(zhuǎn)的方向,也就是組件可能旋轉(zhuǎn)的方向。

布局更新時(shí)機(jī)優(yōu)化

大家很容易就想到,當(dāng)設(shè)備旋轉(zhuǎn)過(guò)后,若組件支持該方向,就通知所有子界面刷新布局(可能有人會(huì)說(shuō)用autolayout,但是考慮到效率和可控性方面的問(wèn)題,本組件都采用frame處理)。

其實(shí)若你是這樣做,已經(jīng)滿足了需求,剩下了可能就是繁雜的布局執(zhí)行流。

然而我會(huì)說(shuō)還能優(yōu)化。試想一下,手機(jī)的兩種豎屏狀態(tài)(home在上,home在下),兩種橫屏狀態(tài)(home在左,home在右),它們的frame是不是一樣?

所以,這里需要加入一個(gè)標(biāo)識(shí),用來(lái)存儲(chǔ)此時(shí)當(dāng)前UIView顯示的frame類型是“豎屏”還是“橫屏”,而不是每一種屏幕狀態(tài)變化都去做所有的布局更新,理論上提高了一倍的布局開(kāi)銷。

引入代理規(guī)范布局流程

由于通知子視圖更新布局、存儲(chǔ)當(dāng)前視圖分別在“豎屏”和“橫屏”下的frame、存儲(chǔ)當(dāng)前適配的屏幕方向等信息是每一個(gè)視圖幾乎都會(huì)做的工作(雖然細(xì)節(jié)有些差異,但我們稍宏觀的看這個(gè)問(wèn)題)。

于是,組件做了一個(gè)代理:

@protocol?YBImageBrowserScreenOrientationProtocol?@required

//?當(dāng)前視圖UI適配的屏幕方向

@property?(nonatomic,?assign)?YBImageBrowserScreenOrientation?so_screenOrientation;

//?當(dāng)前視圖在豎直屏幕的frame

@property?(nonatomic,?assign)?CGRect?so_frameOfVertical;

//?當(dāng)前視圖在橫向屏幕的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

需要跟隨屏幕旋轉(zhuǎn)更新布局的UIView都實(shí)現(xiàn)這個(gè)代理,達(dá)到標(biāo)準(zhǔn)控制的目的,值得注意的是代理里面的屬性需要自己在實(shí)現(xiàn)文件關(guān)聯(lián)一個(gè)實(shí)例變量,類似于

@synthesize?so_frameOfVertical?=?_so_frameOfVertical;

@synthesize?so_frameOfHorizontal?=?_so_frameOfHorizontal;

其實(shí)吧,這個(gè)地方筆者感覺(jué)設(shè)計(jì)得比較雞肋,容筆者有更好的想法的時(shí)候更新組件。

寫在后面

看到這里可能有的朋友有些蒙,這通篇都說(shuō)些什么,沒(méi)一句完整的代碼。哈哈,實(shí)際上這就是組件的核心,是我花了許多時(shí)間做的一些思考和總結(jié),科普基礎(chǔ)知識(shí)挺費(fèi)勁的,百度就是一大篇一大篇的,我相信本文的價(jià)值還是有的。

越來(lái)越覺(jué)得有位朋友的話很有道理:編程是靠思維的東西。

希望大家共勉~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 老污男說(shuō): 和室友一起去上課 晚上回 一直愛(ài)你 ?
    老污男的貓閱讀 371評(píng)論 0 1
  • 中國(guó)的俄羅斯族是古代俄羅斯移民的后裔,經(jīng)過(guò)百年的同化,其外貌、長(zhǎng)相、風(fēng)俗和習(xí)慣等,已與俄羅斯的俄羅斯人完全不同。而...
    93a1386892e2閱讀 783評(píng)論 0 4
  • 公子白洛閱讀 87評(píng)論 0 0
  • 葛然回首,過(guò)往種種都將成為自己的烙印。 就在前幾天,和俱樂(lè)部的同仁聊了一個(gè)話題——你覺(jué)得讀大學(xué)四年到底有沒(méi)有用? ...
    曹陽(yáng)CY閱讀 611評(píng)論 2 5
  • 我走在灰色街道 水泥的苦澀回蕩 雨霧沙沙的彌漫 正好大哭一場(chǎng) 分不清淚和雨 也沒(méi)人會(huì)在乎我的狼狽 可是心里確仍留一...
    蘭石閱讀 317評(píng)論 0 4

友情鏈接更多精彩內(nèi)容