本文概述
- 常见错误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_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.value == someValue永远不会成为true。作为一种解决方法, 我们可以使ThreadSafeCounter在递增后返回其值:
class ThreadSafeCounter {
private var value: Int32 = 0
func 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]。
了解了引擎盖下发生的事情后, 可以得出两个结论:
- 如果密钥的哈希值发生变化, 则应将记录移至另一个链接列表。
- 键应该是唯一的。
让我们在一个简单的类上研究一下:
@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使用故事板。但是, 使用情节提要板有很多缺点, 只有几个(可争议的)优点。
故事板的缺点包括:
- 很难为多个团队成员修改情节提要。从技术上讲, 你可以使用许多情节提要板, 但是在那种情况下, 唯一的好处是可以使情节提要板上的控制器之间保持顺序。
- 情节提要中的控制器和segue名称是字符串, 因此你必须在整个代码中重新输入所有这些字符串(有一天你将破坏它), 或维护大量的情节提要常量。你可以使用SBConstants, 但在情节提要上重命名仍然不是一件容易的事。
- 情节提要会迫使你进入非模块化设计。在使用情节提要时, 几乎没有动机使你的视图可重用。对于最低限度可行的产品(MVP)或快速的UI原型制作, 这可能是可以接受的, 但是在实际应用程序中, 你可能需要在整个应用程序中多次使用同一视图。
故事板(可辩论)的优点:
- 整个应用程序导航一目了然。但是, 实际的应用程序可以有十个以上的控制器, 它们以不同的方向连接。具有这种连接的情节提要看起来像一团团纱, 对数据流没有任何高级了解。
- 静态表。这是我能想到的唯一真正的优势。问题在于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:使用硬编码值
硬编码值存在两个主要问题:
- 通常不清楚它们代表什么。
- 需要在代码中的多个位置使用它们时, 需要重新输入(或复制和粘贴)它们。
考虑以下示例:
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开发人员指南:从Objective-C到Learn Swift
评论前必须登录!
注册