《Effective Objective-C 2.0》读书总结

Objective-C

了解 Objective-C 语言的起源

Objective-C 语言使用”消息结构”而非”函数调用”.Objective-C 语言由 SmallTalk演化而来,后者是消息类型语言的鼻祖.编译器甚至不关心接收消息对象的何种类型.接收消息的对象问题也要在运行时处理,其过程叫做”动态绑定”.

Objective-C为 C 语言添加了面向对象特性,是其超类. Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型.接收一条消息后,究竟应执行何种代码,有运行期环境而非编译器决定.理解 C 语言的核心有助于写好 Objective-C 程序.尤其是掌握内存模型与指针.

在类的头文件中尽量少引用其他头文件

Objective-C 语言编写类的标准行为:以类名做文件名,分别闯将两个文件,有文件后缀用. h,实现文件后缀用. m.

在开发中有时候我们会在. h 文件中引入很多用不到的内容,这当然会增加编译时间.除非有必要,否则不要引入头文件,一般来说,某个类的头文件中使用向前声明来体积别的类,并在实现文件中引入哪些类的头文件,这样做可以尽量降低类之间的耦合.有时无法使用向前声明,比如要声明某个类遵循意向协议,这种情况下,尽量把 “该类遵循某协议”的这条声明移至 class=continuation 分类中,如果不行的话,就把协议单独存放在一个头文件中,然后将其引入.

多用字面量语法,少用与之等价的方法

1
2
NSArray *arr = [NSArray arrayWithObjects:@"num1",@"num2",@"num3", nil];
NSArray *arr = @[@"num1",@"num2",@"num3”];

字面量语法创建字符串,数组,数值,字典.与创建此类对象的常规方法相比,这么做更加简明扼要,并且更加安全。

注意事项:

  • 除了字符串以外,所创建的类必须属于 Foundation 框架才行,如果自定义了这些类的子类,则无法用字面量语法创建其对象.

  • 创建的数组或字典时,若值有 nil, 则会抛出异常.因此,务必确保值中不含 nil。

多用类型常量,少用# deine 预处理指令

不要用预处理指令定义常量,这样定义出来的常量不含类型信息,编译器只会在编译前根据执行查找与替换操作,即使有人重新定义了常量值,编译器也不会产生井道信息,这将导致应用程序常量值不一致.

1
static NSString *const PersonConstant = @"PersonConstantStr” ;

但是我个人认为其实,还是#define用的多, 开发避免不了使用 pch文件, 同时#define还可以定义方法,这个是类型常量无法做到的。 如果有强迫症的同学,定义常量就想使用 staitc,extren,const 这些关键字.那我建议新建一个专门存放这些常量的类,然后在 pch 中导入这个类.

  • static 修饰符意味着该变量仅在定义此变量的单元中可见
  • extern 全局变量

用枚举表示状态,选项,状态码

应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值起个易懂的名字。

如果把传递的给某个方法的选项表示为枚举类型,而多个类型又可同时使用,那么就将各选项值定义为2的幂,通过按位或操作将其结合起来。

1
2
3
4
5
6
enum PersonEnum{
PersonEnumNum1,
PersonEnumNum2,
PersonEnumNum3,
};
typedef enum PersonEnum PersonState;

对象、消息、运行时

理解属性这一概念

属性是 Objective-C 的一项特性,用于封存对象中的数据.
属性特质:原子性 读写权限

内存管理语义:

  • assign 这是方法只会执行针对纯量类型(CGFloat,NSInteger)的简单赋值操作
  • strong 此特质表明该属性定义一种拥有关系,为这种属性设置新值时,这只方法会先保存新值,并释放旧值
  • weak 此特质表明属性定义了一种”非拥有关系”,为这种属性设置新值是,设置方法既不保留新值,也不释放旧值.此特质同 assign 类似,然而在属性所指对象遭到摧毁时,属性值会清空
  • unsafe_unretainde 此特质与 assign 相同,它适用于对象类型,该特质表达一种”非拥有关系”,当目标对象遭到摧毁时,属性不会自动清空,因为它是不安全的,这一点与 weak 的区别
  • copy 此特质所表达的所属关系与 strong 类似,然而设置方法并不保留新值,而是将其拷贝,多用于 NSString.

在对象内部尽量直接访问实例变量

直接访问实例变量的速度比较快,因为不经过 Objective-C 方法派发,编译器所生成的代码会直接访问保存催下实例量的那块内存。
直接访问实例变量时,不会调用设置方法,这就绕过了相关属性所定义的内存管理语义。

读取实例变量的时候采用直接访问的的形式,设置实例变量的时候通过属性来做。

注意:

  • 直接访问访问实例变量,不会触发KVO。
  • 懒加载时,必须通过属性来读取数据。

理解”对象等同性”这一概念

根据等同性来比较对象是一个非常有用的功能,不过,按照 == 操作符比较出来的结果未必是我们想要的,因为该操作比较的事两个指针本身,而不是其所指的对象,应该使用 NSObject 协议中的声明的”isEqual”方法来判断两个对象的等同性,一般来说两个类型不同的对象总是不相等的.直接比较字符串的时候 isEqual 比 isEqualToString慢,因为前者还要执行额外步骤.

NSObjec中有两个判断等同性的关键方法:

1
2
- (BOOL) isEqual:(id)object;
- (NSUInteger)hash;

以”类族模式”隐藏实现细节

“类族”是一种很有用的模式,可以隐藏抽象基类背后实现的细节. 这是一种”工厂模式”.比如iOS 用户界面框架 UIKit 中就有一个名为 UIButton 的类.想创建按钮,需要调用下面这个类方法。

1
+ (UIButton*)buttonWithType:(UIButtonType)type;

在既有类中使用关联对象存放自定义数据

有时需要在对象中存放相关信息,这是我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象.然而并非所有情况下都这么做,有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的实例. Objective-C 中有一项强大的特性可以解决问题,这就是关联对象。

1
2
3
4
5
6
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)

理解objc_msgSend的作用

用Objetive-C的术语来说,这叫做“消息传递”。这里说的是运行时。

理解消息转发机制

当对象接收到无法解读的消息后,就会启动消息转发机制,程序员可经此过程告诉对象应该图和处理未知消息。这里说的是运行时。

  1. 动态方法解析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    + (BOOL)resolveClassMethod:(SEL)sel
    {
    /**
    动态消息转发
    if (sel == @selector(foo:)) {
    class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
    }
    return [super resolveInstanceMethod:sel];
    */
    return YES; // 进入下一步转发
    }
  2. 备用接收者

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
    /**
    备用接收者

    if (aSelector == @selector(foo)) {
    return [Person new]; // 返回一个Person实例作为备用接收者
    }
    return [super forwardingTargetForSelector:aSelector];
    */
    return nil; // 进入下一步转发
    }
  3. 完整的消息转发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
    if (aSelector == @selector(foo)) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];//签名,进入forwardInvocation

    }
    return [super methodSignatureForSelector:aSelector];
    }

    - (void)forwardInvocation:(NSInvocation *)anInvocation
    {
    SEL sel = anInvocation.selector;
    Person *person = [Person new];
    if ([person respondsToSelector:sel]) {
    [anInvocation invokeWithTarget:person]; // 直接调用 person 的 foo 方法
    }else{
    [self doesNotRecognizeSelector:sel];
    }
    }

用方法调配技术调试黑盒方法

运行期间,可以向类中新增或替换选择子所对应的方法实现。
使用另一份实现来替换原有的方法实现,这道工序叫做方法调配,开发者常用此技术想原有实现中添加新功能。
一般来说,只有调试程序的时候才需要运行期修改方法实现,这种做法不易滥用。这里说的是方法交换

理解类对象的用意

每个Objective-C对象实例都是指向某块内存数据的指针,如果把对象所需的内存分配到栈上编译器就会报错.

每个对象结构体的首个成员是Class类的变量,该变量定义了对象所属的类,通常称为isa指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_class *class
struct objc_class{
Class isa;
Class super_class;
const char* name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list *ivars;
struct objc_cache *cache;
struct objc_protocol_list protocols;
}

此结构体存放类的元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objctive-C对象。结构体里还有个变量叫做super_class,它定义本类的超类,类对象所属的类型(isa指针所指向的类型)是另外一个类,叫做元类,用来标书类本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法,每个类仅有一个类对象,每个类对象仅有一个与之相关的元类。(元数据,就是这个类的数据)。

  • isKindOfClass:能够判断对象是否为某类或其派生类的实例
  • isMemberOfClass: 能够判断出对象是否为某个特定类的实例

接口设计

用前缀避免命名空间冲突

Objective-C没有其他语言那种内置的命名空间,所以需要避免命名冲突,否则会直接报错。

提供全能初始化方法

即可以为对象提供必要信息以便其完成能完成工作的初始化方法。

注意:如果子类的全能初始化方法与父类的不一致,就应该覆写父类的全能初始化方法。有时我们不想覆写,这时我们可以覆写父类的全能初始化方法并在里面抛出异常。

实现description方法

调试程序时经常需要打印并查看对象信息。description 很实用。

debugDescription 方法是开发者在调试器中以命令打印对象时候才调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property(nonatomic,assign)int age;
@property(nonatomic ,copy)NSString* name;
@end

Person.m
#import "Person.h"
@implementation Person
- (NSString *)description
{
return [NSString stringWithFormat:@"name %@ , age %d", self.name, self.age];
}
- (NSString *)debugDescription
{
return [NSString stringWithFormat:@"name %@ , age %d", self.name, self.age];
}
@end

尽量使用不可变对象

通过readonly将属性修饰为不可变,如果想修改封装在对象内部的数据,可以在对象的内部将readonly属性重新声明为readwrite

注意:readonly修饰的属性,仍然可以使用KVC来修改。

使用清晰而协调的全名方式

没啥好说的

为私有方法名加前缀

不要单用一个下划线给私有方法做前缀,这个是苹果爸爸用的。

理解Objective-C错误模型

NSError的用法更加灵活,因此经由此对象,我们可以把导致错误的原因汇报给调用者。

  • NSError domain(错误范围,其类型为字符串)
    错误发生的范围,也就是产生错误的根源,通常用一个特有的全局变量来定义,比方说 “处理URL子系统”从URL的解析获取数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围
  • Error code(错误码,其类型为整数)
    独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特性范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码
  • User info(用户信息,其类型为字典)
    有关错误的额外信息,其中或许包含一段“本地化的描述”或许还含有导致错误发生的另外一个错误,经由此种信息,可将相关错误串成一条“错误链”
1
2
3
4
5
6
7
8
9
10
11
12
@try {
NSString *str = @"wotaxiwa";
NSString *strErr = [str substringFromIndex:100];
NSLog(@"%@",str);
} @catch (NSException *exception) {

NSLog(@"ERROR: %@",exception);
} @finally {
NSLog(@"%s",__func__);
}

如果出现exception,异常后面的代码将不会继续执行

理解NSCopying协议

  • copy方法实际上是调用 -(id)copyWithZone:(NSZone*)zone; 实现copy操作, 如果想对自己的类支持拷贝并且做额外操作,那就要实现NSCopying协议此的方法。
    为何出现NSZone呢,以前开发程序时,会据此把内存分成不用的区,而对象会创建在某个区。 现在不用了,每个程序只有一个区:“默认区”,所以不用担心zone参数。
    copy方法由NSObject实现,该方法只是以默认区为参数调用。
  • mutableCopy方法实际上是调用 -(id)mutableCopyWithZone:(NSZone*)zone; 实现mutableCopy操作

协议与分类

通过委托与数据协议进行对象间通信

这一条说的就是delegate(代理设计模式)。但是并没有说delegate的循环引用的问题,在使用代理声明一个 @property的时候,记得用weak。

将类的实现代码分散到便于管理的数个分类之中

  • 使用分类机制把类的实现代码划分成易于管理的小块
  • 将应该视为“私有”的方法归入名为Private的分类中,以隐藏细节。

勿在分类中声明属性

正常的分类是不可以声明属性的,但是从技术上说,分类里可以用runtime声明属性。

1
2
3
4
5
6
7
8
9
10
11
#import <objc.runtime.h>
static const char *kFriendsPropertyKey = “kFriendsPropertyKey”;
@implementation EOCPerson(Friendship)
-(NSArray*)friends{
return objc_getAssociatedObject(self,kFriendsPropertyKey);
}

-(void)setFriends:(NSArray*)friends{
objc_setAssociateObject(self.kFriendsPropertyKey,friends,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

这样做可行,但是不太理想,要把相似的代码写很多遍。而且容易出现Bug,可以使用class-continuation实现分类添加属性。

使用class-continuation分类隐藏实现细节

class-continuation分类和普通的分类不同,它必须在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法应该定义在类的主实现文件里。与其他分类不同,“class-continuation分类”没有名字,比如,有个类叫做EOCPerson,其“class-continuation分类”写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface EOCPerson()
@end

#import "ViewController.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
}

- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];

}
@end

没错它就是 class-continuation分类,在此代码之间可以添加属性,修改属性。

1
2
@interface ViewController ()
@end

使用class-continuation分类的好处

  • 可以向类中新增实例变量。
  • 如果类中的主接口声明为只读,可以再类内部修改此属性。
  • 把私有方法的原型文件生命在”class-continuation分类”里面。
  • 想使类遵循的协议不为人知,可以用“class-continuation分类”中声明。

通过协议提供匿名对象

就说下面这句话

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

内存管理

理解引用计数

理解引用计数,方便于了解iOS的内存管理。不过现在都是ARC的时代了。

引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保存计数降为0,对象就被销毁了。

在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

以ARC简化引用计数

使用ARC要计数,引用计数实际上还是要执行的,只不过保留与释放操作现在由ARC自动为你添加。由于ARC会自动执行retain、release、autorelease等操作,所以直接在ARC下调用这些内存管理方法是非法的。

ARC管理对象生命周期的的办法基本就是:在合适的位置插入“保留”和“释放”操作。

ARC在调用这些方法时,并不用过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本,这样做性能更好,直接调用底层函数节省很多CPU周期。

虽然有了ARC之后无需担心内存管理问题,但是CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease

在dealloc方法中只释放引用并解除监听

  • 当一个对象销毁的时候会调用dealloc方法,但是当开销较大或系统内稀缺资源则不再此列,像是文件描述、套接字、大块内存等都属于这种资源,通常对于开销较大的资源实现一个方法,当程序用完资源对象后,就调用此方法。这样一来,资源对象的生命期就变得明确了。

  • 执行异步任务的方法不应该在dealloc里面调用;只能在正常状态下调用的那些方法也不应该调用,因为此时对象已经处于正在回收的状态了。

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

  • 在使用@try 的时也要注意,在捕获到异常的时候@try{}中的语句执行到异常代码的那一行后不在执行,然后把异常抛给@catch。当然@finally是一定要执行的。
  • 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可以生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

以弱引用避免保留环

  • unsafe_unretained 语义同assign等价。然而assign通常用于int、float、结构体等。unsafe_unretained多用于对象类型。
  • weak 与 unsafe_unretained 作用相同,然而只要系统把属性回收,属性值为nil。
    推荐使用weak,毕竟是ARC时代的产物,而且用的人也很多。

以“自动释放池块”降低内存峰值

自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@autoreleasepool {
<#statements#>
}

for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSNumber *num = [NSNumber numberWithInt:i];
NSString *str = [NSString stringWithFormat:@"%d ", i];
[NSString stringWithFormat:@"%@%@", num, str];

if(lagerNum-1 == i)
{
NSLog(@"end");
}
}
}

用“僵尸对象”调试内存管理问题

在左上角标题栏找到项目单击后选择 Edit scheme 勾选图中检测僵尸对象。

  • 开启后,系统在回收对象的时候,可以不真的将其回收,而是转成僵尸对象。
  • 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能响应所有的方法,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

不要使用retainCount

  • 任何给定时间点上的“绝对保留计数”都无法反应对象生命周期的全貌。
  • ARC的时代,调用该方法会直接报错。

块与大中枢派发

理解”块“这一概念

这里其实就是在说block,复习一下block的语法:
返回值类型(block名称)(参数)

  • 需要注意的是定义block时候,其所占内存区域是分配在栈中的,块只在定义它的那个范围内有效。

  • block所使用的整个内存区域,在编译期已经完全确定,因此,全局block可以生命在全局内存里,而不需要在每次用到的时候于栈中创建,另外,全局block的拷贝是个空操作,因为全局block绝不可能为系统所回收,这种block实际上相当于单例。

  • 可以调用 copy 方法将块从栈拷贝到堆,拷贝之后的块就可以在定义它的范围之外使用了。而且,拷贝到堆以后,块就变成带引用计数的对象了,后续的copy操作不会真的执行,只是递增引用计数。

为常用的块类型创建typedef

就是给block起个别名

1
2
typedef <#returnType#>(^<#name#>)(<#arguments#>);
@property (nonatomic,copy)name nm_blk;

用handler块降低代码分散程度

说的就是block的回调。只不过是把block放在方法中去使用,使代码更加紧致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//  Person.h
#import <Foundation/Foundation.h>
typedef void(^Blk)(NSString *name,int age);
@interface Person : NSObject
-(void)handler:(Blk)blk;
@end

// Person.m
#import "Person.h
@implementation Person
-(void)handler:(Blk)blk{
if(blk){
blk(@"zhangsan" ,28);
}
}
@end

// 使用
Person *per =[Person new];
[per handler:^(NSString *name, int age) {
NSLog(@"%@ %d",name, age);
}];

用块引用其所属对象时不要出现保留环

注意用weak,不要出现循环引用。

多用派发队列,少用同步锁

派发队列可用来表述同步语义,这种做法比使用@synchronize块或NSLock对象更简单
将同步与异步派发结合起来,可以实现与普通枷锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。

使用同步队列及栅栏块,可以令同步行为更加高效(不常用)。

多用GCD,少用performSelector系列方法

这个没啥可说的

掌握GCD及操作队列的适用时机

这个没啥可说的

解决多线程与任务管理问题时,派发队列并非唯一方案

操作队列提供了一套高层的Objective-C API

能实现纯GCD所具备的绝大部分功能,而且还完成一些更为复杂的操作,那些操作弱改用GCD来实现,则需另外编写代码。

使用NSOperation对线程管理

通过Dispatch Group机制,根据系统资源状况来执行任务

这个没啥可说的
一系列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知
通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统西苑状况来调度这些并发执行的任务。开发者若自己来实现此功能。则需要便携大量代码。

使用dispatch_once来执行秩序运行一次的线程安全代码

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
<#code to be executed once#>
});

不用使用dispatch_get_current_queue

这个没啥可说的。
iOS系统6.0版本起,已经正式弃用此函数了。

系统框架

熟悉系统框架

打开Xcode command + shift + 0 选择性的了解一些 Foundation、UIKit
也可以看看这篇博客 http://www.jianshu.com/p/58bc11c800e4

多用块枚举,少用for循环

因为枚举遍历的时候用的多线程(GCD并发执行),所以效率更快些。我觉得其实用什么都行。

1
2
3
NSArray *arr = @[@"b",@"c",@"s"];
[arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];

对自定义其内存管理语义的collection使用无缝桥接

1
2
3
NSArray *anNSArray = @[@1,@3,@5,@8];
CFArrayRef acFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"%@",acFArray);

通过无缝桥接技术,可以在Foundation框架中Objective-C对象与CoreFoundation框架中的C语言数据结构之间来回转换。

在CoreFoundation层面穿件collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素,然后可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。

  • __bridge:ARC仍然具备这个OC对象的所有权
  • bridgeretained:ARC将交出对象的所有权

构建缓存时选用NSCache而非NSDictionary

  • NSCache胜过NSDictionary之处在于,当系统资源耗尽时,它能自动删减缓存。
  • NSCache线程安全

精简Initialize与load的实现代码

类初始化的时候一定会调用两个方法

1
2
3
4
5
6
7
8
+(void)load{}

+ (void)initialize
{
if (self == [<#ClassName#> class]) {
<#statements#>
}
}
  • load方法只会调用一次,不管该类的头文件有没有被使用,该类都会被系统自动调用,而且只调用一次。 当然了,如果不重写这个方法的话,我们是不知道这个方法有没有被调用的。如果分类也重写了load方法,先调用类里的,在调用分类。
  • load方法执行时,运行期系统处于“脆弱状态”,在执行子类的load方法之前,必定会先执行所以超类的load方法,而如果代码还依赖了其他的库,那么其他库的相关类的load方法一定会先执行,但是执行的顺序不好判断,所以在load方法中使用其他类是不安全的。
  • 整个程序在执行load方法时都会阻塞。
  • initialize 和load类似,不过在类被初始化的时候才会被调用(init之前)。需要注意的是,<#ClassName#>如果有子类继承的时候要判断类名。
  • 两个方法的实现都应该精简些,这有助于保持应用程序的响应能力。

别忘了NSTimer会保留其目标对象

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
35
36
37
38
39
40
41
//  Person.h
#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,strong)NSTimer *timer;
-(void)start;
-(void)stop;
@end

// Person.m
#import "Person.h"
@implementation Person

-(void)start{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
}

-(void)run{
NSLog(@"%s",__func__);
}

-(void)dealloc{
NSLog(@"%s",__func__);
}

-(void)stop{
[self.timer invalidate];
}
@end


调用
@property (nonatomic,strong)Person *person;

self.person = [Person new];
[self.person start];


-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person stop];
}

好,那么问题来了,是不是没有调用dealloc方法,没有调用dealloc方法就说明Person对象并没有被销毁,为什么没有被销毁
因为在控制器强引用了self.person,[self.person start]强引用了 self.timer; self.timer 的target指向了self(self.person)所以循环引用了。

怎么解决。 NSTimer销毁的时候,把Person对象为nil即可。

1
2
3
4
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.person stop];
self.person = nil;
}