第三十七条 理解“块”这一概念

首先,块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享同一 个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码。 例如,下面就是个简单的块:

^{
    //Block implementation here
}

块其实就是个值,而且自有其相关类型。与intfloatObjective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock) () = A {
    //Block implementation here
};

这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去也许 =有点怪,不过一旦理解了语法,很容易就能读懂。块类型的语法结构如下:

return_type (^block_name)(parameters)

我们来举个例子,下面这种写法所定义的块,返回int值,并且接受两个int做参数:

int (^addBlock) (int a, int b) = ^(int a, int b){
     return a + b;
};

定义好之后,就可以像函数那样使用了。比方说,addBlock块可以这样用:

int add = addBlock (2, 5) ;  //< add = 12

块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。比如,下面这段代码所定义的块,就使用了块以外的变量:

int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){ 
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 12

默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了additional变量的值,那么编译器就会报错。不过,声明变量的时候可以加上__block 修饰符,这样就可以在块内修改了。

例如:

__block int additional = 5;
int (^addBlock) (int a, int b) = ^(int a, int b){   
    additional++;
    return a + b + additional;
};

int add = addBlock (2, 5);  //< add = 13

块的另一个用法是“内联块”(inline block),例如:

NSArray *array = @[@0, @1, @2, @3, @4, @5];
_block NSInteger count = 0;
[array enumerateObjectsUsingBlock:
    ^(NSNumber *number, NSUInteger idx, BOOL *stop){
        if([number compare:@2] == NSOrderedAscending) { 
        count++;
    }
}];
//count = 2

这段范例代码也演示了“内联块”(inline block)的用法。传给“numerateObjectsUsingBlock:”方法的块并未先賦给局部变量,而是直接内联在函数调用里了。

然后我们在声明和使用块的时候,要注意它的作用范围。定义块的时候,其所占的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。比如下面这个:

void(^block)();
if ( /* some condition */ ){
    block = ^{
        NSLog(@"Block A");
    };
} else {
    block = ^{
        NSLog(@"Block B");
    };
}
block();

定义在ifelse语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存, 然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只 能保证在对应的ifelse语句范围内有效。这样写出来的代码可以编译,但是运行起来时而正确,时而错误。若编译器未覆写待执行的块,则程序照常运行,若覆写,则程序崩溃。

我们为了解决这个问题可以给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。

拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC环境下会自动释放。

改动后跟下面一样:

void (^block)();
if (/* some condition */ ){ 
    block = [^{
        NSLog(@,fBlock Aw);
    } copy];
} else {
    block = [^{
        NSLog(@"Block B");
    } copy];
}
block();

除了“桟块”和“堆块”之外,还有一类块叫做“全局块”(global block)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以声明在全局内存里,而不需要在每次用到 的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。下面就是个全局块:

void (^block)() = ^{
    NSLog(@"This is a block");
};

由于运行该块所需的全部信息都能在编译期确定,所以可把它做成全局块。这完全是种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及丢弃该块时执行一些无谓的操作。

要点

  • 块是C、C++、Objective-C中的词法闭包。
  • 块可接受参数,也可返回值。
  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
2017/9/2 posted in  第六章 块与GCD

通过协议提供匿名方法

协议定义了一系列方法,遵从此协议的对象应该实现它们(如果这些方法不是可选的, 那么就必须实现)。于是,我们可以用协议把自己所写的API之中的实现细节隐藏起来,将返回的对象设计为遵从此协议的纯id类型。

这样的话,想要隐藏的类名就不会出现在API之中了。若是接口背后有多个不同的实现类,而你又不想指明具体使用哪个类,那么可以考虑用这个办法——因为有时候这些类可能会变,有时候它们又无法容纳于标准的类继承体系中,因而不能以某个公共基类来统一表示。

此概念经常称为“匿名对象"(anonymous object),这与其他语言中的“匿名对象”不同,在那些语言中,该词是指以内联形式所创建出来的无名类,而此词在Objective-C中则不是这个意思。

我们之前说的委托与数据源对象,就用到了这个方法。例如在定义“受委托者(delegate)”属性时:

@property {nonatomic, weak) id <EOCDelegate> delegate;

由于该属性的类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性, 即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是“匿名的”(ammymous)。如有需要,可在运行期査出此对象所属的类型(参见第14条)。然而这样做不太好,因为指定属性类型时所写的那个EOCDelegate契约已经表明此对象的具体类型无关紧要了。

当然还有其它的用法,这里我们就不举例了。

要点

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
2017/9/2 posted in  第四章 协议与分类

KVO的简单实现

之间看有关OCiOS的书都会看到KVO这个名词。所以今天来学习和实现一下。简单的说KVOKey-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。

它来源于设计模式中的观察者模式,其基本思想就是:

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

我们一般用到的都是自动实现KVO,所以我这里就不实现手动的KVO了。实现KVO需要两个主要的方法:

//注册观察者
- (void)addObserver:(NSObject *)observer 
        forKeyPath:(NSString *)keyPath 
        options:(NSKeyValueObservingOptions)options 
        context:(void *)context;
        
//当观察的对象属性有所改变就会通知观察者,该方法用来处理变更通知
- (void)observeValueForKeyPath:(NSString *)keyPath 
                     ofObject:(id)object 
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                        context:(void *)context;

这两个方法在Foundation/NSKeyValueObserving.h中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。

我们来举例说明:

创建一个观察者类:

//
//  Observer.h
//  KVO机制
//

#import <Foundation/Foundation.h>

@interface Observer : NSObject

@end



//  Observer.m
//  KVO机制
#import "Observer.h"

@implementation Observer

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    //判断发送过来的通知中更改的属性是否是name
    if ([keyPath isEqualToString:@"name"]) {
        //获取更改属性的类的信息
        Class classInfo = (__bridge Class)context;
        NSString *className = [NSString stringWithFormat:@"%s", object_getClassName(classInfo)];
        NSLog(@" >> class : [%@] , Name changed", className);
        NSLog(@" >> old name is %@", [change objectForKey:@"old"]);
        NSLog(@" >> new name is %@", [change objectForKey:@"new"]);
    
    }else{
        /*
         *注意:在实现处理变更通知方法 observeValueForKeyPath 时,
         *要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。
         */
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

然后创建一个用于观察的Target

//  Target.h
//  KVO机制


#import <Foundation/Foundation.h>

@interface Target : NSObject

@property(nonatomic,assign) NSString *name;

@end


//  Target.m
//  KVO机制


#import "Target.h"

@implementation Target
//给name属性一个初始值,用于检测变化
-(instancetype)init{
    if (self = [super init]) {
        _name = @"yue";
    }
    return self;
}
@end

然后我们在main.m中添加观察者来执行:

//  main.m
//  KVO机制


#import <Foundation/Foundation.h>
#import "Observer.h"
#import "Target.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //分别创建实例
        Observer *observer = [[Observer alloc]init];
        Target *target = [[Target alloc]init];
        
        //target 增加一个观察者,用于观察name属性
        [target addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:(__bridge void * _Nullable)([Target class])];
        //更改name属性的值,响应观察者的动作
        [target setName:@"hao"];
        //移除name的观察者,防止内存泄露
        [target removeObserver:observer forKeyPath:@"name"];
    }
    return 0;
}

我们运行后结果如下:

可以发现是Target类发送过来的通知,说明属性已经变更。

下面我们来讨论一下传过来了的参数。其中addObserver方法中的option:参数用于指定应该包含哪种通知。其中主要有以下几种:

  • NSKeyValueObservingOptionNew:指出change字典应该包含有新的属性(如果适用)。
  • NSKeyValueObservingOptionOld:指出change字典应该包含有旧的属性(如果适用)。
  • NSKeyValueObservingOptionInitial:把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
  • NSKeyValueObservingOptionPrior: 分2次调用。在值改变之前和值改变之后。

其中observeValueForKeyPath方法中的change参数是一个字典型数据。会根据option参数的变化来生成不同的数据。一般会包含newold两个key

2017/9/1 posted in  iOS

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

第一种用途

类中经常会包含一些无须对外公布的方法及实例变最。其实这些内容也可以对外公布, 并且写明其为私有,开发者不应依赖它们。但是OC的动态性,使得不可能实现真正的私有方法或私有实例变量。

但是我们最好还是只把确实需要对外公布的那部分内容公开。那么,这种不需对外公布但却应该具有的方法及实例变量应该怎么写呢?此时,这个特殊的“class-continuation分类”就派上用场了。

“class-continuation分类”和普通的分类不同,它必须定义在其所接续的那个类的实现文件里。

例如:

//EOCPerson.m
@interface EOCPerson () {
    NSString * _anInstanceVariable;
}
// Method declarations here 
@end
@implementation EOCPerson {
    int _anotherInstanceVariable;
}
// Method implementations here 
@end

我们这样定义的目的是将这些方法或者实例变量隐藏起来,只供本类使用。即便在公共接口里将其标注为private,也还是会泄漏实现细节。

例如有个绝密的类,不想给其他人知道。 假设你所写的某个类拥有那个绝密类的实例,而这个实例变量又声明在公共接口里面:

#import <Foundation/Foundation.h>

@class EOCSuperSecretClass;

@interface EOCClass : NSObject {
@private
    EOCSuperSecretClass *_secretInstance;
@end

这样别人就会知道有一个叫EOCSuperSecretClass的类了。

所以我们通常应该这样:

// EOCClass.h
#import <Foundation/Foundation.h>
@interface EOCClass : NSObject 
@end

// EOCClass .m 
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"
@interface EOCClass ()  {
    EOCSuperSecretClass *_secretInstance;
@end

@implementation EOCClass
// Methods here
@end

第二种用途

编写Objective-C++代码时 “class-continuation分类”也很有用。Objective-C++OCC++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后 端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用 C++来编码。在这些情况下,使用"class-continuation分类"会很方便。假设某个类打算这样写:

#import <Foundation/Foundation.h> 
#include "SomeCppClass.h" 
@interface EOCClass : NSObject { 
@private
    SomeCppClass _cppClass;
@end

该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引人SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为 Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终 导致整个应用程序全部都要编译为ObjeCtive-C++。这样显然会增加编码的负担。

也许我们会想用前向声明来避免导入SomeCppClass.h,比如:

#import <Foundation/Foundation.h> 

class SomeCppClass;

@interface EOCClass : NSObject { 
@private
    SomeCppClass *_cppClass;
@end

现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。

虽然我们这样做没有#include "SomeCppClass.h"但是我们前向声明该类时所用的class关键字还是C++下的关键字,所以仍然需要按照OC来编译才行。

我们这里的解决方法还是一样,既然变量是private的,我们还是可以将它在“class-continuation分类”声明,改写成:

// EOCClass. h

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject


// EOCClass.mm 
#import "EOCClass.h"
#include "SomeCppClass.h"

@interface EOCClass ()  {
    SomeCppClass _cppClass;
}
@end

@implementation EOCClass 
@end

改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。

第三种用法

就是将public接口中声明为“只读”的 属性扩展为“可读写”,以便在类的内部设置其值。

例如:

// .h文件
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName;

-(id) initWithFirstName : (NSString*) firstName
                lastName: (NSString*) lastName;

@end

我们一般会在“class-continuaticm分类”中把这两个属性扩展为“可读写”:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

-(void)p_privateMethod;
@end

只需要用上面几行代码就行了。现在EOCPerson的实现代码可以随意调用“setFirstName:”“setLastName:”这两个设置方法,也可以用“点语法”来设置属性。这样做很有用,既能令外界无法修改对象,又能在其内部按照需要管理其数据。

只会在类的实现代码中用到的私有方法也可以声明在“class-continuation分类”中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。上述的私有方法加上了p_前缀。

第四种用法

当我们想要把对象所遵守的协议视为私有,就可以在“class-continuation分类”中声明。例如:

#import "EOCPerson•h"
#import "EOCSecretDelegate.h"
@interface EOCPerson () <EOCSecretDelegate> 
@end
@implementation EOCPerson
    /*.....*/
@end

要点

  • 通过“class-continuation分类”向类中新增实例变量。

  • 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”

  • 把私有方法的原型声明在“class-continuation分类”里面。

  • 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。

2017/9/1 posted in  第四章 协议与分类

第26条 不要在分类中声明属性

属性是封装数据的方式(参见第6条)。尽管从技术上说,分类里也可以声明属性,但这种做法还是要尽量避免。

原因在于,除了“class-continuation分类”(参见第27条)之外,其他分类都无法向类中新增实例变量,因此,它们无法把实现属性所需的实例变量合成出来。如果我们在分类中声明了一个friends属性。会提示我们

warning: property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]

warning: property 'friends' requires method 'setFriends:' to be defined - use @dynamic or provide a method implementation in this category [-Wobjc-property-implementation]

说明系统没有为我们自动合成属性的setget方法。我们要自己在分类中去实现,可以把存取方法声明为@dynamic, 也就是说,这些方法等到运行期再提供,编译器目前是看不见的。如果决定使用消息转发机制(参见第12条)在运行期拦截方法调用,并提供其实现,那么或许可以采用这种做法。

当然我们也可以使用关联对象的方法。但是还是不建议我们在分类中定义封装数据的属性。

正确做法是把所有属性都定义在主接口里。类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量(也就是数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。

但是有时候,只读属性(readonly)可以在分类中使用,但是我们要手动实现它的get方法。当然我们不建议搞特殊。最好还是在主接口中声明。然后在分类中声明一个获取方法,来获取数据。

要点

  • 把封装数据所用的全部属性都定义在主接口里。
  • “class-contimiation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
2017/9/1 posted in  第四章 协议与分类

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

分类机制通常用于向无源码的既有类中新增功能。这个特性极为强大,但在使用时也很容易忽视其中可能产生的问题。

我们在分类中添加方法,系统在运行期时会将分类中的方法加入类中。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次榭盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另外一个分类 中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。

所以我们为了解决此问题,一般做法是:以命名空间来区别各个分类的名称与其中所定义的方法。我们这里的命名空间只是在相关名称前都加上公用的前缀。

所以我们举例来说就是这样的:

@interface NSString (ABC_HTTP)
//Encode a string with URL encoding 
-(NSString*) abc_urlEncodedStiring;

// Decode a URL ©ncodeci string
-(NSString*) abc_urlDecodedString;
@end

要点

  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
  • 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
2017/9/1 posted in  第四章 协议与分类