0%

序言

用最简单、快捷的方法设置 UINavigationBar 的属性。

去掉返回按钮的文字,只保留返回箭头

默认情况下的样式是这样的,现在只需要保留返回箭头,不需要文字。

方法一

自定义一个 UIBarButtonItem 。比较麻烦,还要给图片。创建 UINavigationController 的子类,重写 pushViewController 方法。

1
2
3
4
5
6
7
8
9
10
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.childViewControllers.count) {
viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"back"] style:UIBarButtonItemStylePlain target:self action:@selector(back)];
}
[super pushViewController:viewController animated:animated];
}

- (void)back {
[self popViewControllerAnimated:YES];
}

方法二

将文字偏移到看不到的地方,刚好系统有给出相关的接口。经过实践发现,偏移到左边屏幕之外是最好的方法。同样创建 UINavigationController 的子类,在 initialize 中实现以下方法。

1
2
3
4
+ (void)initialize {
UIBarButtonItem *item = [UIBarButtonItem appearance];
[item setBackButtonTitlePositionAdjustment:UIOffsetMake(NSIntegerMin, 0) forBarMetrics:UIBarMetricsDefault];
}

使用这个方法同样可以将箭头往右偏移。

设置属性和图片颜色

1
2
3
4
5
6
7
+ (void)initialize {
UIBarButtonItem *item = [UIBarButtonItem appearance];
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[NSForegroundColorAttributeName] = [UIColor whiteColor]; // 文字颜色
[item setTitleTextAttributes:attrs forState:UIControlStateNormal];
item.tintColor = [UIColor whiteColor]; // 图片颜色
}

设置返回按钮图片

1
2
3
4
5
6
+ (void)initialize {
UINavigationBar *bar = [UINavigationBar appearance];
UIImage *image = [UIImage imageNamed:@"back"];
bar.backIndicatorImage = image;
bar.backIndicatorTransitionMaskImage = image;
}

去掉 UINavigationBar 下边线

1
2
3
4
5
+ (void)initialize {
UINavigationBar *bar = [UINavigationBar appearance];
[bar setBackgroundImage:[[UIImage alloc] init] forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
[bar setShadowImage:[[UIImage alloc] init]];
}

给 UINavigationBar 加阴影

这里我创建一个分类,可以直接调用这个方法来设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@implementation UINavigationBar (VCHDropShadow)

- (void)dropShadowWithOffset:(CGSize)offset
radius:(CGFloat)radius
color:(UIColor *)color
opacity:(CGFloat)opacity {
// 也可以不创建 path ,直接使用。
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
self.layer.shadowPath = path;
CGPathCloseSubpath(path);
CGPathRelease(path);

self.layer.shadowColor = color.CGColor;
self.layer.shadowOffset = offset;
self.layer.shadowRadius = radius;
self.layer.shadowOpacity = opacity;

self.clipsToBounds = NO;
}

@end

第五章:内存管理(2)

第31条:在 dealloc 方法中只释放引用并解除监听

对象经历其生命期后,最终会为系统所回收,这时候就会执行 dealloc 方法。也就是引用计数为0时调用,且在生命期内仅调用一次,并且我们也无法控制其什么时候调用。

在这个方法里会释放所有的方法引用,也就是把 Objective-C 对象全部释放。ARC 会生成一个 .cxx_destruct 方法,在 dealloc 中为你自动添加这些释放代码。但也有一些对象是需要自己手动释放。

释放 CoreFoundation 对象

CoreFoundation 对象必须手动释放,因为这个是由纯C生成的。这些对象最好在不需要时就立刻释放掉,没必要等到 dealloc 才释放。

释放 KVO && NSNotificationCenter

如果有 KVO 那么最迟应该在这里将其释放。如果注册了通知也应该最迟在这里移除。不然可能会造成程序崩溃。

释放由对象管理的资源

如果此对象管理者某些资源,那么也要在这里释放掉。

注意

不要在 dealloc 中调用属性的存取方法。
不要在这里调用异步方法,因为对象已经处于回收状态了。
不需要用的资源应该及时释放,系统不能保证每个 dealloc 方法都会执行。

第32条:编写“异常安全代码”时留意内存管理问题

有时候我们需要编写异常代码来捕获并处理异常,发生异常时应该如何管理内存是个值得深究的问题。先看看在MRC环境下应该怎么处理,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@try { 
EOCSomeClass *object = [[EOCSomeClass alloc]init];
[object doSomethingThatMayThrow];
[object release];
} @catch (NSException *exception) {
NSLog(@"there was an error.");
}
~~~
事实上当 doSomethingThatMayThrow 发生异常时,就会直接跳出,不会再往下执行,所以 release 方法无法执行,也就出现内存泄漏了。
使用 @finally 可以解决这个问题

~~~ objc
EOCSomeClass *object = nil;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"there was an error.");
} @finally {
[object release];
}
~~~

在 ARC 环境下,也会出现这样的问题,由于 ARC 不能调用 release 方法。上面的代码同样会出问题

~~~ objc
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
} @catch (NSException *exception) {
NSLog(@"there was an error.");
} @finally {

}

默认情况下 如果 doSomethingThatMayThrow 出现异常了,那么 ARC 也不会自动去处理这个问题。导致 object 这个对象无法回收。虽然默认状况下不能处理这个问题,但ARC依然能生成这种安全处理异常所用的附加代码。**-fobjc-arc-exception** 这个编译器标志用来开启此功能。打开这个标志会加入大量的样例代码,会影响运行期的性能。
处于 Objective-C++ 模式时,编译器会自动把 -fobjc-arc-exception 标志打开,因为C++处理异常所用的代码与ARC实现的附加代码类似,所以令ARC加入自己的代码以安全处理异常,其性能损失并不太大。

这里需要了解的是,Objective-C中,只有当应用程序必须因异常状况而终止时才抛出异常。因此,如果应用程序即将终止,那么是否还会发生内存泄露就已经无关紧要了。在应用程序必须立即终止的情况下,还去添加安全处理异常所用的附加代码是没有意义的。

总结

捕获异常时,一定要注意将try块内所创立的对象清理干净。
在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

第33条:用弱引用避免循环引用

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成”环“。由于 Objective-C 内存管理模型使用引用计数架构,所以这种情况通常会泄露内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得他们都能继续存活下去,而不会为系统所回收。
如下图是最简单的一种内存泄漏,两个对象相互引用,永远无法释放。

弱引用

避免循环引用的最佳方式就是弱引用,即表示“非拥有关系”。有两个关键字可以用来修饰这种方式,分别是 unsafe_unretained 和 weak 。

unsafe_unretained

用 unsafe_unretained 修饰的属性特质,其语义同 assign 特质等价,然而 assign 通常只用于数值类型,unsafe_unretained 则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用。也就是 unsafe_unretained 修饰的属性所指向的对象即使已经释放,unsafe_unretained 修饰的属性的值也不会自动置nil(相对于weak)。

weak

weak 和 unsafe_unretained 同样用于修饰对象,唯一不同的是,当 weak 修饰的属性所指的对象被系统回收时,weak会自动置nil。

下图可以看出两者之间的区别。

当对象释放时,unsafe_unretained 属性仍然指向那个已经回收的实例,而weak属性则指向nil。所以 使用 weak 比 unsafe_unretained 安全。

总结

如果某对象不归你所拥有,而只是需要使用这个对象,那么就应该用“弱引用”。

第五章:内存管理(3)

第34条:以“自动释放池块”降低内存

Objective-C 对象的生命期取决于其引用计数。在 Objective-C 的引用计数架构中,有一项特性叫做“自动释放池”(autorelease pool)。释放对象有两种方式:一种是调用 release 方法,使其引用计数立即递减;另一种是调用 autorelease 方法,将其加入“自动释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空自动释放池时,系统会向其中的对象发送 release 消息。
每一次运行循环开始后,系统都会创建一个自动释放池,当一个对象出了作用域之后就会加入到最近的自动释放池中,运行循环结束前会释放自动释放池(池子满了也会释放)。自动释放池工作的原理就是,给每一个池子的每一个对象发送 release 消息。
那么问题来了,在一个运行循环中创建了大量的临时变量,这时就会导致内存峰值很高。例如:

1
2
3
for(int i = 0; i < 100000; i++) {
[self doSomethingWithInt:i]; // 方法内创建了大量的临时变量
}

当循环结束时,大量的对象放到自动释放池中,占用了大量的内存。增加一个自动释放池可以解决这样的问题。

1
2
3
4
5
for(int i = 0; i < 100000; i++) {
@autoreleasepool {
[self doSomethingWithInt:i];
}
}

在循环中加入自动释放池,每次循环结束前都会回收当前池子中的对象。这样程序在执行循环时的峰值就会降低。
自动释放池机制就像“栈”(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。

创建自动释放池会增加额外的开销,是否需要创建还需要根据实际情况来。

总结

自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入到最顶端的池里。
合理运用自动释放池,可降低应用程序的内存峰值。

第35条:用“僵尸对象”调试内存管理问题

向已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有被其他内容所复写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以部分对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存货的对象所占据。在这种情况下,运行期系统会把消息转发到新对象那里,而此对象也许能应答,也许不能。如果能,那程序就不崩溃,可你会觉得奇怪:为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。

Cocoa提供了“僵尸对象”(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化为特殊的“僵尸对象”,而不是真正回收他们。这种对象所在的核心内存无法重用,因此不可能遭到复写。僵尸对象收到消息之后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

点击 Scheme -> Edit Scheme -> Run -> Diagnostics 里面可以设置僵尸模式。

Zombie Object 工作原理

Zombie Object 的实现代码深植于 Objective - C 的运行期程序库、Foundation 框架以及 CoreFoundation 框架中。系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

僵尸类如何将把待回收的对象转换成僵尸对象

这个过程其实就是 NSObject 的 dealloc 方法所做的事。运行期系统如果发现 NSZombieEnabled 环境变量已设置,那么就把 dealloc 方法的“调配“(swizzle)成一个会执行特定代码的方法。执行到程序末尾时,对象所属的类已经变为_NSZombie_OriginalClass了,其中 OriginalClass 指的是原类名。

代码中的关键之处在于:对象所占内存没有通过调用 free() 方法释放,因此,这块内存不可复用。虽说内存泄漏了,但这只是个调试手段,发布正式应用程序时不会把这项功能打开,所以这种泄漏问题无关紧要。

总结

打开 “Zombie Object” 这个功能,系统在回收对象时,可以不将其真正的回收,而是将它转为僵尸对象。
系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使改对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

第36条:不要使用 retainCount

MRC 环境下,retainCount 所返回的引用计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的引用计数了。
ARC 环境下已经废弃此接口。

第五章:内存管理(1)

ARC 几乎把所有内存管理事宜都交由编译器来决定,开发者只需专注于业务逻辑。

第29条:理解引用计数

Objective-C 语言使用引用计数来管理内存,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数变为0,就表示没人关注此对象了,于是,就可以把它销毁。

引用计数的工作原理

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在 Objective-C 中叫做“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:
retain:递增保留计数。
release:递减保留计数。
autorelease:待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。

1
2
3
4
5
6
7
@protocol NSObject

- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;

@end

对象创建出来时,其引用计数至少为1。若想令其继续存活,则调用 retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用 release 或 autorelease 方法。最终当引用计数归零时,对象就回收了(deallocated),也就是说,系统会将其占用的内存标记为“可重用”(reuse)。此时,所有指向该对象的引用也都变得无效了。

调用 release 之后,就已经无法保证所指的对象仍然存活

例如:

1
2
3
4
NSNumber *number = [[NSNumber alloc] initWithInt:1234];
[array addObject:number];
[number release];
NSLog(@"number = %@",number);

调用 release 之后,其引用计数降至0,那么 number 对象所占内存也许会回收,那么再调用NSLog可能会使应用程序崩溃。这里说“可能”,是因为对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaliable pool)。如果执行 NSLog 时尚未覆写对象内存,那么该对象仍然有效,这时程序不会崩溃。

属性存取方法中的内存管理

1
2
3
4
5
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}

这里需要注意的是必须先 retain 对象,然后再 release 。原因就是新对象和旧对象可能是同一个对象,这时如果先 release 这个对象,可能会导致系统永久回收对象。之后再 retain 也无法再复生。

自动释放池

调用 release 会立刻递减对象的保留计数,而且还有可能令系统回收此对象,然而有时候可以不调用它,改为调用 autorelease ,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过也可能执行得更早些(why ??后面会提到)。
这个特性很有用,例如:

1
2
3
4
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
return str;
}

在 MRC 环境下,此时 str 对象的引用计数会比期望值多1 ,因为 alloc 会使引用计数加1,但却没有释放。这时就应该用 autorelease 。此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。

1
2
3
4
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
return [str autorelease];
}

引用循环

使用引用计数机制时,经常要注意的一个问题就是“引用循环”(retain cycle),也就是呈环状相互引用的多个对象(如下图)。这将导致内存泄露,因为循环中的对象其引用计数都不会为0。

总结

引用计数机制通过可以递增递减的计数机制来管理内存。对象创建好之后,其引用计数至少为1。若引用计数为正,则对象继续存活。当引用计数降为0时,对象就被销毁了。
在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

第30条:用 ARC 简化引用计数

在 MRC 环境下,下面代码会出现内存泄漏问题

1
2
3
4
if ([self showLogMsg]) {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
NSLog(@"%@",str);
}

原因是 if 语句结束后,并没有释放 str 对象。所以我们必须手动去释放

1
2
3
4
5
if ([self showLogMsg]) {
NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@",self];
NSLog(@"%@",str);
[str release];
}

而这个操作完全可以交给 ARC (Automatic Reference Counting)来完成,也就是在 ARC 环境下,编译器会在编译时会自动加上内存管理语句。
由于 ARC 会自动执行retain、release、autorelease等操作,所以直接在 ARC 下调用这些内存管理方法是非法的。具体来说,不能调用下列方法:
retain
release
autorelease
dealloc

实际上,ARC在调用这些方法时,并不通过普通的 Objective-C 消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。

使用 ARC 时必须遵循的方法命名规则

将内存管理语义在方法名中表示出来早已成为 Objective-C 的惯例,而 ARC 则将之确立为硬性规定。这些规则简单地体现在方法名上。若方法名以下列词语开头,则其返回的对象归调用者所有:
alloc
new
copy
mutableCopy

归调用者所有的意思是:调用上述四种方法的那段代码要负责释放方法所返回的对象。
举个例子,演示了ARC的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方法名以关键字 new 开头,ARC 不会加入 retain、release 或 autorelease 语句。
+ (EOCPerson *)newPerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}

// 方法名不以关键字开头,ARC 会自动加上 autorelease 语句。
+ (EOCPerson *)somePerson {
EOCPerson *person = [[EOCPerson alloc] init];
return person;
}

// ARC 会在函数末尾给 personOne 加上 release 语句。
- (void)doSomething {
EOCPerson *personOne = [EOCPerson newPerson];
EOCPerson *personTwo = [EOCPerson somePerson];
}

除了会自动调用“保留”与“释放”方法外,ARC 还可以执行一些手工操作很难甚至无法完成的优化。如果发现在同一个对象上执行多次“保留”与“释放”操作,那么ARC有时可以成对地移除这两个操作。

一般,在方法中返回自动释放的对象时,要执行一个特殊函数。此时不直接调用对象的 autorelease 方法,而是改为调用 objc_autoreleaseReturnValue 。此函数会检视当前方法返回之后即将要执行的那段代码。若发现那段代码在返回的对象上执行 retain 操作,则设置全局数据结构(此数据结构的具体内容因处理器而异)中的一个标志位而不执行 autorelease 操作。与之相似,如果方法返回了一个自动释放的对象,而调用方法的代码要保留此对象,那么此时不直接执行 retain,而是改为执行objc_retainAutoreleaseReturnValue 函数。此函数要检测刚才提到的那个标志位,若已经置位,则不执行 retain 操作。设置并检测标志位,要比调用 autorelease 和 retain 更快。

ARC 如何清理实例变量

ARC 会在 dealloc 方法中自动生成回收对象时所执行的代码。ARC 会借用 Objective-C++ 的一项特性来生成清理例程(cleanup routime)。回收 Objective-C++ 对象时,待回收的对象会调用所有C++对象的析构函数(destructor)。编译器如果发现某个对象里含有C++对象,就会生成名为.cxx_destruct的方法。而ARC则借助此特性,在该方法中生成清理内存所需的代码。
如果有非 Objective-C 的对象,比如 CoreFoundation 中的对象或是由malloc()分配在堆中的内存,那么仍然需要手动清理。

总结

用ARC管理内存,可省去类中的许多的“样板代码”。
ARC会在合适的地方插入“保留”及“释放”对象。
CoreFoundation 对象不归 ARC 管理,开发者必须实时调用 CFRetain/CFRelease 手动释放。

第四章:协议与分类

Objective-C 语言有一项特性叫 “协议”(protocol),与 Java 的“接口”(interface)类似。

Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。

protocol 定义了一套公用的接口,和 Java 的接口同样,一个接口只有方法特征没有方法的实现,不同的类可以实现不同的行为。本质上和 Java 的接口是相同的。

Objective-C 不支持多重继承,所以我们可以将某个类应该实现的一系列方法定义在协议里面。协议最常见的用途就是实现委托模式。

“分类”也是 Objective-C 的一个重要特性。利用分类机制,我们无需继承子类即可直接为当前类添加方法。

第23条:通过委托与数据源协议进行对象间通信

对象之间的通信使用最广泛的就是“委托模式”。定义一套接口,某对象若想接受另一对象的委托,则需遵循此接口,以便其成为“委托对象”。此模式可将数据与业务逻辑解耦。

定义

委托属性一定要用 weak 修饰,不然会造成循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@protocol PersonDelegate <NSObject>
@required
- (NSDate *)whatTimeIsIt;

@optional
- (BOOL)isNiceDay;

@end

@interface Person : NSObject

@property (nonatomic, weak) id<PersonDelegate> personDelegate;

@end

实现

委托协议的方法一般会定义“可选的”(optional),当我们在调用这些方法之前就需要先判断委托对象是否有实现这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
@implementation Person

- (void)doWork {
NSDate *date = [self.personDelegate whatTimeIsIt];
NSLog(@"date = %@",date);
if ([self.personDelegate respondsToSelector:@selector(isNiceDay)]) {
BOOL isNiceDay = [self.personDelegate isNiceDay];
NSLog(@"isNiceDay:%zd",isNiceDay);
}
}

@end

如果需要经常调用某个可选方法,可以用一个状态变量来保存“是否实现这个方法”的状态,如果有多个可选方法也可以用结构体来保存状态。这样做可以大大提高程序效率。

调用

委托对象需要先遵守这个协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface ViewController () <PersonDelegate>

@end

@implementation ViewController

Person *person = [[Person alloc] initWithEmail:@"123@163.com"];
person.personDelegate = self;
[person doWork];

@end

// log
// date = Thu May 3 19:43:05 2018
// isNiceDay:1

第24条:将类的实现代码分散到便于管理的数个分类中

可以将类相同功能部分分散到单独的分类中,方便管理。也应该将私有方法放到名为 “private” 的分类中,以“隐藏”实现细节。官方的 NSString 就分成了好几个分类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>
// 0
@end

@interface NSString (NSStringExtensionMethods)
// 1
@end

@interface NSString (NSStringEncodingDetection)
// 2
@end

@interface NSString (NSItemProvider) <NSItemProviderReading, NSItemProviderWriting>
// 3
@end

@interface NSString (NSExtendedStringPropertyListParsing)
// 4
@end

@interface NSString (NSStringDeprecated)
// 5
@end

第25条:总是为第三方类的分类名称加前缀

向第三方类中添加分类时,应给分类名称以及方法加上项目专用的名称。

1
2
3
4
5
6
@interface UIWindow (VCHAnimalWindow)

- (void)vch_setRootViewController:(UIViewController *)rootViewController withOglFlipSubtype:(NSString *)subtype;
- (void)vch_setRootViewController:(UIViewController *)rootViewController animalType:(NSString *)type subtype:(NSString *)subtype duration:(CFTimeInterval)duration;

@end

这样做很大程度上避免了分类方法和原类方法相同的可能。

第26条:勿在分类中申明属性

直接在分类中申明属性编译器只会给一个编译警告。

1
2
3
4
5
6
7
8
9
// 在分类中定义一个属性
@interface Person (Special)

@property (nonatomic, weak) NSString *name;

@end

// Property 'name' requires method 'name' to be defined - use @dynamic or provide a method implementation in this category
// Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category

提示使用 @dynamic 修饰属性或者提供属性的 getter 和 setter 方法。如果没有实现,那么程序会在运行时检测。

关联对象

通过关联对象可以为分类实现属性的功能。使用时应注意内存管理问题。这种方法应该在必要的情况下才使用。

1
2
3
4
5
6
7
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, "kPersonSpecial_name", name, OBJC_ASSOCIATION_COPY);
}

- (NSString *)name {
return objc_getAssociatedObject(self, "kPersonSpecial_name");
}

总之,在必要的情况下可以通过关联对象声明属性,但这种方法应该尽量少用。

第27条:使用 “class-continuation 分类” 隐藏实现细节

类中经常会包含一些无需对外公布的方法及实例变量。这些内容可以对外公布,并写明其为私有。Objective-C 的动态消息系统方式决定了其不可能实现真正的私有方法和私有实例变量。然而,我们最好还是只把确定需要公布的那部分内容公开。此时我们可以将这部分内容放到“class-continuation 分类”中。
“class-continuation 分类” 与其他的分类不同,它必须定义在实现文件中,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在主实现文件里。
若对象遵循的协议只应视为私有,也可在“class-continuation 分类”中声明。

1
2
3
4
5
6
7
8
@interface ViewController () <PersonDelegate>
{
int _count;
}

@property (nonatomic, copy) Person *person;

@end

第28条:通过协议提供匿名对象

协议定义了一系列方法,遵从此协议的对象应该实现它们,如果这些方法不是可选的,那么就必须实现。我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法,因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。此概念称为“匿名对象”。
例如在定义“受委托者”这个对象时,可以这样写:

1
@property (nonatomic, weak) id <VCHDelegate> delegate;

任何遵循了 VCHDelegate 这个协议的对象都可以充当这个属性。对于具备此属性的类来说,delegate就是”匿名的”。
处理数据库连接(database connection)的程序库也用这个思路,以匿名对象来表示从另一个库中所返回的对象。对于处理连接所用的那个类,你也许不想让外人知道其名字,因为不同的数据库可能要用到不同的类来处理。如果没办法令其都继承自同一基类,那么就得返回id类型。不过我们可以把所有数据库连接都具备的那些方法放到协议中,令返回的对象遵从此协议。协议可以这样写:

1
2
3
4
5
6
7
8
@protocol EOCDatabaseConnection

- (void)connect;
- (void)disconnect;
- (BOOL)isConnected;
- (NSArray *)performQuery:(NSString *)query;

@end

然后可以用“数据库处理器”单例来提供数据库连接,接口可以这样写:

1
2
3
4
5
6
7
8
@protocol EOCDatabaseConnection;  

@interface EOCDatabaseManger:NSObject

+ (id)sharedInstance;
- (id<EOCDatabaseConnection>) connectionWithIdentifier:(NSString *)identifier;

@end;

这样的话,处理数据库连接所用的类的名称就不会泄漏了,有可能来自不同框架的那些类现在均可以经由同一个方法来返回。使用此API的人仅仅要求所返回的对象能用来连接、断开并查询数据库即可。至于使用的哪种数据库则不需要关心。如果后续需要更改数据库,那么此时也不需要更改接口。我们关心的并不是对象的类型,而是对象有没有实现相关的方法。

序言

在我的所有项目里面,“登录成功”以及“退出登录”都是需要却换根控制器的,方法很简单,直接设置一个新的控制器就行了。这样做两个界面之间就是直接一闪就跳换过去了,给人体验不太友好。所以在这里可以加个动画优化体验。

实现

由于 rootViewController 是放到 keyWindow 上的,所以可以在设置 rootViewController 时给 keyWindow 加上动画。

.h 头文件

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>

@interface UIWindow (VCHAnimalWindow)

- (void)vch_setRootViewController:(UIViewController *)rootViewController withOglFlipSubtype:(NSString *)subtype;
- (void)vch_setRootViewController:(UIViewController *)rootViewController animalType:(NSString *)type subtype:(NSString *)subtype duration:(CFTimeInterval)duration;

@end

.m 实现文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "UIWindow+VCHAnimalWindow.h"

@implementation UIWindow (VCHAnimalWindow)

- (void)vch_setRootViewController:(UIViewController *)rootViewController withOglFlipSubtype:(NSString *)subtype {
[self vch_setRootViewController:rootViewController animalType:@"oglFlip" subtype:subtype duration:.5f];
}

- (void)vch_setRootViewController:(UIViewController *)rootViewController animalType:(NSString *)type subtype:(NSString *)subtype duration:(CFTimeInterval)duration {
CATransition *animation = [CATransition animation];
[animation setDuration:duration];
[animation setType:type];
[animation setSubtype:subtype];
[self.layer addAnimation:animation forKey:@"VCHAnimalUIWindow"];
[self setRootViewController:rootViewController];
}

@end

CATransition 的父类 CAAnimation 中有一个属性 removedOnCompletion ,默认值为 YES 。
也就是说当动画结束后,会自动移除动画对象,不需要再手动管理内存。

调用

1
2
// [[UIApplication sharedApplication].keyWindow setRootViewController:ctrl];
[[UIApplication sharedApplication].keyWindow vch_setRootViewController:ctrl withOglFlipSubtype:kCATransitionFromRight];

Thinking

一开始想到的是通过 runtime 来实现,这样做优点就是不需要修改调用语句,缺点就是无法传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <objc/runtime.h>

@implementation UIWindow (VCHAnimalWindow)

+ (void)load {
Method originalMethod = class_getInstanceMethod([UIWindow class], @selector(setRootViewController:));
Method swappedMethod = class_getInstanceMethod([UIWindow class], @selector(vch_setRootViewController:));
method_exchangeImplementations(originalMethod, swappedMethod);
}

- (void)vch_setRootViewController:(UIViewController *)rootViewController {
CATransition *animation = [CATransition animation];
[animation setDuration:.5f];
[animation setType:@"oglFlip"];
[animation setSubtype:kCATransitionFromRight];
[self.layer addAnimation:animation forKey:@"VCHAnimalUIWindow"];
[self vch_setRootViewController:rootViewController];
}

@end

第三章:接口与 API 设计

第15条:用前缀避免命名冲突

选择与公司、应用程序或二者皆有关联的名称作为类名的前缀,并在所有的代码中使用这一前缀。也不仅仅是类名,应用程序中所有名称都应该加前缀。

苹果宣称保留使用所有“两个字母前缀”的权利,所以我们的前缀必须多于两个字母。

顶级符号

在编译好的目标文件中,类实现文件所用的纯 C 函数和全局变量的名称要算作“顶级符号”。比如在类中创建了名为 “completion” 的纯 C 函数,会编译成 “_completion” 存在符号表中。此时如果在别的文件中也创建一个名为 “completion” 的函数,就会发出一个 “duplicate symbol” 的错误。

避免第三方库冲突

如果两个第三方库同时引入了相同的第三方库,那么就可能会出现 “duplicate symbol” 的错误。
当自己的第三方库引入了别的第三方库的时候,应该给那份第三方库的代码加上自己的前缀。(😆。。。没看懂)

第16条:提供 “指定初始化方法”

那些可以为对象提供必要信息以便其能完成工作的初始化方法就叫“指定初始化方法”,这类初始化方法一般在后面会有 NS_DESIGNATED_INITIALIZER 这个宏定义。

相关文章

之前已经写过一篇相关的文章,可以去这篇文章看看 iOS开发之Designated Initializer(指定初始化方法)

补充

如果子类的指定初始化方法和父类的指定初始化方法不一样,那么需要在子类中重写父类的初始化方法。

第17条:实现 description 方法

description 方法定义在 NSObject 的协议里面。当想打印某个对象的时候,通常我们会这样做

1
2
3
4
5
Person *p = [[Person alloc] initWithEmail:@"123@163.com"];
NSLog(@"%@",p);

// 输出
// <Person: 0x109ea6170>

直接打印对象实际上就是调用了 description 方法。所以我们只需要重写这个方法就可以打印出感兴趣的信息出来。

description

1
2
3
4
5
6
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, email = %@>", [self class], self, _email];
}

// 输出
// <Person: 0x12bd4f090, email = 123@163.com>

如上,只要我们重写了 description 方法,就可以打印出特定的信息出来。

debugDescription

在合适的地方加入断点,然后在调试控制台输入lldb的 “po” 命令,就可以打印出 debugDescription 里面的信息出来

1
2
3
4
5
6
7
8
9
- (NSString *)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, email = %@>", [self class], self, _email];
}

// 输出
// <Person: 0x113dbff50, email = 123@163.com>
// (lldb) po p
// <Person: 0x113dbff50, email = 123@163.com>
// (lldb)

一般我们可以在 description 里面打印主要的信息,而在 debugDescription 里面打印更详细的信息。

第18条:尽量使用不可变对象

如果属性是不可变的,那么就应该将它设置成 readonly 。
如果把可变对象放到 collection 中,然后又修改其内容,那么很容易破坏 collection 的内部结构,比如:NSSet

看使用场景,把代码设计成最合逻辑的。

第19条:使用清晰而协调的命名方式

1、命名要清晰、易懂。
2、命名不要太啰嗦。
3、驼峰命名(类名首字母要大些,并且要加上前缀)。
4、是否要简写要看具体情况。
5、加前缀,尽量避免命名冲突。

第20条:为私有方法名加前缀

由于 Objective-C 没有 private 关键字。如果父类的私有方法和子类的方法重名了,那么父类的私有方法将无法执行。
苹果自己是通过在私有方法前加下划线(_)来标识的,因此我们就不能再这样做了。

怎样有效避免这个问题

文章给出两个方法。

加前缀 “p_”

即 private 的首字母加下划线作为前缀。

项目前缀加下划线

比如我的项目前缀是 “VCH”,那么就可以加 “vch_” 作为前缀。不过其实分类的方法很多也是使用前缀加下划线来区别原类的。

第21条:理解 Objective-C 错误模型

致命性错误 使用 @throw

只有在极端情况下,才使用 @throw 抛出异常,同时也就意味着程序结束,崩溃。

1
@throw [NSException exceptionWithName:@"errorName" reason:@"errorReason" userInfo:@{@"key":@"value"}];

非致命性错误 返回 nil 或 0

一般对于一些非致命性错误,可以返回 nil 或 0 来提示。

NSError

当我们进行一些网络请求时,会返回一些错误,此时可以通过 NSError 把错误信息封装起来,再交给接受者处理。

Error domain

错误的范围,一般会定义一个全局变量来指示。

Error code

错误码,一般用一个枚举表示。

Error info

包含错误的额外信息,字典类型。

Error 常见处理方法

交给委托处理

可以把错误传递给委托对象处理,至于怎么去处理这个错误由委托对象决定。

返回给调用者

也可以通过返回值、block等将错误返回给调用者,交由调用者处理错误。

第22条:理解 NSCopying 协议

当我们自己的类需要支持拷贝操作时,就需要实现 NSCopying 协议,协议就一个方法。

1
2
3
4
5
@protocol NSCopying

- (id)copyWithZone:(nullable NSZone *)zone;

@end

具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// .h
@interface Person : NSObject<NSCopying>

- (instancetype)initWithEmail:(NSString *)email;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *name;

@end

// .m
- (id)copyWithZone:(NSZone *)zone {
Person *person = [[[self class] allocWithZone:zone] initWithEmail:_email];
person.name = [_name copy];
return person;
}

使用 NSCopying 协议复制出来的对象是不可变的。

NSMutableCopying 协议

当我们需要复制的是可变对象时,就需要实现 NSMutableCopying 这个协议。

1
2
3
4
5
@protocol NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

@end

如果自定义对象分可变版本和不可变版本,那么就要同时实现 NSCopying 和 NSMutableCopying 协议。

深拷贝 & 浅拷贝

浅拷贝只会复制指针,拷贝后的对象和原始对象为同一对象。深拷贝则是将对象也拷贝了一份。Foundation 框架下所有的 collection 类在默认情况下都执行浅拷贝。实现 collection 深拷贝的方法类似如下

1
2
- (instancetype)initWithSet:(NSSet<ObjectType> *)set copyItems:(BOOL)flag;
- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;

第二章:对象、消息、运行期(3)

第12条:理解消息转发机制

当一个对象接收到无法解读的消息后,就会开启“消息转发”机制。如果消息转发也无法解读消息,程序就会抛出异常:

unrecognized selector sent to instance xxxx

消息转发分为两大阶段:

第一阶段:动态方法解析

征询接受者能否动态添加方法来处理这个消息。此时会调用以下两个方法之一:

1
2
3
4
// 以类方法调用时触发
+ (BOOL)resolveClassMethod:(SEL)sel
// 以实例方法调用时触发
+ (BOOL)resolveInstanceMethod:(SEL)sel

如果需要在动态解析时处理消息,那么实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void run(id self, SEL _cmd) {
NSLog(@"missRun -- run");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"missRun")) {
NSLog(@"sel == %@",NSStringFromSelector(sel));
class_addMethod([self class], sel, (IMP)run, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

// 注意这里 class_addMethod 的第一个参数是 [self superclass]
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"missRun")) {
NSLog(@"sel == %@",NSStringFromSelector(sel));
class_addMethod([self superclass], sel, (IMP)run, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

外部调用

1
2
3
4
//Person *person = [[Person alloc] init];
//[person performSelector:NSSelectorFromString(@"missRun") withObject:nil];

[Person performSelector:NSSelectorFromString(@"missRun") withObject:nil];

此时在外部调用 missRun 方法,最终将会访问 void run(id self, SEL _cmd) 方法。

IMP 指向的函数必须要有 id self, SEL _cmd 这两个参数。

class_addMethod 的最后一个参数 “v@:” 中,v 表示返回值 void , @ 表示第一个参数类型为 id ,: 表示 SEL 。具体可看文档 Type Encodings

第二阶段:完整的消息转发机制

接受者尝试能否将这条消息转发给其他接受者接收,如果不行就启用“完整的消息转发”。

备用接受者

此时会调用下面的方法

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector {
Sutdent *student = [[Sutdent alloc] init];
if ([student respondsToSelector:aSelector]) {
return student;
}
return [super forwardingTargetForSelector:aSelector];
}

完整的消息转发

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == NSSelectorFromString(@"missRun")) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([anInvocation selector] == NSSelectorFromString(@"missRun")) {
Sutdent *student = [[Sutdent alloc] init];
[anInvocation invokeWithTarget:student];
}
}

“备用接受者”和“完整的消息转发”区别在于,“完整的消息转发”中可以改变消息的内容。

消息转发流程图


接受者在每一步均有机会处理消息,越到最后,处理的代价会越高。

Demo

GitHub: MessageForwarding

第13条:用 “方法调配技术” 调试 “黑盒方法”(method swizzling)

类对象的方法列表会将“方法名”映射带相应的方法实现上,“动态消息派发系统”会根据这个表找到相应的方法。这些方法均以函数指针的方式表示。这种指针就是 IMP 。下图是 NSString 的部分方法映射表。

Objective-C 运行时系统提供了几个方法可以用来操作这张表。开发者可以在运行时新增方法,改变方法对应的实现,也可以交换两个方法的具体实现。例如我们可以让方法映射表变成下图这样

实现起来也是很简单的,创建一个 NSString 的分类,在 +load 方法中实现

1
2
3
4
5
+ (void)load {
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
}

调用

1
2
3
4
5
6
7
NSString *string = @"This is a String";
NSLog(@"lowercaseString = %@",string.lowercaseString);
NSLog(@"uppercaseString = %@",string.uppercaseString);

// 输出
// lowercaseString = THIS IS A STRING
// uppercaseString = this is a string

此时 lowercaseString 和 uppercaseString 的方法实现已经替换过来了。
lowercaseString 方法对应的是 uppercaseString 的方法实现。
uppercaseString 方法对应的是 lowercaseString 的方法实现。
所以打印出来的log是反过来的。当然这个没有什么意义。

下面实现一个功能:每次调用 lowercaseString 都打印出相应的log出来

1
2
3
4
5
6
7
8
9
10
11
+ (void)load {
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(vch_lowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
}

- (NSString *)vch_lowercaseString {
NSString *string = [self vch_lowercaseString];
NSLog(@"----%@",string);
return string;
}

调用

1
2
3
4
5
6
NSString *string = @"This is a String";
NSLog(@"lowercaseString = %@",string.lowercaseString);

// 输出
// ----this is a string
// lowercaseString = this is a string

由于 lowercaseString 和 vch_lowercaseString 交换了方法实现,所以当我们调用 lowercaseString 方法的时候,执行的是 vch_lowercaseString 里面的方法。所以才会打印出 log 出来。

用途

使用 method swizzling “黑魔法”,开发者可以在原有实现中添加新的功能。

第14条:理解 “类对象” 的本质

看看下面的两个语句

1
2
NSString *string0 = @"this is a string";
id string1 = @"this is a string";

两个语句都创建了一个 NSSring 类型的对象,在编译时,编译器会将 string0 按照 NSString 类型来检测,string1 按照 id 类型来检测。string0 直接调用 NSString 的方法编译器不会报错,string1 直接调用 NSString 的方法则编译器报错。 而在运行时两个对象表示的意思是一样的。

在 objc.h 中是这样定义 id 类型的

1
2
3
4
5
6
7
8
9
10
11
12
// objc.h

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

可以看出 id 是 objc_object 结构体类型的指针,objc_object 包含了一个 Class 类型的变量 isa ,Class 是 objc_class 类型的指针。
再看看 NSObject.h 中的定义

1
2
3
4
5
6
7
8
// NSObject.h

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

这里面包含了一个 Class 类型的变量 isa 。这个 Class 也就是 objc_class 类型的指针。
事实上每个实例变量都会包含一个 objc_object 结构体,该结构体的第一个成员变量就是 isa 指针。既然是指针,那么 objc_class 也是一个对象,我们称之为“类对象”,这个类对象是一个单例,程序运行中只存在一份。

再看看 runtime.h 是怎么定义 objc_class 结构体的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// runtime.h

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

objc_class 的第一个成员变量也是 isa 指针。它指向的是类的元类(metaclass)。objc_class 负责保存类的实例变量、方法列表、缓存方法列表、协议列表等。元类(metaclass)则负责保存类方法列表。

继承体系图


每一个实例对象都有一个 isa 指针指向其类对象,用来表明其类型,类对象也有一个 isa 指针,指向其元类,元类同样存在一个 isa 指针,指向其根元类,根元类的 isa 指针则指向自身。这些类对象则构成了类的继承体系。

在继承体系中查询类型信息

isMemberOfClass 不包含父类,用来判断是否是某个特定类的实例。(需要考虑“类族”)
isKindOfClass 包含父类,用来判断是否是某个特定类或者派生类的实例。

总结

1、类本质也是一个对象(类对象)。
2、类对象会在程序第一次使用时创建一次,是个单例。
3、类对象是一种数据结构。存储了类的版本、描述信息、大小、变量列表、方法列表、方法缓存、协议列表等。
4、元类中保存了类方法列表。