个性化阅读
专注于IT技术分析

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

本文概述

唯一的问题是越野车被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]。

了解了引擎盖下发生的事情后, 可以得出两个结论:

  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开发人员指南:从Objective-C到Learn Swift

赞(0)
未经允许不得转载:srcmini » iOS开发人员不知道的10个最常见错误

评论 抢沙发

评论前必须登录!