iOS開發總結之程式碼規範

憶江南的部落格發表於2015-10-15

本文是投稿文章,作者:RylanJIN


最近被安排fix專案的隨機crash問題, 大大小小修復了差不多10個issue, 總結一下發現這些問題或多或少都是由程式碼習慣和程式設計規範引起的, 可見一個好的編碼習慣是多麼的重要! 趁著這兩天休假將自己所認為的一些比較好的程式碼規範整理一下, 並結合之前遇到的實際case跟大家分享一下.


命名規範



總的來說, iOS命名兩大原則是:可讀性高和防止命名衝突(通過加字首來保證). Objective-C 的命名通常都比較長, 名稱遵循駝峰式命名法. 一個好的命名標準很簡單, 就是做到在開發者一看到名字時, 就能夠懂得它的含義和使用方法. 另外, 每個模組都要加上自己的字首, 字首在程式設計介面中非常重要, 可以區分軟體的功能範疇並防止不同檔案或者類之間命名發生衝突, 比如相簿模組(PhotoGallery)的程式碼都以PG作為字首: PGAlbumViewController, PGDataManager.


1). 常量的命名


對於常量的命名最好在前面加上字母k作為標記. 如:


static const NSTimeInterval kAnimationDuration = 0.3;


定義作為NSDictionary或者Notification等的Key值字串時加上const關鍵字, 以防止被修改. 如:


NSString *const UIApplicationDidEnterBackgroundNotification


Tips:


I. 若常量作用域超出編譯單元(實現檔案), 需要在類外可見時, 使用extern關鍵字, 並加上該類名作為字首. 如 extern NSString *const PGThumbnailSize


II.全域性常量(通知或者關鍵字等)儘量用const來定義. 因為如果使用巨集定義, 一來巨集可能被重定義. 二來引用不同的檔案可能會導致巨集的不同. P.S. 對於#define也新增一下字首k(強迫症, 哈哈...)


2). 列舉的命名


對於列舉型別, 經常會看到之前的C的定義方式:


typedef enum : {
CameraModeFront,
CameraModeLeft,
CameraModeRight,
} CameraMode;


不知道是腫麼了, 每次看到這種定義方式總是感覺怪怪的, 作為一個正宗的iOS開發者當然要以Objective-C的方式來定義啦, 哈哈... 那Objective-C是怎麼定義的呢? 很簡單, 到SDK裡面看看Apple是怎麼做滴:


typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
UIViewAnimationTransitionNone,
UIViewAnimationTransitionFlipFromLeft,
UIViewAnimationTransitionFlipFromRight,
UIViewAnimationTransitionCurlUp,
UIViewAnimationTransitionCurlDown,
};


這邊需要注意的是: 列舉型別命名要加相關類名字首並且列舉值命名要加列舉型別字首.


3). 變數和物件的命名


給一個物件命名時建議採用修飾+型別的方式. 如果只用修飾命名會引起歧義, 比如title (這個到底是個NSString還是UILabel?). 同樣的, 如果只用型別來命名則會缺失作用資訊, 比如label (好吧, 我知道你是個UILabel, 但是我不知道它是用來做什麼的呀?). So, 正確的命名方式為:


titleLabel //表示標題的label, 是UILabel型別
confirmButton //表示確認的button, 是UIButton型別


對於BOOL型別, 應加上is字首, 比如- (BOOL)isEqualToString:(NSString *)aString這樣會更加清晰. 如果某方法返回非屬性的 BOOL 值, 那麼應根據其功能, 選用 has 或 is 當字首, 如- (BOOL)hasPrefix:(NSString *)aString


Tip: 如果某個命名已經很明確了, 為了簡潔可以省去型別名. 比如scores, 很明顯是個array了, 就不必命名成scoreArray了


編碼規範


編碼規範簡單來說就是為了保證寫出來的程式碼具備三個原則:可複用, 易維護, 可擴充套件. 這其實也是物件導向的基本原則. 可複用, 簡單來說就是不要寫重複的程式碼, 有重複的部分要儘量封裝起來重用. 否則修改檔案的時候得滿地找相同邏輯的地方...這個就用no zuo no die來描述吧, 哈哈...易維護, 就是不要把程式碼複雜化, 不要去寫巨複雜邏輯的程式碼, 而是把複雜的邏輯程式碼拆分開一個個小的模組, 這也是Do one thing的概念, 每個模組(或者函式)職責要單一, 這樣的程式碼會易於維護, 也不容易出錯. 可擴充套件則是要求寫程式碼時要考慮後面的擴充套件需求, 這個屬於架構層面的東東, 利用對應的設計模式來保證, 後面有機會單獨寫文探討。


編碼規範直接通過示例來介紹, 畢竟對於程式設計師來說一個Demo勝過千行文字(有同感的小夥伴讓我看到你們的雙手, 哈哈O(∩_∩)O~~). 下面的部分示例選自richieyang博文, 寫的很好的一篇文章, 推薦大家看一下, 我自己也是受益匪淺.


1). 判斷nil或者YES/NO


Preferred:


if (someObject) { ... }
if (!someObject) { ... }


Not preferred:


if (someObject == YES) { ...}
if (someObject != nil) { ...}


if (someObject == YES)容易誤寫成賦值語句, 自己給自己挖坑了...而且if (someObject)寫法很簡潔, 何樂而不為呢?


2). 條件賦值


Preferred:


result = object ? : [self createObject];


Not preferred:


result = object ? object : [self createObject];


如果是存在就賦值本身, 那就可以這樣簡寫, 多簡潔啊, 哈哈...


3). 初始化方法


Preferred:


NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;


第一個好處還是簡潔, 第二個好處是可以防止初始化進去nil值造成crash


4). 定義屬性


Preferred:


@property (nonatomic, readwrite, copy) NSString *name;


建議定義屬性的時候把所有的引數寫全, 尤其是如果想定義成只讀的(防止外面修改)那一定要加上readonly, 這也是程式碼安全性的一個習慣.


如果是內部使用的屬性, 那麼就定義成私有的屬性(定義到.m的class extension裡面)


對於擁有Mutable子型別的物件(e.g. NSString, NSArray, NSDictionary)一定要定義成copy屬性. Why? 示例: NSArray的array = NSMutableArray的mArray; 如果mArray在某個地方改變了, 那array也會跟著改變. So, make sense?


儘量不要暴露mutable型別的物件在public interface, 建議在.h定義一個Inmutable型別的屬性, 然後在.m的get函式裡面返回一個內部定義的mutable變數. Why? For security as well!


5). BOOL賦值


Preferred:


BOOL isAdult = age > 18;


Not preferred:


BOOL isAdult;
if (age > 18)
{
isAdult = YES;
}
else
{
isAdult = NO;
}


為什麼要這麼寫呢, 我不告訴你, 哈哈哈...


6) 拒絕死值


Preferred:


if (car == Car.Nissan)
or
const int adultAge = 18; if (age > adultAge) { ... }


Not preferred:


if (carName == "Nissan")
or
if (age > 18) { ... }


死值每次修改的時候容易被遺忘, 地方多了找起來就悲劇了. 而且定義成列舉或者static可以讓錯誤發生在編譯階段. 另外僅僅看到一個數字, 完全不知道這個數字代表的意義. 納尼?


7). 複雜的條件判斷


Preferred:


if ([self canDeleteJob:job]) { ... }
- (BOOL)canDeleteJob:(Job *)job
{
BOOL invalidJobState = job.JobState == JobState.New
|| job.JobState == JobState.Submitted
|| job.JobState == JobState.Expired;
BOOL invalidJob = job.JobTitle && job.JobTitle.length;
return invalidJobState || invalidJob;
}


Not preferred:


if (job.JobState == JobState.New
|| job.JobState == JobState.Submitted
|| job.JobState == JobState.Expired
|| (job.JobTitle && job.JobTitle.length))
{
//....
}


清晰明瞭, 每個函式DO ONE THING!


8). 巢狀判斷


Preferred:


if (!user.UserName) return NO;
if (!user.Password) return NO;
if (!user.Email) return NO;
return YES;


Not preferred:


BOOL isValid = NO;
if (user.UserName)
{
if (user.Password)
{
if (user.Email) isValid = YES;
}
}
return isValid;


一旦發現某個條件不符合, 立即返回, 條理更清晰


9). 引數過多


Preferred:


- (void)registerUser(User *user)
{
// to do...
}


Not preferred:


- (void)registerUserName:(NSString *)userName
password:(NSString *)password
email:(NSString *)email
{
// to do...
}


當發現實現某一功能需要傳遞的引數太多時, 就預示著你應該聚合成一個model類了...這樣程式碼更整潔, 也不容易因為引數太多導致出錯。


10). 回撥方法


Preferred:


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;


函式呼叫的可知性, 回撥時被呼叫者要知道其呼叫者, 方便資訊的傳遞, 所以建議在回撥方法中第一個引數中加上呼叫者。


Well, 不知不覺已經整理了10個了, 額, 太多了, 不知道童鞋們還有木有耐心看了, 好吧, 這一段就到此為止吧, 下面寫一下block的編碼規範, 各位看官, 預知後事如何, 且繼續look look, 哈哈...


Block的迴圈引用問題



Block確實是個好東西, 但是用起來一定要注意迴圈引用的問題, 否則一不小心你就會發現, Oh My God, 我的dealloc腫木不走了...


__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf turned nil
[weakSelf doSomethingElse]; // weakSelf == nil
};


如此在上面定義一個weakSelf, 然後在block體裡面使用該weakSelf就可以避免迴圈引用的問題. 那麼問題來了...是不是這樣就完全木有問題了? 很不幸, 答案是NO, 還是有問題。問題是block體裡面的self是weak的, 所以就有可能在某一個時段self已經被釋放了, 這時block體裡面再使用self那就是nil, 然後...然後就悲劇了...那麼腫麼辦呢?


__weak typeof(self) weakSelf = self;
myObj.myBlock = ^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf doSomething]; // strongSelf != nil
// preemption, strongSelf still not nil
[strongSelf doSomethingElse]; // strongSelf != nil
}
else {
// Probably nothing...
return;
}
};


解決方法很簡單, 就是在block體內define一個strong的self, 然後執行的時候判斷下self是否還在, 如果在就繼續執行下面的操作, 否則return或丟擲異常.


什麼情況下會出現block裡面self迴圈引用的問題? 這個問題問的好, 哈哈...簡單來說就是雙邊引用, 如果block是self類的property (此時self已經retain了block), 然後在block內又引用了self, 這個情況下就肯定會迴圈引用了...


P.S. RAC裡面有定義好的@weakify(self)和@strongify(self), 用起來灰常灰常的方便, 勸君嘗試一下^_^


那些年遇到的Crash



  • 多執行緒同步問題造成的Crash


這個其實還蠻常見的, 尤其是在多執行緒氾濫使用的今天...你可以使用多執行緒, 但你要知道保護它呀, 哈哈. 對於資料來源或model類一定要注意多執行緒同時訪問的情況, 我個人比較喜歡用GCD的序列佇列來同步執行緒.


  • Observer的移除


現在的程式碼裡面很多需要用到Observer, 根據被觀察物件的狀態來相應的Update UI或者執行某個操作. 註冊observer很簡單, 但是移除的時候就出問題了, 要麼是忘記移除observer了, 要麼是移除的時機不對. 如果某個被觀察物件已經被釋放了, observer還在, 那結果只能是crash了, 所以切記至少在dealloc裡面移除一下observer...


  • NSArray, NSDictionary成員的判空保護


在addObject或insertObject到NSArray或者NSDictionary時最好加一下判空保護, 尤其是網路相關的邏輯, 如果網路返回為空(jason解析出來為空), 但你還是毅然決然的add到array裡面, 那麼...


最後一點就是commit程式碼之前一定要保證木有warning, 木有記憶體洩露, 確保都OK之後再上傳程式碼. 其實很簡單, 上傳程式碼之前Command + Shift + B靜態分析一下, 看看有木有什麼issue...就先寫這麼多吧, 以後遇到更多的坑後, 我一定會爬出來再過來補充的, to be continued...

相關文章