iOS开发人员不知道的10个最常见错误

本文概述

  • 常见错误1:不了解异步过程
  • 常见错误2:在主队列以外的线程上运行与UI相关的代码
  • 常见错误3:误解并发和多线程
  • 常见错误4:不知道可变对象的陷阱
  • 常见错误5:不了解iOS NSDictionary在内部如何工作
  • 常见错误6:使用情节提要板而不是XIB
  • 常见错误7:混淆的对象和指针比较
  • 常见错误8:使用硬编码值
  • 常见错误9:在switch语句中使用默认关键字
  • 常见错误10:使用NSLog进行日志记录
  • 本文总结
唯一的问题是越野车被App Store拒绝吗?接受了。一星级评论开始陆续推出后, 几乎无法恢复。这使公司付出了金钱, 并使开发人员付出了工作。
iOS现在是世界上第二大移动操作系统。它的采用率也很高, 最新版本的用户超过了85%。如你所料, 参与度高的用户寄予厚望-如果你的应用程序或更新不是完美无缺的, 你会听到的。
随着对iOS开发人员的需求持续飙升, 许多工程师已转向移动开发(每天有1000多个新应用程序提交给Apple)。但是, 真正的iOS专业知识远远超出了基本编码范围。以下是iOS开发人员容易犯的10个常见错误, 以及如何避免这些错误。
85%的iOS用户使用最新的OS版本。这意味着他们希望你的应用程序或更新是完美无缺的。
鸣叫
常见错误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将在接收新数据之前被调用。为了解决这个错误, 我们应该在代码块中的#1行之后立即移动#2行。
@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设备具有多个核心, 那么为什么用户可以并行执行任务时又要等待任务按顺序完成?
但是, 并发的优点并非没有复杂性, 也有可能引入过时的bug, 例如确实难以复制的竞争条件。
让我们考虑一些实际示例(请注意, 为简单起见, 省略了一些代码)。
情况1
final class SpinLock { private var lock = OS_SPINLOCK_INITfunc 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 = http://www.srcmini.com/newValue } } } }

多线程代码:
let counter = ThreadSafeVar< Int> (value: 0)// this code might be called from several threads counter.value += 1if (counter.value =http://www.srcmini.com/= someValue) { // do something }

乍一看, 由于ThreadSaveVar会包装计数器并使线程安全, 因此所有内容都会同步并看起来像应该可以正常工作。不幸的是, 这是不正确的, 因为两个线程可能同时到达增量线, 并且counter.value =http://www.srcmini.com/= someValue永远不会成为true。作为一种解决方法, 我们可以使ThreadSafeCounter在递增后返回其值:
class ThreadSafeCounter {private var value: Int32 = 0func increment() -> Int { return Int(OSAtomicIncrement32(& value)) } }

情况二
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

上面的代码是正确的, 因为NSMutableArray是NSArray的子??类。那么这段代码会出什么问题?
首先也是最明显的事情是, 另一个开发人员可能会来执行以下操作:
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计数]> [childBoxes计数], 但是如果不是这样怎么办?然后, 该类的设计不够好, 因为它会突变已经返回的值。如果你认为不平等不应该成立, 请尝试使用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:如果有一个链表, 它将遍历对象, 直到[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";

我们的字典应该怎么办?由于名称已被更改, 因此我们现在有了另一个哈希。现在, 我们的对象放置在错误的位置(由于字典不知道数据更改, 它仍然具有旧的哈希值), 并且还不清楚我们应该使用哪种哈希值来在字典中查找数据。可能会有更糟的情况。想象一下, 如果我们的词典中已经有了” 乔恩·雪诺” (Jon Snow), 其评分为5。该词典最终将为同一个键提供两个不同的值。
如你所见, 在NSDictionary中具有可变密钥会引起许多问题。避免此类问题的最佳做法是在存储对象之前先将其复制, 然后将属性标记为复制。这种做法还可以帮助你保持课堂的一致性。
常见错误6:使用情节提要板而不是XIB大多数新的iOS开发人员都遵循Apple的建议, 默认情况下, UI使用故事板。但是, 使用情节提要板有很多缺点, 只有几个(可争议的)优点。
故事板的缺点包括:
  1. 很难为多个团队成员修改情节提要。从技术上讲, 你可以使用许多情节提要板, 但是在那种情况下, 唯一的好处是可以使情节提要板上的控制器之间保持顺序。
  2. 情节提要中的控制器和segue名称是字符串, 因此你必须在整个代码中重新输入所有这些字符串(有一天你将破坏它), 或维护大量的情节提要常量。你可以使用SBConstants, 但在情节提要上重命名仍然不是一件容易的事。
  3. 情节提要会迫使你进入非模块化设计。在使用情节提要时, 几乎没有动机使你的视图可重用。对于最低限度可行的产品(MVP)或快速的UI原型制作, 这可能是可以接受的, 但是在实际应用程序中, 你可能需要在整个应用程序中多次使用同一视图。
故事板(可辩论)的优点:
  1. 整个应用程序导航一目了然。但是, 实际的应用程序可以有十个以上的控制器, 它们以不同的方向连接。具有这种连接的情节提要看起来像一团团纱, 对数据流没有任何高级了解。
  2. 静态表。这是我能想到的唯一真正的优势。问题在于90%的静态表在应用程序开发过程中趋于转变为动态表, 并且XIB可以更轻松地处理动态表。
常见错误7:混淆的对象和指针比较在比较两个对象时, 我们可以考虑两个相等:指针和对象相等。
当两个指针都指向同一对象时, 指针相等。在Objective-C中, 我们使用==运算符比较两个指针。当两个对象代表两个逻辑上相同的对象时, 例如数据库中的同一用户, 则对象相等。在Objective-C中, 我们使用isEqual或更好的类型特定的isEqualToString, isEqualToDate等运算符来比较两个对象。
考虑以下代码:
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等于b, 因为对象a和b都指向内存中的同一对象。
但是现在让我们将第2行更改为:
NSString *b = [[@"a" mutableCopy] copy];

现在我们得到a不等于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语句, 则代码将编译, 但是将无法按预期工作。但是, 如果开发人员从一开始就使用枚举值而不是default关键字, 则将在编译时识别监督, 并且可以在进行测试或生产之前将其修复。这是在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是一个功能强大且发展迅速的平台。苹果一直在不遗余力地为iOS本身引入新的硬件和功能, 同时还在不断扩展Swift语言。
提高Objective-C和Swift技能将使你成为一名出色的iOS开发人员, 并提供使用尖端技术从事具有挑战性的项目的机会。
【iOS开发人员不知道的10个最常见错误】相关:iOS开发人员指南:从Objective-C到Learn Swift

    推荐阅读