iOS程式碼實踐總結

發表於2015-09-20

前幾個月完成對MVVM/RAC的學習之後,最近一直在默默地對專案程式碼進行重構,寫碼比較多,過了一段時間回頭發現自己的程式碼風格還有程式碼質量都有大大的改善。過去幾年在一家小公司負責iOS客戶端後來負責客戶端的研發工作,被雜亂的事情分神比較多,所以到去年的時候,寫碼已經不太多了。在新公司待了大半年,目前只是寫碼的小角色,所以精力基本上在寫業務程式碼和業餘學習亂七八糟的技術上面。

最近一個月除了專門抽時間和精力重構之外,還有就是遇到需要新增功能的模組的時候,由於專案中的程式碼歷史因素比較多,第一件乾的事情往往是重構整理程式碼,發現很多之前的程式碼寫的時候沒有注意的事情特別多,比如全域性變數亂用;方法沒有層次感,胡亂新增;對業務不瞭解的情況下,通過打補丁的方式實現功能等等。所以我決定寫一篇文章,把自己的覺得實踐中需要注意的一些事項,具體總結一下分享給大家。

減少物件屬性
這個是最容易改善程式碼質量的一個點,很多程式碼一眼看上去就會讓人感覺很凌亂,一上來就是幾十個不同的物件變數定義在裡面,這讓不同邏輯之間莫名其妙沒法分開。一個是定義的方式不對,很多莫名其妙的內部變數暴露在標頭檔案中,讓外部呼叫者根本不知道哪些才是public可以操作的方法。另外實際上,經過我自己這段時間的重構經驗來看,大多數是可以通過區域性變數或者__block變數來代替的。

1. 標頭檔案中儘可能少暴露變數或方法,而要使用extension或者category放在.m檔案,或者專門的private標頭檔案中
標頭檔案中暴露的資訊越少越好,一切不必要的資訊都不要暴露出來

m檔案的extension中,定義conforms protocol和物件屬性,對於物件屬性的定義,使用getter/setter 來定義。

2. 使用區域性變數或者__block變數代替
區域性變數不需要多說,需要寫碼的時候思路清晰一些,寫完之後在commit之前即使review一定要check一遍,對自己的程式碼質量負責,code review往往檢查不出來冗餘或者廢棄的程式碼。不新增一個多餘的物件屬性,不留註釋掉的程式碼,不留沒有用途的程式碼,這些都是基本功,但是很多開發者就是做不到,或者說對寫碼沒有愛,所以很多廢棄的程式碼,我重構程式碼的時候,雖然對業務不熟悉,但是大多數模組都能刪除掉十分之一的程式碼和大量的物件屬性,這個是單純的不夠用心。

關於使用__block變數,這個是Android開發中我感覺到最不滿意的地方,這個特性簡直太他媽爽了。
比如這裡,使用block的時候回傳一些變數


再比如這裡,我需要記錄一個pan手勢開始時,headerView的頂部座標,結合RAC之後,本來需要全域性變數來記錄的值,使用__block變數即可搞定

3. 可以儘可能避免迴圈引用
有個地方很多開發者會疏漏,在block中使用_XXX物件變數的時候,block會retain self指標,一不小心就會造成迴圈引用的出現。所以使用區域性變數的話,就能扼殺這種問題在搖籃之中。

減少和模組化物件訊息
1. 減少物件訊息
減少UI的action類訊息,感謝block和RAC,或者blockskit,讓我們得以通過hook來把之前target-action模型換為block來實現,UI和action的程式碼終於可以一起了,使整個邏輯變得緊湊,在檢視程式碼的時候終於不用跳來跳去了。還有就是日常開發中,把自己寫的各種protocol或者傳遞target/selector的地方,儘量使用block來代替,相信我,這個會使程式碼好讀很多。

2. 模組化
使用”#pragma mark – XXX”進行分割不同邏輯之間的界限,讓整個檔案閱讀起來更加結構化。還有一個我現在最常用的就是是設定Xcode的快捷鍵,把Ctrl + 6 顯示文件結構的快捷鍵改為:Command + J ,搜尋來快速跳轉到對應的訊息和模組,要儘量避免文件結構顯示超過兩螢幕,超過兩螢幕說明有點多了,你肯定考慮一下重構了。

我個人習慣一般劃分的模組有: life cycle,ui helper,datasource/delegate,依據功能進行劃分的模組等等,如下是我最近重構的一個ViewController的文件結構

MVVM && RAC
我自己使用MVVM思路的感覺是太爽了,說一下,MVVM不一定需要使用RAC,但是data binding少不了,在iOS中也就是KVO了,建議大家都去嘗試一下,我自己感覺這個基本上MVVM的最核心的東西了,連Android SDK也不得不引入這個特性。把資料部分的邏輯抽取放在ViewModel中,然後讓UI和ViewModel中的資料binding,這個不會減少程式碼量,但是絕對可以大大簡化開發時邏輯的復度,再也不用重寫-setXXX:方法來update一大堆不相關的UI了,關於UI開發,後面會專門再講講新的。這裡說一下我自己的理解,有人說RAC影響效能,回撥棧太深,這個的確是會有的,但是個人感覺RACObserver是基於KVO實現的,呼叫的時候是同步呼叫的,所以對效能的影響有限,也不會出現呼叫順序的問題,所以我敢在列表開發中使用data binding,實踐之後還好,對使用者體驗沒什麼影響。

關於RAC,即使你不使用RAC,有一些東西也是絕對值得你在專案中引入的,比如@weakify(self)/@strongify(self),通過預編譯檢視的話,這個的做法是設定一個區域性變數self來覆蓋全域性的self,進而避免迴圈引用的,需要注意的是block層次較深的時候使用的問題,http://stackoverflow.com/questions/21716982/explanation-of-how-weakify-and-strongify-work-in-reactivecocoa-libextobjc

RAC/MVVM,我剛開始學習的時候,寫了兩篇文章,算是我自己的總結,理解上面還有不足,跟大家參考一下:http://blog.csdn.net/colorapp/article/details/46524893http://blog.csdn.net/colorapp/article/details/46537729。大家可以通過我部落格中文章的參考連結學習。

UI開發
1. 重寫setter方法和Code Block Evaluation C Extension語法
重寫UI的getter方法,把UI的初始化放在getter中,減輕 -viewDidLoad的負荷,同時可以使整個頁面變得清晰;同時,可以通過使用使用GCC Code Block Evaluation C Extension ({…})語法,結構化區域性變數初始化和處理的邏輯。關於這個語法,參考我之前的部落格:http://blog.csdn.net/colorapp/article/details/47006771。關於setter程式碼風格,可以參考別人寫的一篇文章,http://casatwy.com/iosying-yong-jia-gou-tan-viewceng-de-zu-zhi-he-diao-yong-fang-an.html,這個問題之前在我們Q群裡探討之後我也非常認同這種方式寫UI。

舉一個例子,-viewDidLoad中,做為邏輯的入口,程式碼會變少但是變清晰,程式碼如下:


然後重寫bgView的getter方法,包括View和frame這些都可以使用({…})語法使程式碼結構化層次化:

2. 複雜UI的開發
有時候我們開發業務的時候,產品需求往往非常複雜,酷炫的UI加上各種考慮全面的邏輯,這個的結果就是,碼農的超長程式碼,而我們平時工作面對的也大多數都是這類問題。關於這個問題,我的解決方式,組合式UI / custom view / child view controller來解決。
(1)  組合式view
這個概念是從Android中借鑑而來。重構時檢視專案中的程式碼,發現大家用的做UI的時候,對這個概念不是很強烈,感覺是對UIView的view hierarchy理解不夠。比如一個複雜的UI,直接把所有的subviews直接堆積到super view上面,這樣的結果就是,調整subview的frame非常困難。我個人的做法是,首先對複雜UI進行分塊,從左到右或者從上倒下,把各個UI元素放到不同的container view上面,然後組合這些container view放到super view上面,這樣的好處非常明顯,首先UI乾淨清晰,閱讀起來不那麼費勁。其次就是你計算座標或者設定約束會變得很簡單,因為你調整一個UI元素的時候,只需要考慮它與包含它的container view的座標關係即可,而不是通過一大堆無趣計算跟最外層super view關聯起來。還有就是可以充分利用Auto Layout和autoresiziingmask這些UI利器,使用的時候會非常方便。再有就是結合RACObserver這個利器之後,你能很容易做到根據data來update ui。

舉個例子,是我們專案中前一段時間我重構的一個頁面,這個首頁列表,效能要求比較高。並沒有使用Auto Layout來實現,但是不使用Auto Layout並不是不把它寫的很乾淨的理由。


這是我對一個UITableViewCell的分層,最外層由 icon view / right view / bottom view這些container view組成,而right view這個container view則又是由right top view / right middle view /right bottom view 這些 sub container view組合而成,而具體的UI元素則是放在這些sub container view之中。這樣UI程式碼就會以一種層次化樣式展示出來,init/layoutsubviews只需要維護self與container view的關係即可,而具體展示資料的UI元素也只跟sub container view存在座標關係。我們看一下right view這個container view的程式碼實現:

關於效能的話,感謝iOS,我們不存在Android中頁面層次較深效能卡頓的問題,放心把UI層次化就行

(2) custom view
對於非常複雜並且相對獨立或者可以重用的UI,及時使用custom view子類化。對於單純的展示UI,我們只需要簡單通過組合式view就可以實現了。但是有時候,我們會遇到一些包含無論是動畫,邏輯都比較複雜的情況,這個時候使用組合式View去實現,一方面容易把邏輯弄混亂,會把檔案的文件結構變得很複雜,簡單來說就是物件的訊息數量很多。這個時候,我們可以通過custom view來實現,實際上這個也是組合式view,但是我們是把這些組合式view變成了一個類而已,只暴露少量的介面給外部呼叫。如果這個custom view會出現在多個業務模組中,那麼有必要使用一個單獨的檔案來容納這個類,如果僅僅是這個模組一個使用的話,可以直接寫在這個業務模組的檔案中即可,沒有必要對所有的類都單獨一個檔案,我們就當作這個“內部類”來弄了。

什麼時候使用custom view而不是組合view,我想了很久,你覺得組合式view的程式碼很亂的時候,別客氣,包裝為一個custom view就行了。我這邊最近遇到的幾個問題是使用UICollectionView來做部分UI的時候,同時還有其他很多UI元素,我會寫一個custom view。比如下面這個檔案,把一個左右滑動檢視圖片的UI使用PhotoView這個custom view進行包裝,內部使用UICollectionView實現一部分相對獨立的模組,這個時候這個控制元件實際上是可以包裝為一個相對獨立的模組的,用子類我感覺比較合適一些。

(3) container view controller
這個用法很多開發者不熟悉或者說是用的不多,但實際業務中,這個技術非常有用途,可以大大提高開發效率。對這部分知識不熟悉的,可以參考我之前的部落格:http://blog.csdn.net/colorapp/article/details/45765601。對於有相對獨立業務邏輯以及生命週期要求的業務,使用child view controller進行包裝,如果parent view contrller與child view controller之間非常密切,則使用View Model以及block來對parent view controller和 child view controller 進行銜接。

使用child view controller來開發UI而不是custom view的優勢很多,我個人認為最大優勢在於可以方便利用View Controller的生命週期以及View Controller Hierarchy,比如在-viewWillAppear/-viewDidDisappear中做一些操作,再比如直接獲取UINavigationController指標等等。之前的做法一般是在View Controller的對應生命週期內呼叫custom view的方法,傳遞self.navigationController指標給custom view等。所以可以不僅僅把UI相關的程式碼包裝進入這個child view controller,也可以把網路請求,資料處理這些這些邏輯放到child view controller中,這樣下來就能避免那種動不動超過1k行的view controller的出現了。

利用MVVM之後,還有一個比較有好處的用法,比如公用一些資料的時候,之前我們是把物件傳遞來傳遞去,這樣的問題是很容易出現混亂,這個時候我們是傳遞ViewModel就可以避免這個問題,ViewModel既負責網路請求又負責資料處理,而parent view controller與child view controller所需要做的事情就是跟ViewModel進行binding而已。

Auto Layout/Masonry

在一些效能要求不是那麼強烈的非列表頁,我們可以大量使用Auto Layout來開發UI,充分利用UI根據資料的自適應能力,連在container view中調整UI的步驟都不需要了。之前有一段時間我根本不想開發iOS,原因很簡單,Android的佈局式以及可見式的開發方式非常方便,再加上AS這樣的神器,我自己感覺效率不比iOS低。自從專案最低支援變到iOS6之後,我才開始使用Auto Layout,雖然比較費勁,但是感覺這個對UI開發來說是個解脫。

至於Masonry這個框架,之前我對這個抱有一定的懷疑不敢使用,所以我把原始碼讀了一遍,發現這個包裝很薄很巧妙,很多設計思路也值得借鑑,對原始碼有興趣的可以參考我的部落格:http://blog.csdn.net/colorapp/article/details/45030163。我讀完原始碼之後,嘗試著完全使用Mansory來開發一個展示資訊的頁面,感覺太爽了!

這個的優勢就是你設定UI的資料之後,不需要再考慮去update ui了,這樣世界瞬時就清淨了。。。。,下面是我一個簡單的示例,結合({….})語法和RAC,可以使用最簡單的label這樣的命名來對UI設定資料,這個對我們開發UI來說,絕對是一種解脫。

說一下Auto Layout的問題:
1. 首先一個問題,是如果一個view不是leaf view的話,那麼這個UIView如果hidden的話,它的約束仍然是work的,所以會留下空白,不會像Android中那樣設定GONE那麼方便。國內sunny大神開源一個不錯的解決方式,https://github.com/forkingdog/UIView-FDCollapsibleConstraints。這裡說一下我之前的解決方式,比較土逼,直接子類化:

2. 動畫的問題
使用Auto Layout有一個比較大的問題在於動畫,通過更改約束來進行動畫,一直是我比較頭疼的問題,所以一般遇到這類問題的時候,我都會盡量避免使用Auto Layout來解決,而是使用frame的方式來做。可以參考objc.io上面的一篇文章:http://www.objc.io/issues/3-views/advanced-auto-layout-toolbox/

3. 多行UILabel的問題
iOS7以及以下的作業系統上,UILabel顯示多行文字是又不足的,你需要設定UILabel的preferredMaxLayoutWidth為一個固定值才能顯示多行文字。在iOS8以後就不再需要設定這個了。

4. UIScrollView的問題以及約束歧義和其他問題
參考我的文章:http://blog.csdn.net/colorapp/article/details/47007143

這個地方,我的建議是根據具體問題來選擇實現方式 :spring & structs也好,Auto Layout也好,那種解決問題較為簡潔快速就用那種,不一定非要固定於一種行為,尤其是開發的頁面有大量動畫的時候。

註釋
不要寫一堆中文註釋,程式碼不要出現大量的中文,OC已經夠囉嗦,不要這麼囉嗦地寫碼。除了提供服務的public功能或者方法,業務程式碼僅在某些關鍵點上註釋一下就行,不需要一大堆中文,這樣太low,程式碼自注釋即可,需要註釋的,可以通過喵神的Xcode外掛來實現,https://github.com/onevcat/VVDocumenter-Xcode

而對於出現拼音命名程式碼的人,能做主的話,別猶豫,開掉吧。這裡吐一下槽,之前的公司就有這樣的哥們,不是我招進來的,老闆硬塞給我的。

善用OC的新語法
OC有很多新的語法糖,可以大大提高我們的效率,參考Apple Guide:https://developer.apple.com/library/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html
比如列印數字的時候,我們可以用@(xxx)來列印,定義列舉的時候使用typedef NS_ENUM,使用instancetype而不用id等等。而最近又到了每年技能槽重新整理的日子了,iOS 9釋出了,OC又有了一些新語法,去學習一下多用用吧。

JSON資料的處理
新手往往會被這個稍微困惑一下,比如伺服器返回的資料格式不正確啦,包含null啦,都很容易引起專案崩潰。這個問題可以使用Mantle來解決,很多兄弟都在使用這個,我自己倒是一直沒有用過。之前寫了一個小框架放在了github上面,https://github.com/lihei12345/CYJSONValidator,這個在我們專案內部也在使用,效果不錯,用來解析資料的時候,對資料的型別以及是否為null等進行校驗,確保解析出來資料型別的正確性。對於可能不存在key的時候,還可以設定一些預設值。
舉個例子:

block
使用delegate代替block,這個沒啥可多說的,把程式碼變得非常緊湊,減少檔案的訊息數量,最主要的是關係沒那麼緊密了。對於有大量的delegate方法才考慮使用protocol實現,這個時候block太多也影響閱讀。

同時,對於傳遞target/selector,也儘量使用block吧,這種閱讀查詢起來太不方便了。

提交程式碼

及時stage,這個非常重要,開發過程中經常需要經常比對上一步的程式碼,這樣才能最大程度上確保自己的改動是正確的。如果有一些小問題,也可以即使找到歷史版本。
及時commit,每完成一個相對完整的需求,就commit,小提交是個好習慣。
PR code review要做好,要花大量的時間做,有條件的話,最好每個版本開一次總結會。

RAC封裝網路請求
返回的signal要避免多次出現side effect,但不使用replay/replayLazily,因為dispose不會被呼叫。

使用RACCommand封裝請求,檢視這幾篇文章:http://codeblog.shape.dk/blog/2013/12/05/reactivecocoa-essentials-understanding-and-using-raccommand/https://github.com/ReactiveCocoa/ReactiveCocoa/issues/963https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1326
結合RACCommand和takeUntil:來封裝一個可以cancel的請求。

相關文章