欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

《禅与Objective-C编程艺术》读书笔记(二)

程序员文章站 2022-03-26 19:34:24
五、categories 我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)zoc_mycategorymethod。 这种实践同样被苹果推荐。这是非常必要的...

五、categories

我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)zoc_mycategorymethod。 这种实践同样被苹果推荐。这是非常必要的。因为如果在扩展的 category 或者其他 category 里面已经使用了同样的方法名,会导致不可预计的后果。实际上,实际被调用的是最后被实现的那个方法。如果想要确认你的分类方法没有覆盖其他实现的话,可以把环境变量 objc_print_replaced_methods 设置为 yes,这样那些被取代的方法名字会打印到 console 中。现在 llvm 5.1 不会为此发出任何警告和错误提示,所以自己小心不要在分类中重载方法。一个好的实践是在 category 名中使用前缀。

* 例子 *

@interface nsdate (zoctimeextensions)
- (nsstring *)zoc_timeagoshort;
@end

* 不要这样做 *

@interface nsdate (zoctimeextensions)
- (nsstring *)timeagoshort;
@end

六、protocols

在 objective-c 的世界里面经常错过的一个东西是抽象接口。接口(interface)这个词通常指一个类的 .h 文件,但是它在 java 程序员眼里有另外的含义: 一系列不依赖具体实现的方法的定义。在 objective-c 里是通过 protocol 来实现抽象接口的。我们会解释 protocol 的强大力量(用作抽象接口),用具体的例子来解释:把非常糟糕的设计的架构改造为一个良好的可复用的代码。这个例子是在实现一个 rss 订阅的器(它可是经常在技术面试中作为一个测试题呢)。
要求很简单明了:把一个远程的 rss 订阅展示在一个 tableview 中。
最小的步骤是遵从单一功能原则,创建至少两个组成部分来完成这个任务:
(a)一个 feed 解析器来解析搜集到的结果
(b)一个 feed 阅读器来显示结果
这些类的接口可以是这样的:

@interface zocfeedparser : nsobject

@property (nonatomic, weak) id  delegate;
@property (nonatomic, strong) nsurl *url;

- (id)initwithurl:(nsurl *)url;

- (bool)start;
- (void)stop;

@end

@interface zoctableviewcontroller : uitableviewcontroller

- (instancetype)initwithfeedparser:(zocfeedparser *)feedparser;

@end

zocfeedparser 用一个 nsurl 来初始化来获取 rss 订阅(在这之下可能会使用 nsxmlparser 和 nsxmlparserdelegate 创建有意义的数据),zoctableviewcontroller 会用这个 parser 来进行初始化。 我们希望它显示 parser 接受到的指并且我们用下面的 protocol 实现委托:

@protocol zocfeedparserdelegate 
@optional
- (void)feedparserdidstart:(zocfeedparser *)parser;
- (void)feedparser:(zocfeedparser *)parser didparsefeedinfo:(zocfeedinfodto *)info;
- (void)feedparser:(zocfeedparser *)parser didparsefeeditem:(zocfeeditemdto *)item;
- (void)feedparserdidfinish:(zocfeedparser *)parser;
- (void)feedparser:(zocfeedparser *)parser didfailwitherror:(nserror *)error;
@end

用合适的 protocol 来来处理 rss 非常完美。view controller 会遵从它的公开的接口:

@interface zoctableviewcontroller : uitableviewcontroller 

最后创建的代码是这样子的:

nsurl *feedurl = [nsurl urlwithstring:@"http://bbc.co.uk/feed.rss"];

zocfeedparser *feedparser = [[zocfeedparser alloc] initwithurl:feedurl];

zoctableviewcontroller *tableviewcontroller = [[zoctableviewcontroller alloc] initwithfeedparser:feedparser];
feedparser.delegate = tableviewcontroller;

七、nsnotification

当你定义你自己的 nsnotification 的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。你应该在公开的接口文件中将其声明为 extern 的, 并且在对应的实现文件里面定义。因为你在头文件中暴露了符号,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。同时,用一个 did/will 这样的动词以及用 “notifications” 后缀来命名这个通知也是一个好的实践。

// foo.h
extern nsstring * const zocfoodidbecomebarnotification

// foo.m
nsstring * const zocfoodidbecomebarnotification = @"zocfoodidbecomebarnotification";

八、美化代码

1.空格
(a)缩进使用 4 个空格。 永远不要使用 tab, 确保你在 xcode 的设置里面是这样设置的。
(b)方法的大括号和其他的大括号(if/else/switch/while 等) 总是在同一行开始,在新起一行结束。
推荐:

if (user.ishappy) {
    //do something
}
else {
    //do something else
}

//不推荐:

if (user.ishappy)
{
  //do something
} else {
  //do something else
}

(c)方法之间应该要有一个空行来帮助代码看起来清晰且有组织。 方法内的空格应该用来分离功能,但是通常不同的功能应该用新的方法来定义。 优先使用 auto-synthesis。但是如果必要的话, @synthesize and @dynamic。
(d)在实现文件中的声明应该新起一行。
(e)应该总是让冒号对其。有一些方法签名可能超过三个冒号,用冒号对齐可以让代码更具有可读性。总是用冒号对其方法,即使有代码块存在。

推荐:

[uiview animatewithduration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(bool finished) {
                     // something
                 }];

不推荐:

[uiview animatewithduration:1.0 animations:^{
    // something 
} completion:^(bool finished) {
    // something
}];

如果自动对齐让可读性变得糟糕,那么应该在之前把 block 定义为变量,或者重新考虑你的代码签名设计。

2.line breaks 换行
本指南关注代码显示效果以及在线浏览的可读性,所以换行是一个重要的主题。
举个例子:

self.productsrequest = [[skproductsrequest alloc] initwithproductidentifiers:productidentifiers];

一个像上面的长行的代码在第二行以一个间隔(2个空格)延续

self.productsrequest = [[skproductsrequest alloc] 
  initwithproductidentifiers:productidentifiers];

3.括号
在以下的地方使用 egyptian风格 括号 (译者注:又称 k&r 风格,代码段括号的开始位于一行的末尾,而不是另外起一行的风格。关于为什么叫做 egyptian brackets,可以参考 )
控制语句 (if-else, for, switch)
非 egyptian 括号可以用在:
类的实现(如果存在)
方法的实现

九、代码组织

1.利用代码块
一个 gcc 非常模糊的特性,以及 clang 也有的特性是,代码块如果在闭合的圆括号内的话,会返回最后语句的值

nsurl *url = ({
    nsstring *urlstring = [nsstring stringwithformat:@"%@/%@", baseurlstring, endpoint];
    [nsurl urlwithstring:urlstring];
});

这个特性非常适合组织小块的代码,通常是设置一个类。他给了读者一个重要的入口并且减少相关干扰,能让读者聚焦于关键的变量和函数中。此外,这个方法有一个优点,所有的变量都在代码块中,也就是只在代码块的区域中有效,这意味着可以减少对其他作用域的命名污染。

2.pragma
2.1 pragma mark

#pragma mark - 是一个在类内部组织代码并且帮助你分组方法实现的好办法。 我们建议使用  #pragma mark - 来分离:

(1) 不同功能组的方法
(2) protocols 的实现
(3) 对父类方法的重写

- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

 #pragma mark - view lifecycle (view 的生命周期)

- (void)viewdidload { /* ... */ }
- (void)viewwillappear:(bool)animated { /* ... */ }
- (void)didreceivememorywarning { /* ... */ }

 #pragma mark - custom accessors (自定义访问器)

- (void)setcustomproperty:(id)value { /* ... */ }
- (id)customproperty { /* ... */ }

 #pragma mark - ibactions  

- (ibaction)submitdata:(id)sender { /* ... */ }

 #pragma mark - public 

- (void)publicmethod { /* ... */ }

 #pragma mark - private

- (void)zoc_privatemethod { /* ... */ }

 #pragma mark - uitableviewdatasource

- (uitableviewcell *)tableview:(uitableview *)tableview cellforrowatindexpath:(nsindexpath *)indexpath { /* ... */ }

 #pragma mark - zocsuperclass

// ... 重载来自 zocsuperclass 的方法

 #pragma mark - nsobject

- (nsstring *)description { /* ... */ }

上面的标记能明显分离和组织代码。你还可以用 cmd+click 来快速跳转到符号定义地方。 但是小心,即使 paragma mark 是一门手艺,但是它不是让你类里面方法数量增加的一个理由:类里面有太多方法说明类做了太多事情,需要考虑重构了。

2.2 关于pragma
大多数 ios 开发者平时并没有和很多编译器选项打交道。一些选项是对控制严格检查(或者不检查)你的代码或者错误的。有时候,你想要用 pragma 直接产生一个异常,临时打断编译器的行为。当你使用arc的时候,编译器帮你插入了内存管理相关的调用。但是这样可能产生一些烦人的事情。比如你使用 nsselectorfromstring 来动态地产生一个 selector 调用的时候,arc不知道这个方法是哪个并且不知道应该用那种内存管理方法,你会被提示 performselector may cause a leak because its selector is unknown(执行 selector 可能导致泄漏,因为这个 selector 是未知的).如果你知道你的代码不会导致内存泄露,你可以通过加入这些代码忽略这些警告

 #pragma clang diagnostic push

 #pragma clang diagnostic ignored "-warc-performselector-leaks"

[myobj performselector:myselector withobject:name];

 #pragma clang diagnostic pop

注意我们是如何在相关代码上下文中用 pragma 停用 -warc-performselector-leaks 检查的。这确保我们没有全局禁用。如果全局禁用,可能会导致错误。

3.忽略没用使用变量的编译警告
这对表明你一个定义但是没有使用的变量很有用。大多数情况下,你希望移除这些引用来(稍微地)提高性能,但是有时候你希望保留它们。为什么?或许它们以后有用,或者有些特性只是暂时移除。无论如何,一个消除这些警告的好方法是用相关语句进行注解,使用 #pragma unused():

- (void)givemefive
{
    nsstring *foo;
    #pragma unused (foo)

    return 5;
}

现在你的代码不用任何编译警告了。注意你的 pragma 需要标记到未定义的变量之下。

4.明确编译器警告和错误
编译器是一个机器人,它会标记你代码中被 clang 规则定义为错误的地方。但是,你总是比 clang 更聪明。通常,你会发现一些讨厌的代码 会导致这个问题,而且不论怎么做,你都解决不了。你可以这样明确一个错误:

- (nsinteger)pide:(nsinteger)pidend by:(nsinteger)pisor
{
    #error whoa, buddy, you need to check for zero here!
    return (pidend / pisor);
}

类似的,你可以这样标明一个 警告

- (float)pide:(float)pidend by:(float)pisor
{
    #warning dude, don't compare floating point numbers like this!
    if (pisor != 0.0) {
        return (pidend / pisor);
    }
    else {
        return nan;
    }
}

5.字符串文档
所有重要的方法,接口,分类以及协议定义应该有伴随的注释来解释它们的用途以及如何使用。简而言之:有长的和短的两种字符串文档。
短文档适用于单行的文件,包括注释斜杠。它适合简短的函数,特别是(但不仅仅是)非 public 的 api:

// return a user-readable form of a frobnozz, html-escaped.

文本应该用一个动词 (“return”) 而不是 “returns” 这样的描述。

如果描述超出一行,你应该用长的字符串文档: 一行斜杠和两个星号来开始块文档 (/*, 之后是总结的一句话,可以用句号、问号或者感叹号结尾,然后空一行,在和第一句话对齐写下剩下的注释,然后用一个 (/)来结束。

/**
 this comment serves to demonstrate the format of a docstring.

 note that the summary line is always at most one line long, and
 after the opening block comment, and each line of text is preceded
 by a single space.
*/

一个函数必须有一个字符串文档,除非它符合下面的所有条件:
(a) 非公开
(b) 很短
(c) 显而易见
字符串文档应该描述函数的调用符号和语义,而不是它如何实现。

6.注释
当它需要的时候,注释应该用来解释特定的代码做了什么。所有的注释必须被持续维护或者干脆就删除。块注释应该被避免,代码本身应该尽可能就像文档一样表示意图,只需要很少的打断注释 例外: 这不能适用于用来产生文档的注释

7.头文档
一个类的文档应该只在 .h 文件里用 doxygen/appledoc 的语法书写。 方法和属性都应该提供文档。

例子:

/**
 *  designated initializer.
 *
 *  @param  store  the store for crud operations.
 *  @param  searchservice the search service used to query the store.
 *
 *  @return a zoccrudoperationsstore object.
 */
- (instancetype)initwithoperationsstore:(id)store
                          searchservice:(id)searchservice;

十、对象间的通讯

1.blocks
blocks 是 objective-c 版本的 lambda 或者 closure(闭包)。使用 block 定义异步接口:

- (void)downloadobjectsatpath:(nsstring *)path
                   completion:(void(^)(nsarray *objects, nserror *error))completion;

当你定义一个类似上面的接口的时候,尽量使用一个单独的 block 作为接口的最后一个参数。把需要提供的数据和错误信息整合到一个单独 block 中,比分别提供成功和失败的 block 要好。以下是你应该这样做的原因:
(a) 通常这成功处理和失败处理会共享一些代码(比如让一个进度条或者提示消失);
(b) apple 也是这样做的,与平台一致能够带来一些潜在的好处;
(c) block 通常会有多行代码,如果不是在最后一个参数的话会打破调用点;
(d) 使用多个 block 作为参数可能会让调用看起来显得很笨拙,并且增加了复杂性。

看上面的方法,完成处理的 block 的参数很常见:第一个参数是调用者希望获取的数据,第二个是错误相关的信息。这里需要遵循以下两点:
若 objects 不为 nil,则 error 必须为 nil
若 objects 为 nil,则 error 必须不为 nil

因为调用者更关心的是实际的数据,就像这样:

- (void)downloadobjectsatpath:(nsstring *)path
                   completion:(void(^)(nsarray *objects, nserror *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

此外,apple 提供的一些同步接口在成功状态下向 error 参数(如果非 null) 写入了垃圾值,所以检查 error 的值可能出现问题。

2.深入 blocks
一些关键点:
(1) block 是在栈上创建的
(2) block 可以复制到堆上
(3) block 有自己的私有的栈变量(以及指针)的常量复制
(4) 可变的栈上的变量和指针必须用 __block 关键字声明
如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。当在栈上的时候,一个 block 对访问的任何内容不会有影响。如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 cocoa 对象一样增加引用计数。当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。如果一个 block指向一个栈变量或者指针,那么这个block初始化的时候它会有一份声明为 const 的副本,所以对它们赋值是没用的。当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制之后栈上的以及产生的堆上的 block 都会引用这个堆上的变量。
最重要的事情是 __block 声明的变量和指针在 block 里面是作为显示操作真实值/对象的结构来对待的。block 在 objective-c 里面被当作一等公民对待:他们有一个 isa 指针,一个类也是用 isa 指针来访问 objective-c 运行时来访问方法和存储数据的。在非 arc 环境肯定会把它搞得很糟糕,并且悬挂指针会导致 crash。__block 仅仅对 block 内的变量起作用,它只是简单地告诉 block:嗨,这个指针或者原始的类型依赖它们在的栈。请用一个栈上的新变量来引用它。我是说,请对它进行双重解引用,不要 retain 它。 谢谢,哥们。如果在定义之后但是 block 没有被调用前,对象被释放了,那么 block 的执行会导致 crash。 __block 变量不会在 block 中被持有,最后… 指针、引用、解引用以及引用计数变得一团糟。

3.self 的循环引用
当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 引用会导致引用循环。 此外,把持有 blocks 的属性设置为 nil (比如 self.completionblock = nil) 是一个好的实践。它会打破 blocks 捕获的作用域带来的引用循环。

例子:

__weak __typeof(self) weakself = self;
[self executeblock:^(nsdata *data, nserror *error) {
    [weakself dosomethingwithdata:data];
}];

不要这样做:

[self executeblock:^(nsdata *data, nserror *error) {
    [self dosomethingwithdata:data];
}];

多个语句的例子:

__weak __typeof(self)weakself = self;
[self executeblock:^(nsdata *data, nserror *error) {
    __strong __typeof(weakself) strongself = weakself;
    if (strongself) {
        [strongself dosomethingwithdata:data];
        [strongself dosomethingwithdata:data];
    }
}];

不要这样做:

__weak __typeof(self)weakself = self;
[self executeblock:^(nsdata *data, nserror *error) {
    [weakself dosomethingwithdata:data];
    [weakself dosomethingwithdata:data];
}];

你应该把这两行代码作为 snippet 加到 xcode 里面并且总是这样使用它们。

__weak __typeof(self)weakself = self;
__strong __typeof(weakself)strongself = weakself;

这里我们来讨论下 block 里面的 self 的 __weak 和 __strong 限定词的一些微妙的地方。简而言之,我们可以参考 self 在 block 里面的三种不同情况。
(1)直接在 block 里面使用关键词 self
如果我们直接在 block 里面用 self 关键字,对象会在 block 的定义时候被 retain,(实际上 block 是 copied 但是为了简单我们可以忽略这个)。一个 const 的对 self 的引用在 block 里面有自己的位置并且它会影响对象的引用计数。如果 block 被其他 class 或者/并且传送过去了,我们可能想要 retain self 就像其他被 block 使用的对象,从他们需要被block执行

dispatch_block_t completionblock = ^{
    nslog(@"%@", self);
}

myviewcontroller *mycontroller = [[myviewcontroller alloc] init...];
[self presentviewcontroller:mycontroller
                   animated:yes
                 completion:completionhandler];

不是很麻烦的事情。但是, 当 block 被 self 在一个属性 retain(就像下面的例子)呢

self.completionhandler = ^{
    nslog(@"%@", self);
}

myviewcontroller *mycontroller = [[myviewcontroller alloc] init...];
[self presentviewcontroller:mycontroller
                animated:yes
                       completion:self.completionhandler];

这就是有名的 retain cycle, 并且我们通常应该避免它。这种情况下我们收到 clang 的警告:

capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 里面发现了 `self` 的强引用,可能会导致循环引用)

所以可以用 weak 修饰

(2)在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用
这样会避免循环引用,也是我们通常在 block 已经被 self 的 property 属性里面 retain 的时候会做的。

__weak typeof(self) weakself = self;
self.completionhandler = ^{
    nslog(@"%@", weakself);
};

myviewcontroller *mycontroller = [[myviewcontroller alloc] init...];
[self presentviewcontroller:mycontroller
                   animated:yes
                 completion:self.completionhandler];

这个情况下 block 没有 retain 对象并且对象在属性里面 retain 了 block 。所以这样我们能保证了安全的访问 self。 不过糟糕的是,它可能被设置成 nil 的。问题是:如果和让 self 在 block 里面安全地被销毁。举个例子, block 被一个对象复制到了另外一个(比如 mycontroler)作为属性赋值的结果。之前的对象在可能在被复制的 block 有机会执行被销毁。

(3)在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用
你可能会想,首先,这是避免 retain cycle 警告的一个技巧。然而不是,这个到 self 的强引用在 block 的执行时间 被创建。当 block 在定义的时候, block 如果使用 self 的时候,就会 retain 了 self 对象。apple 文档 中表示 “为了 non-trivial cycles ,你应该这样” :

myviewcontroller *mycontroller = [[myviewcontroller alloc] init...];
// ...
myviewcontroller * __weak weakmycontroller = mycontroller;
mycontroller.completionhandler =  ^(nsinteger result) {
    myviewcontroller *strongmycontroller = weakmycontroller;
    if (strongmycontroller) {
        // ...
        [strongmycontroller dismissviewcontrolleranimated:yes completion:nil];
        // ...
    }
    else {
        // probably nothing...
    }
};

首先,我觉得这个例子看起来是错误的。如果 block 本身被 completionhandler 属性里面 retain 了,那么 self 如何被 delloc 和在 block 之外赋值为 nil 呢? completionhandler 属性可以被声明为 assign 或者 unsafe_unretained 的,来允许对象在 block 被传递之后被销毁。
我不能理解这样做的理由,如果其他对象需要这个对象(self),block 被传递的时候应该 retain 对象,所以 block 应该不被作为属性存储。这种情况下不应该用 __weak/__strong。
总之,其他情况下,希望 weakself 变成 nil 的话,就像第二种情况解释那么写(在 block 之外定义一个弱应用并且在 block 里面使用)。还有,apple的 “trivial block” 是什么呢。我们的理解是 trivial block 是一个不被传送的 block ,它在一个良好定义和控制的作用域里面,weak 修饰只是为了避免循环引用。
在 block 内用强引用的优点是,抢占执行的时候的鲁棒性。看上面的三个例子,在 block 执行的时候
(a) 直接在 block 里面使用关键词 self
如果 block 被属性 retain,self 和 block 之间会有一个循环引用并且它们不会再被释放。如果 block 被传送并且被其他的对象 copy 了,self 在每一个 copy 里面被 retain
(b) 在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用
没有循环引用的时候,block 是否被 retain 或者是一个属性都没关系。如果 block 被传递或者 copy 了,在执行的时候,weakself 可能会变成 nil。block 的执行可以抢占,并且后来的对 weakself 的不同调用可以导致不同的值(比如,在 一个特定的执行 weakself 可能赋值为 nil )

__weak typeof(self) weakself = self;
dispatch_block_t block =  ^{
    [weakself dosomething]; // weakself != nil
    // preemption, weakself turned nil
    [weakself dosomethingelse]; // weakself == nil
};

(c) 在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用。
不论管 block 是否被 retain 或者是一个属性,这样也不会有循环引用。如果 block 被传递到其他对象并且被复制了,执行的时候,weakself 可能被nil,因为强引用被复制并且不会变成nil的时候,我们确保对象 在 block 调用的完整周期里面被 retain了,如果抢占发生了,随后的对 strongself 的执行会继续并且会产生一样的值。如果 strongself 的执行到 nil,那么在 block 不能正确执行前已经返回了。

__weak typeof(self) weakself = self;
myobj.myblock =  ^{
    __strong typeof(self) strongself = weakself;
    if (strongself) {
      [strongself dosomething]; // strongself != nil
      // preemption, strongself still not nil(抢占的时候,strongself 还是非 nil 的)
      [strongself dosomethingelse]; // strongself != nil
    }
    else {
        // probably nothing...
        return;
    }
};

在一个 arc 的环境中,如果尝试用 ->符号来表示,编译器会警告一个错误: