iOS程序员修炼我爱编程

《Effective Objective-C 2.0》读书/实战

2018-08-05  本文已影响5人  极客学伟

<p align='center'>
<img src='http://p95ytk0ix.bkt.clouddn.com/2018-07-31-8726ab1532ca52746711381b07cc9971.jpg'>
</p>

《Effective Objective-C 2.0》读书/实战笔记 二

第3章:接口与API设计

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

顾名思义就是说在自己开发的类需要加前缀,iOS程序员开发工程师普遍使用双字母的前缀,就像我在开发时习惯加前缀 XW,其实,这是不科学的,因为苹果爸爸公司保留使用所有“两字母前缀”的权利,所以自己的前缀应该是三个字母的,不仅仅是类名,还有分类、全局变量...

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

举一个生动形象的例子:
Chinese 类

//.h
//  中国人
#import <Foundation/Foundation.h>
@interface Chinese : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, assign, readonly) NSUInteger age;
/// 全能初始化对象方法
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 全能初始化类方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 其他初始化对象方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end

//.m
#import "Chinese.h"
@interface Chinese()
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Chinese
/// 全能初始化函数-只有全能初始化函数才能进行赋值操作
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    if (self = [super init]) {
        self.firstName = firstName;
        self.lastName = lastName;
        self.age = age;
    }
    return self;
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    Chinese *people = [[self alloc] initWithFirstName:firstName lastName:lastName age:age];
    return people;
}
- (instancetype)init {
    return [self initWithFirstName:@"龙的" lastName:@"传人" age:1]; // 调用指定初始化函数赋予其默认值
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
    return [self chineseWithFirstName:firstName lastName:lastName age:1];
}
@end

Student 类继承自 Chinese

//.h
//  中国学生
#import "Chinese.h"
@interface Student : Chinese
@property (nonatomic, strong, readonly) NSArray *homework;
/// 指定初始化函数-需直接调用父类初始化函数
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 指定初始化类方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework;
@end

//.m
#import "Chinese.h"
@implementation Student {
    NSMutableArray *p_homework;
}
/// 子类重写父类全能初始化函数-更改默认值!
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    return [self initWithFirstName:firstName lastName:lastName age:age homework:@[]];
}
/// 指定初始化函数-需直接调用父类初始化函数
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
    if (self = [super initWithFirstName:firstName lastName:lastName age:age]) {
        p_homework = homework.mutableCopy;
    }
    return self;
}
/// 指定初始化类方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
    return [[self alloc] initWithFirstName:firstName lastName:lastName age:age homework:homework];
}
/// 重写系统初始化方法
- (instancetype)init {
    return [self initWithFirstName:@"祖国的" lastName:@"花朵" age:6 homework:@[]];
}
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework {
    return [self studentWithHomework:homework];
}
@end

🇦🇴 第17条:实现 description 方法

若直接打印自定义对象,控制台仅仅是显示该对象的地址,不会显示对象的具体细节,在程序开发中对象指针的地址或许有用,但大多数情况下,我们需要得知对象内部的具体细节,所以OC提供了 description 方法可以实现。

@interface Chinese()
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Chinese
- (NSString *)description {
    return [NSString stringWithFormat:@"<%@ : %p, %@>",[self class],self,
            @{
              @"firstName":_firstName,
              @"lastName" :_lastName,
              @"age": @(_age)
              }];
}
@end

这种使用字典输出各属性或成员变量内存的方式比较好,若之后需要增删属性直接修改字典的键值对就可以了。
另外 debugDescription 方法是在控制台使用 po 命令打印对象信息所调用的方式,若已经实现 description 方法, 可不覆写 debugDescription 方法,因为系统会默认调用 description 方法。

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

在开发自定义类时,在 .h 里声明的属性尽量设置为不可变,只读的属性,外界只能通过特定的方法更改其内容,这对于一个功能的封装性是至关重要的。例如我们之前所声明的 Student 类:

// .h
@interface Student : Chinese
@property (nonatomic, copy, readonly) NSString *school;
@property (nonatomic, strong, readonly) NSArray *homework;

- (void)addHomeworkMethod:(NSString *)homework;
- (void)removeHomeworkMethod:(NSString *)homework;
@end

// .m
@interface Student()
@property (nonatomic, copy) NSString *school;
@end
@implementation Student {
    NSMutableArray *p_homework;
}
- (void)addHomeworkMethod:(NSString *)homework {
    [p_homework addObject:homework];
}
- (void)removeHomeworkMethod:(NSString *)homework {
    [p_homework removeObject:homework];
}
- (instancetype)initWithSchool:(NSString *)school homework:(NSArray *)homework {
    if (self = [self init]) {
        self.school = school;
        p_homework = homework.mutableCopy;
    }
    return self;
}
@end

如此定义外界只能通过固定的方法对对象内的属性进行更新,便于功能的封装,减少 bug 出现的概率。
另外使用不可变对象也增强程序的执行效率。

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

就是说在为自己创建的属性、成员变量、方法、协议等起名要见名知意。

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

对于一个写好的类而言,若为公开方法更改名称,则需要在外部调用此类的方法的地方同样做修改,这样比较麻烦,在类内部实现的私有方法不会有这个问题,所以为私有方法加前缀可更好的区分两者。便于后期开发。用何种前缀取决于开发者的开发习惯,不建议使用下划线开头的前缀,因为这是Apple Dad 专属的方式。作者的习惯是私有方法的前缀是 p_ ,例如:

/// 这是一个私有方法
- (id)p_playAirplaneMethod {
    id xx = @"**";
    return xx;
}

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

在项目中可以自定义一个错误类型模型:

//  .h
//  自定义错误类型
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, XWErrorCode) {
    XWErrorCodeUnknow       = -1, //未知错误
    XWErrorCodeTypeError    = 100,//类型错误
    XWErrorCodeNullString   = 101,//空字符串
    XWErrorCodeBadInput     = 500,//错误的输入
};
extern NSString * const XWErrorDomain;
@interface XWError : NSError
+ (instancetype)errorCode:(XWErrorCode)errorCode userInfo:(NSDictionary *)userInfo;
@end

// .m
#import "XWError.h"
@implementation XWError
NSString * const XWErrorDomain = @"XWErrorDomain";
+ (instancetype)errorCode:(XWErrorCode)errorCode userInfo:(NSDictionary *)userInfo {
    XWError *error = [[XWError alloc] initWithDomain:XWErrorDomain code:errorCode userInfo:userInfo];
    return error;
}
@end

在调试程序合适的回调中可传入自定义错误信息。

🇦🇺 第22条:理解 NSCopying 协议

我想让我创建的 1Student 类具备拷贝属性,那我需要实现 NSCopying 协议,实现它仅有的一个 - (id)copyWithZone:(nullable NSZone *)zone 方法。 如下:

@interface Student() <NSCopying>
@end
@implementation Student {
    NSMutableArray *p_homework;
}
#pragma mark - NSCopying
- (id)copyWithZone:(nullable NSZone *)zone {
    Student *stuCopy = [[Student allocWithZone:zone] initWithFirstName:self.firstName lastName:self.lastName age:self.age homework:p_homework.copy];
    return stuCopy;
}

如此在调用 Studentcopy 方法便会生成一个内容相同的不同 Student 对象

Student *stu = [Student studentWithFirstName:@"小极客" lastName:@"学伟" age:6 homework:@[@"小提琴",@"篮球"]];
Student *stu2 = [stu copy];

若希望自定义对象拥有 深拷贝 功能,那需要实现 NSMutableCopying 协议,并实现其唯一的方法
- (id)mutableCopyWithZone:(nullable NSZone *)zone 具体实现如下:

#pragma mark - NSMutableCopying
- (id)mutableCopyWithZone:(nullable NSZone *)zone {
    Student *stuMtableCopy = [[Student allocWithZone:zone] initWithFirstName:self.firstName lastName:self.lastName.mutableCopy age:self.age homework:p_homework.copy];
    return stuMtableCopy;
}

补充一个 Array 和 Dictionary 分别指向浅复制和深复制之后的类型列表:

Array

首先声明两个数组:

NSArray *array = @[@1,@2];
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];

对其进行浅拷贝和深拷贝,打印结果如下:

2018-08-01 11:46:32.255187+0800 XWInterviewDemos[80249:5837261] [array copy]:__NSArrayI
2018-08-01 11:46:32.255337+0800 XWInterviewDemos[80249:5837261] [array mutableCopy]:__NSArrayM
2018-08-01 11:46:32.255431+0800 XWInterviewDemos[80249:5837261] [mutableArray copy]:__NSArrayI
2018-08-01 11:46:32.255516+0800 XWInterviewDemos[80249:5837261] [mutableArray mutableCopy]:__NSArrayM

其中 __NSArrayI 为不可变数组,__NSArrayM 为可变数组,结论:

原类 操作 拷贝结果
NSArray 浅拷贝(copy) 不可变(__NSArrayI)
NSArray 深拷贝(mutableCopy) 可变(__NSArrayM)
NSMutableArray 浅拷贝(copy) 不可变(__NSArrayI)
NSMutableArray 深拷贝(mutableCopy) 可变(__NSArrayM)
Dictionary

首先声明两个字典:

NSDictionary *dictionary = @{@"key":@"value"};
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionary];

对其进行浅拷贝和深拷贝,打印结果如下:

2018-08-01 11:57:20.810019+0800 XWInterviewDemos[80385:5844478] [dictionary copy]:__NSSingleEntryDictionaryI
2018-08-01 11:57:20.810162+0800 XWInterviewDemos[80385:5844478] [dictionary mutableCopy]:__NSDictionaryM
2018-08-01 11:57:20.810277+0800 XWInterviewDemos[80385:5844478] [mutableDictionary copy]:__NSFrozenDictionaryM
2018-08-01 11:57:20.810374+0800 XWInterviewDemos[80385:5844478] [mutableDictionary mutableCopy]:__NSDictionaryM

其中 __NSSingleEntryDictionaryI__NSFrozenDictionaryM 为不可变字典,__NSDictionaryM 为可变字典,结论:

原类 操作 拷贝结果
NSDictionary 浅拷贝(copy) 不可变(__NSSingleEntryDictionaryI)
NSDictionary 深拷贝(mutableCopy) 可变(__NSDictionaryM)
NSMutableDictionary 浅拷贝(copy) 不可变(__NSFrozenDictionaryM)
NSMutableDictionary 深拷贝(mutableCopy) 可变(__NSDictionaryM)

第4章:协议与分类

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

委托与数据源协议我们在使用 UITableView 时经常用到,我们在开发时可仿照其设计模式,将需要的数据通过数据源获取;将执行操作后的事件通过代理回调;并弱引用其代理对象。

@class Chinese;
@protocol ChineseDelegate <NSObject>
@optional
- (void)chinese:(Chinese *)chinese run:(double)kilometre;
- (void)chinese:(Chinese *)chinese didReceiveData:(NSData *)data;
- (void)chinese:(Chinese *)chinese didReceiveError:(NSError *)error;
@end

@interface Chinese : NSObject
// 委托对象-需弱引用
@property (nonatomic, weak) id<ChineseDelegate> delegate;
@end

在对象跑步时,通过代理方法回调给委托对象:

- (void)run {
    double runDistance = 0.0;
    if (self.delegate && [self respondsToSelector:@selector(chinese:run:)]) {
        [self.delegate chinese:self run:runDistance];
    }
}

倘若此方法每分钟都会调用 成百上千次,每次都执行 respondsToSelector 方法难免会对性能有一定影响,因为除第一次有效外其余都是重复判断,所以我们可以将是否能够响应此方法进行缓存!如例所示:

#import "Chinese.h"
@interface Chinese() {
    /// 定义一个结构体拥有三个位段,分别存储是否实现了三个对应的代理方法
    struct {
        unsigned int didReceiveData     : 1;    //是否实现 didReceiveData
        unsigned int didReceiveError    : 1;    //是否实现 didReceiveError
        unsigned int didRun             : 1;    //是否实现 run
    }_chineseDelegateFlags;
}
@end
@implementation Chinese
/// 重写 Delegate 方法,为 位段进行赋值
- (void)setDelegate:(id<ChineseDelegate>)delegate {
    _delegate = delegate;
    _chineseDelegateFlags.didRun = [delegate respondsToSelector:@selector(chinese:run:)];
    _chineseDelegateFlags.didReceiveData = [delegate respondsToSelector:@selector(chinese:didReceiveData:)];
    _chineseDelegateFlags.didReceiveError = [delegate respondsToSelector:@selector(chinese:didReceiveError:)];
}
/// 在调用delegate 的相关协议方法不再进行方法查询,直接取结构体位段存储的内容进行调用
- (void)run {
    double runDistance = 0.0;
    if (_chineseDelegateFlags.didRun) {
        [self.delegate chinese:self run:runDistance];
    }
    ///if (self.delegate && [self respondsToSelector:@selector(chinese:run:)]) {
    ///    [self.delegate chinese:self run:runDistance];
    ///}
}

若代理方法可能回调多次,那此项优化将大大提升程序运行效率!

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

在开发一个类一般将所有的代码都放在一起,即便都是高聚合低耦合的代码,若程序越来越大,难免也会感觉不优雅,优雅的方式是按照功能将实现抽离到不同的分类中实现,在主类中引入其分类,直接调用分类中实现的方法。这样也便于管理。
根据分类的名称,可快速定位代码所属功能区,便于扩展维护。另外可创建一个所开发类名对应的 Private 分类,存放一些私有方法。这些方法无需暴露给外界,开发者自己维护。

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

分类中所实现的方法最终会在编译时加载到本类的方法列表中,若存在相同名称的方法,后编译的分类会覆盖前编译的,所以为分类中的方法加前缀是很有必要的。

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

原本分类中声明属性仅仅是自动生成该属性 getter 方法和 setter 方法的声明,不会生成成员变量和对应属性的getter 方法和 setter 方法
虽然 可以使用 runtime 的关联对象的方式为分类添加属性 getter 方法和 setter 方法的实现,使得分类能够定义属性。
但是分类的本质在于扩展类的功能,而非封装数据。使用上述方式需要写大量相似的代码,并且在内存管理上容易出错,改动属性的类型需要改变关联对象的相关类型,不利于维护,代码不优雅!

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

例如:

// .h  对外声明为只读,防止外界随意修改
@interface Chinese : NSObject
@property (nonatomic, weak) id<ChineseDelegate> delegate;
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end

// .m 对内声明为可读写。使用扩展声明一些外界不得而知的私有成员变量
@interface Chinese() <NSCopying> {
    struct {
        unsigned int didReceiveData     : 1;    //是否实现 didReceiveData
        unsigned int didReceiveError    : 1;    //是否实现 didReceiveError
        unsigned int didRun             : 1;    //是否实现 run
    }_chineseDelegateFlags;
    
    NSString *p_girlFriend; 
}
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end

或者在 “class-continuation 分类” 里面声明 C++ 对象,这样 .m 编译为 .mm 文件,对外界暴露依然是纯正的 OC 接口,在 Foundation 框架中,经常使用此策略。

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

第5章:内存管理

🇵🇸 第29条:理解引用计数

何为引用计数,用一张图表示便是:

Snip20180801_8
图转自 《Objective-C高级编程+iOS与OS+X多线程和内存管理》图1.3

看完此图差不多已经理解引用计数了,OK,本条完结。。。

另外,补充一个概念-自动释放池
使用自动释放池可使对象的生命周期跨越 “方法调用边界”后存活到 runloop 的下一次事件循环。

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

ARC会以一种安全的方式设置:先保留新值,再释放旧值,最后设置实例变量,其中可以使用以下修饰符改变局部变量和实例变量的语义:

__strong 默认语义,保留此值
__unsafe_unretained 不保留此值,这么做可能不安全,因为等再次使用变量时,其对象可能已经回收了
__weak 不保留此值,但是变量可安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空 - 可避免循环引用
__autoreleasing 把对象“按引用传递”给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放

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

dealloc 方法是对象释放所调用的方法,此时若使用对象的成员变量可能已经被释放掉了,若使用异步回调时自身已经被释放,若回调中包含 self 会导致程序崩溃。

- (void)dealloc {
    // 移除通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    // 释放需手动释放的资源
    //CFRelease(coreFoundationObject);
}

另外 即便对象释放,在极个别情况下并不会调用 dealloc 方法,程序终止时一定会调用的是在 application delegate 的 - (void)applicationWillTerminate:(UIApplication *)application 方法, 若一定要清理某些对象,可在此方法中处理。

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

🇧🇾 第33条:以弱引用避免保留环

若两个对象互相引用,会形成保留环(循环引用),如图:


Snip20180802_9

保留环会引起内存泄露,对象间的互相持有导致保留环内的所有对象均无法正常释放。
避免保留环最佳方式是弱引用,通过“非拥有关系”的声明将环打破。这种关系可用 weakunsafe_unretained 实现。两者的区别是 weak 修饰的对象在释放之后本身会置 nil, 而 unsafe_unretained 不会,在对象释放之后会依然指向被释放的那块内存。如图:

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

 __weak typeof(self) weakSelf = self;
[NSTimer xw_timerTimeInterval:1.0 block:^{
    [weakSelf timerMethod];
} repeats:YES];

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

主线程和GCD机制中的线程默认都会有自动释放池,无需程序员手动创建,并且系统会自动在 runloop 的执行下次时间循环时将池内对象清空。
如果在一个大的循环体中需要创建n多个对象时,使用 “自动释放池块” 可降低内存峰值,如例所示:

未做优化的方式:

- (void)testFor1 {
    NSMutableArray *arrayM = [NSMutableArray array];
    for (int i = 0; i < 100000; i++) {
        NSString *str = [NSString stringWithFormat:@"%d",i];
        [arrayM addObject:str];
        NSLog(@"%@",str);
    }
}

此时内存使用情况:


Snip20180802_13

CPU使用情况:


Snip20180802_14

使用 “自动释放池块” 优化的方式:

- (void)testFor2 {
    NSMutableArray *arrayM = [NSMutableArray array];
    for (int i = 0; i < 100000; i++) {
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"%d",i];
            [arrayM addObject:str];
            NSLog(@"%@",str);
        }
    }
}

优化后内存使用情况:


Snip20180802_15

优化后CPU使用情况:


Snip20180802_16

显而易见根据Xcode 显示:并没有什么卵用,此条原理上是可以降低内存占用峰值,但实际情况确实两者没有太大区别,能否起到优化的作用还需日后继续观察...

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

在Xcode 中勾选 Zombie Objects 可启用僵尸对象检测,此时给僵尸对象发送消息将会在控制台打印相关信息:


Snip20180802_1

🇲🇵 第36条:不要使用 retainCount

在 ARC 时代下本身就不会使用 retainCount, 书中所讲述的几种情况仅出现在 MRC 编程环境下, 例如 retainCount 可能不为 0 的时候对象就会被系统释放,所以 retainCount 引用计数可能永远不为0,这是系统优化对象的释放行为所导致的。

上一篇下一篇

猜你喜欢

热点阅读