详解键值观察(KVO)及其实现机理
From:https://mikeash.com/pyblog/friday-qa-2009-01-23.html
##一,前言
Objective-C 中的键(key)-值(value)观察(KVO)并不是什么新鲜事物,它来源于设计模式中的观察者模式
,其基本思想就是:
一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
在 Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键)。下面将一一讲述这些,并会深入 Objective-C 内部一窥键值观察是如何实现的。
本文源码下载:点此下载
##二,运用键值观察
###1,注册与解除注册
如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察对象)上调用名为 NSKeyValueObserverRegistration 的 category 方法将观察者对象与被观察者对象注册与解除注册:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
这两个方法的定义在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。在该头文件中,我们还可以看到 NSObject 还实现了 NSKeyValueObserverNotification 的 category 方法(更多类似方法,请查看该头文件):
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
这两个方法在手动实现键值观察时会用到,暂且不提。
值得注意的是:不要忘记解除注册,否则会导致资源泄露。
###2,设置属性
将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过 key-path 来设置:
[target setAge:30];
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
###3,处理变更通知
观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。
###4,下面来看看一个完整的使用示例:
观察者类:
// Observer.h
@interface Observer : NSObject
@end
// Observer.m
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"
@implementation Observer
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
@end
注意:在实现处理变更通知方法 observeValueForKeyPath 时,要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。
使用示例:
Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];
[target addObserver:observer
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:[Target class]];
[target setAge:30];
//[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
[target removeObserver:observer forKeyPath:@"age"];
在这里 observer 观察 target 的 age 属性变化,运行结果如下:
>> class: Target, Age changed
old age is 10
new age is 30
##三,手动实现键值观察
上面的 Target 应该怎么实现呢?首先来看手动实现。
@interface Target : NSObject
{
int age;
}
// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;
@end
@implementation Target
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
// for manual KVO - age
- (int) age
{
return age;
}
- (void) setAge:(int)theAge
{
[self willChangeValueForKey:@"age"];
age = theAge;
[self didChangeValueForKey:@"age"];
}
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。
##四,自动实现键值观察
自动实现键值观察就非常简单了,只要使用了属性即可。
@interface Target : NSObject// for automatic KVO - age
@property (nonatomic, readwrite) int age;
@end
@implementation Target
@synthesize age; // for automatic KVO - age
- (id) init
{
self = [super init];
if (nil != self)
{
age = 10;
}
return self;
}
@end
##五,键值观察依赖键
有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。
###1,观察依赖键
观察依赖键的方式与前面描述的一样,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加处理变更通知的代码:
#import "TargetWrapper.h"
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([keyPath isEqualToString:@"age"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Age changed", className);
NSLog(@" old age is %@", [change objectForKey:@"old"]);
NSLog(@" new age is %@", [change objectForKey:@"new"]);
}
else if ([keyPath isEqualToString:@"information"])
{
Class classInfo = (Class)context;
NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
encoding:NSUTF8StringEncoding];
NSLog(@" >> class: %@, Information changed", className);
NSLog(@" old information is %@", [change objectForKey:@"old"]);
NSLog(@" new information is %@", [change objectForKey:@"new"]);
}
else
{
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
###2,实现依赖键
在这里,观察的是 TargetWrapper 类的 information 属性,该属性是依赖于 Target 类的 age 和 grade 属性。为此,我在 Target 中添加了 grade 属性:
@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end
@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end
下面来看看 TragetWrapper 中的依赖键属性是如何实现的。
@class Target;
@interface TargetWrapper : NSObject
{
@private
Target * _target;
}
@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;
-(id) init:(Target *)aTarget;
@end
#import "TargetWrapper.h"
#import "Target.h"
@implementation TargetWrapper
@synthesize target = _target;
-(id) init:(Target *)aTarget
{
self = [super init];
if (nil != self) {
_target = [aTarget retain];
}
return self;
}
-(void) dealloc
{
self.target = nil;
[super dealloc];
}
- (NSString *)information
{
return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}
- (void)setInformation:(NSString *)theInformation
{
NSArray * array = [theInformation componentsSeparatedByString:@"#"];
[_target setGrade:[[array objectAtIndex:0] intValue]];
[_target setAge:[[array objectAtIndex:1] intValue]];
}
+ (NSSet *)keyPathsForValuesAffectingInformation
{
NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
return keyPaths;
}
//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
// NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
// NSArray * moreKeyPaths = nil;
//
// if ([key isEqualToString:@"information"])
// {
// moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
// }
//
// if (moreKeyPaths)
// {
// keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
// }
//
// return keyPaths;
//}
@end
首先,要手动实现属性 information 的 setter/getter 方法,在其中使用 Target 的属性来完成其 setter 和 getter。
其次,要实现 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法来告诉系统 information 属性依赖于哪些其他属性,这两个方法都返回一个key-path 的集合。在这里要多说几句,如果选择实现 keyPathsForValuesAffectingValueForKey,要先获取 super 返回的结果 set,然后判断 key 是不是目标 key,如果是就将依赖属性的 key-path 结合追加到 super 返回的结果 set 中,否则直接返回 super的结果。
在这里,information 属性依赖于 target 的 age 和 grade 属性,target 的 age/grade 属性任一发生变化,information 的观察者都会得到通知。
###3,使用示例:
Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];
TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
forKeyPath:@"information"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:[TargetWrapper class]];
[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];
输出结果:
>> class: TargetWrapper, Information changed
old information is 0#10
new information is 0#30
>> class: TargetWrapper, Information changed
old information is 0#30
new information is 1#30
##六,键值观察是如何实现的
###1,实现机理
键值观察用处很多,Core Binding 背后的实现就有它的身影,那键值观察背后的实现又如何呢?想一想在上面的自动实现方式中,我们并不需要在被观察对象 Target 中添加额外的代码,就能获得键值观察的功能,这很好很强大,这是怎么做到的呢?答案就是 Objective C 强大的 runtime 动态能力,下面我们一起来窥探下其内部实现过程。
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。
派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。
如果你对类和对象的关系不太明白,请阅读《深入浅出Cocoa之类与对象》;如果你对如何动态创建类不太明白,请阅读《深入浅出Cocoa 之动态创建类》。
苹果官方文档说得很简洁:
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
###2,代码分析
由于派生类中被重写的 class 对我们撒谎(它说它就是起初的基类),我们只有通过调用 runtime 函数才能揭开派生类的真面目。 下面来看 Mike Ash 的代码:
首先是带有 x, y, z 三个属性的观察目标 Foo:
@interface Foo : NSObject
{
int x;
int y;
int z;
}
@property int x;
@property int y;
@property int z;
@end
@implementation Foo
@synthesize x, y, z;
@end
下面是检验代码:
#import <objc/runtime.h>
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString * name, id obj)
{
NSString * str = [NSString stringWithFormat:
@"\n\t%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
name,
obj,
class_getName([obj class]),
class_getName(obj->isa),
[ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
NSLog(@"%@", str);
}
int main (int argc, const char * argv[])
{
@autoreleasepool {
// Deep into KVO: kesalin@gmail.com
//
Foo * anything = [[Foo alloc] init];
Foo * x = [[Foo alloc] init];
Foo * y = [[Foo alloc] init];
Foo * xy = [[Foo alloc] init];
Foo * control = [[Foo alloc] init];
[x addObserver:anything forKeyPath:@"x" options:0 context:NULL];
[y addObserver:anything forKeyPath:@"y" options:0 context:NULL];
[xy addObserver:anything forKeyPath:@"x" options:0 context:NULL];
[xy addObserver:anything forKeyPath:@"y" options:0 context:NULL];
PrintDescription(@"control", control);
PrintDescription(@"x", x);
PrintDescription(@"y", y);
PrintDescription(@"xy", xy);
NSLog(@"\n\tUsing NSObject methods, normal setX: is %p, overridden setX: is %p\n",
[control methodForSelector:@selector(setX:)],
[x methodForSelector:@selector(setX:)]);
NSLog(@"\n\tUsing libobjc functions, normal setX: is %p, overridden setX: is %p\n",
method_getImplementation(class_getInstanceMethod(object_getClass(control),
@selector(setX:))),
method_getImplementation(class_getInstanceMethod(object_getClass(x),
@selector(setX:))));
}
return 0;
}
在上面的代码中,辅助函数 ClassMethodNames 使用 runtime 函数来获取类的方法列表,PrintDescription 打印对象的信息,包括通过 -class 获取的类名, isa 指针指向的类的名字以及其中方法列表。
在这里,我创建了四个对象,x 对象的 x 属性被观察,y 对象的 y 属性被观察,xy 对象的 x 和 y 属性均被观察,参照对象 control 没有属性被观察。在代码的最后部分,分别通过两种方式(对象方法和 runtime 方法)打印出参数对象 control 和被观察对象 x 对象的 setX 方面的实现地址,来对比显示正常情况下 setter 实现以及派生类中重写的 setter 实现。
编译运行,输出如下:
control: <Foo: 0x10010c980>
NSObject class Foo
libobjc class Foo
implements methods <x, setX:, y, setY:, z, setZ:>
x: <Foo: 0x10010c920>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
y: <Foo: 0x10010c940>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
xy: <Foo: 0x10010c960>
NSObject class Foo
libobjc class NSKVONotifying_Foo
implements methods <setY:, setX:, class, dealloc, _isKVOA>
Using NSObject methods, normal setX: is 0x100001df0, overridden setX: is 0x100001df0
Using libobjc functions, normal setX: is 0x100001df0, overridden setX: is 0x7fff8458e025
从上面的输出可以看到,如果使用对象的 -class 方面输出类名始终为:Foo,这是因为新诞生的派生类重写了 -class 方法声称它就是起初的基类,只有使用 runtime 函数 object_getClass 才能一睹芳容:NSKVONotifying_Foo。注意看:x,y 以及 xy 三个被观察对象真正的类型都是 NSKVONotifying_Foo,而且该类实现了:setY:, setX:, class, dealloc, _isKVOA 这些方法。其中 setX:, setY:, class 和 dealloc 前面已经讲到过,私有方法 _isKVOA 估计是用来标示该类是一个 KVO 机制声称的类。在这里 Objective C 做了一些优化,它对所有被观察对象只生成一个派生类,该派生类实现所有被观察对象的 setter 方法,这样就减少了派生类的数量,提供了效率。所有 NSKVONotifying_Foo 这个派生类重写了 setX,setY方法(留意:没有必要重写 setZ 方法)。
接着来看最后两行输出,地址 0x100001df0 是 Foo 类中的实现,而地址是 0x7fff8458e025 是派生类 NSKVONotifying_Foo 类中的实现。那后面那个地址到底是什么呢?可以通过 GDB 的 info 命令加 symbol 参数来查看该地址的信息:
(gdb) info symbol 0x7fff8458e025
_NSSetIntValueAndNotify in section LC_SEGMENT.__TEXT.__text of /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
看起来它是 Foundation 框架提供的私有函数:_NSSetIntValueAndNotify。更进一步,我们来看看 Foundation 到底提供了哪些用于 KVO 的辅助函数。打开 terminal,使用 nm -a 命令查看 Foundation 中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
其中查找到我们关注的函数:
00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey
Foundation 提供了大部分基础数据类型的辅助函数(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但没有 C++中的 bool),此外还包括一些常见的 Cocoa 结构体如 Point, Range, Rect, Size,这表明这些结构体也可以用于自动键值观察,但要注意除此之外的结构体就不能用于自动键值观察了。对于所有 Objective C 对象对应的是 __NSSetObjectValueAndNotify 方法。
###七,总结
KVO 并不是什么新事物,换汤不换药,它只是观察者模式在 Objective C 中的一种运用,这是 KVO 的指导思想所在。其他语言实现中也有“KVO”,如 WPF 中的 binding。而在 Objective C 中又是通过强大的 runtime 来实现自动键值观察的。至此,对 KVO 的使用以及注意事项,内部实现都介绍完毕,对 KVO 的理解又深入一层了。Objective 中的 KVO 虽然可以用,但却非完美,有兴趣的了解朋友请查看《KVO 的缺陷》 以及改良实现 MAKVONotificationCenter 。
###八,引用
Key-value observing:官方文档
Key-Value Observing Done Right : 官方 KVO 实现的缺陷
MAKVONotificationCenter : 一个改良的 Notification 实现,托管在 GitHub 上
简单了解XCode工程模板的创建知识
使用Xcode 6新建工程时,Apple准备了好些模板,这些模板写个Demo还是没有问题的,但是用来组织项目文件还是太弱了,所以情况经常是不得不每次去新建各种目录,这种重复性的劳动一来乏味,二来浪费时间。那么我们能不像创建自己的模板呢?这样新建的工程就能按自己的想法包含各种目录和文件。好消息是可以,坏消息是Apple没有提供相应的文档。虽然没有文档,还是试着来创建一个模板,每次都重复实在太烦(就是这么任性)。
既然没有文档,我们就把Apple的模板复制一份,在它的基础上修改成我们需要的样子。
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/
有iOS所有工程模板。用户自定义的模板建议放到~/Library/Developer/Xcode/Templates/
,目录如果不存在就创建。模板至少要包含两部分:一是扩展名为 .xctemplate 的文件夹;二是名称为 TemplateInfo.plist 的属性列表文件。好了,我们来创建一个自定义模板:
// Step 1:
$ mkdir ~/Library/Developer/Xcode/Templates/CocoaBite.xctemplate/
// Step 2:
$ cp /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Single\ View\ Application.xctemplate/* ~/Library/Developer/Xcode/Templates/CocoaBite.xctemplate/
现在我们有了一个和Single View Application一样的模板,但这和我们目标还相差很远。接下来我们要做就是修改 TemplateInfo.plist ,让模板为我们做更多准备工作。
Keys | Advice |
---|---|
Ancestors No | Import settings from another Project Template. |
Concrete Recommended | Visible or hide Template form New Project Window. |
Definitions No | Work with workplace. Can write to file example source code. |
Description Recommended | New Project Window – Project Template Description. |
Identifier Yes | Project Template Unique Identifier. |
Kind Yes | XCode Template Kind. Project or File. |
Nodes Recommended | Create or Copy Files to Project. Copy works |
Options Recommended | New Project Wizard >> Choose Options for Project. Add Text Fields, Combo Boxes. |
Platforms Recommended | Set Platform. |
Project Yes | Set Project Build Settings. |
Targets Recommended | Set Build Settings, Build Phases for Targets. Link Libraries. |
上面列出了TemplateInfo.plist大部分键,详细介绍在 这里 。
我自己新建的模板主要用到Definitions和Nodes,它们俩组合起来可以控制模板会新建哪些文件。例如我想让模板包含Models目录:
// Step 1:
$ cd ~/Library/Developer/Xcode/Templates/CocoaBite.xctemplate/
// Step 2:
$ mkdir -p Models
// Step 3: 编辑TemplateInfo.plist 如下图所示。
完整的模板放在 这里 Xcode-6-Project-Templates。
Reference
Creating Custom Xcode 4 Project Templates
About XCode 4 Project Template (How To Create Custom Project Template)
Xcode-6-Project-Templates
链接
美味不用等4.0版本数据管理类设计简介
Controller-->Manager: Request
Note left of Manager: 多个管理类进行数据处理
Manager->Controller:
Note right of Manager: HTTP,Cache,数据处理
Manager->Controller: Model
Controller->View: Data
一直想总结一下自己对一个项目工程架构的经验,在我看来无论是MVC,还是MVVM,其实都是为了将业务数据与具体的UI分离开,这个UI 不单单是具体的View也是UIController,要达到数据的低耦合,降低各模块的依赖,必须要使用一个中间层来管理和数据处理,一直以来我一直使用MVC的基础设计结构,但是我所理解的与之稍有出入。
MVC的概念指的是模型-控制器-视图,而在我看来,如果单一的使用控制器来管理诸多业务,类似网络请求,缓存,通知等,会造成控制器的臃肿和请求的散乱不便于管理,所以我对M的理解更倾向于Manager
,当然在某种程度上也可以理解成为管理类和模型类的结合。管理类负责各工具类的业务调用和对结果数据的封装整理,存储,和下发给控制器,这里工具类是最基础的框架封装,比如工程中的对AFNetworking网络框架的的二次封装NWOperationManager
,
##Manager管理类
- 封装网络请求工具类|NWOperationManager
这里我使用的是AFNetworking作为网络请求框架,继承它进行一些数据的基础封装。
注:目前该框架使用的是2.6.3,所以依然继承的是AFHTTPRequestOperationManager
其中回调的block为:
//Block
typedef void (^OperationCompleteBlock)(AFHTTPRequestOperation *operation, NWResult *result);
NWResult
是针对服务器返回的数据结构的一个最基础的封装:
- 缓存工具类|NWCache
NWOperationManager
是一个网络请求基础工具类,它实现了网络请求的GET,POST等请求功能,同时也实现了缓存管理,当然如果和服务器进行配合可以较好的实现缓存详情参考:iOS网络缓存扫盲篇,但是目前服务器还未有该功能设计,而且由于该工程在最初的版本的时候是没有缓存设计的,是后来功能需求增加,所以需要一个缓存工具来实现,NWCache缓存设计的初步思想如下:
st=>start: 发起数据请求
e=>end: 数据
op=>operation: 本地缓存
op1=>operation: 发起网络请求
op2=>operation: 存储数据
cond=>condition: 缓存有效?
st->op->cond
cond(yes)->e(left)
cond(no)->op1->e
op1->op2(left)->e
NWCache类包含缓存的存储有效时间,和存储路径等作为最底层的工具封装。在网络模块中缓存的实现是请求工具类中处理的,管理类中只负责具体请求的缓存要求,
在请求工具类中其中一个基础接口是:
- (AFHTTPRequestOperation *)requestURL:(NSString *)URLString
inQueue:(dispatch_queue_t)queue
inGroup:(dispatch_group_t)group
HttpMethod:(HttpMethod)method
parameters:(NSDictionary *)parameters
cacheBodyWithBlock:(void (^)(id<NWCacheArgumentProtocol> cacheArgumentProtocol))block
operationCompleteBlock:(OperationCompleteBlock)completeBlock;
NWCacheArgumentProtocol 是一个缓存的配置协议:
@protocol NWCacheArgumentProtocol <NSObject>
- (void)cacheResponseWithIgnoreCache:(BOOL)ignoreCache
supportOffLine:(BOOL)supportOffLine
cacheTimeInSeconds:(NSInteger)cacheTimeInSeconds;
@end
@interface NWCacheArgument : NSObject<NWCacheArgumentProtocol>
@property (nonatomic, strong, readonly) NSString *key;
/** 忽略缓存直接请求*/
@property (nonatomic, assign) BOOL ignoreCache;
/** 是否支持离线缓存,默认不支持*/
@property (nonatomic, assign) BOOL supportOffLine;
@property (nonatomic, assign) NSInteger cacheTimeInSeconds;
- (id)initWithKey:(NSString *)key;
@end
数据管理类中某一个请求是这样的:
- (void)fetchReferencesShopsWithNextCursor:(NSInteger)nextCursor
useCache:(BOOL)useCache
expiredTime:(NSInteger)expiredTime
completeBlock:(CompleteBlock)completeBlock
可以看到该方法中有两个参数是有关缓存的,是否缓存和过期时间,在具体实现的时候有一个方法来处理这两个参数
#pragma mark private base method
- (void)httpRequestMethod:(HttpMethod)method
url:(NSString *)url
parameters:(NSDictionary *)parameters
useCache:(BOOL)useCache
expiredTime:(NSInteger)expiredTime
requestCompleteBlock:(RequestCompleteBlock)requestCompleteBlock{
AFHTTPRequestOperation *opreration = nil;
NSMutableDictionary *param = [self commonArgumentWithUserParameters:parameters];
DDLogInfo(@"%@:%@",url,param);
opreration = [[NWOperationManager sharedClient] requestURL:url
inQueue:self.httpRequest_queue_t
inGroup:self.httpRequest_group_t
HttpMethod:method
parameters:param
cacheBodyWithBlock:^(id<NWCacheArgumentProtocol> cacheArgumentProtocol) {
[cacheArgumentProtocol cacheResponseWithIgnoreCache:!useCache supportOffLine:YES cacheTimeInSeconds:expiredTime];
} operationCompleteBlock:^(AFHTTPRequestOperation *operation, NWResult *result) {
[self requestURI:url operationCompleteBlock:requestCompleteBlock result:result];
DDLogInfo(@"%@:%@",url,result.response);
}];
}
cacheBodyWithBlock该Block就是配置该请求的缓存设置的,
该设计在没有大改早先控制器方法调用的基础上只是修改了 管理类的请求工具调用和增加缓存工具类就实现了自定义的网络请求缓存,目前还能够满足需求。
当然在具体实现中遇到的一些问题是要避免的,首先在缓存数据的时候是有一个Key是很关键的,该key由接口和识别参数,是该参数是需要注意分页中的页码和诸如经纬度等,避免出错。
总之数据管理类的功能是管理和处理数据请求的,解放控制器,让控制器不再进行数据逻辑处理,控制器只负责简单的数据请求,它只要数据不必管这个数据是来自缓存还是来此云端。数据管请求工具返回的数据是简单封装的原始数据(NWResult),然后数据管理进行数据的再封装转为model返回给控制器,当要进行UI展示的时候将数据传输给视图View,这里的数据不建议是model传输,因为要保证解耦,不能让视图与model过多的联系,所以建议以原始数据的形式进行传输。
iOS网络缓存扫盲篇
FROM:iOS网络缓存扫盲篇
由于微信、QQ、微博、这类的应用使用缓存很“重”,使一般的用户也对缓存也非常习惯。缓存已然成为必备。
“缓存的目的的以空间换时间”
这句话在动辄就是 300M、600M 的大应用上,得到了很好的诠释。但能有缓存意识的公司,还在少数。
“只有你真正感受到痛的时候,你才会考虑使用缓存。”
这个痛可能是:
服务器压力、客户端网络优化、用户体验等等。
##当我们在谈论缓存的时候,我们在谈论什么?
我们今天将站在小白用户的角度,给缓存这个概念进行重新的定义。
缓存有不同的分类方法:
这里所指的缓存,是一个宽泛的概念。
我们这里主要按照功能进行划分:
- | 第一种 | 第二种
---|---|----
目的 | 优化型缓存 | 功能型缓存
具体描述 | 出于优化考虑:服务器压力、用户体验、为用户剩流量等等。同时优化型缓存也有内存缓存和磁盘缓存之分。 | App离线也能查看,出于功能考虑,|属于存储范畴
常见概念 | GET网络请求缓存、WEB缓存 | 离线存储
典型应用 | 微信首页的会话列表、微信头像、朋友圈、网易新闻新闻列表、 | 微信聊天记录、
Parse对应的类 | PFCachedQueryController | PFOfflineStore
重度使用缓存的App: 微信、微博、网易新闻、携程、去哪儿等等。
##GET网络请求缓存
###概述
首先要知道,POST请求不能被缓存,只有 GET 请求能被缓存。因为从数学的角度来讲,GET 的结果是 幂等 的,就好像字典里的 key 与 value 就是幂等的,而 POST 不 幂等 。缓存的思路就是将查询的参数组成的值作为 key ,对应结果作为value。从这个意义上说,一个文件的资源链接,也叫 GET 请求,下文也会这样看待。
对于POST请求不能缓存只是在特定的诸如上传接口等,大部分类似FETCH请求其实是可以缓存的,不过需要自定义而已。
###80%的缓存需求:两行代码就可满足
设置缓存只需要三个步骤:
第一个步骤:请使用 GET 请求。
第二个步骤:
如果你已经使用 了 GET 请求,iOS 系统 SDK 已经帮你做好了缓存。你需要的仅仅是设置下内存缓存大小、磁盘缓存大小、以及缓存路径。甚至这两行代码不设置也是可以的,会有一个默认值。代码如下:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:urlCache];
第三个步骤:没有第三步!
你只要设置了这两行代码,基本就可满足80%的缓存需求。AFNetworking 的作者 Mattt曾经说过:
“无数开发者尝试自己做一个简陋而脆弱的系统来实现网络缓存的功能,殊不知 NSURLCache 只要两行代码就能搞定且好上 100 倍。”
(AFN 是不是在暗讽 SDWebImage 复杂又蹩脚的缓存机制??)
要注意
- iOS 5.0开始,支持磁盘缓存,但仅支持 HTTP。
- iOS 6.0开始,支持 HTTPS 缓存。
###控制缓存的有效性
我们知道:
只要是缓存,总会过期。
那么缓存的过期时间如何控制?
上文中的两行代码,已经给出了一个方法,指定超时时间。但这并也许不能满足我们的需求,如果我们对数据的一致性,时效性要求很高,即使1秒钟后数据更改了,客户端也必须展示更改后的数据。这种情况如何处理?
下面我们将对这种需求,进行解决方案的介绍。顺序是这样的:先从文件类型的缓存入手,引入两个概念。然后再谈下,一般数据类型比如 JSON 返回值的缓存处理。
###文件缓存:借助ETag或Last-Modified判断文件缓存是否有效。
Last-Modified
服务器的文件存贮,大多采用资源变动后就重新生成一个链接的做法。而且如果你的文件存储采用的是第三方的服务,比如七牛、青云等服务,则一定是如此。
这种做法虽然是推荐做法,但同时也不排除不同文件使用同一个链接。那么如果服务端的file更改了,本地已经有了缓存。如何更新缓存?
这种情况下需要借助 ETag
或 Last-Modified
判断图片缓存是否有效。
Last-Modified 顾名思义,是资源最后修改的时间戳,往往与缓存时间进行对比来判断缓存是否过期。
在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式类似这样:
Last-Modified: Fri, 12 May 2006 18:53:33 GMT
客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间之后文件是否有被修改过:
If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
总结下来它的结构如下:
-- | -- |
---|---|
请求 HeaderValue | 响应 HeaderValue |
Last-Modified | If-Modified-Since |
如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
判断方法用伪代码表示:
if ETagFromServer != ETagOnClient || LastModifiedFromServer != LastModifiedOnClient
GetFromServer
else
GetFromCache
LastModifiedFromServer != LastModifiedOnClient
而非使用:
LastModifiedFromServer > LastModifiedOnClient
原因是考虑到可能出现类似下面的情况:服务端可能对资源文件,废除其新版,回滚启用旧版本,此时的情况是:
LastModifiedFromServer <= LastModifiedOnClient
但我们依然要更新本地缓存。
参考链接:What takes precedence: the ETag or Last-Modified HTTP header?
Demo10和 Demo11 给出了一个完整的校验步骤:
并给出了 NSURLConnection 和 NSURLSession 两个版本:
/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。
@details
步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 LastModified
3. 下次发送请求的同时,将LastModified一起发送给服务器(由服务器比较内容是否发生变化)
@return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kLastModifiedImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// // 发送 etag
// if (self.etag.length > 0) {
// [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
// }
// 发送 LastModified
if (self.localLastModified.length > 0) {
[request setValue:self.localLastModified forHTTPHeaderField:@"If-Modified-Since"];
}
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// NSLog(@"%@ %tu", response, data.length);
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
}
// 获取并且纪录 etag,区分大小写
// self.etag = httpResponse.allHeaderFields[@"Etag"];
// 获取并且纪录 LastModified
self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
// NSLog(@"%@", self.etag);
NSLog(@"%@", self.localLastModified);
dispatch_async(dispatch_get_main_queue(), ^{
!completion ?: completion(data);
});
}] resume];
}
更多响应码
##ETag
###ETag 是什么?
HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另一种说法是,ETag是一个可以与Web资源关联的记号(token)。它是一个 hash 值,用作 Request 缓存请求头,每一个资源文件都对应一个唯一的 ETag 值, 服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,以下是服务器端返回的格式:
ETag: "50b1c1d4f775c61:df3"
客户端的查询更新格式是这样的:
If-None-Match: W/"50b1c1d4f775c61:df3"
其中:
If-None-Match
- 与响应头的 Etag 相对应,可以判断本地缓存数据是否发生变化
如果ETag没改变,则返回状态304
然后不返回,这也和Last-Modified
一样。
总结下来它的结构如下:
- | -
----|----
请求 HeaderValue 响应| HeaderValue
ETag | If-None-Match
ETag 是的功能与 Last-Modified 类似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的ETag 值,服务器会根据客户端与服务端的 ETag 值是否相等,来决定是否返回 data,同时总是返回对应的 HTTP 状态码。客户端通过 HTTP 状态码来决定是否使用缓存。比如:服务端与客户端的 ETag 值相等,则 HTTP 状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag 值不等,并且状态值会变为200,同时返回 data。
因为修改资源文件后该值会立即变更。这也决定了 ETag 在断点下载时非常有用。 比如 AFNetworking 在进行断点下载时,就是借助它来检验数据的。详见在 AFHTTPRequestOperation 类中的用法:
//下载暂停时提供断点续传功能,修改请求的HTTP头,记录当前下载的文件位置,下次可以从这个位置开始下载。
- (void)pause {
unsigned long long offset = 0;
if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
} else {
offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
}
NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
//若请求返回的头部有ETag,则续传时要带上这个ETag,
//ETag用于放置文件的唯一标识,比如文件MD5值
//续传时带上ETag服务端可以校验相对上次请求,文件有没有变化,
//若有变化则返回200,回应新文件的全数据,若无变化则返回206续传。
[mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
}
//给当前request加Range头部,下次请求带上头部,可以从offset位置继续下载
[mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
self.request = mutableURLRequest;
[super pause];
}
七牛等第三方文件存储商现在都已经支持ETag,Demo8和9 中给出的演示图片就是使用的七牛的服务,见:
static NSString *const kETagImageURL = @"http://ac-g3rossf7.clouddn.com/xc8hxXBbXexA8LpZEHbPQVB.jpg";
下面使用一个 Demo 来进行演示用法,
以 NSURLConnection
搭配 ETag 为例,步骤如下:
请求的缓存策略使用 NSURLRequestReloadIgnoringCacheData
,忽略本地缓存
服务器响应结束后,要记录 Etag,服务器内容和本地缓存对比是否变化的重要依据
在发送请求时,设置 If-None-Match,并且传入 Etag
连接结束后,要判断响应头的状态码,如果是 304,说明本地缓存内容没有发生变化
以下代码详见 Demo08 :
/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。
@details
步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 etag
3. 下次发送请求的同时,将etag一起发送给服务器(由服务器比较内容是否发生变化)
@return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 发送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// NSLog(@"%@ %tu", response, data.length);dd
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
}
// 获取并且纪录 etag,区分大小写
self.etag = httpResponse.allHeaderFields[@"Etag"];
NSLog(@"etag值%@", self.etag);
!completion ?: completion(data);
}];
}
相应的 NSURLSession
搭配 ETag 的版本见 Demo09:
/*!
@brief 如果本地缓存资源为最新,则使用使用本地缓存。如果服务器已经更新或本地无缓存则从服务器请求资源。
@details
步骤:
1. 请求是可变的,缓存策略要每次都从服务器加载
2. 每次得到响应后,需要记录住 etag
3. 下次发送请求的同时,将etag一起发送给服务器(由服务器比较内容是否发生变化)
@return 图片资源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 发送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// NSLog(@"%@ %tu", response, data.length);
// 类型转换(如果将父类设置给子类,需要强制转换)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判断响应的状态码是否是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加载本地缓存图片");
// 如果是,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到缓存的数据
data = cacheResponse.data;
}
// 获取并且纪录 etag,区分大小写
self.etag = httpResponse.allHeaderFields[@"Etag"];
NSLog(@"%@", self.etag);
dispatch_async(dispatch_get_main_queue(), ^{
!completion ?: completion(data);
});
}] resume];
}
运行效果:
##总结
在官方给出的文档中提出 ETag 是首选的方式,优于 Last-Modified 方式。因为 ETag 是基于 hash ,hash 的规则可以自己设置,而且是基于一致性,是“强校验”。 Last-Modified 是基于时间,是弱校验,弱在哪里?比如说:如果服务端的资源回滚客户端的 Last-Modified 反而会比服务端还要新。
虽然 ETag 优于 Last-Modified,但并非所有服务端都会支持,而 Last-Modified 则一般都会有该字段。 大多数情况下需要与服务端进行协调支持 ETag,如果协商无果就只能退而求其次。
Demo 也给出了一个不支持 ETag 的链接,基本随便找一张图片都行:
static NSString *const kLastModifiedImageURL = @"http://image17-c.poco.cn/mypoco/myphoto/20151211/16/17338872420151211164742047.png";
作为通用型的网络请求工具 AFNetworking 对该现状的处理方式是,判断服务端是否包含 ETag ,然后再进行相应处理。可见AFHTTPRequestOperation 类中的用法,也就是上文中已经给出的断点下载的代码。
在回顾下思路:
为资源分派 hash 值,然后对比服务端与本地缓存是否一致来决定是否需要更新缓存。
这种思路,在开发中经常使用,比如:处于安全考虑,登陆操作一般不会传输账号密码,而是传输对应的 hash 值-- token ,这里的 token 就可以看做一个 file 资源,如果想让一个用户登陆超时时间是三天,只需要在服务端每隔三天更改下 token 值,客户端与服务端值不一致,然后服务端返回 token 过期的提示。
值得注意的一点是:
如果借助了 Last-Modified 和 ETag,那么缓存策略则必须使用 NSURLRequestReloadIgnoringCacheData 策略,忽略缓存,每次都要向服务端进行校验。
如果 GET 中包含有版本号信息
众多的应用都会在 GET 请求后加上版本号:
这种情况下, ?v1.0
和 ?v2.0
两个不同版本,请求到的 Last-Modified 和 ETag 会如预期吗?
这完全取决于公司服务端同事的实现, Last-Modified 和 ETag 仅仅是一个协议,并没有统一的实现方法,而服务端的处理逻辑完全取决于需求。
你完全可以要求服务端同事,仅仅判断资源的异同,而忽略掉 ?v1.0 和 ?v2.0 两个版本的区别。
参考链接:if-modified-since vs if-none-match
一般数据类型借助 Last-Modified 与 ETag 进行缓存
以上的讨论是基于文件资源,那么对一般的网络请求是否也能应用?
控制缓存过期时间,无非两种:设置一个过期时间;校验缓存与服务端一致性,只在不一致时才更新。
一般情况下是不会对 api 层面做这种校验,只在有业务需求时才会考虑做,比如:
- 数据更新频率较低,“万不得已不会更新”---只在服务器有更新时才更新,以此来保证2G 等恶略网络环境下,有较好的体验。比如网易新闻栏目,但相反微博列表、新闻列表就不适合。
- 业务数据一致性要求高,数据更新后需要服务端立刻展示给用户。客户端显示的数据必须是服务端最新的数据。
- 有离线展示需求,必须实现缓存策略,保证弱网情况下的数据展示的速度。但不考虑使用缓存过期时间来控制缓存的有效性。
- 尽量减少数据传输,节省用户流量。
一些建议:
- 如果是 file 文件类型,用 Last-Modified 就够了。即使 ETag 是首选,但此时两者效果一致。九成以上的需求,效果都一致。
- 如果是一般的数据类型--基于查询的 get 请求,比如返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified 服务端支持起来就会困难一点。因为比如 你做了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified 指的是10条中任意一个博客的更改。那么服务端需要在你发出请求后,遍历下10条数据,得到“10条中是否至少一个被修改了”。而且要保证每一条博客表数据都有一个类似于记录 Last-Modified 的字段,这显然不太现实。
- 如果更新频率较高,比如最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,然后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。
参考链接:《(慕课网)imooc iPhone3.3 接口数据缓存》
剩下20%的网络缓存需求
真的有NSURLCache 不能满足的需求?
有人可能要问:
NSURLCache 不是帮我们做了硬盘缓存么?那我们为什么要自己用数据库做本地缓存啊。为啥不直接用NSURLCache 不是更方便?
系统帮我们做的缓存,好处是自动,无需我们进行复杂的设置。坏处也恰恰是这个:不够灵活,不能自定义。只能指定一个缓存的总文件夹,不能分别指定每一个文件缓存的位置,更不能为每个文件创建一个文件夹,也不能指定文件夹的名称。缓存的对象也是固定的:只能是 GET请求的返回值。
对象关联 Associated Objects
From:http://nshipster.cn/associated-objects/
#import <objc/runtime.h>
Objective-C开发者应该小心谨慎地遵循这个危险咒语的各种准则。一个很好的原因的就是:混乱的运行时代码会改变运行在其架构之上的所有代码。
从利的角度来讲, <objc/runtime.h>
中的函数具有其他方式做不到的、能为应用和框架提供强大功能的能力。而从弊的角度来讲,它可能会会毁掉代码的sanity meter
,一切代码和逻辑都可能被异常糟糕的副作用影响(terrifying side-effects
)。
因此,我们怀着巨大的恐惧来思考这个与“魔鬼的交易”(Faustian bargain
),一起来看看这个最多地被NSHipster读者们要求讲讲的主题之一:对象关联(associated objects)。
对象关联(或称为关联引用)本来是Objective-C 2.0运行时的一个特性,起始于OS X Snow Leopard和iOS 4。相关参考可以查看 <objc/runtime.h> 中定义的以下三个允许你将任何键值在运行时关联到对象上的函数:
- objc_setAssociatedObject
- objc_getAssociatedObject
- objc_removeAssociatedObjects
为什么我说这个很有用呢?因为这允许开发者对已经存在的类在扩展中添加自定义的属性,这几乎弥补了Objective-C最大的缺点。
NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
通常推荐的做法是添加的属性最好是 static char 类型的,当然更推荐是指针型的。通常来说该属性应该是常量、唯一的、在适用范围内用getter
和setter
访问到:
static char kAssociatedObjectKey;
objc_getAssociatedObject(self, &kAssociatedObjectKey);
然而可以用更简单的方式实现:用selector
。
Since SELs are guaranteed to be unique and constant, you can use _cmd as the key for objc_setAssociatedObject(). #objective-c #snowleopard
— Bill Bumgarner (@bbum) August 28, 2009
##关联对象的行为
属性可以根据定义在枚举类型 objc_AssociationPolicy
上的行为被关联在对象上:
Behavior | @property Equivalent | Description |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (nonatomic, strong) | 指定一个关联对象的强引用,不能被原子化使用。 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (nonatomic, copy) | 指定一个关联对象的copy引用,不能被原子化使用。 |
OBJC_ASSOCIATION_RETAIN | @property (atomic, strong) | 指定一个关联对象的强引用,能被原子化使用。 |
OBJC_ASSOCIATION_COPY | @property (atomic, copy) | 指定一个关联对象的copy引用,能被原子化使用。 |
以 OBJC_ASSOCIATION_ASSIGN 类型关联在对象上的弱引用不代表0 retian的 weak 弱引用,行为上更像 unsafe_unretained 属性,所以当在你的视线中调用weak的关联对象时要相当小心。
根据WWDC 2011, Session 322 (第36分钟左右)发布的内存销毁时间表,被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被 NSObject
-dealloc
调用的object_dispose()
方法中释放。
##删除属性
你可以会在刚开始接触对象关联时想要尝试去调用 objc_removeAssociatedObjects()
来进行删除操作,但如文档中所述,你不应该自己手动调用这个函数:
此函数的主要目的是在“初试状态”时方便地返回一个对象。你不应该用这个函数来删除对象的属性,因为可能会导致其他客户对其添加的属性也被移除了。规范的方法是:调用 objc_setAssociatedObject 方法并传入一个 nil 值来清除一个关联。
##优秀样例
-
添加私有属性用于更好地去实现细节.
当扩展一个内建类的行为时,保持附加属性的状态可能非常必要。注意以下说的是一种非常教科书式的关联对象的用例: AFNetworking在 UIImageView 的category上用了关联对象来保持一个operation对象,用于从网络上某URL异步地 获取一张图片。
-
添加public属性来增强category的功能。
有些情况下这种(通过关联对象)让category行为更灵活的做法比在用一个带变量的方法来实现更有意义。在这些情况下,可以用关联对象实现一个一个对外开放的属性。回到上个AFNetworking的例子中的 UIImageView category,它的 imageResponseSerializer方法允许图片通过一个滤镜来显示、或在缓存到硬盘之前改变图片的内容。
-
创建一个用于KVO的关联观察者。
当在一个category的实现中使用KVO时,建议用一个自定义的关联对象而不是该对象本身作观察者。ng an associated observer for KVO**. When using KVO in a category implementation, it is recommended that a custom associated-object be used as an observer, rather than the object observing itself.
##反例
- 当值不需要的时候建立一个关联对象。
一个常见的例子就是在view上创建一个方便的方法去保存来自model的属性、值或者其他混合的数据。如果那个数据在之后根本用不到,那么这种方法虽然是没什么问题的,但用关联到对象的做法并不可取。 - 当一个值可以被其他值推算出时建立一个关联对象。
例如:在调用 cellForRowAtIndexPath: 时存储一个指向view的 UITableViewCell 中accessory view的引用,用于在 tableView:accessoryButtonTappedForRowWithIndexPath: 中使用。 - 用关联对象替代X,这里的X可以代表下列含义:
- 当继承比扩展原有的类更方便时用子类化。
- 为事件的响应者添加响应动作。
- 当响应动作不方便使用时使用的手势动作捕捉。
- 行为可以在其他对象中被代理实现时要用代理(delegate)。
- 用
NSNotification
和NSNotificationCenter
进行松耦合化的跨系统的事件通知。
比起其他解决问题的方法,关联对象应该被视为最后的选择(事实上category也不应该作为首选方法)。
和其他精巧的trick、hack、workaround一样,一般人都会在刚学习完之后乐于寻找场景去使用一下。尽你所能去理解和欣赏它在正确使用时它所发挥的作用,同时当你选择这个解决办法时,也要避免当被轻蔑地问起“这是个什么玩意?”时的尴尬。
iOS程序员如何使用python写网路爬虫
转载于:http://www.jianshu.com/p/b87413a9307e
iOS程序员如何使用python写网路爬虫
我上一篇blog说过,iOS开发如果之前没接触过除了c和c++(c++太难了,不花个十来年基本不可能精通)的语言,第二门语言最好的选择就是python.
原因就是
1.语法简单
2.库太多,随便想要什么功能的库都找得到,简直编程界的哆啦A梦.
3.语法优美,不信?你去看看python超过两千行的代码再回头看看用oc写的超过两千行的代码,oc写的简直丑到极致(没命名空间,点语法调用和括号调用混用).
##为什么要会写爬虫?
春节前有一件活无人认领,我就自告奋勇认领了,具体如下:
自己写程序在豆瓣读书上抓取人
熊节觉得一个好的程序员应该读过那20本好书 ——《重构》《精益创业》《敏捷软件开发》《测试驱动开发》等等。他在为ThoughtWorks组建成都分公司团队的时候,发愁正统招聘方法太慢了。于是,他花了几个晚上用自己高中自学的水货代码水平写了一个程序,去抓取豆瓣上读过这些技术书籍的人。然后不断递归,再抓到这些人都读过其它什么书,再继续抓读过那些书的人。抓了几万人之后,他再用Hadoop来分析,筛选出了几十个技术大牛。
他把这些大牛的豆瓣账号扔给了公司女HR,让HR去一个个发豆邮勾搭。
春节期间断断续续边看边学写了个爬豆瓣上优秀iOS开发人员的爬虫.所以感觉iOS开发人员有必要掌握这项技术.
再举个例子,你如果想自己弄个app,例如每日精选美女之类的app,你服务端总得有图吧,怎么弄?自己用爬虫爬啊,爬到链接了塞到数据库里,传个json,app直接sdwebimage就好了.多爽!
废话不多说.开始写.
我先假设你用的是mac,然后mac都预装了python2.x,然后呢,你有了python没用,你得有库.没库怎么干活?怎么安装库呢?python界也有个类似于我们iOS开发里cocoapods的东西,这个东西叫做pip.
pip和cocoapods用起来的命令都极其类似,我们只需要两个库,一个叫做urllib2,一个叫做beautifulsoup.
urllib2是干什么的呢?它的作用就是把网页down下来,然后你就可以分析网页了.
beautifulsoup干什么的呢?你用urllib2把网页down下来了之后,里面都是html+css什么的,你想要从乱七八糟的一堆html里面找到正确的图片链接那可不是件简单的事,据我这几天的学习,做法无非两个,一个是自己写正则表达式然后用一个叫re的python库,另一个是使用lxml解析xpath.这两个说实话都不太好用,一个正则就够你吃一壶的.后来我搜索了很久,发现了一个库叫做beautifulsoup,用这个库解析html超级好用.
然后你们打开terminal敲入下面这个命令.
pip install BeautifulSoup
然后就会自动帮你安装BeautifulSoup这个东西了.urllib2因为是自带的,所以不用你下载了.
好的我们打www.dbmeizi.com,这个邪恶的网站,首页都是软妹子.直接右键打开源文件.
你看到的是这些东西.
看上去和乱码没什么区别,但是我们需要仔细观察.终于找到了图片的链接.
图片链接就在li这个标签下地img标签里.现在我们需要做的就是尝试着把这种类型的li从所有html中分离出来.我们可以看到li这个标签有个属性叫做class,这个属性的值是class="span3",我们把这段话li class="span3"
搜索一下,我们发现有20个结果.恰巧,我们这个页面的图片也只有20个,那么可以确定的是我们找到了区别于其他标签的唯一性.
再仔细分析下,img这个标签在li这个标签里有且只有一个.那么,也就是说,我们先搜索出所有符合条件的li标签,然后找到里面的img标签就可以找到所有的图片链接了.
然后看代码.
#!/usr/bin/python
#-*- coding: utf-8 -*-
#encoding=utf-8
import urllib2
import urllib
import os
from BeautifulSoup import BeautifulSoup
def getAllImageLink():
html = urllib2.urlopen('http://www.dbmeizi.com').read()
soup = BeautifulSoup(html)
liResult = soup.findAll('li',attrs={"class":"span3"})
for li in liResult:
imageEntityArray = li.findAll('img')
for image in imageEntityArray:
link = image.get('data-src')
imageName = image.get('data-id')
filesavepath = '/Users/weihua0618/Desktop/meizipicture/%s.jpg' % imageName
urllib.urlretrieve(link,filesavepath)
print filesavepath
if __name__ == '__main__':
getAllImageLink()
我们来一句一句分析下.其实python的语法超级简单.
凡是#打头的就是python里面的注释语句类似于oc里的//.
分别说明我们的环境是python,编码是utf-8
然后import了四个库,分别是urllib2
,urllib
,os
,和beautifulsoup
库.
导入beautifulsoup库的方式和其他三个不太一样.我暂时也不清楚为什么python用这种导入方式,不过照猫画虎就行了.
然后def打头的就是定义一个函数,python里面是不用分号做句与句的分隔符的.他用缩进来表示.与def缩进一个tab的都是函数体.
html = urllib2.urlopen('http://www.dbmeizi.com').read()
这句很简单,就是读取网页的html.然后把值赋给html这个变量.python里声明变量前面不用加任何东西,不用加声明语句和变量类型,就连javascript声明变量还要加个var呢.
我们获取了网页的html之后呢,声明了一个beautifulsoup变量soup,用来准备解析html.
liResult = soup.findAll('li',attrs={"class":"span3"})
这句话的意思就是,寻找html中所有li标签,并且这个li标签有个属性class,class的值是span3.
注意这个findAll函数,有点常识的话你应该清楚,凡是带all的函数基本上返回的都是一个数组,所以我们liResult这个变量实际上是一个数组.
for li in liResult:
这句话基本和oc里的遍历数组语法完全一样.就是遍历liResult里的每一个变量.那么每一个变量就是一个标签.
imageEntityArray = li.findAll('img')
获得了li标签,我们再找出所有的img标签.
一样的道理,遍历所有img标签(实际上只有一个).
link = image.get('data-src')
imageName = image.get('data-id')
这两句的意思就是,获取img标签里的'data-src'属性和'data-id'属性,data-src就是我们最想要的图片链接了.data-id我们会用来当做下载图片之后的名字.
filesavepath = '/Users/weihua0618/Desktop/meizipicture/%s.jpg' % imageName
urllib.urlretrieve(link,filesavepath)
这两句,第一句是设置一个文件存放地址,第二句用urllib这个库的urlretrieve这个方法下载我们的图片,并且把图片放到刚才的路径里.
好了,我们的图片就下载完了.
说说我是怎么爬虫所有豆瓣ios开发的,我先找到所有标签为ios开发的书籍,然后把所有书的id抓到,然后用id找到所有阅读过书的用户id,把所有用户id抓下来之后用hadoop分析,哪些用户id读过的书最多,列出前一百个.然后,你们懂得...(昨天我的ip还是mac地址已经被豆瓣封了)
我感觉,我可以在简历上郑重的写下"精通python和大数据分析" -_-!
如果你认为这篇文章不错,也有闲钱,那你可以用支付宝扫描下方二维码随便捐助一点,以慰劳作者的辛苦
Launch Screen在iOS7/8中的实现
目前项目中需要解决的问题是:
- 兼容iOS7和iOS8,之前的版本不需要支持了
- 实现兼容3.5、4、4.7和5.5寸屏幕,竖屏的Lauch Screen
创建所需的PNG图片
有关iPhone6/6+相关尺寸见这里:iPhone 6 Screens Demystified
需要如下尺寸图片:
- 用于iPhone6+的1242x2208,或者1080x1920,也就是Retina HD 5.5
- 用于iPhone6的750x1334,也就是Retina HD 4.7
- 用于4寸屏(iPhone5/5s)的640x1136,就是Retina 4
- 用于3.5寸(iPhone4/4s)的640x960,就是2x
###使用LaunchImage
使用LaunchImage,可以兼容iOS7和iOS8。
因为iOS8也会调用LaunchScreen.xib
,所以我的做法是直接删除它。就是这个文件:
然后,在Images.xcassets
中创建一个LaunchImage
需要在项目属性里,launch Images Source里设置为LaunchImage
测试了一下:
- iPhone6+使用的是Retina HD 5.5
- iPhone6使用的是Retina HD 4.7
- iPhone5s使用的是Retina 4
- iPhone4s使用的是2x
###使用LauchScreen.xib
这是Xcode6/iOS8的新功能,也就是说,这个步骤,是为了支持iOS8的,而不支持iOS7。其实在目前的项目用不上,不过做了技术准备,就写下来吧。
看到这个方形的xib文件,怪怪的,这是为了支持横屏和竖屏的,因为这个项目只需要竖屏,也可以这样:
不改也没关系。在Images.xcassets
里创建backgroundImage
给LaunchScreen.xib中加一个图片,用刚刚创建的backgroundImage。
测试一下,iOS8下面的Launch Screen都没有问题了:
- iPhone6+用的是3x
- iPhone5s用的是Retina 4 2x
- iPhone6和iPhone4s都是用的2x
因此我没有添加1x的图片,基本没用。
###结论
- 目前比较好的方式是使用Launch Image的方式创建各种设备的图片文件,兼容iOS7/8
- 使用LaunchScreen.xib,功能更强大,但是仅支持iOS8,可能再过几年可以成为主流方法
源代码见这里:GitHub,是使用LaunchImage的方式。
iOS开发之如何跳到系统设置里的WiFi界面
之前以为,苹果不支持直接从应用跳到系统设置里的WiFi界面。后来发现,这个小功能是可以实现的,而且实现起来并不麻烦。让我们一起来看看吧!
##需求
从应用跳到系统设置里的WiFi界面有这个需求存在吗?答案是肯定的。比如以下两个例子:
在没有网的状态下,你可能想提醒用户去设置界面连接WiFi。如果不能跳到WiFi界面,只能在APP里面做出文字提示。这样很多小白用户可能不会看提示,只会觉得APP没有做好。
还有一种情况,做智能家居的APP,智能硬件设备自带WiFi(局域网)。如果用户没有连接设备的WiFi进入APP时,需要提示用户去设置界面连接WiFi。
以上这两种情况只是举个例子,这个小功能的用处还是很多的,大家可以自行探索。
##实现
###info里面设置
在项目中的info.plist中添加 URL types 并设置一项URL Schemes为prefs,如下图:
###实现代码
NSURL *url = [NSURL URLWithString:@"prefs:root=WIFI"];
if ([[UIApplication sharedApplication] canOpenURL:url])
{
[[UIApplication sharedApplication] openURL:url];
}
代码已上传iOSStrongDemo
代码已经push到iOSStrongDemo,大家可以clone下来测试一下。
我在这里抛砖迎玉,大家还想跳到系统设置的什么界面,可以评论留言告诉我,或者大家动手来实现更多的跳转功能!
ReactiveCocoa入门教程 — 第二部分
本文翻译自RayWenderlich ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2
ReactiveCocoa是一个框架,它能让你在iOS应用中使用函数响应式编程(FRP)技术。在本系列教程的第一部分中,你学到了如何将标准的动作与事件处理逻辑替换为发送事件流的信号。你还学到了如何转换、分割和聚合这些信号。
在本系列教程的第二部分,你将会学到一些ReactiveCocoa的高级功能,包括:
另外两个事件类型:error
和 completed
- 节流
- 线程
- 延伸
- 其他
是时候深入研究一下了。
Twitter Instant
在本教程中你将要开发的应用叫Twitter Instant(基于Google Instant的概念),这个应用能搜索Twitter上的内容,并根据输入实时更新搜索结果。
这个应用的初始工程包括一些基本的UI和必须的代码。和第一部分一样,你需要使用CocoaPods来获取ReactiveCocoa框架,并集成到项目中。初始工程已经包含必须的Podfile.
Copyright © 2015 Powered by MWeb, 豫ICP备09002885号-5