移动开发技术前沿iOS DeveloperiOS点点滴滴

《Effective Objective-C》重读校验自己的知识

2017-06-28  本文已影响300人  奥卡姆剃须刀
Effective Objective-C

读后感先放在前边
现在详细来看这本书应该也不晚吧,iOS 开发之类的书籍其实网上的总结还是蛮多的 有很多文章写得都是挺不错的, 但是终归是别人的的读后感总结,看着别人的总结终归不能完全吸收为自己的,所以近期抽空把 iOS 相关书籍看一遍 来对自己的知识体系做一个校验
书中举得很多例子都是挺好的 此文章也总结了此书的大纲,只有一些本人比较生疏的知识点才会展开详细描述,书中有很多细节并不是我们日常开发中能注意到的但是很重要的一些知识点, 此篇文章写得耗费时间还是挺久的

第一章 熟悉 Objective-C

1 了解 Objective-C 语言的起源

OC 语言使用了"消息结构" 而非是"函数调用"
消息结构与函数调用区别关键在于:
一 使用消息结构的语言,其运行时所应执行的代码有运行环境来决定
二 使用函数调用的语言,则有编译器决定的
OC 语言使用动态绑定的消息结构,也就是说在在运行时才会检查对象类型,接受一条消息之后,究竟应执行何种代码, 有运行期环境而非编译器来决定

下图来看一下 OC 对象的内存分配


WechatIMG86.jpeg

此图布局演示了一个分配在对堆中的 NSString 实例, 有两个分配在栈上的指针指向改实例
OC 系统框架中也有很多使用结构体的, 比如 CGRect, 因为如果改用 OC 对象来做的话, 性能就会受影响

2 在类的头文件中尽量少引用其他头文件
3 多用字面量语法 少用与之等价的方法

推荐使用字面量语法:

NSString * someString = @"奥卡姆剃须刀";
NSNumber *number = @18;
NSArray *arr = @[@"123",@"456];
NSDictionary *dict = @{
                         @"key":@"value"
                              };

对应的非字面量语法

    NSString *str = [NSString stringWithString:@"奥卡姆"];
    NSNumber *number = [NSNumber numberWithInt:18];
    NSArray *arr = [NSArray arrayWithObject:@"123",@"456"]; 
4 多用类型常量,少用 #define 预处理指令

举例说明
不合适的写法

//动画时间
#define ANIMATION_DUATION 0.3

正确的写法

视图修改 const修饰的变量则会报错 
static const NSTimeInterval KAnimationDuration = 0.3
// EOCAnimatedView.h
extern const NSTiemInterval EOCAnimatedViewANmationDuration
//  EOCAnimatedView.m
const NSTiemInterval EOCAnimatedViewANmationDuration = 0.3

这样定义的常量要优于# Define 预处理指令, 因为编译器会确保常量不变, 而且外部也可以使用

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

按位或操作符枚举

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
};

第二章 对象,消息,运行期

6 理解"属性"这一概念
7 在对象内部尽量直接访问实例变量
8 理解"对象等同性"这一概念
9 以类族模式隐藏实现细节

此小节比较抽象,用文中的规则来总结一下 大概如下

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

这种方法我在分类中经常使用,而且屡试不爽 以下是本人在项目中的用法

static void *callBackKey = "callBackKey";

@implementation UIView (category)
- (void)addTapWithBlock:(callBack)callback{    
    objc_setAssociatedObject(self, callBackKey, callback, OBJC_ASSOCIATION_COPY);
    self.userInteractionEnabled = YES;
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapClick)];
    [self addGestureRecognizer:tap];
}
- (void)tapClick{
    callBack block = objc_getAssociatedObject(self, callBackKey);
    if (block) {
        block();
    }
}
11 理解 objc_msgSend 的作用

objc_msgSend 函数会依据接受者与选择子的类型来调用适当的方法,为了完成此操作, 该方法需要在接受者所属的类中搜寻其"方法列表" ,如果能找到与选择名称相符的方法,就跳至其实现代码, 若是找不到 就沿着继承体系继续向上查找, 等找到合适的方法在挑战, 如果还是找不到相符的方法,那就执行"消息转发"操作 这个会在12条来讲

12 理解 消息转发机制 重点再次复习一遍

消息转发分为两大阶段,第一阶段先征询接受者,所属的类, 看其是否能动态添加方法,以处理当前这个"未知的的选择子" 这叫做"动态方法解析",第二阶段涉及完整的消息转发机制. 如果运行期系统已经把第一阶段执行完了, 那么接受者自己就无法再以动态新增方法的手段来响应包含盖选择子的消息了, 此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用, 这又细分两小步. 首先请接受者看看有没有其他对象能处理这条消息,若有 则运行期系统会吧消息转给那个对象,于是消息转发过程结束,一切如常, 若没有背援的接受者,则启动完整的消息转发机制,运行期系统会吧消息有关的全部细节都封装在 NSInvocation 对象中, 在给接受者最后一次机会, 令其设法解决当前还没处理的这条消息

动态方法解析
+ (Bool) resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为 Boolean 类型,表示这个类是否能新增一个实例方法用以处理此选择子.在继续往下执行转发机制之前, 本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法, 那么运行期系统就会调用另外一个方法 和当前方法类似 resolveClassMethod

备援接受者
当前接受者还有第二次机会能处理未知的选择子,在这一步,运行期系统会问它: 能不能把这条消息转给其他接受者来处理. 与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelestor:(SEL)selector
方法参数代表未知的选择子, 若当前接受者能找到备援对象,则将其返回,若找不到,就返回 nil

完整的消息转发
如果转发算法已经到这一步的话,俺那么唯一能做的就是启用完整的消息转发机制了.首先创建 NSInvocation 对象, 把与尚未处理的那条消息有关的全部细节, 都封装于其中,此对象包含选择子、目标,及参数, 在触发 NSInvocation 对象时, "消息派发系统"将亲自出马,把消息指派给目标对象 此步骤会调用下列方法来转发消息
- (void)forwardInvocation:(NSInvocation * )invocation
再触发消息前, 先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等
实现此方法是,若发现某调用不应有本类处理,择婿调用超类的同名方法, 这样的话,继承体系中的每个类都有机会处理此调用请求,直至 NSObject, 如果最后调用了 NSOBject 方法,那么该方法还会继而调用doesNotRecognizeSelector以抛出异常,此异常表明选择子最终未能得到处理

消息转发全流程

消息转发全流程.jpg
13 用"方法调配技术"调试"黑盒方法"

通俗讲 其实就是利用 runtime 实现方法交换 这个就不再详细解说了

14 理解"类对象"的用意

每个 Objective-C 对象实例都是指向某块内存数据的指针,描述 Objective-C对象所用的数据结构定义在运行期程序库的头文件里, id 类型本身也是定义在这里

typedef struct objc_object {
Class isa;
} * id

由此可见,每个对象结构体的首个成员是 Class 类的变量. 该变量定义了对象所属的类,通常称为 isa 指针
Class 对象也定义在运行期程序库的头文件中中:

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 **methodLists;
         struct objc_cache *cache;
         struct objc_protocol_list *protocols;
}

此结构体存放类的元数据,此结构体的首个变量也是 isa 指针, 这说明, Class 本身也是 Objective-C 对象,结构体中的 super_class 它定义了本类的超类, 类对象所属的类型(也就是 isa 指针所指向的类型)是另外一个类, 叫做元类,用来表述类对象本身所具备的元数据.每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类
假设有一个 someClass 的子类从 NSObject 中继承而来,则它的继承体系可由下图表示

继承体系.jpg

在类继承体系中查询类型信息
可以用类型信息查询方法来检视类继承体系,isMemberOfClass能够判断出对象是否是特定类的实例 而isKindOfClass则能够判断出对象是否为某类或某派生派类的实例

第三章 接口与 API 设计

15 用前缀避免命名空间冲突
16 提供"全能初始化方法"

UITableViewCell 初始化该类对象的时候,需要指明其样式及标识符, 标识符能够区分不同类型的单元格, 由于这种对象的创建成本比较高, 所以绘制表格时 可依照标识符来服用,提升程序执行效率,这种可以为对象提供必要信息以便其能完成工作的初始化方法叫做"全能初始化方法"

这一点写开源框架的时候十分的受用

17 实现 description 方法

这个就不多说了 实际开发中经常用

18 尽量使用不可变对象
LLPerson.h
@interface LLPerson : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
@end
LLPerson.m

@interface LLPerson()

@property (nonatomic, copy, readwrite) NSString *name;
@property (nonatomic, assign, readwrite) NSInteger age;

@end

@implementation LLPerson

- (instancetype)initWithName:(NSString *)name age:(NSInteger)age{
    if (self = [super init]) {
        self.name = name;
        self.age = age;
    }
    return self;
}
19 使用清晰而协调的命名方式

方法命名的几条规则

20 为私有方法名加前缀
21 理解 OBjective -C 错误类型
// 比如 有一个抽象基类, 他的正确用法是先从中继承一个类,然后使用这个子类, 在这种情况下,如果有人直接使用了一个抽象基类,那么久抛出异常
- (void)mustOverrideMethod{
    NSString *reason = [NSString stringWithFormat:@"%@m must be overridden",
                        NSStringFromSelector(_cmd)];
    @throw [NSException
            exceptionWithName:NSInternalInconsistencyException
                                   reason:reason
                                 userInfo:nil];
}
22 理解 NSCopying 协议

第四章 协议与分类

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

这个就是常规我们使用的代理了 但是书中讲了一个新的知识点 我倒是从前从没有见过的 可以一起来看一下

// 定义一个结构体
@interface LLNetWorkFetcher(){
  struct {
    unsigned int didReceiveData       : 1;
    unsigned int didDailWIthError     : 1;
    unsigned int didUpdateProgressTo  : 1;
} _delegateFlags;

// 在外界设置代理的时候 重写 delegate 的 set 方法 对此结构体进行赋值

- (void)setDelegate:(id<LLNetworkFetcherDelegate>)delegate{
    _delegate = delegate;
    _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didDailWIthError = [delegate respondsToSelector:@selector(networkFetcher:didDailWIthError:)];
    _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}

// 这样在调用的时候只需判断 结构体里边的标志就可以了 不需要一直调用 respondsToSelector 这个方法
     if (_delegateFlags.didUpdateProgressTo) {
            [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
        }
}
24 将类的实现代码分散到便于管理的数个分类之中
25 总是为第三方类的分类名称加前缀

分类的方法加入到类中这一操作是在运行期系统加载分类是完成的.运行期系统会把分类中所实现的每个方法都加入到类的方法列表中,如果类中本来就有此方法,而分类又实现了一次, 那么分类中的方法会覆盖原来那一份实现代码, 实际上可能会发生多次覆盖, 多次覆盖的结果一最后一个分类为准

26 勿在分类中声明属性

这个老生常谈了

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

class - continuation分类 通俗点来讲其实就是我们平时所说的延展

28 通过协议提供匿名对象

第五章 内存管理

29 理解引用计数器

这一点也不多说了 不过有一个概念确实是之前没想过的
UIApplication 对象是 跟对象

30 以 ARC 简化引用计数
31 在 dealloc 方法中只释放引用并解除监听
32 编写"异常安全代码"时留意内存管理问题
    NSObject *object;
    @try {
        object = [NSObject new];
        [object doSomeThingThatMayThrow];
    }
    @catch(...){        
    }
    @finally{
    }    
33 以弱引用避免保留环
34 以"自动释放池块"降低内存峰值
    NSArray *dataArr = [NSArray array];
    NSMutableArray *personArrM = [NSMutableArray array];
    for (NSDictionary *recode in dataArr) {
        @autoreleasepool{            
            LLPerson *person = [[LLPerson alloc]initWithRecode:recode];
            [personArrM addObject:person];
        }
    }
35 用"僵尸对象"调试内存管理问题
36 不要使用retainCount

第六章 块与大中枢派发

37 块的内部结构
块对象内部结构.jpeg

块本身也是对象,在存放块对象内存区域中, 首个变量是指向 Class 对象的指针,该指针叫做 isa, 其余内存里含有块对象正常运转所需的各种信息, 在内存布局中,最重要的就是 invoke 变量,这就是函数指针,指向块的实现代码, 函数原型只要要接受一个 void* 型的参数, 此参数代表块.刚才说过,块其实就是一种代替函数指针的语法结构, 原来使用函数指针是,需要用不透明的 void 指针来传递状态 而改用块之后, 则可以把原来用标准 C 语言特性所编写的代码封装成简明且易用的接口.

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

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

38 为常用的块类型创建 typedef
39 用 Handel 块降低代码分散程度 其实也就是我们所说的 block 回调
40 用块引用其所属对象时不要出现保留环
41 多用派发队列,少用同步锁

这一点就详细说说吧

在 OC 中多线程要执行同一份代码,那么有时可能会出问题, 这种情况下,通常要使用锁来实现某种同步机制.

在 GCD 出现之前, 有两种方法:

-  (void)synchronizedMethod{
      @synchronized(self){
          // safe
      }
}
_lock = [[NSLock alloc]init];
- (void)synchronizedMethod{
  [_lock lock];
// safe
  [_lock unlock];
}

这两种方法都很好不过也都有缺陷 比如说,在极端情况下,同步块会导致死锁, 另外 其效率也不见得高, 而如果直接使用锁对象的话,一旦遇到死锁, 就会非常麻烦

GCD 的到来它能以更简单更高效的形式为代码加锁

我们都知道属性就是开发者经常需要同步的地方,这种属性需要做成"原子的", 用 atomic 即可实现这一点, 但如果我们自己实现的话就可以用 GCD 来实现

    _syncQueue = dispatch_queue_create("aokamu.syncQueue", NULL);
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
    dispatch_sync(_syncQueue, ^{
        _someString = someString;
    })
}

上述代码: 把设置操作与获取操作都安排在序列化的队列里执行了, 这样的话, 所有针对属性的访问操作就都同步了, 全部加锁任务都在 GCD 中处理, 而 GCD 是相当深的底层来实现的,于是能够做许多优化

- (void)setSomeString:(NSString *)someString{    
    dispatch_async(_syncQueue, ^{
        _someString = someString;
    })
}

这个吧同步派发改成异步派发,可以提升设置方法的执行速度, 而读取操作与写入操作依然会按顺序执行, 不过这样写昂写还是有一个弊端. :如果你测一下程序性能,那么可能会发现这种写法比原来慢, 因为执行异步派发时需要拷贝块.

    _syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);    
- (NSString *)someString{
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}
- (void)setSomeString:(NSString *)someString{    
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    })
}

在队列中 栅栏块必须单独执行, 不能与其他块并行, 这只对并发队列有意义, 因为串行队列中的块总是按顺序逐个来执行的, 并发队列如果发现接下来要处理的块是个栅栏块,那么久一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块 待栅栏块执行过后 再按正常方式向下处理 如下图

Snip20171031_1.png
42 多用 GCD 少用 performSelector 系列方法

这个现在已经没有人去用performSelector 系列方法了

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

在来简单总结一下操作队列(NSOPeration)的几种使用方法
① 取消某个操作
运行任务前可以调用 cancel 方法 ,该方法会设置对象内的标志位,用以表明此任务不需要执行, 不过已经启动的任务无法取消了,
②指定操作间的依赖关系
一个操作可以依赖其他多个操作
③ 通过键值观测机制监控 NSOperation 对象的属性.
NSOPeration 对象有许多属性都适合通过键值观测机制来监听
④指定操作的优先级
NSOperation 对象也有"线程优先级",这决定了运行此操作的线程处在何种优先级上

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

这个也简单记录一下把
Dispatch Group 俗称 GCD 任务组,我们 用伪代码来看一下 Dispatch Group的用法

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t dispatchGroup = dispatch_group_create();    
    for (id object in collectin) {        
        dispatch_group_async(dispatchGroup,
                             queue,
                             ^{
            [object performTask];
        })
    }
    dispatch_group_notify(dispatchGroup,
                          dispatch_get_main_queue(),
                          ^{
        [self updateUI];
    })

notify回调的队列完全可以自己来定 可以用自定义的串行队列或全局并发队列

这里还有 GCD 的另一个函数平时比较少用的 那就是dispatch_apply

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(array.count,
                   queue,
                   ^(size_t i) {                       
                       id object = array[i];
                       [object performTask];
    })    

dispatch_apply所使用的队列可以使并发队列, 也可以是串行队列, 加入把块派给了当前队列(或体系中高于当前队列的某个串行队列),这将会导致死锁,

45 使用 dispatch_once 来执行只需要运行一次的线程安全代码

这个就是老生常谈的单例了 也就不多说了

46 不要使用 dispatch_get_current_queue

这个函数已经废弃了 此处就不多说了

第七章 系统框架

47 熟悉系统框架

我们开发者经常碰到的就是 Foundation 框架 像NSobject,NSArray,NSDictionary 等类 都在其中,

还有一个与Foundation相伴的框架是 CoreFoundation,CoreFoundation 不是 OC 框架,但是确定编写 OC 应用程序时所应熟悉的重要框架,Foundation框架中的许多功能都可以在此框架中找到对应的 C 语言 API
除了 Foundation和CoreFoundation还有以下系统库:

48 多用块枚举 少用 for 循环
    NSArray<LLPerson *> *dataArr = [NSArray array];    
    [dataArr enumerateObjectsUsingBlock:^(LLPerson * _Nonnull obj,
                                          NSUInteger idx,
                                          BOOL * _Nonnull stop) {        
    }];
49 对自定义其内存管理语义的 collection 使用无缝桥接
    NSArray *anNSArray = @[@1,@2,@3,@4,@5];
    CFArrayRef aCFArray = (__bridge CFArrayRef)(anNSArray);
    NSLog(@"count = %li",CFArrayGetCount(aCFArray));
// Output: count = 5 ;   
50 构建缓存时选用 NSCache 而非 NSDIctionary

typedef void(^LLNetWorkFetcherCompleteHandler)(NSData *data);

@interface LLNetWorkFetcher : NSObject

- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(LLNetWorkFetcherCompleteHandler)handler;
@end


#import "LLClass.h"
#import "LLNetWorkFetcher.h"

@implementation LLClass{    
    NSCache *_cache;    
}
- (instancetype)init{
    if (self = [super init]) {        
        _cache = [NSCache new];        
        _cache.countLimit = 100;        
        _cache.totalCostLimit = 5 * 1024 * 1024;        
    }
    return self;
}
- (void)downLoadDataForURL:(NSURL *)url{
    NSData *cacheData = [_cache objectForKey:url];
    if (cacheData) {
        [self useData:cacheData];
    }else{        
        LLNetWorkFetcher *fetcher = [[LLNetWorkFetcher alloc]initWithURL:url];        
        [fetcher startWithCompletionHandler:^(NSData *data) {            
            [_cache setObject:data forKey:url cost:data.length];            
            [self useData:cacheData];            
        }];
    }
}

51 精简 initialize 与 load 的实现代码
52 别忘了 NSTimer 会保留其目标对象

计时器是一种很方便也很有用的对象,但是 由于计时器会保留其目标对象, 所以反复执行任务通常会导致应用程序出问题,也就是很容易引入"保留环"
来看下列代码

@interface LLClass : NSObject

- (void)startPolling;
- (void)stopPolling;

@end


@implementation LLClass{
    NSTimer *_pollTimer;
}

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

计时器的目标对象是 self, 然后计时器使用实例变量来存放的, 所以实例变量也保存李计时器, 于是就产生了保留环

本书中提供了一个用"块"来解决的方案 虽然计时器当前并不直接支持块,但是可以用下面这段代码添加功能

@implementation NSTimer (LLBlocksSupport)

+ (NSTimer *)ll_schedeledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                       repeats:(BOOL)repeats{
    
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(ll_blockInvoke:) userInfo:[block copy] repeats:repeats];
   
}
+ (void)ll_blockInvoke:(NSTimer *)timer{
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}

上边的代码是在 NSTimer 分类中添加的代码 来看一下具体的使用

- (void)startPolling{
    __weak LLClass *weakSelf = self;
    _pollTimer = [NSTimer ll_schedeledTimerWithTimeInterval:5.0
                                                      block:^{
                                                         LLClass *strongSelf = weakSelf;                                                          
                                                         [strongSelf p_doPoll];
   
                                                      }
                                                    repeats:YES];

先定义弱引用,然后用block捕获这个引用,但是在用之前在立刻生成 strong 引用.保证实例在执行期间持续存活

上一篇下一篇

猜你喜欢

热点阅读