作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nikita Tuk's profile image

Nikita Tuk

Nikita是一名iOS开发者/顾问,使用Objective-C和Swift.

Previously At

Facebook
Share

还有什么比一个漏洞百出的应用被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个错误,以及如何避免这些错误.

85%的iOS用户使用最新的操作系统版本. 这意味着他们希望你的应用或更新是完美无缺的.

Common Mistake No. 1:不理解异步进程

新手程序员常犯的一个错误是错误地处理异步代码. 让我们考虑一个典型的场景:用户打开一个带有表视图的屏幕. 从服务器获取一些数据并显示在表视图中. 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];
});

Common Mistake No. 3:误解并发和多线程

并发性可以比作一把非常锋利的刀:如果你不小心或经验不足,你很容易割伤自己, 但一旦你知道如何正确安全地使用它,它就会非常有用和高效.

You can try to avoid using concurrency, 但不管你在开发什么样的应用, 你很有可能离不开它. 并发性可以为应用程序带来显著的好处. Notably:

  • 几乎每个应用程序都有对web服务的调用(例如, 执行一些繁重的计算或从数据库读取数据). 如果这些任务是在主队列上执行的, the application will freeze for some time, making it non-responsive. 此外,如果耗时太长,iOS将完全关闭应用程序. 将这些任务移到另一个队列允许用户在执行操作时继续使用应用程序,而不会出现应用程序冻结的情况.
  • 现代iOS设备拥有不止一个核心, 那么,当任务可以并行执行时,用户为什么要等待任务顺序完成呢?

但是,没有复杂性和引入严重错误的可能性,并发性的优势就不会出现, 比如很难复制的竞态条件.

让我们考虑一些现实世界的例子(注意,为了简单起见,省略了一些代码).

Case 1

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))
    }
}

Case 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])
    }
    
    修改函数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, 你可以提高应用程序的速度和响应能力.

Common Mistake No. 4:不知道可变对象的缺陷

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];
}

Common Mistake No. 5: Not Understanding How iOS 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].

了解了幕后发生的事情,可以得出两个结论:

  1. 如果键的哈希值发生变化,则应该将该记录移动到另一个链表中.
  2. Keys should be unique.

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. 这种做法也会帮助你保持课堂一致性.

Common Mistake No. 6: Using Storyboards Instead of XIBs

大多数新iOS开发者都遵循苹果的建议,使用故事板 default for the UI. 然而,使用故事板有很多缺点,只有一些(有争议的)优点.

Storyboard drawbacks include:

  1. 为几个团队成员修改故事板真的很难. Technically, you can use many storyboards, but the only advantage, in that case, 能否在storyboard中实现控制器间的segue.
  2. 故事板中的控制器和segue名称是字符串, 因此,您必须在代码中重新输入所有这些字符串(并且有一天您 will 打破它),或者维护一个巨大的故事板常量列表. You could use SBConstants,但在故事板上重命名仍然不是一件容易的事.
  3. 故事板迫使你进入一个非模块化的设计. 在使用故事板时,很少有动机使您的视图可重用. 这对于最小可行产品(MVP)或快速UI原型来说是可以接受的, 但在实际应用中,你可能需要在整个应用中多次使用同一个视图.

Storyboard (debatable) advantages:

  1. 整个app导航一目了然. 然而,实际的应用程序可以有十个以上的控制器,连接在不同的方向上. 具有这种连接的故事板看起来像一团纱线,并且不能提供对数据流的任何高级理解.
  2. Static tables. 这是我能想到的唯一真正的优势. 问题在于,90%的静态表在应用程序开发过程中会转变为动态表,而动态表可以更容易地由xib处理.

Common Mistake No. 7:混淆对象和指针比较

在比较两个对象时,可以考虑两个相等性:指针和对象相等性.

指针相等是指两个指针都指向同一个对象的情况. 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]) { 

Common Mistake No. 8: Using Hardcoded Values

硬编码值有两个主要问题:

  1. It’s often not clear what they represent.
  2. 当需要在代码中的多个位置使用它们时,需要重新输入(或复制和粘贴)它们.

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 

Common Mistake No. 9:在Switch语句中使用Default关键字

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, 我们添加另一种用户类型会发生什么, “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
    }
}

Common Mistake No. 10: Using NSLog for Logging

Many 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.

Wrap Up

iOS是一个非常强大且发展迅速的平台. 苹果一直在努力为iOS系统引入新的硬件和功能, 同时也在不断扩展Swift语言.

提高你的Objective-C和Swift技能会让你成为一个伟大的 iOS developer 并提供使用尖端技术从事具有挑战性项目的机会.

Hire a Toptal expert on this topic.
Hire Now
Nikita Tuk's profile image
Nikita Tuk

Located in San Francisco, CA, United States

Member since September 29, 2015

About the author

Nikita是一名iOS开发者/顾问,使用Objective-C和Swift.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Facebook

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Toptal Developers

Join the Toptal® community.