iOS开发过程中几种消息传递机制的理解

几种消息传递机制

首先我们来看看每种机制的具体特点。在这个基础上,下一节我们会画一个流程图来帮我们在具体情况下正确选择应该使用的机制。最后,我们会介绍一些苹果框架里的例子并且解释为什么在那些用例中会选择这样的机制。

KVO

KVO 是提供对象属性被改变时的通知的机制。KVO 的实现在 Foundation 中,很多基于 Foundation 的框架都依赖它。

如果只对某个对象的值的改变感兴趣的话,就可以使用 KVO 消息传递。不过有一些前提:第一,接收者(接收对象改变的通知的对象)需要知道发送者 (值会改变的对象);第二,接收者需要知道发送者的生命周期,因为它需要在发送者被销毁前注销观察者身份。如果这两个要去符合的话,这个消息传递机制可以一对多(多个观察者可以注册观察同一个对象的变化)

如果要在 Core Data 上使用 KVO 的话,方法会有些许差别。这和 Core Data 的惰性加载 (faulting) 机制有关。一旦一个 managed object 被惰性加载处理的话,即使它的属性没有被改变,它还是会触发相应的观察者。

编者注 把属性值先取入缓存中,在对象需要的时候再进行一次访问,这在 Core Data 中是默认行为,这种技术称为 Faulting。这么做可以避免降低内存开销,但是如果你确定将访问结果对象的具体属性值时,可以禁用 Faults 以提高获取性能。关于这个技术更多的情况,请移步官方文档

通知

要在代码中的两个不相关的模块中传递消息时,通知机制是非常好的工具。通知机制广播消息,当消息内容丰富而且无需指望接收者一定要关注的话这一招特别有用。

通知可以用来发送任意消息,甚至可以包含一个 userInfo 字典。你也可以继承 NSNotification 写一个自己的通知类来自定义行为。通知的独特之处在于,发送者和接收者不需要相互知道对方,所以通知可以被用来在不同的相隔很远的模块之间传递消息。这就意味着这种消息传递是单向的,我们不能回复一个通知。

委托 (Delegation)

Delegation 在苹果的框架中广泛存在。它让我们能自定义对象的行为,并收到一些触发的事件。要使用 delegation 模式的话,发送者需要知道接收者,但是反过来没有要求。因为发送者只需要知道接收者符合一定的协议,所以它们两者结合的很松。

因为 delegate 协议可以定义任何的方法,我们可以照着自己的需求来传递消息。可以用方法参数来传递消息内容,delegate 可以通过返回值的形式来给发送者作出回应。如果只要在相对接近的两个模块间传递消息,delgation 是很灵活很直接的消息传递机制。

过度使用 delegation 也会带来风险。如果两个对象结合得很紧密,任何其中一个对象都不能单独运转,那么就不需要用 delegate 协议了。这些情况下,对象已经知道各自的类型,可以直接交流。两个比较新的例子是 UICollectionViewLayout 和 NSURLSessionConfiguration。

Block

Block 是最近才加入 Objective-C 的,首次出现在 OS X 10.6 和 iOS 4 平台上。Block 通常可以完全替代 delegation 消息传递机制的角色。不过这两种机制都有它们自己的独特需求和优势。

一个不使用 block 的理由通常是 block 会存在导致 retain 环 (retain cycles) 的风险。如果发送者需要 retain block 但又不能确保引用在什么时候被赋值为 nil, 那么所有在 block 内对 self 的引用就会发生潜在的 retain 环。

假设我们要实现一个用 block 回调而不是 delegate 机制的 table view 里的选择方法,如下所示:

1
2
3
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// 处理选择
};

这儿的问题是,self 会 retain table view,table view 为了让 block 之后可以使用而又需要 retain 这个 block。然而 table view 不能把这个引用设为 nil,因为它不知道什么时候不需要这个 block 了。如果我们不能保证打破 retain 环并且我们需要 retain 发送者,那么 block 就不是一个的好选择。

NSOperation 是使用 block 的一个好范例。因为它在一定的地方打破了 retain 环,解决了上述的问题。

1
2
3
4
5
6
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];

一眼看来好像上面的代码有一个 retain 环:self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, 而 completionBlock retain 了 self。然而,把 operation 加入 queue 中会使 operation 在某个时间被执行,然后被从 queue 中移除。(如果没被执行,问题就大了。)一旦 queue 把 operation 移除,retain 环就被打破了。

另一个例子是:我们在写一个视频编码器的类,在类里面我们会调用一个 encodeWithCompletionHandler: 的方法。为了不出问题,我们需要保证编码器对象在某个时间点会释放对 block 的引用。其代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end

@implementation Encoder

- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// 进行异步处理...
}

// 这个方法会在完成后被调用一次
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- 不要忘了这个!
}

@end

一旦任务完成,completion block 调用过了以后,我们就应该把它设为 nil。

如果一个被调用的方法需要发送一个一次性的消息作为回复,那么使用 block 是很好的选择, 因为这样做我们可以打破潜在的 retain 环。另外,如果将处理的消息和对消息的调用放在一起可以增强可读性的话,我们也很难拒绝使用 block 来进行处理。在用例之中,使用 block 来做完成的回调,错误的回调,或者类似的事情,是很常见的情况。

Target-Action

Target-Action 是回应 UI 事件时典型的消息传递方式。iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持这个机制。Target-Action 在消息的发送者和接收者之间建立了一个松散的关系。消息的接收者不知道发送者,甚至消息的发送者也不知道消息的接收者会是什么。如果 target 是 nil,action 会在响应链 (responder chain) 中被传递下去,直到找到一个响应它的对象。在 iOS 中,每个控件甚至可以和多个 target-action 关联。

基于 target-action 传递机制的一个局限是,发送的消息不能携带自定义的信息。在 Mac 平台上 action 方法的第一个参数永远接收者。iOS 中,可以选择性的把发送者和触发 action 的事件作为参数。除此之外就没有别的控制 action 消息内容的方法了。

做出正确的选择

基于上述对不同消息传递机制的特点,我们画了一个流程图来帮助我们在不同情境下做出不同的选择。一句忠告:流程图的建议不代表最终答案。有些时候别的选择依然能达到应有的效果。只不过大多数情况下这张图能引导你做出正确的决定。

图中有些细节值得深究:

有个框中说到: 发送者支持 KVO。这不仅仅是说发送者会在值改变的时候发送 KVO 通知,而且说明观察者需要知道发送者的生命周期。如果发送者被存在一个 weak 属性中,那么发送者有可能会自己变成 nil,那时观察者会导致内存泄露。

一个在最后一行的框里说,消息直接响应方法调用。也就是说方法调用的接收者需要给调用者一个消息作为方法调用的直接反馈。这也就是说处理消息的代码和调用方法的代码必须在同一个地方。

最后在右下角的地方,一个选择分支这样说:发送者能确保释放对 block 的引用吗?这涉及到了我们之前讨论 block 的 API 存在潜在的 retain 环的问题。如果发送者不能保证在某个时间点会释放对 block 的引用,那么你会惹上 retain 环的麻烦。

Framework 示例

本节我们通过一些苹果框架里的例子来验证流程图的选择是否有道理,同时解释为什么苹果会选择用这些机制。

KVO

NSOperationQueue 用了 KVO 观察队列中的 operation 状态属性的改变情况 (isFinished,isExecuting,isCancelled)。当状态改变的时候,队列会收到 KVO 通知。为什么 operation 队列要用 KVO 呢?

消息的接收者(operation 队列)知道消息的发送者(operation),并 retain 它并控制后者的生命周期。另外,在这种情况下只需要单向的消息传递机制。当然如果考虑到 oepration 队列只关心那些改变 operation 的值的改变情况的话,就还不足以说服大家使用 KVO 了。但我们可以这么理解:被传递的消息可以被当成值的改变来处理。因为 state 属性在 operation 队列以外也是有用的,所以这里适合用 KVO。

当然 KVO 不是唯一的选择。我们也可以将 operation 队列作为 operation 的 delegate 来使用,operation 会调用类似 operationDidFinish: 或者 operationDidBeginExecuting: 等方法把它的 state 传递给 queue。这样就不太方便了,因为 operation 要保存 state 属性,以便于调用这些 delegate 方法。另外,由于 queue 不能主动获取 state 信息,所以 queue 也必须保存所有 operation 的 state。

Notifications

Core Data 使用 notification 传递事件(例如一个 managed object context 中的改变————NSManagedObjectContextObjectsDidChangeNotification)

发生改变时触发的 notification 是由 managed object contexts 发出的,所以我们不能假定消息的接收者知道消息的发送者。因为消息的源头不是一个 UI 事件,很多接收者可能在关注着此消息,并且消息传递是单向的,所以 notification 是唯一可行的选择。

Delegation

Table view 的 delegate 有多重功能,它可以从管理 accessory view,直到追踪在屏幕上显示的 cell。例如我们可以看看 tableView:didSelectRowAtIndexPath: 方法。为什么用 delegate 实现而不是 target-action 机制?

正如我们在上述流程图中看到的,用 target-action 时,不能传递自定义的数据。而选中 table view 的某个 cell 时,collection view 不仅需要告诉我们一个 cell 被选中了,也要通过 index path 告诉我们哪个 cell 被选中了。如果我们照着这个思路,流程图会引导我们使用 delegation 机制。

如果不在消息传递中包含选中 cell 的 index path,而是让选中项改变时我们像 table view 主动询问并获取选中 cell 的相关信息,会怎样呢?这会非常不方便,因为我们必须记住当前选中项的数据,这样才能在多选择中知道哪些 cell 是被新选中的。

同理,我们可以想象通过观察 table view 选中项的 index path 属性,当该值发生改变的时候,获得一个选中项改变的通知。不过我们会遇到上述相似问题:不做记录的话我们就不能分辨哪一个 cell 被选择或取消选择了。

Block

我们用 -[NSURLSession dataTaskWithURL:completionHandler:] 来作为一个 block API 的介绍。那么从 URL 加载部分返回给调用者是怎么传递消息的呢?首先,作为 API 的调用者,我们知道消息的发送者,但是我们并没有 retain 它。另外,这是个单向的消息传递————它直接调用 dataTaskWithURL: 的方法。如果我们对照流程图,会发现这属于 block 消息传递机制。

有其他的选项吗?当然,苹果自己的 NSURLConnection 就是最好的例子。NSURLConnection在 block 问世之前就存在了,所以它并没有用 block 来实现消息传递,而是使用 delegation 来完成。当 block 出现以后,苹果就在 OS X 10.7 和 iOS 5 平台上的 NSURLConnection 中加了 sendAsynchronousRequest:queue:completionHandler:,所以我们不再在简单的任务中使用 delegate 了。

因为 NSURLSession 是个最近在 OS X 10.9 和 iOS 7 才出现的 API,所以它们使用 block 来实现消息传递机制(NSURLSession 有一个 delegate,但是是用于其他目的)。

Target-Action

一个明显的 target-action 用例是按钮。按钮在不被按下的时候不需要发送任何的信息。为了这个目的,target-action 是 UI 中消息传递的最佳选择。

如果 target 是明确指定的,那么 action 消息会发送给指定的对象。如果 target 是 nil, action 消息会一直在响应链中被传递下去,直到找到一个能处理它的对象。在这种情况下,我们有一个完全解耦的消息传递机制:发送者不需要知道接收者,反之亦然。

Target-action 机制非常适合响应 UI 的事件。没有其他的消息传递机制能够提供相同的功能。虽然 notification 在发送者和接收者的松散关系上最接近它,但是 target-action 可以用于响应链——只有一个对象获得 action 并响应,action 在响应链中传递,直到能遇到响应这个 action 的对象。

总结

一开始接触这么多的消息传递机制的时候,我们可能有些无所适从,觉得所有的机制都可以被选用。不过一旦我们仔细分析每个机制的时候,它们各自都有特殊的要求和能力。

文中的选择流程图是帮助你清楚认识这些机制的好的开始,当然它不是所有问题的答案。如果你觉得这和你自己选择机制的方式相似或是有任何缺漏,欢迎来信指正。


观点仅代表自己,期待你的留言。
http://objccn.io/issue-7-4/

Effective Objective-C 理解消息传递机制

最简单的动态

Objective-C 是一门极其动态的语言,许多东西都可以推迟到运行时决定、修改。那么到底何为动态、何为静态?我们通过一个简单的例子对比下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/***********  例1 静态绑定   ***********/
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void saySomething(int type)
{
if (type == 0) {
printHello();
} else {
printGoodbye();
}
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********  例2 动态绑定   ***********/
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void saySomething(int type)
{
void (*func)();
if (type == 0) {
func = printHello;
} else {
func = printGoodbye;
}
func();
return 0;
}

例1的代码在编译期,编译器就已经知道了有 void printHello()、void printGoodbye() 俩函数,并且在 saySomething() 函数中,调用的函数明确,可以直接将函数名硬编码成地址,生成调用指令,这就是 静态绑定(static binding)。那么例2呢?例2的调用的是 func() 函数,而这函数实际调用的地址只能到程序运行时才能确定,这就是所谓的 动态绑定(dynamic binding)。动态绑定将函数调用从编译期推迟到了运行时。

在 Objective-C 中,向对象传递消息,就会使用这种动态绑定机制来决定需要调用的方法,这种动态特性使得 Objective-C 成为一门真正动态的语言。

objec_msgSend 函数

Objective-C 的方法调用通常都是下面这种形式

1
id returnValue = [someObject messageName:parameter];

这种方法调用其实就是消息传递,编译器看到这条消息会转换成一条标准的 C 语言函数调用

1
2
3
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);

用消息传递的话来解释就是:向 someObject 对象发送了一个名叫 messageName 的消息,这个消息携带了一个叫 parameter 的参数。这里用到了一个 objc_msgSend 函数,其函数原型如下

1
void objc_msgSend(id self, SEL cmd, ...);

这是一个可变参数的函数,第一个参数代表消息接收者,第二个代表 SEL 类型,后面的参数就是消息传递中使用的参数。

那么什么是 SEL 呢?SEL 就是代码在编译时,编译器根据方法签名来生成的一个唯一 ID。此 ID 可以用以区分不同的方法,只要 ID 一致,即看成同一个方法,ID 不同,即为不同的方法。

当进行消息传递,对象在响应消息时,是通过 SEL 在 methodlist 中查找函数指针 IMP,找到后直接通过指针调用函数,这其实就是前文介绍的 动态绑定。若是找到对应函数就跳转到实现代码,若找不到,就沿着继承链往上查找,直到找到相应的实现代码为止。若最终还是没找到实现代码,说明当前对象无法响应此消息,接下来就会执行 消息转发 操作,以试图找到一个能响应此消息的对象。

1
2
3
4
// 获取 SEL 
SEL sel = @selector(methodName);
// 获取 IMP
IMP imp = methodForSelector(sel);

消息转发

消息转发并不神奇,我们其实早已接触过,只是不知道而已

1
2
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'

这段异常代码就是由 NSObject 的 doesNotRecognizeSelector: 方法所抛出的,异常表明:消息的接收者类型为 __NSCFNumber,无法响应 lowercaseString 消息,从而转发给 NSObject 处理。

消息转发分为三大阶段

  • 第一阶段先征询消息接收者所属的类,看其是否能动态添加方法,以处理当前这个无法响应的 selector,这叫做 动态方法解析(dynamic method resolution)。如果运行期系统(runtime system) 第一阶段执行结束,接收者就无法再以动态新增方法的手段来响应消息,进入第二阶段。
  • 第二阶段看看有没有其他对象(备援接收者,replacement receiver)能处理此消息。如果有,运行期系统会把消息转发给那个对象,转发过程结束;如果没有,则启动完整的消息转发机制。
  • 第三阶段 完整的消息转发机制。运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。

动态方法解析

对象在收到无法响应的消息后,会调用其所属类的下列方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 如果尚未实现的方法是实例方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增实例方法用以处理selector
*/
+ (BOOL)resolveInstanceMethod:(SEL)selector;
/**
* 如果尚未实现的方法是类方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增类方法用以处理selector
*/
+ (BOOL)resolveClassMethod:(SEL)selector;

方法返回布尔类型,表示是否能新增一个方法来处理 selector,此方案通常用来实现 @dynamic 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/************** 使用 resolveInstanceMethod 实现 @dynamic 属性 **************/
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */)
{
if ([selectorString hasPrefix:@"set"])
{
// 添加 setter 方法
class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
}
else
{
// 添加 getter 方法
class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}

备援接收者

如果无法 动态解析方法,运行期系统就会询问是否能将消息转给其他接收者来处理,对应的方法为

1
2
3
4
5
6
7
8
/**
* 此方法询问是否能将消息转给其他接收者来处理
*
* @param aSelector 未处理的方法
*
* @return 如果当前接收者能找到备援对象,就将其返回;否则返回nil;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;

在对象内部,可能还有其他对象,该对象可通过此方法将能够处理 selector 的相关内部对象返回,在外界看来,就好像是该对象自己处理的似得。

完整的消息转发机制

如果前面两步都无法处理消息,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节装在里面,在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)将会把消息指派给目标对象。对应的方法为

1
2
3
4
5
6
/**
* 消息派发系统通过此方法,将消息派发给目标对象
*
* @param anInvocation 之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation;

这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。

总结


观点仅代表自己,期待你的留言。
https://www.zybuluo.com/MicroCai/note/64270

NSObject的+load与+initialize方法的区别

先来看看NSObject Class Reference里对这两个方法说明:
+(void)initialize

The runtime sends initialize to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.

+(void)load
The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
The order of initialization is as follows:

All initializers in any framework you link to.
All +load methods in your image.
All C++ static initializers and C/C++ attribute(constructor) functions in your image.
All initializers in frameworks that link to you.
In addition:

A class’s +load method is called after all of its superclasses’ +load methods.
A category +load method is called after the class’s own +load method.
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

Apple的文档很清楚地说明了initialize和load的区别在于:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。

它们的相同点在于:方法只会被调用一次。(其实这是相对runtime来说的,后边会做进一步解释)。

文档也明确阐述了方法调用的顺序:父类(Superclass)的方法优先于子类(Subclass)的方法,类中的方法优先于类别(Category)中的方法。

总结:

- +(void)load +(void)initialize
执行时机 在程序运行后立即执行 在类的方法第一次被调时执行
若自身未定义,是否沿用父类的方法?
类别中的定义 全都执行,但后于类中的方法 覆盖类中的方法,只执行一个

观点仅代表自己,期待你的留言。

iOS中UIViewController初始化过程及LoadView默认实现

Xib或者Storyboard方式初始化UIViewController

1、系统会通过 (instancetype)initWithCoder:(NSCoder *)aDecoder创建Controller对象实例。
2、当需要将UIController的View添加到父级View时则会通过loadViewIfRequired方法来先判断self.view对象是否为nil
如果为nil则会调用 (void)loadView进行view的初始化。然后会调用viewDidLoad方法

编码的方式初始化UIViewController

[[UIViewController alloc] init],系统会调用[[UIViewController alloc] initWithNibName:(NSString )nibNameOrNil bundle:(NSBundle )nibBundleOrNil]创建实例对象

系统默认loadView实现

通过initWithNibName保存的nibName来加载对应的nib资源并赋值于self.view, 如果nibName为空,则创建一个空的UIView实例赋值。

注意:在loadView方法自定义实现中由于view未进行初始化,如果使用self.view获取值,由会再次触发调用loadView方法造成loadView的递归调用。在ios9.3环境下通过self.view进行赋值则不存在此情况。

附一张UIViewController生命周期图:


观点仅代表自己,期待你的留言。

iOS-hitTest:withEvent与pointInside:withEvent

简介

对于触摸事件的响应,首先要找到能够响应该事件的对象,iOS是用hit-testing 来找到哪个视图被触摸了(hit-test view),也就是以keyWindow为起点,hit-test view为终点,逐级调用hitTest:withEvent。

hitTest:withEvent:方法的处理流程:

先调用pointInside:withEvent:判断触摸点是否在当前视图内
1.如果返回YES,那么该视图的所有子视图调用hitTest:withEvent,调用顺序由层级低到高(top->bottom)依次调用。

2.如果返回NO,那么hitTest:withEvent返回nil,该视图的所有子视图的分支全部被忽略

如果某视图的pointInside:withEvent:返回YES,并且他的所有子视图hitTest:withEvent:都返回nil,或者该视图没有子视图,那么该视图的hitTest:withEvent:返回自己。
如果子视图的hitTest:withEvent:返回非空对象,那么当前视图的hitTest:withEvent:也返回这个对象,也就是沿原路回推,最终将hit-test view传递给keyWindow
以下视图的hitTest:withEvent:方法会返回nil,导致自身和其所有子视图不能被hit-testing发现,无法响应触摸事件:
1.隐藏(hidden=YES)的视图
2.禁止用户操作(userInteractionEnabled=NO)的视图
3.alpha<0.01的视图
4.视图超出父视图的区域


观点仅代表自己,期待你的留言。

UIView的layoutSubviews和drawRect

UIView的setNeedsDisplay和setNeedsLayout方法。首先两个方法都是异步执行的。setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以处理子视图中的一些数据。
综上两个方法都是异步执行的,layoutSubviews方便数据计算,drawRect方便视图重绘。

先大概看下ios layout机制相关的这几个方法:

  • (CGSize)sizeThatFits:(CGSize)size
  • (void)sizeToFit
    ——————-
  • (void)layoutSubviews
  • (void)layoutIfNeeded
  • (void)setNeedsLayout
    ——————–
  • (void)setNeedsDisplay
  • (void)drawRect

一、layoutSubviews在以下情况下会被调用:

1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。
8、直接调用setNeedsLayout。
在苹果的官方文档中强调:You should override this method only if the autoresizing behaviors of the subviews do not offer the behavior you want.
layoutSubviews, 当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

刷新子对象布局
-layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
-setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
-layoutIfNeeded方法:如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

二、drawRect在以下情况下会被调用:

1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量 值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
sizeToFit会自动调用sizeThatFits方法;
sizeToFit不应该在子类中被重写,应该重写sizeThatFits
sizeThatFits传入的参数是receiver当前的size,返回一个适合的size
sizeToFit可以被手动直接调用
sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
-setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘
以上1,2推荐;而3,4不提倡

drawRect方法使用注意点:
1、 若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate 的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或 者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似鱼drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

三、layoutSubviews对subviews重新布局

layoutSubviews方法调用先于drawRect
setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews
layoutIfNeeded方法如其名,UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded方法应该是这样的
layoutIfNeeded遍历的不是superview链,应该是subviews链
drawRect是对receiver的重绘,能获得context
setNeedDisplay在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘


原地址:http://justsee.iteye.com/blog/1886463

开放平台Api的版本控制方案

简介

做开放平台供外部调用的Api理论上变化越少针对调用者来讲越有利,随着开放平台的功能不断完善(优化)不可避免的需要添加新的资源,或者修改现有资源。因此Api的更新升级必不可少,但是,对于开放平台来讲从一开始就应该将对外提供的Api加入版本管理。一个长期稳定可用的开放平台才能被越来越多的用户使用。
同理,Native的App与服务器Api也存在同样的关系,Native App安装在不同的移动设备上,如果Api变动都需要提示用户进行强制更新来适配新版本的Api接口,那么,App用户将异常烦躁甚至于将App从设备中Uninstall掉。

查看众多论坛与博客,总结出目前实现有三种方式:

  • URI
    在访问Api中增加version段来告诉服务器所使用的Api版本(如:http://xxxx/api/customers/v1.1/1234 )。 这种方式正是由于在uri中增加了version段,破坏掉了Api的统一访问URL。
  • Request Parameter
    在Api后增加请求参数来告诉服务器所使用的Api版本(如:http://xxxx/api/customers/1234?v=1.1 )这种方式与第一种URI的方式类似。
  • Request Header
    在请求Api时在Request的请求头中增加Version值来告诉服务器所使用的Api版本。这种方式保留了Api统一访问的URL,版本号传递更隐秘。但需要客户端在请求时增加此参数来完成,所以需要App在开发之初就需要增加版本号的传递。

综上所述,个人认为第三种 Request Header的传递方式具有统一了URL和Version段隐秘的优点,为最适合的传递方式。

Request Header方式实现版本控制

说完了客户端传递版本号的方式之后,我们来看一下服务端的处理。按流行的MVC的方式来看,个人觉得应该在控制层之前增加Api的版本控制层来先对客户端的请求Api进行版本的预判。

个人总结了一下,有以下几种预判结果:
1: Api为所有版本提供服务。
2: Api提供了最低版本,为大于此最低版本的请求提供服务。
3: Api提供了服务的版本范围,为大于最低版本且小于最高版本的请求提供服务。
4: Api已过期,由其它的Api代替。
5: Api已废弃不再提供服务。

按以上思路,按Java实现为例。可以通过 自定义注解(Annotation)+ 过滤器(Filter)实现。
相似的方案还有:自定义注解 + 拦截器, 自定义注解 + Spring AOP
同理,其它的语言均有类似技术可以实现。

接下来我将在 https://github.com/stotem/header-version-filter.git 中公布我的Annotation+Filter实现。


观点仅代表自己,期待你的留言。

Nginx+php-fpm替代apache服务器

简介

nginx本身不能处理PHP,它只是个web服务器,当接收到请求后,如果是php请求,则发给php解释器处理,并把结果返回给客户端。

nginx一般是把请求发fastcgi管理进程处理,fascgi管理进程选择cgi子进程处理结果并返回被nginx

本文以php-fpm为例介绍如何使nginx支持PHP
一、编译安装php-fpm

什么是PHP-FPM

PHP-FPM是一个PHP FastCGI管理器,是只用于PHP的,可以在 http://php-fpm.org/download 下载得到.

PHP-FPM其实是PHP源代码的一个补丁,旨在将FastCGI进程管理整合进PHP包中。必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。

新版PHP已经集成php-fpm了,不再是第三方的包了,推荐使用。PHP-FPM提供了更好的PHP进程管理方式,可以有效控制内存和进程、可以平滑重载PHP配置,比spawn-fcgi具有更多有点,所以被PHP官方收录了。在./configure的时候带 –enable-fpm参数即可开启PHP-FPM。

新版php-fpm安装(推荐安装方式)

1
2
3
4
5
wget http://cn2.php.net/distributions/php-5.4.7.tar.gz
tar zvxf php-5.4.7.tar.gz
cd php-5.4.7
./configure --prefix=/usr/local/php --enable-fastcgi --enable-fpm --with-mcrypt --with-zlib --enable-mbstring --disable-pdo --with-curl --disable-debug --enable-pic --disable-rpath --enable-inline-optimization --with-bz2 --with-xml --with-zlib --enable-sockets --enable-sysvsem --enable-sysvshm --enable-pcntl --enable-mbregex --with-mhash --enable-xslt --enable-memcache --enable-zip --with-pcre-regex --with-mysql
make all install

旧版手动打补丁php-fpm安装

1
2
3
4
5
6
7
8
9
10
11
wget http://cn2.php.net/get/php-5.2.17.tar.gz
wget http://php-fpm.org/downloads/php-5.2.17-fpm-0.5.14.diff.gz

tar zvxf php-5.2.17.tar.gz
gzip -cd php-5.2.17-fpm-0.5.14.diff.gz | patch -d php-5.2.17 -p1
cd php-5.2.17
./configure --prefix=/usr/local/php --enable-fpm --with-mcrypt --enable-mbstring --disable-pdo --with-curl --disable-debug --disable-rpath --enable-inline-optimization --with-bz2 --with-zlib --enable-sockets --enable-sysvsem --enable-sysvshm --enable-pcntl --enable-mbregex --with-mhash --enable-zip --with-pcre-regex --with-mysql --with-mysqli --with-gd --with-jpeg-dir
make all install
cd /usr/local/php
cp etc/php-fpm.conf.default etc/php-fpm.conf
/usr/local/php/sbin/php-fpm

以上两种方式都可以安装php-fpm,安装后内容放在/usr/local/php目录下

二、修改nginx配置文件以支持php-fpm
nginx安装完成后,修改nginx配置文件为,nginx.conf

其中server段增加如下配置,注意标红内容配置,否则会出现No input file specified.错误

#pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000

1
2
3
4
5
6
7
location ~ \.php$ {
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

转自:http://www.cnblogs.com/hujiong/archive/2013/02/20/2918509.html


Nginx反向代理多个域名

简介

由于公司内网有多台服务器的http服务要映射到公司外网静态IP,如果用路由的端口映射来做,就只能一台内网服务器的80端口映射到外网80端口,其他服务器的80端口只能映射到外网的非80端口。非80端口的映射在访问的时候要域名加上端口,比较麻烦。所以我们可以在内网搭建个nginx反向代理服务器,将nginx反向代理服务器的80映射到外网IP的80,这样指向到公司外网IP的域名的HTTP请求就会发送到nginx反向代理服务器,利用nginx反向代理将不同域名的请求转发给内网不同机器的端口,就起到了“根据域名自动转发到相应服务器的特定端口”的效果,而路由器的端口映射做到的只是“根据不同端口自动转发到相应服务器的特定端口”,真是喜大普奔啊。

Nginx配置

vim /usr/local/nginx/conf/reverse-proxy.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
listen 80;
server_name tomcat1.vip.com;
location /{
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8081;
#proxy_pass http://tomcat;
}
access_log logs/tomcat1_access.log;
}

server {
listen 80;
server_name tomcat2.vip.com;
location /{
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8082;
#proxy_pass http://tomcat;
}
access_log logs/tomcat2_access.log;
}

在nginx.conf的http节点中include配置文件reverse-proxy.conf

1
2
3
4
5
6
7
8
9
10
11
12
http {
include reverse-proxy.conf;

#按server段顺序匹配,如果没有匹配到则可通过泛域名匹配返回错误码或错误页
server {
listen 80;
server_name *.vip.com;
location /{
return 404;
}
}
}

测试生效

热部署nginx配置

1
2
3
[root@localhost ~]# /usr/local/nginx/sbin/nginx -t; /usr/local/nginx/sbin/nginx -s reload;
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

在客户机的hosts中配置tomcat1.vip.com和tomcat2.vip.com (如果是已申请好的域名,则可跳过此步骤)

1
2
10.28.10.218 tomcat1.vip.com
10.28.10.218 tomcat2.vip.com

从以上访问结果来看,浏览器的请求会分别引导到了tomcat1和tomcat2上。


观点仅代表自己,期待你的留言。

linux-strace跟踪运行中进程对系统资源的访问

简介

strace常用来跟踪进程执行时的系统调用和所接收的信号。 在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通 过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。

输出参数含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@ubuntu:/usr# strace cat /dev/null 
execve("/bin/cat", ["cat", "/dev/null"], [/* 22 vars */]) = 0
brk(0) = 0xab1000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f29379a7000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
...
brk(0) = 0xab1000
brk(0xad2000) = 0xad2000
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
open("/dev/null", O_RDONLY) = 3
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
read(3, "", 32768) = 0
close(3) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?

每一行都是一条系统调用,等号左边是系统调用的函数名及其参数,右边是该调用的返回值。
strace 显示这些调用的参数并返回符号形式的值。strace 从内核接收信息,而且不需要以任何特殊的方式来构建内核。

strace参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-c 统计每一系统调用的所执行的时间,次数和出错的次数等. 
-d 输出strace关于标准错误的调试信息.
-f 跟踪由fork调用所产生的子进程.
-ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.
-h 输出简要的帮助信息.
-i 输出系统调用的入口指针.
-q 禁止输出关于脱离的消息.
-r 打印出相对时间关于,,每一个系统调用.
-t 在输出中的每一行前加上时间信息.
-tt 在输出中的每一行前加上时间信息,微秒级.
-ttt 微秒级输出,以秒了表示时间.
-T 显示每一调用所耗的时间.
-v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出.
-V 输出strace的版本信息.
-x 以十六进制形式输出非标准字符串
-xx 所有字符串以十六进制形式输出.
-a column
设置返回值的输出位置.默认 为40.
-e expr
指定一个表达式,用来控制如何跟踪.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如:
-eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none.
注意有些shell使用!来执行历史记录里的命令,所以要使用\\.
-e trace=set
只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all.
-e trace=file
只跟踪有关文件操作的系统调用.
-e trace=process
只跟踪有关进程控制的系统调用.
-e trace=network
跟踪与网络有关的所有系统调用.
-e strace=signal
跟踪所有与系统信号有关的 系统调用
-e trace=ipc
跟踪所有与进程通讯有关的系统调用
-e abbrev=set
设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all.
-e raw=set
将指 定的系统调用的参数以十六进制显示.
-e signal=set
指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号.
-e read=set
输出从指定文件中读出 的数据.例如:
-e read=3,5
-e write=set
输出写入到指定文件中的数据.
-o filename
将strace的输出写入文件filename
-p pid
跟踪指定的进程pid.
-s strsize
指定输出的字符串的最大长度.默认为32.文件名一直全部输出.
-u username
以username 的UID和GID执行被跟踪的命令

命令实例

通用的完整用法:

1
strace -o output.txt -T -tt -e trace=all -p 28979

上面的含义是 跟踪28979进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示),最后将记录结果存在output.txt文件里面。

限制strace只跟踪特定的系统调用

如果你已经知道你要找什么,你可以让strace只跟踪一些类型的系统调用。例如,你需要看看在configure脚本里面执行的程序,你需要监视的系统调 用就是execve。让strace只记录execve的调用用这个命令:

1
strace -f -o configure-strace.txt -e execve ./configure


精品博文转自: http://www.cnblogs.com/ggjucheng/archive/2012/01/08/2316692.html