《Effective Objective-C 20 编写高质量i

2018-12-25  本文已影响82人  我才是臭吉吉

第1章:熟悉Objective-C

第4条:多用类型常量,少用#define预处理指令

// 在ViewController.h中声明通知名
/** ViewController视图已加载通知 */
extern NSString * const ViewControllerViewDidLoadNotification;

// 在ViewController.m中赋值通知名
NSString *const ViewControllerViewDidLoadNotification = @"ViewControllerViewDidLoadNotification";
- **局部常量**(类内使用的):*使用static开头,“k”作为常量前缀,const修饰常量名,在实现文件中声明及赋值*。这种局部常量(static + const)的优点是:**编译器不会创建符号,会像#define一样,把遇到的所有变量直接替换,但是带有类型信息**。
/** 动画时长 */
static const NSTimeInterval kAnimationDuration = 0.3;

第5条:用枚举表示状态、选项、状态码

凡是需要以按位或操作来组合的枚举都应使用NS_OPTIONS定义。若是枚举不需要相互组合,则应使用NS_ENUM来定义。

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,
    UIViewAnimationTransitionFlipFromLeft,
    UIViewAnimationTransitionFlipFromRight,
    UIViewAnimationTransitionCurlUp,
    UIViewAnimationTransitionCurlDown,
};

第2章:对象、消息、运行期

第7条:在对象内部尽量直接访问实例变量

  • 在对象内部读取数据时,应该直接通过实例变量来读;而写入数据时,则应通过属性来写。
  • 在初始化和dealloc方法中,总应该直接通过实例变量来读写数据。
  • 有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。

第8条:理解“对象等同性”这一概念

- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
// 协议里hash为只读属性: @property (readonly) NSUInteger hash;

第11条:理解objc_msgSend的作用

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

第3章 接口与API设计

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

第16条:提供“全能初始化”方法

// 父类:矩形类

// EOCRectangle.h
@interface EOCRectangle: NSObject

@property (nonatomic, assign, readonly) float width;
@property (nonatomic, assign, readonly) float height;

- (id)initWithWidth:(float)width 
         andHeight:(float)height;
         
@end

// EOCRectangle.m
@implementation EOCRectangle

/** designed initializer */
- (id)initWithWidth:(float)width 
         andHeight:(float)height {
    if (self = [super init]) {
        _width = width;
        _height = height;
    }
    return self;
}

/** overrided initializer */
- (id)init {
    return [self initWithWidth:5.0f andHeight:10.0f];
}

@end

// 子类:正方形类

// EOCSquare.h
@interface EOCSquare: EOCRectangle

- (id)initWithDimension:(float)dimension;
         
@end
// EOCSquare.m
@implementation EOCSquare
/** designed initializer */
- (id)initWithDimension:(float)dimension {
    return [super initWithWidth:dimension height:dimension];
}

/** overrided initializer */
- (id)initWithWidth:(float)width 
         andHeight:(float)height {
    float dimension = MAX(width, height);
    return [self initWithDimension:dimenson];
}

@end

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

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

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

第4章 协议与分类

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

@porotocol DownloadProtocol: NSObject
...
@optional
- (void)onDownloadingProgress:(CGFloat)progress;
...
@end

...
#import "DownloadProtocol.h"

@interface MyClass: NSObject

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

@end

@interface MyClass () {
    // 声明结构体变量成员
    struct {
        NSInteger onDownloadProgress: 0
    } _downloadProtocolFlag;
}
@end

@implementation MyClass: NSObject 

// 手动实现delegate的setter,对代理对象的方法实现情况进行检查并缓存
- (void)setDelegate:(id<DownloadProtocol>)delegate {
    _delegate = delegate;
    // 对所有optional方法依次检查,这里只是一个
    _downloadProtocolFlag.onDownloadProgress = [_delegate respondsToSelector:@selector(onDownloadProgress:)]; 
}

// 实际调用时,即可简化判断,提高效率
- (void)testMyDownloadProgress:(CGFloat)progress {
    if (_downloadProtocolFlag.onDownloadProgress) {
    [self.delegate onDownloadProgress: progress];
}
@end

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

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

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

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

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

第5章 内存管理

第29条:理解引用计数

在手动管理内存模式下,内存管理语义为“strong”的属性,其setter一般都是如此实现的:

- (void)setFoo:(id)foo {
    // 1.保留新值
    [foo retain];
    // 2.释放原有值
    [_foo release];
    // 3.赋值
    _foo = foo
}

其中,为何1和2步骤不能交换呢?
因为,当使用重复的foo对象进行设置时(假设foo对象只被本类实例保留),先释放,会导致foo对象内存被系统回收。致使setter失败【使用ARC即可避免此问题发生】。

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

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

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

// 获取待释放对象所属的类
Class cls = object_getClass(self);

// 获取类名
const char *clsName = class_getName(cls);

// 生成僵尸类名
const char *zombieClsName = "_NSZombie_" + clsName;

// 查看是否存在此类
Class zombieCls = objc_lookUpClass(zombieClsName);

// 不存在,则创建此类
if (!zombieCls) {
    // 获取名为“_NSZombie_”的样板类
    Class baseZombieCls = objc_lookUpClass("_NSZombie_");
    
    // 复制样板类来创建此类
    zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}

// 正常释放实例
objc_destructInstance(self);

// 将实例所属类指向新的僵尸类(替换isa指向)
objc_setClass(self, zombieCls);

// 现在,self即为“_NSZombie_原始类名”的实例了
// 获取对象类名
Class cls = objc_getClass(self);

// 查看是否为”僵尸类“
if (String_has_prefix(clsName, "_NSZombie_")) {
    // 是,则对象是”僵尸对象“
    
    // 获取原始类名(去掉前缀)
    const char *originClsName = substring_from(clsName, 10);
    
    // 获取消息的选择器名
    const char *selectorName = sel_getName(_cmd);
    
    // 输出消息
    Log("*** -[%s %s]: message sent to deallocated instance %p", originClsName, selectorName, self);
    
    // 结束程序
    abort();
}

第6章 块与大中枢派发

第37条:理解“块”这一概念

Block
void* isa 指向Class对象的指针
int flags
int reserved
void (*)(void *, ...) invoke 实现函数的指针
struct * descriptor 块的描述信息
捕获到的外部变量...
  1. isa指针,指向的是Block的类型。Block分为三种类型:_NSConcreteStackBlock,_NSConcreteMallocBlock和_NSConcreteGlobalBlock。其中,_NSConcreteStackBlock分配在占内存上;_NSConcreteMallocBlock分配在堆内存上,有引用计数(即为对象),会捕获外部变量;_NSConcreteGlobalBlock不捕获变量(内部不使用外部变量的Block即为全局Block),内存在编译期即可确定,相当于单例。
  2. 其中,invoke实现函数的参数为Block结构体实例的指针,使用它可以方便地从内存中读取出捕获到的变量
  3. 捕获到的变量,对于对象,只是拷贝了其指针(对象的引用计数+1)
descriptor
unsigned long int reserved
unsigned long int size
void (*)(void *, void *) copy 拷贝辅助函数
void (*)(void *, coid *) dispose 释放辅助函数

描述结构体中,copy和dispose函数的作用是拷贝和释放Block实例时,对捕获到的变量进行拷贝和释放操作。

第39条:用handler块降低代码分散程度

第41条:多用派发队列,少用同步锁

我们知道,在iOS端,为了保证性能,类中property的内存语义一般被设置为“nonatomic”。但是极端情况下,为了保证属性的读写为原子操作,需要单独进行处理。一般来说,可以使用synchronized关键字对self进行同步加锁,或者使用NSLock进行锁操作,但是频繁读写时效率会很低。
为了保证效率,使用GCD的派发队列进行property的读写操作优化:

// TestClass.h
@interface TestClass : NSObject

@property (nonatomic, strong) NSString *testName;

@end

// TestClass.m
@interface TestClass () {
    NSString *_testName;
    dispatch_queue_t _propertyQueue;
}
@end

@implementation
- (instancetype)init {
    if (self = [super init]) {
        // 创建并发队列
        _propertyQueue = dispatch_queue_create("com.TestClass.p_queue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}
- (NSString *)testName {
    // 并发队列读取(使用async是因为需要同步返回函数值,实质上也是并发执行)
    __block NSString *tmpValue;
    dispatch_sync(_propertyQueue, ^{
        tmpValue = self->_testName;
    });
    return tmpValue;
}

- (void)setTestName:(NSString *)testName {
    // 使用barrier,可以保证此处为原子操作(其余操作等待完成后才开始执行)
    dispatch_barrier_async(_propertyQueue, ^{
        self->_testName = testName;
    });
}
@end

注意

对于property,getter使用自定义并发队列的async操作,setter使用自定义并发队列的barrier操作。

第42条:多用GCD,少用performSelector系列方法

除了performSelector系列方法的API局限性以外(不能传多于两个参数、线程相关API参数过少、参数只能为对象类型、不能调用c方法等),最重要的是内存管理方面的缺失。示例如下:

SEL selector;
if (/** 条件1 */) {
    selector = @SEL(newObject);
} else if (/** 条件2 */) {
    selector = @SEL(copy);
} else {
    selector = @SEL(someProperty)
}
// object为类实例,包含newObject构造器、copy方法和someProperty属性
id ret = [object performSelector:selector];

在ARC环境下,如果是条件1和2,编译器会将执行后返回对象的内存管理权交给接收者;如果是直接设置属性,则不进行内存管理。由于现在selector的选择是在运行时进行绑定,编译器就无法根据方法签名(名称、参数及返回值类型)使用ARC进行内存管理了(甚至不知道改selector是否存在)。所以,这种写法下,编译器会提示警告,可能会发生内存泄漏。
使用Block配合GCD的相关方法可以有效解决这些问题,如参数、线程、内存管理等。

第43条:掌握GCD及操作队列的使用时机

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

在后台自动执行一系列任务,完成后通知主线程

// 对象数组,这里是模拟多个需要执行任务的对象
NSArray *objects = @[...];

// 后台执行,需要获取并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 一系列任务,可以编写成组
dispatch_group_t group = dispatch_group_create();

// 自动执行:异步加入组中,不能阻塞执行队列
for (id object in objects) {
    dispatch_group_async(
        group, 
        globalQueue,
        ^{ [object performTask]; }
    );
}

// 完成后通知主线程
dispatch_group_notify(
    group,
    dispatch_get_main_queue(),
    ^{
        // 主线程执行代码
    }
);

主要是利用了GCD并发队列的强大功能,GCD会根据系统资源占用情况,自动分配CPU核心和不同数量的执行线程去并发执行任务,可以最大限度的优化多线程编程性能。通过dispatch_group_t对象,可以根据需要阻塞(可以使用dispatch_group_wait)执行队列或是监控任务的执行过程。

第46条:不要使用dispatch_get_current_queue

首先结论是:dispatch_get_current_queue对避免代码死锁没有任何作用,因为它返回的只是当前队列的名称,而不是当前执行任务所在的队列!!!

我们以队列层级的例子一步步进行验证:

  1. 父队列为串行队列rootQueue;子队列有两个,分别为串行队列serialQueue和并行队列concurrentQueue。我们分别向两个子队列中派发任务,看一下实际的执行情况:
// 根队列
const void *rootQueueKey = "com.jiji.rootQueue";
dispatch_queue_t rootQueue = dispatch_queue_create(rootQueueKey, DISPATCH_QUEUE_SERIAL);

// 创建两个队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.jiji.concurrent1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("com.jiji.serial1", DISPATCH_QUEUE_SERIAL);

// 设置两个子队列的根队列为rootQueue(串行)
dispatch_set_target_queue(concurrentQueue, rootQueue);
dispatch_set_target_queue(serialQueue, rootQueue);

// 配置任务:
// 并行子队列异步派发两个任务
dispatch_async(concurrentQueue, ^{
    NSLog(@"task1");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"task2");
});
// 串行子队列异步派发两个任务
dispatch_async(serialQueue, ^{
    NSLog(@"task3");
});
dispatch_async(serialQueue, ^{
    NSLog(@"task4");
});

运行结果如下:

TestIos[32776:25904922] task1
TestIos[32776:25904922] task2
TestIos[32776:25904922] task3
TestIos[32776:25904922] task4

以上情况表明:不管子队列是串行还是并行队列,由于根队列为串行,最终任务的执行情况为串行执行。

  1. 现在,我的疑问是,由于任务串行执行,是否证明真正执行任务的队列是串行队列(根队列)?带着这个疑问,我们先验证一下:
    我们修改一下代码,在task1的执行块中,查看一下当前队列:
dispatch_async(concurrentQueue, ^{
    NSLog(@"task1");
    NSLog(@"%@", dispatch_get_current_queue());
});

执行结果:

TestIos[32776:25904922] <OS_dispatch_queue: com.jiji.concurrent1>

看来不是,还是在并发队列这个子队列中。
那现在做个假设,如果在此任务中,再向其根队列派发一个同步任务进行验证(如果死锁,证明实际任务运行在根队列中;否则就在当前子队列中):

dispatch_async(concurrentQueue, ^{
    NSLog(@"task1");
    NSLog(@"%@", dispatch_get_current_queue());
    // 添加同步任务
    dispatch_sync(rootQueue, ^{
        NSLog(@"new task!!!");
    });
});

结果很显然,发生了死锁,证明实际上此任务是在根队列(target queue)中执行(向serialQueue中派发同步任务也会如此,虽然不会死锁,但线程一直等待,永远不会返回)。

这也就证明了,实际上dispatch_get_current_queue方法并没有返回真正的运行队列**。如果以此API返回值进行判断,则无法保证多线程环境下代码执行的准确性。

  1. 那若是如此,如何来确保任务执行在正确的队列中?使用dispatch_queue_set_specficdispatch_get_specific两个API即可。我们修改一下示例代码:
// 根队列
const void *rootQueueKey = "com.jiji.rootQueue";
dispatch_queue_t rootQueue = dispatch_queue_create(rootQueueKey, DISPATCH_QUEUE_SERIAL);
// 根队列用指定key进行标记
dispatch_queue_set_specific(rootQueue, rootQueueKey, &rootQueueKey, NULL);

// 创建两个队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.jiji.concurrent1", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t serialQueue = dispatch_queue_create("com.jiji.serial1", DISPATCH_QUEUE_SERIAL);

// 设置两个子队列的根队列为rootQueue(串行)
dispatch_set_target_queue(concurrentQueue, rootQueue);
dispatch_set_target_queue(serialQueue, rootQueue);
// 配置任务:
dispatch_async(concurrentQueue, ^{
    NSLog(@"task1");
    
    // 根据key获取队列绑定的值(存在,则当前队列即为key所对应的队列)
    void *context = dispatch_get_specific(rootQueueKey);
    if (context) {
        // 当前执行在根队列上,不可以向根队列及任何子队列派发同步任务
        NSLog(@"NO!!!!!!");
        // 但是,调用dispatch_get_current_queue()返回的队列,可以让你误以为是在本队列执行,所以可能会向其他队列派发同步任务,继而发生死锁。这也就是绝对不要使用此API的原因。
        NSLog(@"%@", dispatch_get_current_queue());
    } else {
        // 可以派发任意任务
        NSLog(@"OK~");
        dispatch_sync(rootQueue, ^{
            NSLog(@"new task!!!");
        });
    }
});

如代码所示,当dispatch_get_specific返回对应的数据时,证明当前运行队列即为检查的目标队列。如果此时还需要向此队列派发同步任务,只要直接执行任务即可,无需派发。

第7章 系统框架

第48条:多用块枚举,少用for循环

  1. 快速枚举(for...in):

    • 比传统for循环更高效,与NSEnumerator一样但语法更简洁。
    • 可以遍历如NSArray、NSDictionary、NSSet及自定义Collection(需遵循NSFastEnumeration协议)。
    • NSEnumerator由于也遵循NSFastEnumeration协议,所以可以支持用快速枚举对集合进行反向遍历。
    • 缺点是不支持获取对象索引。
  2. 使用集合带有Block参数的遍历API进行集合遍历:

    • 方便获取对象索引及字典键值
    • 可以直接修改Block的参数类型为对象类型,利用编译器特性,省去了进行显示地类型转换
    • 对于可以配置NSEnumerationOptions的版本,可以方便设置如反向遍历、并发遍历(底层使用GCD队列)等功能。

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

  1. 主要先说一下OC对象和CF变量指针的转换方式:

    • __bridge: 互相转换均可,不进行内存所有权转换。即转换后仍然使用原系统对对象或变量进行内存管理(OC使用ARC,CF手动使用CFRelease)
    • __bridge_transfer:一般用于CF->OC的过程中,转换所有权。转换后的OC对象,系统自动使用ARC对其进行内存管理。
    • __bridge_retained:一般用于OC->CF的过程中,转换所有权。转换后的CF变量,其指针的引用计数+1,需要使用CFRelease等函数进行手动内存管理。
  2. 可以通过CF框架,使用C语言API创建集合对象,之后利用桥接转换为OC对象,即可得到符合自定义内存管理语义的集合对象。

    • 如使用NSDictionary时,需要key无需支持NSCopy协议,则可以使用此方法,创建CFDictionaryRef指针(在CFDictionaryRetainCallBack和CFDictionaryReleaseCallBack中进行修改)后,使用__bridge_transfer转换为OC对象并转换所有权。

第51条:精简initialize与load的实现代码

  1. +(void)load:

自己只在Category中利用load方法,swizzle过所属Class的方法,在内部实现自定义功能(如记录日志)。

  1. +(void)initialize:

正确用法举例:

  1. 在initialize中初始化声明为全局static的OC对象(由于在编译期只能初始化基本数据类型或NSString变量或常量)。
  2. 单例类可以用于初始化内部数据。

第52条:别忘了NSTimer会保留其目标对象

举例来说,以常用的直接在当前runloop中配置计时器“scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:”为例:

@interface EOCClass: NSObject {
    NSTimer *_pollTimer;
}
- (void)startPolling;
- (void)stopPolling;
@end

@implementation EOCClass
- (void)dealloc {
    [_pollTimer invalidate];
}

- (void)stopPolling {
    [_pollTimer invalidate];
    _pollTimer = nil;
}

- (void)startPolling {
    _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
        target:self
        selector:@selector(p_doPoll)
        userInfo:nil
        repeat:YES
    ];
}

- (void)p_doPoll {
    // ...task
}
@end

以上代码可以很明显地看出,由于_pollTimer的target是self,即NSTimer保留了EOCClass实例,且_pollTimer是EOCClass的实例变量,在ARC下隐含为强引用的内存管理方式,最终导致了引用循环。

解决方法:
由于类对象本身即为单例,使用类对象作为NSTimer的target即可巧妙“避免”此问题发生。

这里使用了NSTimer的Category进行处理,免除使用第三方类或者自定义单例对象:

@interface NSTimer (EOCBlocksSupport)
+ (NSTimer *)scheduledTimerWithTimerInterval:(NSTimeInterval)interval
    block:(void (^)())block
    repeats:(BOOL)repeats;
@end

@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer *)scheduledTimerWithTimerInterval:(NSTimeInterval)interval
    block:(void (^)())block
    repeats:(BOOL)repeats {
    
    return [self scheduledTimerWithTimerInterval:interval
        target:self
        selector:@selector(eoc_blockInvoke:)
        userInfo:[block copy]
        repeats:repeats
    ];
}

+ (void)eoc_blockInvoke:(NSTimer *)timer {
    // 取出timer中设置的userInfo,并转换为block对象
    void (^block)() = timer.userInfo;
    if (block) {
        // 执行
        block();
    }
}
@end

配置定时器时,按如下方式:

- (void)startPolling {
    // 声明self的弱引用指针(使Block对象通过弱指针保留对象,引用计数不变)
    __weak EOCClass *weakSelf = self;
    _pollTimer = [NSTimer scheduledTimerWithTimerInterval:5.0
    block:^{
        // 声明强指针指向weakSelf,可防止使用时对象被释放
        __strong EOCClass *strongSelf = weakSelf;
        [strongSelf p_doPoll];
    }
    repeats:YES];
}

如上所示,

注意:NSTimer类在iOS10中新增了带有Block参数的API,不过使用时依然要注意循环引用的问题。

上一篇下一篇

猜你喜欢

热点阅读