iOS 開發者不知道的 10 個最常見錯誤

已發表: 2022-03-11

有什麼比 App Store 拒絕一個有問題的應用程序更糟糕的事情? 讓它接受。 一旦一星評論開始滾動,幾乎不可能恢復。 這會花費公司的資金和開發人員的工作。

iOS現在是世界上第二大移動操作系統。 它的採用率也非常高,超過 85% 的用戶使用最新版本。 正如您所料,高度參與的用戶有很高的期望——如果您的應用或更新並非完美無缺,您就會聽到。

隨著對 iOS 開發人員的需求持續飆升,許多工程師轉向了移動開發(每天有超過 1,000 個新應用程序提交給 Apple)。 但真正的 iOS 專業知識遠遠超出了基本的編碼範圍。 以下是 iOS 開發人員容易犯的 10 個常見錯誤,以及如何避免這些錯誤。

85% 的 iOS 用戶使用最新的操作系統版本。 這意味著他們希望您的應用或更新完美無缺。
鳴叫

常見錯誤 1:不了解異步進程

新程序員中一種非常常見的錯誤類型是不正確地處理異步代碼。 讓我們考慮一個典型的場景:用戶打開一個帶有表格視圖的屏幕。 從服務器獲取一些數據並顯示在表格視圖中。 我們可以寫得更正式:

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

乍一看,一切正常:我們從服務器獲取數據,然後更新 UI。 但是,問題是獲取數據是一個異步過程,不會立即返回新數據,這意味著在接收新數據之前會調用reloadData 。 為了修正這個錯誤,我們應該將第 2 行移動到塊內第 1 行之後。

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

但是,在某些情況下,此代碼仍無法按預期運行,這使我們...

常見錯誤 2:在主隊列以外的線程上運行 UI 相關代碼

假設我們使用了前面常見錯誤的更正代碼示例,但即使在異步過程成功完成後,我們的表視圖仍然沒有更新新數據。 這麼簡單的代碼可能有什麼問題? 為了理解它,我們可以在塊內設置一個斷點,並找出這個塊在哪個隊列中被調用。 由於我們的調用不在主隊列中,所有與 UI 相關的代碼都應該在主隊列中執行,因此很有可能發生所描述的行為。

大多數流行的庫——例如 Alamofire、AFNetworking 和 Haneke——被設計為在執行異步任務後在主隊列上調用completionBlock 。 但是,您不能總是依賴它,並且很容易忘記將代碼分派到正確的隊列。

為確保所有與 UI 相關的代碼都在主隊列中,不要忘記將其分派到該隊列:

 dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });

常見錯誤 3:對並發和多線程的誤解

並發可以比作一把非常鋒利的刀:如果您不小心或經驗不足,您很容易割傷自己,但是一旦您知道如何正確安全地使用它,它就會非常有用和高效。

您可以嘗試避免使用並發,但無論您正在構建什麼樣的應用程序,您都離不開它。 並發可以為您的應用程序帶來顯著的好處。 尤其:

  • 幾乎每個應用程序都會調用 Web 服務(例如,執行一些繁重的計算或從數據庫中讀取數據)。 如果這些任務在主隊列上執行,應用程序將凍結一段時間,使其無響應。 此外,如果這需要太長時間,iOS 將完全關閉應用程序。 將這些任務移動到另一個隊列允許用戶在執行操作時繼續使用應用程序,而不會出現應用程序凍結的情況。
  • 現代 iOS 設備擁有多個內核,那麼當任務可以並行執行時,為什麼用戶還要等待任務按順序完成呢?

但是並發的優勢並非沒有復雜性和引入粗糙錯誤的可能性,例如很難重現的競爭條件。

讓我們考慮一些真實世界的例子(請注意,為簡單起見,省略了一些代碼)。

情況1

 final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }

多線程代碼:

 let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }

乍一看,一切都是同步的,看起來好像應該按預期工作,因為ThreadSaveVar包裝了counter並使其成為線程安全的。 不幸的是,這不是真的,因為兩個線程可能同時到達增量線,因此counter.value == someValue永遠不會變為真。 作為一種解決方法,我們可以使ThreadSafeCounter在遞增後返回其值:

 class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }

案例2

 struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }

在這種情況下, dispatch_barrier_sync用於同步對數組的訪問。 這是確保訪問同步的常用模式。 不幸的是,這段代碼沒有考慮到 struct 每次我們向它追加一個項目時都會創建一個副本,因此每次都有一個新的同步隊列。

在這裡,即使第一眼看起來正確,它也可能無法按預期工作。 它還需要大量的工作來測試和調試它,但最終,您可以提高應用程序的速度和響應能力。

常見錯誤 4:不知道可變對象的陷阱

Swift 在避免值類型錯誤方面非常有幫助,但仍然有很多開發人員使用 Objective-C。 可變對象非常危險,可能會導致隱藏的問題。 不可變對象應該從函數中返回是一個眾所周知的規則,但大多數開發人員不知道為什麼。 讓我們考慮以下代碼:

 // Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end

上面的代碼是正確的,因為NSMutableArrayNSArray的子類。 那麼這段代碼有什麼問題呢?

首先也是最明顯的事情是另一個開發人員可能會出現並執行以下操作:

 NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }

這段代碼會搞砸你的課。 但在這種情況下,這是一種代碼異味,由開發人員自行解決。

但是,在這種情況下,情況要糟糕得多,並且表現出意外的行為:

 Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];

這裡的期望是[newChildBoxes count] > [childBoxes count] ,但如果不是呢? 然後這個類設計得不好,因為它改變了一個已經返回的值。 如果您認為不等式不正確,請嘗試使用 UIView 和[view subviews]進行試驗。

幸運的是,我們可以通過重寫第一個示例中的 getter 輕鬆修復我們的代碼:

 - (NSArray *)boxes { return [self.m_boxes copy]; }

常見錯誤 5:不了解 iOS NSDictionary如何在內部工作

如果您曾經使用過自定義類和NSDictionary ,您可能會意識到如果您的類不符合NSCopying作為字典鍵,您將無法使用它。 大多數開發者從來沒有問過自己為什麼蘋果會增加這個限制。 為什麼 Apple 複製密鑰並使用該副本而不是原始對象?

理解這一點的關鍵是弄清楚NSDictionary如何在內部工作。 從技術上講,它只是一個哈希表。 讓我們快速回顧一下它在為鍵添加對象時如何在較高級別上工作(為簡單起見,此處省略了表調整大小和性能優化):

第 1 步:計算hash(Key) 。 第 2 步:根據哈希值,尋找放置對象的位置。 通常,這是通過將哈希值與字典長度取模來完成的。 然後使用生成的索引來存儲鍵/值對。 第 3 步:如果該位置沒有對象,它會創建一個鍊錶並存儲我們的記錄(對象和鍵)。 否則,它將記錄附加到列表的末尾。

現在,讓我們描述如何從字典中獲取記錄:

第 1 步:計算hash(Key) 。 第 2 步:它通過哈希搜索密鑰。 如果沒有數據,則返回nil 。 第 3 步:如果存在鍊錶,則遍歷 Object 直到[storedkey isEqual:Key]

通過了解幕後發生的事情,可以得出兩個結論:

  1. 如果鍵的哈希值發生變化,記錄應該被移動到另一個鍊錶。
  2. 鍵應該是唯一的。

讓我們在一個簡單的類上檢查一下:

 @interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end

現在想像NSDictionary不復制鍵:

 NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;

哦! 我們那裡有一個錯字! 讓我們修復它!

 p.name = @"Jon Snow";

我們的字典應該怎麼做? 由於名稱發生了變化,我們現在有一個不同的哈希值。 現在我們的對象放錯了地方(它仍然有舊的哈希值,因為字典不知道數據的變化),而且還不清楚我們應該使用什麼哈希來查找字典中的數據。 可能還有更糟糕的情況。 想像一下,如果我們的字典中已經有了評分為 5 的“Jon Snow”。字典最終會為同一個 Key 提供兩個不同的值。

如您所見,在NSDictionary中使用可變鍵可能會導致許多問題。 避免此類問題的最佳做法是在存儲之前複製對象,並將屬性標記為copy 。 這種做法也將幫助您保持課堂的一致性。

常見錯誤 6:使用故事板而不是 XIB

大多數新的 iOS 開發人員都遵循 Apple 的建議,並默認為 UI 使用故事板。 然而,使用故事板有很多缺點,只有幾個(有爭議的)優點。

故事板的缺點包括:

  1. 為幾個團隊成員修改故事板真的很難。 從技術上講,您可以使用許多故事板,但在這種情況下,唯一的優勢是可以在故事板上的控制器之間進行轉場。
  2. 情節提要中的控制器和轉場名稱是字符串,因此您必須在整個代碼中重新輸入所有這些字符串(有一天您破壞它),或者維護一個龐大的情節提要常量列表。 您可以使用 SBConstants,但在情節提要上重命名仍然不是一件容易的事。
  3. 故事板迫使您進行非模塊化設計。 在使用故事板時,幾乎沒有動力讓您的視圖可重用。 這對於最小可行產品 (MVP) 或快速 UI 原型設計可能是可以接受的,但在實際應用程序中,您可能需要在整個應用程序中多次使用相同的視圖。

故事板(有爭議的)優點:

  1. 整個應用導航一目了然。 但是,實際應用程序可以有十多個控制器,以不同的方向連接。 具有這種連接的情節提要看起來像一團毛線,並不能提供對數據流的任何高級理解。
  2. 靜態表。 這是我能想到的唯一真正的優勢。 問題是 90% 的靜態表在應用程序演變過程中往往會變成動態表,而動態表可以更容易地由 XIB 處理。

常見錯誤 7:混淆對象和指針的比較

在比較兩個對象時,我們可以考慮兩個相等:指針相等和對象相等。

指針相等是兩個指針都指向同一個對象的情況。 在 Objective-C 中,我們使用==運算符來比較兩個指針。 對象相等是兩個對象代表兩個邏輯上相同的對象的情況,例如數據庫中的同一個用戶。 在 Objective-C 中,我們使用isEqual甚至更好的類型特定isEqualToStringisEqualToDate等運算符來比較兩個對象。

考慮以下代碼:

 NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }

當我們運行該代碼時,控制台會打印什麼? 我們將得到a is equal to b ,因為對象ab都指向內存中的同一個對象。

但現在讓我們將第 2 行更改為:

 NSString *b = [[@"a" mutableCopy] copy];

現在我們得到a is NOT equal to b因為這些指針現在指向不同的對象,即使這些對象具有相同的值。

這個問題可以通過依賴isEqual或類型特定的函數來避免。 在我們的代碼示例中,我們應該將第 3 行替換為以下代碼,以使其始終正常工作:

 if ([a isEqual:b]) {

常見錯誤 8:使用硬編碼值

硬編碼值有兩個主要問題:

  1. 通常不清楚它們代表什麼。
  2. 當需要在代碼中的多個位置使用它們時,需要重新輸入(或複制粘貼)。

考慮以下示例:

 if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];

172800代表什麼? 為什麼要使用它? 這與 2 天內的秒數相對應可能並不明顯(一天中有 24 x 60 x 60 或 86,400 秒)。

您可以使用#define語句定義一個值,而不是使用硬編碼值。 例如:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define是一個預處理器宏,它用代碼中的值替換命名定義。 因此,如果您在頭文件中有#define並將其導入某處,則該文件中所有出現的已定義值也將被替換。

這很好用,除了一個問題。 為了說明剩下的問題,請考慮以下代碼:

 #define X = 3 ... CGFloat y = X / 2;

在這段代碼執行之後,您希望y的值是多少? 如果你說的是 1.5,那你就錯了。 執行此代碼後, y將等於 1(而不是1.5)。 為什麼? 答案是#define沒有關於類型的信息。 因此,在我們的例子中,我們有兩個Int值(3 和 2)的除法,這會產生一個Int (即 1),然後將其轉換為Float

這可以通過使用常量來避免,根據定義,這些常量是類型化的:

 static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected

常見錯誤 9:在 switch 語句中使用默認關鍵字

在 switch 語句中使用default關鍵字可能會導致錯誤和意外行為。 考慮一下 Objective-C 中的以下代碼:

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }

用 Swift 編寫的相同代碼:

 enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }

此代碼按預期工作,僅允許管理員用戶能夠更改其他記錄。 但是,如果我們添加另一個用戶類型“經理”,它應該也可以編輯記錄,會發生什麼? 如果我們忘記更新此switch語句,代碼將編譯,但不會按預期工作。 但是,如果開發人員從一開始就使用枚舉值而不是默認關鍵字,則會在編譯時識別出疏忽,並且可以在進行測試或生產之前進行修復。 這是在 Objective-C 中處理這個問題的好方法:

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }

用 Swift 編寫的相同代碼:

 enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }

常見錯誤 10:使用NSLog進行日誌記錄

許多 iOS 開發人員在他們的應用程序中使用NSLog進行日誌記錄,但大多數時候這是一個可怕的錯誤。 如果我們查看 Apple 文檔中的NSLog函數描述,我們會發現它非常簡單:

 void NSLog(NSString *format, ...);

它可能出了什麼問題? 事實上,什麼都沒有。 但是,如果您將設備連接到 Xcode 管理器,您將在那裡看到所有調試消息。 僅出於這個原因,你永遠不應該使用NSLog進行日誌記錄:它很容易顯示一些不需要的內部數據,而且它看起來不專業。

更好的方法是用可配置的 CocoaLumberjack 或其他一些日誌框架替換NSLogs

包起來

iOS 是一個非常強大且快速發展的平台。 Apple 不斷努力為 iOS 本身引入新的硬件和功能,同時也在不斷擴展 Swift 語言。

提高您的 Objective-C 和 Swift 技能將使您成為一名出色的 iOS 開發人員,並提供機會使用尖端技術從事具有挑戰性的項目。

相關: iOS 開發者指南:從 Objective-C 到學習 Swift