作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nikita是一名iOS开发者/顾问,使用Objective-C和Swift.
还有什么比一个漏洞百出的应用被app Store拒绝更糟糕的呢? Having it accepted. 一旦一星评论开始涌入,几乎不可能恢复. 这让公司付出了金钱,也让开发者丢掉了工作.
iOS现在是全球第二大移动操作系统. 它的采用率也非常高,超过 85% of users on the latest version. As you might expect, 如果你的应用或更新并非完美无瑕,那么高参与度用户的期望值也会很高, you’ll hear about it.
随着市场对iOS开发者的需求持续飙升, 许多工程师已经转向移动开发(超过1,每天有5000个新应用提交给苹果。. 但真正的iOS专业知识远不止于基本的编码. 以下是iOS开发者常犯的10个错误,以及如何避免这些错误.
新手程序员常犯的一个错误是错误地处理异步代码. 让我们考虑一个典型的场景:用户打开一个带有表视图的屏幕. 从服务器获取一些数据并显示在表视图中. We can write it more formally:
@property (nonatomic, strong) NSArray *dataFromServer;
- (void)viewDidLoad {
__weak __typeof(self) weakSelf = self;
[[ApiManager共享]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. 然而,问题是获取数据是一个 asynchronous 处理,不会立即返回新数据,这意味着 reloadData
在收到新数据之前会被调用吗. 为了修正这个错误,我们应该将第2行移到块内第1行之后.
@property (nonatomic, strong) NSArray *dataFromServer;
- (void)viewDidLoad {
__weak __typeof(self) weakSelf = self;
[[ApiManager共享]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;
}
However, 可能在某些情况下,代码的行为仍然不符合预期, which brings us to …
让我们假设我们使用了前面常见错误的更正代码示例, 但是,即使在异步进程成功完成之后,我们的表视图仍然没有使用新数据进行更新. What might be wrong with such simple code? To understand it, 我们可以在块中设置一个断点,并找出调用该块的队列. 描述的行为很有可能发生,因为我们的调用不在主队列中, where all UI-related code should be performed.
大多数流行的库(如Alamofire、AFNetworking和haneke)都被设计为调用 completionBlock
在执行异步任务后的主队列上. However, 您不能总是依赖于此,而且很容易忘记将代码分派到正确的队列.
确保所有与ui相关的代码都在主队列上, 别忘了把它分派到那个队列:
设置(dispatch_get_main_queue()、^ {
[self.tableView reloadData];
});
并发性可以比作一把非常锋利的刀:如果你不小心或经验不足,你很容易割伤自己, 但一旦你知道如何正确安全地使用它,它就会非常有用和高效.
You can try to avoid using concurrency, 但不管你在开发什么样的应用, 你很有可能离不开它. 并发性可以为应用程序带来显著的好处. Notably:
但是,没有复杂性和引入严重错误的可能性,并发性的优势就不会出现, 比如很难复制的竞态条件.
让我们考虑一些现实世界的例子(注意,为了简单起见,省略了一些代码).
final class SpinLock {
private var lock = OS_SPINLOCK_INIT
func withLock(@noescape body: () -> Return) -> Return {
OSSpinLockLock(&lock)
defer { OSSpinLockUnlock(&lock) }
return body()
}
}
class ThreadSafeVar {
private let lock: ReadWriteLock
private var _value: Value
var value: Value {
get {
return lock.withReadLock {
return _value
}
}
set {
lock.withWriteLock {
_value = newValue
}
}
}
}
The multithreaded code:
let counter = ThreadSafeVar(value: 0)
//该代码可以从多个线程调用
counter.value += 1
if (counter.value == someValue) {
// do something
}
乍一看,一切都是同步的,似乎应该按预期工作,因为 ThreadSaveVar
wraps counter
and makes it thread safe. 不幸的是,这不是真的,因为两个线程可能同时到达增量行 counter.value == someValue
will never become true as a result. As a workaround, we can make 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])
}
修改函数appendItems(items: [DataType]) {
dispatch_barrier_sync (synchronizationQueue) {
self._data += items
}
}
}
In this case, dispatch_barrier_sync
was used to sync access to the array. 这是确保访问同步的常用模式. Unfortunately, 这段代码没有考虑到每次向结构体追加一项时都会进行复制, 这样每次都有一个新的同步队列.
在这里,即使乍一看是正确的,它也可能不像预期的那样起作用. 它还需要大量的工作来测试和调试它, but in the end, 你可以提高应用程序的速度和响应能力.
Swift在避免值类型错误方面很有帮助, 但仍然有很多开发人员使用Objective-C. 可变对象非常危险,可能导致隐藏的问题. 不可变对象应该从函数返回,这是一条众所周知的规则, but most developers don’t know why. Let’s consider the following code:
// Box.h
@interface Box: NSObject
@property (nonatomic, readonly, strong) NSArray *boxes;
@end
// Box.m
@interface Box()
@property (nonatomic, strong) NSMutableArray *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
The code above is correct, because NSMutableArray
is a subclass of NSArray
. So what can go wrong with this code?
首先也是最明显的一件事是,另一个开发者可能会出现并做以下事情:
NSArray *childBoxes = [box boxes];
if ([childBoxes isKindOfClass:[NSMutableArray class]]) {
// add more boxes to childBoxes
}
This code will mess up your class. 但在这种情况下,这是一种代码气味,留给开发人员去收拾残局.
不过,下面这种情况要糟糕得多,而且表现出了一种意想不到的行为:
Box *box = [[Box alloc] init];
NSArray *childBoxes = [box boxes];
[box addBox:[[Box alloc] init]];
NSArray *newChildBoxes = [box boxes];
The expectation here is that [newChildBoxes count] > [childBoxes count]
, but what if it is not? 那么这个类的设计就不是很好,因为它会改变已经返回的值. 如果你认为不平等不应该是真的,试着用UIView和 [view subviews]
.
幸运的是,我们可以很容易地修复我们的代码,通过重写第一个例子中的getter:
- (NSArray *)boxes {
return [self.m_boxes copy];
}
NSDictionary
Works Internally如果你用过自定义类 NSDictionary
,您可能会意识到,如果您的类不符合 NSCopying
as a dictionary key. 大多数开发者从来没有问过自己,为什么苹果会添加这样的限制. 为什么苹果要复制密钥并使用该副本而不是原始对象?
理解这一点的关键是找出原因 NSDictionary
works internally. Technically, it’s just a hash table. 让我们快速回顾一下它在为键添加对象时是如何在高层次上工作的(为了简单起见,这里省略了表大小调整和性能优化):
Step 1: It calculates
hash(Key)
. 步骤2:根据哈希值寻找放置对象的位置. 通常,这是通过对哈希值与字典长度取模数来实现的. 然后使用生成的索引来存储键/值对. 步骤3:如果该位置没有物体, 它创建一个链表并存储我们的记录(对象和键). 否则,它将记录追加到列表的末尾.
现在,让我们描述一下如何从字典中获取记录:
Step 1: It calculates
hash(Key)
. Step 2: It searches a Key by hash. If there is no data,nil
is returned. 步骤3:如果有链表,它遍历对象直到[storedkey isEqual:Key]
.
了解了幕后发生的事情,可以得出两个结论:
Let’s examine this on a simple class:
@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
Now imagine NSDictionary
doesn’t copy keys:
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init];
Person *p = [[Person alloc] init];
p.name = @"Job Snow";
gotCharactersRating[p] = @10;
Oh! We have a typo there! Let’s fix it!
p.name = @"Jon Snow";
What should happen with our dictionary? 由于名称发生了变化,我们现在有了一个不同的散列. 现在我们的对象位于错误的位置(它仍然有旧的哈希值), 因为字典不知道数据的变化), 我们应该用什么哈希来查找字典里的数据还不是很清楚. There could be an even worse case. 想象一下,如果我们的字典里已经有了评分为5分的“琼恩·雪诺”. 对于同一个Key,字典最终会得到两个不同的值.
正如您所看到的,使用可变键可能会产生许多问题 NSDictionary
. 避免此类问题的最佳实践是在存储对象之前复制对象, and to mark properties as copy
. 这种做法也会帮助你保持课堂一致性.
大多数新iOS开发者都遵循苹果的建议,使用故事板 default for the UI. 然而,使用故事板有很多缺点,只有一些(有争议的)优点.
Storyboard drawbacks include:
Storyboard (debatable) advantages:
在比较两个对象时,可以考虑两个相等性:指针和对象相等性.
指针相等是指两个指针都指向同一个对象的情况. In Objective-C, we use the ==
operator for comparing two pointers. 对象相等是指两个对象表示两个逻辑上相同的对象, like the same user from a database. In Objective-C, we use isEqual
, or even better, type specific isEqualToString
, isEqualToDate
, etc. operators for comparing two objects.
Consider the following code:
NSString *a = @"a"; // 1
NSString *b = @"a"; // 2
如果(a == b) {// 3
NSLog(@"%@ is equal to %@", a, b);
} else {
NSLog(@"%@ is NOT equal to %@", a, b);
}
当我们运行这段代码时,控制台将打印出什么? We will get a is equal to b
, as both objects a
and b
are pointing to the same object in memory.
But now let’s change line 2 to:
NSString *b = [[@"a" mutableCopy] copy];
Now we get a is NOT equal to b
因为这些指针现在指向不同的对象,即使这些对象有相同的值.
This problem can be avoided by relying on isEqual
, or type specific functions. 在我们的代码示例中,我们应该用以下代码替换第3行,以使其始终正常工作:
if ([a isEqual:b]) {
硬编码值有两个主要问题:
Consider the following example:
if ([[NSDate日期]timeIntervalSinceDate:self.lastAppLaunch] < 172800) {
// do something
}
or
[self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"];
...
[self.tableView dequeueReusableCellWithIdentifier: @“SimpleCell”);
What does 172800 represent? Why is it being used? 这与2天中的秒数(24 x 60 x 60)相对应,这可能并不明显, or 86,400, seconds in a day).
定义一个值,而不是使用硬编码的值 #define
statement. For example:
#define SECONDS_PER_DAY 86400
#define SIMPLE_CELL_IDENTIFIER @"SimpleCell"
#define
预处理器宏是否用代码中的值替换命名定义. So, if you have #define
in a header file and import it somewhere, 该文件中所有出现的定义值也将被替换.
This works well, except for one issue. 为了说明剩下的问题,考虑下面的代码:
#define X = 3
...
CGFloat y = X / 2;
What would you expect the value of y
to be after this code executes? If you said 1.5, you are incorrect. y
will be equal to 1 (not 1.5) after this code executes. Why? The answer is that #define
has no information about the type. 在我们的例子中,除法是2 Int
values (3 and 2), which results in an Int
(i.e., 1) which is then cast to a Float
.
这可以通过使用常量来避免,根据定义,常量是类型化的:
static const CGFloat X = 3;
...
CGFloat y = X / 2; // y will now equal 1.5, as expected
Using the default
关键字在switch语句中可能导致错误和意外行为. 考虑Objective-C中的以下代码:
typedef NS_ENUM(NSUInteger, UserType) {
UserTypeAdmin,
UserTypeRegular
};
- (BOOL)canEditUserWithType:(UserType) UserType {
switch (userType) {
case UserTypeAdmin:
return YES;
default:
return NO;
}
}
The same code written in Swift:
enum UserType {
case Admin, Regular
}
func canEditUserWithType(type: UserType) -> Bool {
switch(type) {
case .Admin: return true
default: return false
}
}
这段代码按预期工作,只允许管理员用户能够更改其他记录. However, what might happen we add another user type, “manager,,也应该能够编辑记录? If we forget to update this switch
语句,代码可以编译,但不能按预期工作. However, 如果开发人员从一开始就使用枚举值而不是默认关键字, 这种疏忽将在编译时被识别出来, 并且可以在测试或生产之前进行修复. 下面是在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;
}
}
The same code written in 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
}
}
NSLog
for LoggingMany iOS developers use NSLog
在他们的应用程序中进行日志记录,但大多数时候这是一个可怕的错误. 如果我们查看Apple文档 NSLog
function description, we will see it is very simple:
void NSLog(NSString *format, ...);
What could possibly go wrong with it? In fact, nothing. 然而,如果你把你的设备连接到Xcode管理器,你会在那里看到所有的调试信息. For this reason alone, you should never use NSLog
对于日志记录:很容易显示一些不需要的内部数据,而且它看起来不专业.
Better approach is to replace NSLogs
with configurable CocoaLumberjack or some other logging framework.
iOS是一个非常强大且发展迅速的平台. 苹果一直在努力为iOS系统引入新的硬件和功能, 同时也在不断扩展Swift语言.
提高你的Objective-C和Swift技能会让你成为一个伟大的 iOS developer 并提供使用尖端技术从事具有挑战性项目的机会.
Located in San Francisco, CA, United States
Member since September 29, 2015
Nikita是一名iOS开发者/顾问,使用Objective-C和Swift.
World-class articles, delivered weekly.
World-class articles, delivered weekly.
Join the Toptal® community.