Effective Objective-C 总结

Effective Objective-C 总结


1、类的头文件中尽量少引入其他头文件,有可能的话使用@class代替

2、字面量语法在可读性上优于与之等价的方法,但是要做好数据校验,不能将nil数据直接存入其中。
例如:

1
NSArray *arr = @[@"1",@"asd",nil];

3、能用常量定义,就尽量不要使用#define预处理指令。常量定义带有数据类型,编译前会进行数据格式校验。
只在本文件使用,在.m文件中

1
static const NSTimeInterval kAnimationDuration = 0.3;

要做为全局常量,在.h文件中

1
2
extern NSString *const EOCStringConstant;
extern const NSTimeInterval EOCAnimationDuration;

在.m文件中

1
2
NSString *const EOCStringConstant = @"Value";
const NSTimeInterval EOCAnimationDuration = 0.3;

4、枚举类型定义使用 typedef NS_ENUM/NS_OPTIONS (type, name),
NS_ENUM做状态枚举时,在switch中不要实现default,或者在default中添加断言。防止枚举中添加了状态而switch中忘记修改。
NS_OPTIONS用于实现枚举多选,各选项的值定义为2的幂,通过按位或操作进行组合。通过位与运算判断是否包含某个值。

5、属性在@interface中通过@property定义,在@implementation中可以使用@synthesize 自定义属性对应的实例变量名。@dynamic会告诉编译器不要为属性创建实例变量,也不要创建存取方法。
属性特性:
原子性 : atomic会给属性添加同步锁,防止多线程中修改属性出现问题。nonatomic则不使用同步锁。使用同步锁后,属性的读取效率会降低,一般如果不是非常必要,属性会定义为nonatomic。
读写权限:readwrite为默认特性,默认编译器会自动生成getter 和 setter方法。readonly只读特性,一般的使用方法是在头文件中定义只读属性,在实现文件的分类中重新将其定义为readwrite。
例如
.h文件中

1
2
3
@interface Class: NSObject
@property (nonatomic, readonly) NSString *parmString;
@end

.m文件中

1
2
3
@interface Class()
@property (nonatomic, readwrite) NSString *parmString;
@end

内存管理语义:影响setter方法在设置时属性的引用计数变化。

  • assign:只针对纯量类型 CGFloat、NSInteger等简单的赋值使用。
  • strong:会导致传入数据的引用计数加一。当前类会持有该属性
  • weak:不会增加属性的引用计数,当前类不持有该属性。
  • unsafe_unretained:该特性与weak的区别在于,对象释放时属性的值不会清空。
  • copy:与strong的区别在于,copy特性会直接copy一份setter方法传进来的值,而不是直接持有该值。copy特性经常用于保护属性的封装性。例如NSString类型的属性,外部传过来的是NSMutableString类型,这时如果不copy一份不可变的数据 ,属性的值就有可能被外部修改。

方法名:getter= 指定一个自定义的getter方法,例如

1
@property (nonatomic, getter = isOn) BOOL on;

setter=指定一个自定义的设置方法,这种用法不太常见。

6、直接使用属性的实例变量要比调用属性的getter/setter方法效率高。

7、NSObject 中定义的isEqual:方法要比在子类中定义的isEqualXXXX:方法慢。
例如 NSString调用 isEqualToString:方法比调用isEqual:快,因为isEqual:还需要检测对象类型。

8、hash在collection(集合)中作为对象的索引,对象相等时其hash值一定相等,相反hash相等并不一定对象就相等。在重写hash函数时,一定要满足此条件。为了让hash能起到索引作用,又不会带来额外的性能代价,一般使用子类的hash进行与操作。
可变对象放入collection(集合)中后,其hash就不能再次改变。因为在对象会根据hash索引放入不同的“箱子数组”中,这时如果改变了hash值,那对象存放的位置就会变成错误的。这时要保证对象的hash值是根据其不可变部分生成的。

9、数据库对象判断相等可以简化为判断属性 identifier相等,identifier在数据库中作“主键”(primary key)

10、NSArrayNSMutableArrayNSDictionaryNSMutableDictionary都是个类簇,通过其方法创建的对象真正类型是一些内部类型,可以使用[obj class]打印真正的类型。

11、用obj_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)方法给对象添加动态属性。
objc_AssociationPolicy@property等价特性

关联类型 等效的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

12、objc_msgSend是objective-C语言实现的最核心方法。所有的objective-C的消息发送最终都是转换为了objc_msgSend进行传递。其函数原型如下
<return_type> Class_selector(id self, SEL _cmd, …)

objc_msgSend根据对象所属类搜寻其方法列表(list of methods)。如果能找到与selector名字相同的方法,就跳转至实现代码。找不到则沿着继承体系继续向上查找。如果最终找不到相符合的方法,就执行“消息转发”(message forwarding)。

objc_msgSend将匹配结果缓存至“快速映射表”(fast map),下次再向该类发送改方法,会直接从快速映射表中查找,提高查询效率。

13、消息转发机制。
消息转发分为两大阶段。第一阶段询问所属类是否能动态添加方法,处理这个unknown selector,此过程叫“动态方法解析”(dynamics method resolution)。第二阶段涉及“完整的消息转发机制”(full forwarding mechanism),首先会询问是否有其他对象可以处理这条消息,若存在备援接收者(replacement receiver),则将消息转发给备援接收者,否则启动完整的消息转发机制。
消息转发全流程

14、类对象
Objective-C中的每个实例对象都是指向某块内存数据的指针。通用数据类型id本身就是指针。
id类型的定义:

1
2
3
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

Class对象的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
}

此结构体存放的“元数据”(metadata)。例如类的实例实现了几个方法。具备多个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身也是Objective-C对象。
super_class:结构体里还有个变量叫做super_class,它定义了本类的超类。
isa:类对象所属的类型(也就是isa指针所指向的类型)是另一个类,叫做“元类”(metaclass),用来表达类对象本身所具备的元数据。“类方法”就定义在isa中,这些方法可以理解为isa的实例方法。每个类只有一个isa,每个isa仅有一个与之相关的“元类”。

假设有个名为ComeClass的字类从NSObject中继承而来,则其继承体系如下图:

super_class指针确立了继承关系,isa指针描述了实例所属的类。

15、-(id)initWithCoder:(NSCoder *)decoder 方法通过“解码器”(decoder)将对象数据解压缩,例如:

1
2
3
4
5
6
7
-(id) initWithCoder:(NSCoder *)decoder
{
If ([self = [super initWithCoder:decoder]]){
_property = [decoder decodeFloatForKey:@"width"];
}
return self;
}

16、利用description实现NSLog打印对象的自定义信息,在NSLog一个自定义对象时,obj对象会收到description消息,返回的描述信息将输出到console中。系统类重写了description类,所以在NSLog中可以看到比较详细的输出数据,自定义的Class需要用户重写description方法,以便能打印真正用户的信息。例如:

1
2
3
4
-(NSString *)description
{
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",self.class, self, _firstName, _lastName];
}

上面的信息打印了对象对应的类,对象地址,和包含的属性信息。

17、为了更好的区分私有方法和共有方法,可以给私有方法添加p_前缀,p表示“private”

18、NSCopyingNSMutableCopying协议
在对象调用copy方法时,真正进行复制的方法是copyWithZone:(NSZone *)zone, copy方法只是使用了默认的zone来调用copyWithZone:。在一些自定义的类中,如果要实现copy方法,需要声明该类遵从NSCopying协议,并重写copyWithZone:方法。例如:

1
2
3
4
5
6
7
8
9
@interface EOCPerson : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
- (id)initWithFirstName:(NSString *)firstName
andLastName:(NSString *)lastName;
@end

然后,实现协议中规定的方法:

1
2
3
4
5
- (id)copyWithZone:(NSZone *)zone{
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName
andLastName:_lastName];
return copy;
}

如果要拷贝一份可变数据,比如 [NSArray mutableCopy];则需要重写mutableCopyWithZone:方法。
Foundation框架中所有collection类默认 情况下都是执行浅拷贝,只拷贝容器本身,不复制其中数据。原因是容器中的对象不一定支持拷贝,而调用者也未必想要拷贝容器数据。

19、在循环语句中使用autoreleasepool降低内存峰值,如果在for循环中创建了占用大量内存的临时变量,因为自动释放池要等线程执行下一次事件循环时才会清空。在for循环过程中内存无法释放,就会导致内存持续上涨。在for循环中嵌套 autoreleasepool使用,在每次循环结束,都会把分配的临时内存回收。如下:

1
2
3
4
5
6
7
8
9
NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray array];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}

20、block其实是objective-C对c函数指针的封装。block本身也是对象。
块的内部结构
每个Objective-C对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大有小。块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa(参见第14条)。其余内存里含有块对象正常运转所需的各种信息。下表详细描述了块对象的内存布局。

在内存布局中,最重要的就是invoke变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void*型的参数,此参数代表块。刚才说过,块其实就是一种代替函数指针的语法结构,原来使用函数指针时,需要用“不透明的void指针”来传递状态。而改用块之后,则可以把原来用标准C语言特性所编写的代码封装成简明且易用的接口。

descriptor变童是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丟弃块对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,而后者则将之释放。

块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke函数为何需要把块对象作为参数传进来呢?原因就在于,执行块时,要从内存中吧这些捕获到的变量读出来。

21、利用GCD 栅栏(barrier)实现同步锁功能。
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block block); void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block block);
在队列中,栅栏快必须单独执行,不能语其他块执行。并发队列如果发现接下来要处理的块是栅栏块(barrier block),那么就一直等当前所有并发块执行完毕,才会单独执行这个栅栏块。栅栏块执行过后,在按正常方式继续向下处理。代码实现可以参见:FileCache

这里使用dispatch_barrier_sync代替dispatch_barrier_aync会让setSomeString:方法效率更高,原因是dispatch_barrier_sync会在执行完当前操作后,才会插入后续要添加到queue中代码。
dispatch_barrier_syncdispatch_barrier_aync的区异同:
共同点:
1、等待在它前面插入队列的任务先执行完
2、等待他们自己的任务执行完再执行后面的任务
不同点:
1、dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们
2、dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。

本文作者: ctinusdev
本文链接: https://ctinusdev.github.io/2017/07/30/EffectiveObjcLearn
转载请注明出处!

坚持原创技术分享,您的支持将鼓励我继续创作!