理解 Objective-C Runtime
本文转载于 JustinYan.me
注:本文是对 Colin Wheeler 的 Understanding the Objective-C Runtime 的翻译。
初学 Objective-C(以下简称ObjC) 的人很容易忽略一个 ObjC 特性 —— ObjC Runtime
。这是因为这门语言很容易上手,几个小时就能学会怎么使用,所以程序员们往往会把时间都花在了解 Cocoa 框架以及调整自己的程序的表现上。然而 Runtime 应该是每一个 ObjC 都应该要了解的东西,至少要理解编译器会把
[target doMethodWith:var1];
编译成:
objc_msgSend(target,@selector(doMethodWith:),var1);
这样的语句。理解 ObjC Runtime 的工作原理,有助于你更深入地去理解 ObjC 这门语言,理解你的 App 是怎样跑起来的。我想所有的 Mac/iPhone 开发者,无论水平如何,都会从中获益的。
##ObjC Runtime 是开源的
-
ObjC Runtime 的代码是开源的,可以从这个站点下载: opensource.apple.com。
-
这个是所有开源代码的链接: http://www.opensource.apple.com/source/
-
这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/
-
应该代表的是build版本而不是语言版本,现在是ObjC 2.0
##动态 vs 静态语言
ObjC 是一种面向*runtime(运行时)*的语言,也就是说,它会尽可能地把代码执行的决策从编译和链接的时候,推迟到运行时。这给程序员写代码带来很大的灵活性,比如说你可以把消息转发给你想要的对象,或者随意交换一个方法的实现之类的。这就要求 runtime 能检测一个对象是否能对一个方法进行响应,然后再把这个方法分发到对应的对象去。我们拿 C 来跟 ObjC 对比一下。在 C 语言里面,一切从 main 函数开始,程序员写代码的时候是自上而下地,一个 C 的结构体或者说类吧,是不能把方法调用转发给其他对象的。举个栗子:
#include < stdio.h >
int main(int argc, const char **argv[])
{
printf("Hello World!");
return 0;
}
这段代码被编译器解析,优化后,会变成一堆汇编代码:
.text
.align 4,0x90
.globl _main
_main:
Leh_func_begin1:
pushq %rbp
Llabel1:
movq %rsp, %rbp
Llabel2:
subq $16, %rsp
Llabel3:
movq %rsi, %rax
movl %edi, %ecx
movl %ecx, -8(%rbp)
movq %rax, -16(%rbp)
xorb %al, %al
leaq LC(%rip), %rcx
movq %rcx, %rdi
call _printf
movl $0, -4(%rbp)
movl -4(%rbp), %eax
addq $16, %rsp
popq %rbp
ret
Leh_func_end1:
.cstring
LC:
.asciz "Hello World!"
然后,再链接 include 的库,完了生成可执行代码。对比一下 ObjC,当我们初学这门语言的时候教程是这么说滴:用中括号括起来的语句,
[self doSomethingWithVar:var1];
被编译器编译之后会变成:
objc_msgSend(self,@selector(doSomethingWithVar:),var1);
一个 C 方法,传入了三个变量,self指针,要执行的方法 @selector(doSomethingWithVar:) 还有一个参数 var1。但是在这之后就不晓得发生什么了。
##什么是 Objective-C Runtime?
ObjC Runtime 其实是一个 Runtime 库,基本上用 C 和汇编写的,这个库使得 C 语言有了面向对象的能力(脑中浮现当你乔帮主参观了施乐帕克的 SmallTalk 之后嘴角一抹浅笑)。这个库做的事前就是加载类的信息,进行方法的分发和转发之类的。
##Objective-C Runtime 术语
再往下深谈之前咱先介绍几个术语。
- Runtimes
目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。 - Basic types of Methods
一种 Instance Method,还有 Class Method。instance method 就是带“-”号的,需要实例化才能用的,如 :
-(void)doFoo;
[aObj doFoot];
Class Method 就是带“+”号的,类似于静态方法可以直接调用:
+(id)alloc;
[ClassName alloc];
这些方法跟 C 函数一样,就是一组代码,完成一个比较小的任务。
-(NSString *)movieTitle
{
return @"Futurama: Into the Wild Green Yonder";
}
Selector
一个 Selector 事实上是一个 C 的结构体,表示的是一个方法。定义是:
typedef struct objc_selector *SEL;
使用起来就是:
SEL aSel = @selector(movieTitle);
这样可以直接取一个selector,如果是传递消息(类似于C的方法调用)就是:
[target getMovieTitleForObject:obj];
在 ObjC 里面,用'[]’括起来的表达式就是一个消息。包括了一个 target,就是要接收消息的对象,一个要被调用的方法还有一些你要传递的参数。类似于 C 函数的调用,但是又有所不同。事实上上面这个语句你仅仅是传递了 ObjC 消息,并不代表它就会一定被执行。target 这个对象会检测是谁发起的这个请求,然后决策是要执行这个方法还是其他方法,或者转发给其他的对象。
Class
Class 的定义是这样的:
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
我们可以看到这里这里有两个结构体,一个类结构体一个对象结构体。所有的 objc_object 对象结构体都有一个 isa 指针,这个 isa 指向它所属的类,在运行时就靠这个指针来检测这个对象是否可以响应一个 selector。完了我们看到最后有一个 id 指针。这个指针其实就只是用来代表一个 ObjC 对象,有点类似于 C++ 的泛型。当你拿到一个 id 指针之后,就可以获取这个对象的类,并且可以检测其是否响应一个 selector。这就是对一个 delegate 常用的调用方式啦。这样说还有点抽象,我们看看 LLVM/Clang 的文档对 Blocks 的定义:
struct Block_literal_1 {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 {
unsigned long int reserved; // NULL
unsigned long int size; // sizeof(struct Block_literal_1)
// optional helper functions
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
} *descriptor;
// imported variables
};
可以看到一个 block 是被设计成一个对象的,拥有一个 isa 指针,所以你可以对一个 block 使用 retain, release, copy 这些方法。
##IMP (Method Implementations)
接下来看看啥是IMP。
typedef id (*IMP)(id self,SEL _cmd,...);
一个 IMP 就是一个函数指针,这是由编译器生成的,当你发起一个 ObjC 消息之后,最终它会执行的那个代码,就是由这个函数指针指定的。
Objective-C Classes
OK,回过头来看看一个 ObjC 的类。举一个栗子:
@interface MyClass : NSObject {
//vars
NSInteger counter;
}
//methods
-(void)doFoo;
@end
定义一个类我们可以写成如上代码,而在运行时,一个类就不仅仅是上面看到的这些东西了:
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
可以看到运行时一个类还关联了它的父类指针,类名,成员变量,方法,cache 还有附属的 protocol。
##那么类定义了对象并且自己也是个对象?这是咋整滴?
上面我提到过一个 ObjC 类同时也是一个对象,为了处理类和对象的关系,runtime 库创建了一种叫做 标签类 元类(Meta Class)的东西。当你发出一个消息的时候,比方说
[NSObject alloc];
你事实上是把这个消息发给了一个类对象(Class Object),这个类对象必须是一个 Meta Class 的实例,而这个 Meta Class 同时也是一个根 MetaClass 的实例。当你继承了 NSObject 成为其子类的时候,你的类指针就会指向 NSObject 为其父类。但是 Meta Class 不太一样,所有的 Meta Class 都指向根 Meta Class 为其父类。一个 Meta Class 持有所有能响应的方法。所以当 [NSObject alloc] 这条消息发出的时候,objc_msgSend() 这个方法会去 NSObject 它的 Meta Class 里面去查找是否有响应这个 selector 的方法,然后对 NSObject 这个类对象执行方法调用。
##为啥我们要继承 Apple Classes
初学 Cocoa 开发的时候,多数教程都要我们继承一个类比方 NSObject,然后我们就开始 Coding 了。比方说:
MyObject *object = [[MyObject alloc] init];
这个语句用来初始化一个实例,类似于 C++ 的 new 关键字。这个语句首先会执行 MyObject 这个类的 +alloc 方法,Apple 的官方文档是这样说的:
The isa instance variable of the new instance is initialized to a data structure that describes the class; memory for all other instance variables is set to 0.
新建的实例中,isa 成员变量会变初始化成一个数据结构体,用来描述所指向的类。其他的成员变量的内存会被置为0.
所以继承 Apple 的类我们不仅是获得了很多很好用的属性,而且也继承了这种内存分配的方法。
##那么啥是 Class Cache(objc_cache *cache)
刚刚我们看到 runtime 里面有一个指针叫 objc_cache *cache
,这是用来缓存方法调用的。现在我们知道一个实例对象被传递一个消息的时候,它会根据 isa 指针去查找能够响应这个消息的对象。但是实际上我们在用的时候,只有一部分方法是常用的,很多方法其实很少用或者根本用不到。比如一个object你可能从来都不用copy方法,那我要是每次调用的时候还去遍历一遍所有的方法那就太笨了。于是 cache 就应运而生了,每次你调用过一个方法,之后,这个方法就会被存到这个 cache 列表里面去,下次调用的时候 runtime 会优先去 cache 里面查找,提高了调用的效率。举一个栗子:
MyObject *obj = [[MyObject alloc] init]; // MyObject 的父类是 NSObject
@implementation MyObject
-(id)init {
if(self = [super init]){
[self setVarA:@”blah”];
}
return self;
}
@end
这段代码是这样执行的:
- [MyObject alloc] 先被执行。但是由于 MyObject 这个类没有 +alloc 这个方法,于是去父类 NSObject 查找。
- 检测 NSObject 是否响应 +alloc 方法,发现响应,于是检测 MyObject 类,根据其所需的内存空间大小开始分配内存空间,然后把 isa 指针指向 MyObject 类。那么 +alloc 就被加进 cache 列表里面了。
- 完了执行 -init 方法,因为 MyObject 响应该方法,直接加入 cache。
- 执行 self = [super init] 语句。这里直接通过 super 关键字调用父类的 init 方法,确保父类初始化成功,然后再执行自己的初始化逻辑。
OK,这就是一个很简单的初始化过程,在 NSObject 类里面,alloc 和 init 没做什么特别重大的事情,但是,ObjC 特性允许你的 alloc 和 init 返回的值不同,也就是说,你可以在你的 init 函数里面做一些很复杂的初始化操作,但是返回出去一个简单的对象,这就隐藏了类的复杂性。再举个栗子:
#import < Foundation/Foundation.h>
@interface MyObject : NSObject
{
NSString *aString;
}
@property(retain) NSString *aString;
@end
@implementation MyObject
-(id)init
{
if (self = [super init]) {
[self setAString:nil];
}
return self;
}
@synthesize aString;
@end
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
id obj1 = [NSMutableArray alloc];
id obj2 = [[NSMutableArray alloc] init];
id obj3 = [NSArray alloc];
id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
id obj5 = [MyObject alloc];
id obj6 = [[MyObject alloc] init];
NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));
NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));
[pool drain];
return 0;
}
如果你是ObjC的初学者,那么你很可能会认为这段代码执的输出会是:
NSMutableArray
NSMutableArray
NSArray
NSArray
MyObject
MyObject
但事实上是这样的:
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject
这是因为 ObjC 是允许运行 +alloc 返回一个特定的类,而 init 方法又返回一个不同的类的。可以看到 NSMutableArray 是对普通数组的封装,内部实现是复杂的,但是对外隐藏了复杂性。
OK说回 objc_msgSend
这个方法
这个方法做的事情不少,举个栗子:
[self printMessageWithString:@"Hello World!"];
这句语句被编译成这样:
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");
这个方法先去查找 self 这个对象或者其父类是否响应 @selector(printMessageWithString:),如果从这个类的方法分发表或者 cache 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:
- 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
- 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
- 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
- 如果 cache 找不到就找一下方法分发表。
- 如果还找不到就要开始消息转发逻辑了。
在编译的时候,你定义的方法比如:
-(int)doComputeWithNum:(int)aNum
会编译成:
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)
然后由 runtime 去调用指向你的这个方法的函数指针。那么之前我们说你发起消息其实不是对方法的直接调用,其实 Cocoa 还是提供了可以直接调用的方法的:
// 首先定义一个 C 语言的函数指针
int (computeNum *)(id,SEL,int);
// 使用 methodForSelector 方法获取对应与该 selector 的杉树指针,跟 objc_msgSend 方法拿到的是一样的
// **methodForSelector 这个方法是 Cocoa 提供的,不是 ObjC runtime 库提供的**
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(doComputeWithNum:)];
// 现在可以直接调用该函数了,跟调用 C 函数是一样的
computeNum(obj,@selector(doComputeWithNum:),aNum);
如果你需要的话,你可以通过这种方式你来确保这个方法一定会被调用。
消息转发机制
在 ObjC 这门语言中,发送消息给一个并不响应这个方法的对象,是合法的,应该也是故意这么设计的。换句话说,我可以对任意一个对象传递任意一个消息(看起来有点像对任意一个类调用任意一个方法,当然事实上不是),当然如果最后找不到能调用的方法就会 Crash 掉。
Apple 设计这种机制的原因之一就是——用来模拟多重继承(ObjC 原生是不支持多重继承的)。或者你希望把你的复杂设计隐藏起来。这种转发机制是 Runtime 非常重要的一个特性,大概的步骤如下:
1.查找该类及其父类的 cahce 和方法分发表,在找不到的情况下执行2。
2.执行 + (BOOL) resolveInstanceMethod:(SEL)aSEL 方法。
这就给了程序员一次机会,可以告诉 runtime 在找不到改方法的情况下执行什么方法。举个栗子,先定义一个函数:
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing Foo");
}
完了重载 resolveInstanceMethod 方法:
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(doFoo:)){
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
其中 “v@:”
表示返回值和参数,这个符号涉及 Type Encoding,可以参考Apple的文档 ObjC Runtime Guide。
3.接下来 Runtime 会调用 – (id)forwardingTargetForSelector:(SEL)aSelector
方法。
这就给了程序员第二次机会,如果你没办法在自己的类里面找到替代方法,你就重载这个方法,然后把消息转给其他的Object。
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
这样你就可以把消息转给别人了。当然这里你不能 return self,不然就死循环了=.=
4.最后,Runtime 会调用– (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法。NSInvocation
其实就是一条消息的封装。如果你能拿到 NSInvocation,那你就能修改这条消息的 target, selector 和 arguments。举个栗子:
-(void)forwardInvocation:(NSInvocation *)invocation
{
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
默认情况下 NSObject 对 forwardInvocation 的实现就是简单地执行 -doesNotRecognizeSelector: 这个方法,所以如果你想真正的在最后关头去转发消息你可以重载这个方法(好折腾-.-)。
原文后面介绍了 Non Fragile ivars (Modern Runtime), Objective-C Associated Objects 和 Hybrid vTable Dispatch。鉴于一是底层的可以不用理会,一是早司空见惯的不用详谈,还有一个是很简单的,就是一个建立在方法分发表里面填入默认常用的 method,所以有兴趣的读者可以自行查阅原文,这里就不详谈鸟。
##References:
iOS7的多任务处理
在iOS7之前,当程序退出后,开发者对程序几乎做不了什么。除了VOIP和基于位置的特性,唯一能够在后台运行代码的途径只有使用后台任务(background tasks),但后台任务只会执行几分钟。如果你想要下载一部很大的视频以便离线观看,或者将用户图片备份到服务器,你只能完成部分的任务。
ios7新添加了两个可以在后台更新应用程序界面和内容的APIs。第一个API是后台获取(Background Fetch),允许你在定期间隔内从网络获取新内容。第二个API是远程通知 (Remote Notification),它是一个新特性,它在当新事件发生时利用推送通知(Push Notifications)去告知程序。这两个新的机制,帮助你保持程序界面最新,还可以在新的后台传输服务(Background Transfer Service)中安排任务,这允许你在进程外执行网络传输(下载和上传)。
后台获取(Background Fetch)和远程通知(Remote Notification)基于简单的应用程序委托钩子,在应用程序挂起之前的30秒时钟时间开始执行工作。它们不是用于CPU频繁工作或者长时间运行任务,而是用来处理长时间运行的网络请求队列,例如下载一部很大的电影,或者执行快速的内容更新。
在用户看来,多任务处理唯一明显的变化就是新的程序切换器(app switcher),它会显示当程序退出前台时每一个程序的界面快照。显示这些快照是有原因的:当完成后台工作时,开发者可以更新程序快照,显示新内容的预览。社交网络,新闻,或者天气的应用程序,可以在用户不打开应用程序的情况下显示最新的内容。接下来我们会展示怎么样更新快照。
##后台获取(Background Fetch)
后台获取(Background Fetch
)是一种智能的轮询机制,它很适合需要经常更新内容的程序,像社交网络,新闻或天气的程序。为了在用户启动程序前提前触发后台获取,系统会根据用户行为唤醒应用程序。举个例子,如果用户经常在下午1点使用某个应用程序,系统会学习,适应并在使用周期前执行后台获取。为了减少电池使用,后台获取(Background Fetch)会跨应用程序被设备的无线电合并,如果你向系统报告新数据无法获取,iOS会适应并使用此信息避免会继续获取。
开启后台获取的第一步是在info plist文件中的UIBackgroundModes
健值指定使用的特性。最简单的途径是在Xcode5的project editor中新的性能标签页中(Capabilities tab)设置,这个标签页包含了后台模式部分,可以方便配置多任务选项。
或者,你可以手动编辑这个健值:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>
下一步,告诉iOS你希望多久进行一次后台获取:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
return YES;
}
iOS默认不进行后台获取,所以你需要设置一个时间间隔,否则,你的应用程序永远不行在后台进行获取数据。UIApplicationBackgroundFetchIntervalMinimum
这个值要求系统尽可能经常去管理应用程序什么时候会被唤醒,但如果不需要这个值,你应该指定你的时间间隔。例如,一个天气的应用程序,可能只需要几个小时才更新一次,iOS将会在后台获取之间至少等待你指定的时间间隔。
如果你的应用允许用户退出登录,那么就没有获取新数据的需要了,你应该把minimumBackgroundFetchInterval
设置为UIApplicationBackgroundFetchIntervalNever
,这样可以节省资源。
最后一步是在应用程序委托中实现下列方法:
- (void) application:(UIApplication *)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];
NSURL *url = [[NSURL alloc] initWithString:@"http://yourserver.com/data.json"];
NSURLSessionDataTask *task = [session dataTaskWithURL:url
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
completionHandler(UIBackgroundFetchResultFailed);
return;
}
// Parse response/data and determine whether new content was available
BOOL hasNewData = ...
if (hasNewData) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}];
// Start the task
[task resume];
}
系统唤醒应用程序后将会执行这个委托方法。需要注意的是,你只有30秒的时间来确定获取的新内容是否可用,然后处理新内容并更新界面。30秒时间应该足够去从网络获取数据和获取界面的缩略图,最多只有30秒。当完成了网络请求和更新界面后,你应该调用完成的处理代码。
完成的处理代码有两个目的。首先,系统会估量你的进程消耗的电量,并根据你传递的UIBackgroundFetchResult 参数记录新数据是否可用。其次,当你调用完成的处理代码时,应用的界面缩略图会被采用,并更新应用程序切换器。当用户在应用间切换时,用户将会看到新内容。这种快照行为的完成代码,在新的多任务处理APIs中,很很常见的。
在实际应用中,你应该将completionHandler 传递到应用程序的子组件,然后在处理完数据和更新界面后调用。
在这里,你可能想知道iOS是如何在应用程序后台运行时获得界面快照的,并且想知道应用程序的生命周期与后台获取之间有什么关系。如果应用程序处于挂起状态,系统会先唤醒应用,然后再调用application: performFetchWithCompletionHandler:。如果应用程序还没有启动,系统将会启动它,然后调用常见的委托方法,包括application: didFinishLaunchingWithOptions:。你可以把这种应用程序运行的方式想像为用户从Springboard启动这个程序,区别仅仅在于界面是看不见的,在屏幕外渲染的。
大多数情况下,无论应用在后台启动或者在前台,你会执行相同的工作,但你可以通过查看UIApplication的applicationState
属性来判断应用是不是从后台启动。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState);
return YES;
}
##测试后台获取(Testing Background Fetch)
有两种可以模拟后台获取的途径。最简单是从Xcode运行你的应用,当应用运行时,在Xcode的Debug菜单选择Simulate Background Fetch.
第二种方法,使用scheme更改Xcode运行程序的方式。在Xcode菜单的Product选项,选择Scheme然后选择Manage Schemes.在这里,你可以编辑或者添加一个新的scheme,然后选中Launch due to a background fetch event。如下图:
##远程通知(Remote Notifications)
远程通知允许你在重要事件发生时,告知你的应用。你可能需要发送新的即时信息,突发新闻的提醒,或者用户喜爱电视的最新剧集已经可以下载以便离线观看的消息。远程通知很适合偶尔出现,但当前很重要的内容,这在后台获取之间出现的延迟是不允许的。远程通知会比后台获取更有效率,因为应用程序只有在需要的时候才会启动。
一条远程通知实际上只是一条普通的带有content-available标志的推送通知。当你在后台更新界面时,你可以发送一条带有提醒信息的推送去告诉用户。但远程通知可以做到在安静地,没有提醒消息或者任何声音的情况下,只去更新应用界面或者触发后台工作。然后你可以在完成下载或者处理完新内容后,发送一条本地通知。
静默的推送通知有速度限制,所以你可以勇敢地根据应用程序的需要发送通知。iOS和苹果推送服务会控制推送通知多久被递送,发送很多推送通知是没有问题的。如果你的推送通知被禁止,推送通知可能会被延迟,直到设备下次发送保持活动状态的数据包,或者收到另外一个通知。
##发送远程通知(Sending Remote Notifications)
要发送一条远程通知,需要在推送通知的有效负载(payload)设置content-available标志。content-available标志和用来通知Newsstand应用的健值是一样的,因此,大多数推送脚本和库都已经支持远程通知。当你发送一条远程通知时,你可能还想要包含一些通知有效负载(payload)中的数据,让你应用程序可以引用时间。这可以为你节省一些网络请求,并提高应用程序的响应度。
我建议在开发的时候,使用Nomad CLI’s Houston工具发送推送消息,你也可以使用你喜欢的库或脚本。
你可以通过nomad-cli ruby gem安装Houston
gem install nomad-cli
然后通过包含在Nomad的apn实用工具发送一条通知:
# Send a Push Notification to your Device
apn push <device token> -c /path/to/key-cert.pem -n -d content-id=42
在这里,-n标志指定应该包含content-available健值,-d标志允许添加我们自定义的数据健值到有效负荷(payload)。
通知的有效负荷(payload)结果和下面类似:
{
"aps" : {
"content-available" : 1
},
"content-id" : 42
}
iOS7添加了一个新的应用程序委托方法,当接收到一条带有content-available
的推送通知时,这个方法被调用:
- (void) application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"Remote Notification userInfo is %@", userInfo);
NSNumber *contentID = userInfo[@"content-id"];
// Do something with the content ID
completionHandler(UIBackgroundFetchResultNewData);
}
然后,应用程序进入后台启动,有30秒的时间去获取新内容并更新界面,最后调用完成的处理代码。我们可以像后台获取那样,执行快速的网络请求,但我们可以使用新的强大的后台传输服务,处理任务队列,下面看看我们如何在任务完成后更新界面。
##NSURLSession and Background Transfer Service
NSURLSession是iOS7添加的一个新类,它也是Foundation networking中的新技术。作为NSURLConnection的替代品,一些熟悉的概念和类都保留下来了,例如NSURL,NSURLRequest和NSURLRespond。所以,你可以使用NSURLConnection的替代品——NSURLSessionTask,处理网络请求及响应。一共有3中会话任务:数据,下载和上传。每一种都向NSURLSessionTask添加了语法糖(syntactic sugar),根据你的需要,适当选择一种。
一个NSURLSession对象协调一个或多个NSURLSessionTask对象,并根据NSURLSessionTask创建的NSURLSessionConfiguration实现不同的功能。使用相同的配置,你也可以创建多组具有相关任务的NSURLSession对象。要利用后台传输服务,你将会使用[NSURLSessionConfiguration backgroundSessionConfiguration]来创建一个会话配置。添加到后台会话的任务在外部进程运行,即使应用程序被挂起,崩溃,或者被杀死,依然会运行。
NSURLSessionConfiguration允许你设置默认的HTTP头部,配置缓存策略,限制使用蜂窝数据等等。其中一个选项是discretionary标志,这个标志允许系统为分配任务进行性能优化。这意味着只有当设备有足够电量时,设备才通过Wifi进行数据传输。如果电量低,或者只仅有一个蜂窝连接,传输任务是不会运行的。后台传输总是在discretionary模式下运行。
目前为止,我们大概了解了NSURLSession,以及一个后台会话如何进行,接下来,让我们回到远程通知的例子,添加一些代码来处理后台传输服务的下载队列。当下载完成后,我们会通知用户该文件已经可以使用了。
##NSURLSessionDownloadTask
首先,我们先处理一条远程通知,并把一个NSURLSessionDownloadTask添加到后台传输服务的队列。在backgroundURLSession方法中,我们根据后台会话配置,创建一个NSURLSession对象,并把应用程序委托对象(application delegate)作为会话的委托对象。文档反对对于相同的标识符(identifier)创建多个会话对象,所以我们使用dispatch_once来避免潜在的问题。
- (NSURLSession *)backgroundURLSession
{
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *identifier = @"io.objc.backgroundTransferExample";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
- (void) application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"Received remote notification with userInfo %@", userInfo);
NSNumber *contentID = userInfo[@"content-id"];
NSString *downloadURLString = [NSString stringWithFormat:@"http://yourserver.com/downloads/%d.mp3", [contentID intValue]];
NSURL* downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request];
task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", [contentID intValue]];
[task resume];
completionHandler(UIBackgroundFetchResultNewData);
}
我们使用NSURLSession类方法创建一个下载任务,配置请求,并提供说明供以后使用。因为所有会话任务一开始处于挂起状态,你必须谨记要调用[task resume]保证开始了任务。
现在,我们需要实现NSURLSessionDownloadDelegate的委托方法,当下载完成时,调用回调函数。如果你需要处理认证或会话生命周期的其他事件,你可能还需要实现NSURLSessionDelegate或NSURLSessionTaskDelegate的方法。你应该阅读Apple的Life Cycle of a URL Session with Custom Delegates文档,它讲解了所有类型的会话任务的完整生命周期。
NSURLSessionDownloadDelegate中的委托方法全部是必须实现的,尽管在这个例子中我们只需要用到[NSURLSession downloadTask:didFinishDownloadingToURL:]。任务完成下载时,你会得到一个磁盘上该文件的临时URL。你必须把这个文件移动或复制你的应用程序空间,因为当你从这个委托方法返回时,该文件将从临时存储中删除。
#Pragma Mark - NSURLSessionDownloadDelegate
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location);
// Copy file to your app's storage with NSFileManager
// ...
// Notify your UI
}
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}
当后台会话任务完成时,如果你的应用程序仍然在前台运行,上面的代码已经足够了。然而,在大多数情况下,你的应用程序没有运行,或者在后台被挂起。在这些情况下,你必须实现应用程序委托的两个方法,这样系统就可以唤醒你的应用程序。不同于以往的委托回调,该应用程序委托会被调用两次,因为您的会话和任务委托可能会收到一系列消息。应用程序委托的:handleEventsForBackgroundURLSession:
方法,在这些NSURLSession委托的消息发送前被调用,然后,URLSessionDidFinishEventsForBackgroundURLSession
被调用。在前面的方法中,储存了一个后台完成处理代码(completionHandler),并在后面的方法中调用该代码更新界面。
- (void) application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
// You must re-establish a reference to the background session,
// or NSURLSessionDownloadDelegate and NSURLSessionDelegate methods will not be called
// as no delegate is attached to the session. See backgroundURLSession above.
NSURLSession *backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
// Store the completion handler to update your UI after processing session events
[self addCompletionHandler:completionHandler forSession:identifier];
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);
if (session.configuration.identifier) {
// Call the handler we stored in -application:handleEventsForBackgroundURLSession:
[self callCompletionHandlerForSession:session.configuration.identifier];
}
}
- (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier
{
if ([self.completionHandlerDictionary objectForKey:identifier]) {
NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");
}
[self.completionHandlerDictionary setObject:handler forKey:identifier];
}
- (void)callCompletionHandlerForSession: (NSString *)identifier
{
CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
if (handler) {
[self.completionHandlerDictionary removeObjectForKey: identifier];
NSLog(@"Calling completion handler for session %@", identifier);
handler();
}
}
如果当后台传输完成时,应用程序不再在前台,那么,对于更新程序界面来说,这两步是必要的。此外,如果当后台传输完成时,应用程序根本没有在运行,iOS将会在后台启动该应用程序,然后前面的应用程序和会话的委托方法会在application:didFinishLaunchingWithOptions:
.方法被调用之后被调用。
##配置和限制(Configuration and Limitation)
我们简单地体验了后台传输的强大之处,但你应该深入文档,阅读NSURLSessionConfiguration部分,以便最好地满足你的情况。例如,NSURLSessionTasks通过NSURLSessionConfiguration的timeoutIntervalForResource属性,支持资源超时特性。你可以使用这个特性指定你允许完成一个传输所需的最长时间。内容只在有限的时间可用,或者在用户只有有限Wifi带宽的时间内无法下载或上传资源的情况下,你也可以使用这个特性。
除了下载任务,NSURLSession也全面支持上传任务,因此,你可能会在后台将视频上传到服务器,这保证用户不需要再像iOS6那样离开正在运行的应用程序。如果当传输完成时你的应用程序不需要在后台运行,一个比较好的做法是,把NSURLSessionConfiguration
的sessionSendsLaunchEvents
属性设置为NO。高效利用系统资源,是一件让iOS和用户都高兴的事。
最后,我们来说一说使用后台会话的几个限制。作为一个必须实现的委托,您不能对NSURLSession使用简单的基于块的回调方法。后台启动应用程序,是相对耗费较多资源的,所以总是采用HTTP重定向。后台传输服务只支持HTTP和HTTPS,你不能使用自定义的协议。系统会根据可用的资源进行优化,在任何时候你都不能强制传输任务在后台进行。
另外,要注意,在后台会话中,NSURLSessionDataTasks
是完全不支持的,你应该只出于短期的,小请求为目的使用这些任务,而不是用来下载或上传。
##总结
iOS7中新添加的多任务处理和网络的APIs十分强大,它们为现有和新的应用程序开辟了一系列可能。如果你的应用程序可以从进程外的网络传输和数据中获益,那么尽情地使用这些美妙的APIs!一般情况下,实现后台传输,可以假装你的应用程序正在前台运行,并进行适当的界面更新,而这大部分的工作已经为你完成了。
- 使用适当的新API,为你的应用程序提供内容服务。
- 尽可能早地有效率调用完成处理代码。
- 让完成的处理代码为应用程序更新界面快照。
NSURLSession的简单使用
说到 iOS 7 和 Mac OS X 10.9 Mavericks 的显著变化,其中一个就是Foundation框架中URL加载系统的优化。
此时可能有人正沉浸在Apple的网络基础架构,我想在这里分享一下我对这些新APIs的看法,并展示这些新APIs如何改变我们构建应用程序的方式,以及这些它们在API设计理念演变中的意义。
作为Core Foundation / CFNetwork
框架的APIs之上的一个抽象,NSURLConnection伴随着2003年Safari浏览器的原始发行版本,诞生于10年前。NSURLConnection这个名字,实际上指的是一组构成Foundation框架中URL加载系统的相互关联的组件:NSURLRequest
,NSURLResponse
,NSURLProtocol
,NSURLCache
,NSHTTPCookieStorage
,NSURLCredentialStorage
,以及和它同名的NSURLConnection
。
NSURLRequest对象被传递给一个NSURLConnection
对象。委托(遵守从前的非正式
一个请求发送到服务器前,共享的高速缓存先被访问,然后根据策略(policy)和可用性(availability),一个缓存的响应可能立即透明地返回,如果所有缓存的响应都不可用,则该请求根据选项,被用于为任何后续请求缓存它的响应。
在协商发送一个请求到服务器的过程中,该服务器可发出验证质询,这可以由共享的cookie,证书存储(credential storage)或通过连接委托自动处理。必要的时候,为了无缝地改变装载行为,传出请求也可以被注册的NSURLProtocol对象截获。
不管怎样,考虑到NSURLConnection作为一个网络基础架构,成千上万的Cocoa和Cocoa Touch应用程序从中获益,它已经表现得相当好。但是,这些年来,iPhone和iPad新兴的用例,特别是有一些已经向NSURLConnection的几个核心设想提出了挑战,对其重构已经迫在眉睫。
在2013年的WWDC上,Apple揭开了NSURLConnection继任者的面纱:NSURLSession
。
与NSURLConnection类似,除了同名类NSURLSession,NSURLSession也是指一组相互依赖的类。NSURLSession包括与之前相同的组件,例如NSURLRequest, NSURLCache等。NSURLSession的不同之处在于,它把 NSURLConnection替换为NSURLSession, NSURLSessionConfiguration,以及3个NSURLSessionTask的子类:NSURLSessionDataTask, NSURLSessionUploadTask, 和NSURLSessionDownloadTask.
与NSURLConnection相比,NSURLSession
最直接的改善就是提供了配置每个会话的缓存,协议,cookie和证书政策(credential policies),甚至跨应用程序共享它们的能力。这使得框架的网络基础架构和部分应用程序独立工作,而不会互相干扰。每一个NSURLSession对象都是根据一个NSURLSessionConfiguration
初始化的,该NSURLSessionConfiguration指定了上面提到的政策,以及一系列为了提高移动设备性能而专门添加的新选项。
NSURLSession
的另一重要组成部分是会话任务,它负责处理数据的加载,以及客户端与服务器之间的文件和数据的上传下载服务。NSURLSessionTask与NSURLConnection是及其相似的,因为它负责加载数据,而主要的区别在于,任务共享它们父类NSURLSession的共同委托(common delegate)。
我们现在首先深入探讨任务,然后再介绍更多关于会话配置的知识。
##NSURLSessionTask
NSURLSessionTask是一个抽象子类,它有三个具体的子类是可以直接使用的:NSURLSessionDataTask,NSURLSessionUploadTask和NSURLSessionDownloadTask。这三个类封装了现代应用程序的三个基本网络任务:获取数据,比如JSON或XML,以及上传下载文件。
st=>start: NSURLSessionTask
e=>end: NSURLSessionUploadTask
op0=>operation: NSURLSessionTask
op1=>operation: NSURLSessionDataTask
op2=>operation: NSURLSessionDownloadTask
op3=>operation: NSURLSessionUploadTask
c0=>condition: download
st->c0
c0(no, left)->op1->op3
c0(yes)->op2(right)
当一个NSURLSessionDataTask完成时,它具有关联的数据,而一个NSURLSessionDownloadTask完成时,它具有一个已下载文件的临时文件路径。 NSURLSessionUploadTask 继承了 NSURLSessionDataTask,因为服务器响应一个上传请求时,往往伴随着相关联的数据。 所有任务均可撤销,也可以暂停和恢复。当一个下载任务被取消时,它可以选择创建恢复数据,然后可以传递给下一次新创建的下载任务,以便继续之前的下载。
不同于直接使用alloc-init‘d初始化方法,任务是由一个NSURLSession创建的。每个任务的构造方法都对应一个版本,有或者没有completionHandler属性,例如:–dataTaskWithRequest: 和 –dataTaskWithRequest:completionHandler:。这与NSURLConnection的 -sendAsynchronousRequest:queue:completionHandler:类似,通过指定completionHandler属性创建并使用一个隐含的委托,而不是使用任务的会话。在任何一种任务会话委托的默认行为需要被重写的情况下,这种不太方便的非completionHandler的变体将需要被使用。
##Constructors
iOS5中,NSURLConnection添加了sendAsynchronousRequest:queue:completionHandler:方法,这大大简化了一次性请求的使用,同时可以作为sendSynchronousRequest:returningResponse:error:
:的异步替代品。
NSURL *URL = [NSURL URLWithString:@"http://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ...
}];
NSURLSession与它的任务构造方法在此模式上迭代。在执行resume方法前,该任务对象为了进行进一步的配置而返回,而不是立即执行resume方法。
数据任务可以通过NSURL或NSURLRequest创建(前者是一个标准GET请求URL的快捷方式).
NSURL *URL = [NSURL URLWithString:@"http://example.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];
[task resume];
上传任务也可以通过一个请求以及一个需要上传的本地文件的URL对应的NSData对象创建。
NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSData *data = ...;
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request
fromData:data
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];
[uploadTask resume];
下载任务也需要一个请求,但不同之处在于它们的completionHandler。数据和上传任务在完成时立即返回,但下载任务将数据写入本地的临时文件。completionHandler有责任将文件从它的临时位置移动到一个永久位置,这个永久位置就是块的返回值。
NSURL *URL = [NSURL URLWithString:@"http://example.com/file.zip"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request
completionHandler:
^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:documentsPath];
return [documentsDirectoryURL URLByAppendingPathComponent:[[response URL] lastPathComponent]];
}];
[downloadTask resume];
##NSURLSession & NSURLConnection Delegate Methods
总体而言,NSURLSession的委托方法,是NSURLConnection的演化的十年中 ad-hoc 模式出现以来的一个显著改善。对于一个完整的概述,可以查看此映射表。
以下是一些具体的观察:
NSURLSession同时具有用来处理身份验证挑战会话和任务委托方法。这个会话的委托方法处理连接级别的问题,如服务器信任和客户端证书的评估,NTLM和Kerberos,而任务的委托处理以请求为基础的挑战,如Basic, Digest, 或者代理身份验证。
NSURLConnection由两个方法可以表明一个请求已经完成(NSURLConnectionDataDelegate -connectionDidFinishLoading: 和 NSURLConnectionDelegate -connection:didFailWithError:),而NSURLSession只有一个委托方法(NSURLSessionTaskDelegate -URLSession:task:didCompleteWithError:)。
与NSURLConnection使用的 long long类型相比,委托方法指定在NSURLSession中一定数量的字节传输使用int64_t类型的参数。
NSURLSession在Foundation框架对于委托方法的completionHandler:参数使用上 ,引入了一种新的模式。这允许委托方法可以安全地在主线程以非阻塞方式运行;委托可以简单地在后台运行dispatch_async ,然后在完成时调用completionHandler。同时,它可以有效地拥有多个返回值,不需要使用笨拙的参数指针。就NSURLSessionTaskDelegate的URLSession:task:didReceiveChallenge:completionHandler:方法而言,completionHandler接受两个参数:身份验证质询的处理( the authentication challenge disposition)以及需用使用的证书(如果适用)。
想要查看更多关于会话任务的信息,可以查看 WWDC Session 705: “What’s New in Foundation Networking”
##NSURLSessionConfiguration
NSURLSessionConfiguration对象用于初始化NSURLSession对象。展开请求级别中与NSMutableURLRequest相关的可供选择的方案,我们可以看到NSURLSessionConfiguration对于会话如何产生请求,提供了相当多的控制和灵活性。从网络访问性能,到cookie,安全性,缓存策略,自定义协议,启动事件设置,以及用于移动设备优化的几个新属性,你会发现你一直在寻找的,正是NSURLSessionConfiguration。
会话在初始化时复制它们的配置,NSURLSession有一个只读的配置属性,使得该配置对象上的变化对这个会话的政策无效。配置在初始化时被读取一次,之后都是不会变化的。
##Constructors
NSURLSessionConfiguration
有三个类构造函数,这很好地说明了NSURLSession是为不同的用例而设计的。
+ defaultSessionConfiguration
:返回标准配置,这实际上与NSURLConnection的网络协议栈是一样的,具有相同的共享NSHTTPCookieStorage,共享NSURLCache和共享NSURLCredentialStorage。
+ ephemeralSessionConfiguration
:返回一个预设配置,没有持久性存储的缓存,Cookie或证书。这对于实现像秘密浏览功能的功能来说,是很理想的。
+ backgroundSessionConfiguration
:独特之处在于,它会创建一个后台会话。后台会话不同于常规的,普通的会话,它甚至可以在应用程序挂起,退出,崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程提供上下文。
想要查看更多关于后台会话的信息,可以查看WWDC Session 204: “What’s New with Multitasking”
##Properties
NSURLSessionConfiguration
拥有20个属性。熟练掌握这些属性的用处,将使应用程序充分利用其网络环境。
##General
HTTPAdditionalHeaders
指定了一组默认的可以设置出站请求的数据头。这对于跨会话共享信息,如内容类型,语言,用户代理,身份认证,是很有用的。
NSString *userPasswordString = [NSString stringWithFormat:@"%@:%@", user, password];
NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:@"Basic: %@", base64EncodedCredential];
NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";
configuration.HTTPAdditionalHeaders = @{@"Accept": @"application/json",
@"Accept-Language": @"en",
@"Authorization": authString,
@"User-Agent": userAgentString};
networkServiceType对标准的网络流量,网络电话,语音,视频,以及由一个后台进程使用的流量进行了区分。大多数应用程序都不需要设置这个。
allowsCellularAccess 和 discretionary 被用于节省通过蜂窝连接的带宽。建议在使用后台传输的时候,使用discretionary属性,而不是allowsCellularAccess属性,因为它会把WiFi和电源可用性考虑在内。
timeoutIntervalForRequest 和 timeoutIntervalForResource指定了请求以及该资源的超时时间间隔。许多开发人员试图使用timeoutInterval去限制发送请求的总时间,但这误会了timeoutInterval的意思:报文之间的时间。timeoutIntervalForResource实际上提供了整体超时的特性,这应该只用于后台传输,而不是用户实际上可能想要等待的任何东西。
HTTPMaximumConnectionsPerHost 是 Foundation 框架中URL加载系统的一个新的配置选项。它曾经被用于NSURLConnection管理私人连接池。现在有了NSURLSession,开发者可以在需要时限制连接到特定主机的数量。
HTTPShouldUsePipelining 也出现在NSMutableURLRequest,它可以被用于开启HTTP管道,这可以显着降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的。
sessionSendsLaunchEvents 是另一个新的属性,该属性指定该会话是否应该从后台启动。
connectionProxyDictionary指定了会话连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性。
关于连接代理的更多信息可以在 CFProxySupport Reference 找到。
##Cookie Policies
HTTPCookieStorage 是被会话使用的cookie存储。默认情况下,NSHTTPCookieShorage的+ sharedHTTPCookieStorage会被使用,这与NSURLConnection是相同的。
HTTPCookieAcceptPolicy 决定了该会话应该接受从服务器发出的cookie的条件。
HTTPShouldSetCookies 指定了请求是否应该使用会话HTTPCookieStorage的cookie。
##Security Policies
URLCredentialStorage 是会话使用的证书存储。默认情况下,NSURLCredentialStorage 的+ sharedCredentialStorage 会被使用使用,这与NSURLConnection是相同的。
TLSMaximumSupportedProtocol 和 TLSMinimumSupportedProtocol 确定是否支持SSLProtocol版本的会话。
##Caching Policies
URLCache 是会话使用的缓存。默认情况下,NSURLCache 的+ sharedURLCache 会被使用,这与NSURLConnection是相同的。
requestCachePolicy 指定了一个请求的缓存响应应该在什么时候返回。这相当于NSURLRequest 的-cachePolicy方法。
##Custom Protocols
protocolClasses是注册NSURLProtocol类的特定会话数组。
##总结
iOS 7 和 Mac OS X 10.9 Mavericks 中URL加载系统的变化,是NSURLConnection的一个深思熟虑而自然的进化。总体而言,Foundation框架团队做出了令人惊讶的工作,他们研究并预测了移动开发者现有的和新兴的用例,创造了能够满足日常任务的, 真正有用的APIs。
就可组合性和可扩展性而言,尽管在会话任务的体系结构中,某些决定是一种倒退,NSURLSession仍然可以很好地作为更高级别的网络功能的一个基础。
SDWebImage 存储原理
SDWebImage
托管在github上。https://github.com/rs/SDWebImage
这个类库提供一个UIImageView类别以支持加载来自网络的远程图片。具有缓存管理、异步下载、同一个URL下载次数控制和优化等特征。
使用示范的代码:
UITableView使用UIImageView+WebCache
类(基本应用,UIImageView的一个category)
前提#import导入UIImageView+WebCache.h
文件,然后在tableview的cellForRowAtIndexPath:
方法下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
cell.textLabel.text = @"My Text";
return cell;
}
基本代码:
[imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/image.jpg"]];
使用SDWebImageManager
类:可以进行一些异步加载的工作。
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImage *cachedImage = [manager imageWithURL:url]; // 将需要缓存的图片加载进来
if (cachedImage) {
// 如果Cache命中,则直接利用缓存的图片进行有关操作
// Use the cached image immediatly
} else {
// 如果Cache没有命中,则去下载指定网络位置的图片,并且给出一个委托方法
// Start an async download
[manager downloadWithURL:url delegate:self];
}
当然你的类要实现SDWebImageManagerDelegate
协议,并且要实现协议的webImageManager:didFinishWithImage:
方法。
// 当下载完成后,调用回调方法,使下载的图片显示
- (void)webImageManager:(SDWebImageManager *)imageManager didFinishWithImage:(UIImage *)image {
// Do something with the downloaded image
}
##独立的异步图像下载
可能会单独用到异步图片下载,则一定要用downloaderWithURL:delegate:
来建立一个SDWebImageDownloader
实例。
downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self];
这样SDWebImageDownloaderDelegate
协议的方法imageDownloader:didFinishWithImage:
被调用时下载会立即开始并完成。
##独立的异步图像缓存
SDImageCache类提供一个创建空缓存的实例,并用方法imageForKey:
来寻找当前缓存。
UIImage *myCachedImage = [[SDImageCache sharedImageCache] imageFromKey:myCacheKey];
存储一个图像到缓存是使用方法storeImage: forKey:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
默认情况下,图像将被存储在内存缓存和磁盘缓存中。如果仅仅是想内存缓存中,要使用storeImage:forKey:toDisk:
方法的第三个参数带一负值
来替代。
SDWebImage
支持异步的图片下载+缓存,提供了UIImageView+WebCacha
的 category
,方便使用。纪录一下 SDWebImage 加载图片的流程。
入口 setImageWithURL:placeholderImage:options:
会先把 placeholderImage
显示,然后 SDWebImageManager
根据 URL 开始处理图片。
进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:
,交给 SDImageCache
从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:
.
先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate
回调 imageCache:didFindImage:forKey:userInfo:
到 SDWebImageManager。 SDWebImageManagerDelegate
回调 webImageManager:didFinishWithImage:
到 UIImageView+WebCache
等前端展示图片。
如果内存缓存中没有,生成 NSInvocationOperation
添加到队列开始从硬盘查找图片是否已经缓存。
根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation
进行的操作,所以回主线程进行结果回调 notifyDelegate:
。
如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate
回调 imageCache:didFindImage:forKey:userInfo:
。进而回调展示图片。
如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:
。
共享或重新生成一个下载器 SDWebImageDownloader
开始下载图片。
图片下载由NSURLConnection
来做,实现相关 delegate
来判断图片下载中、下载完成和下载失败。
connection:didReceiveData:
中利用 ImageIO
做了按图片下载进度加载效果。
connectionDidFinishLoading:
数据下载完成后交给 SDWebImageDecoder
做图片解码处理。
图片解码处理在一个 NSOperationQueue
完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
在主线程 notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:
回调给 SDWebImageDownloader。 imageDownloader:didFinishWithImage:
回调给 SDWebImageManager
告知图片下载完成。
通知所有的 downloadDelegates
下载完成,回调给需要的地方展示图片。
将图片保存到 SDImageCache
中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation
完成,避免拖慢主线程。
SDImageCache
在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。
SDWI 也提供了 UIButton+WebCache
和 MKAnnotationView+WebCache
,方便使用。
SDWebImagePrefetcher
可以预先下载图片,方便后续使用。
##SDWebImage库的作用:
通过对UIImageView的类别扩展来实现异步加载替换图片的工作。
主要用到的对象:
1、UIImageView (WebCache)
类别,入口封装,实现读取图片完成后的回调
2、SDWebImageManager
,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader) 。
实现SDImageCache
和SDWebImageDownloader
的回调。
3、SDImageCache
,根据URL的MD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4、SDWebImageDownloader
,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)
其他类:
SDWebImageDecoder
,异步对图像进行了一次解压⋯⋯
目前不明白为什么要做这么道工序。(现在清楚了,功能解释见下文)
##有趣的点:
- SDImageCache是怎么做数据管理的?
SDImageCache
分两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面的相当是个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。
用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。
当SDWebImageManager
向SDImageCache
要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder
,将图片对象放到内存层面做备份,再返回调用层。
2. 为啥必须做Decoder?
通过这个博客:http://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
现在明白了,由于UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,
所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。
为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。
这种做法是典型的空间换时间的做法。
NSTimer的使用
##1. NSRunLoopCommonModes和Timer
当使用NSTimer的scheduledTimerWithTimeInterval
方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode
。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode
模式,在这个过程中,默认的NSDefaultRunLoopMode
模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval
添加到Run Loop中的Timer就不会执行。
所以为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:
方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes
,这个模式等效于NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
的结合。(参考Apple文档)
参考代码:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"主线程 %@", [NSThread currentThread]);
//创建Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
//使用NSRunLoopCommonModes模式,把timer加入到当前Run Loop中。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
//timer的回调方法
- (void)timer_callback
{
NSLog(@"Timer %@", [NSThread currentThread]);
}
输出:
主线程 <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
Timer <NSThread: 0x71501e0>{name = (null), num = 1}
##2. NSThread和Timer
上面讲的NSRunLoopCommonModes
和Timer
中有一个问题,这个Timer本质上是在当前线程的Run Loop中循环执行的,因此Timer的回调方法不是在另一个线程的。那么怎样在真正的多线程环境下运行一个Timer呢?
可以先试试NSThread
。同上,我们还是会把Timer加到Run Loop中,只不过这个是在另一个线程中,因此我们需要手动执行Run Loop
(通过NSRunLoop的run
方法),同时注意在新的线程执行中加入@autoreleasepool
。
完整代码如下:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"主线程 %@", [NSThread currentThread]);
//创建并执行新的线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}
- (void)newThread
{
@autoreleasepool
{
//在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];
//开始执行新线程的Run Loop
[[NSRunLoop currentRunLoop] run];
}
}
//timer的回调方法
- (void)timer_callback
{
NSLog(@"Timer %@", [NSThread currentThread]);
}
输出:
主线程 <NSThread: 0x7118800>{name = (null), num = 1}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
Timer <NSThread: 0x715c2e0>{name = (null), num = 3}
##3. GCD中的Timer
GCD中的Timer应该是最灵活的,而且是多线程的。GCD中的Timer是靠Dispatch Source来实现的。
因此先需要声明一个dispatch_source_t
本地变量:
@interface ViewController ()
{
dispatch_source_t _timer;
}
接着通过dispatch_source_create
函数来创建一个专门的Dispatch Source
,接着通过dispatch_source_set_timer
函数来设置Timer
的参数,注意这里的时间参数有些蛋疼。
开始时间的类型是dispatch_time_t
,最好用dispatch_time
或者dispatch_walltime
函数来创建dispatch_time_t
对象。如果需要Timer立即执行,可以传入dispatch_time(DISPATCH_TIME_NOW, 0)
。
internal
和leeway
参数分别表示Timer的间隔时间和精度。类型都是uint64_t
。间隔时间的单位竟然是纳秒。可以借助预定义的NSEC_PER_SEC
宏,比如如果间隔时间是两秒的话,那interval
参数就是:2 * NSEC_PER_SEC
。
leeway就是精度参数,代表系统可以延时的时间间隔,最高精度当然就传0。
然后通过dispatch_source_set_event_handler
函数来设置Dispatch Source
的事件回调,这里当然是使用Block了。
最后所有dispatch_source_t
创建后默认都是暂停状态的,所以必须通过dispatch_resume
函数来开始事件监听。这里就代表着开始Timer。
完整代码:
NSLog(@"主线程 %@", [NSThread currentThread]);
//间隔还是2秒
uint64_t interval = 2 * NSEC_PER_SEC;
//创建一个专门执行timer回调的GCD队列
dispatch_queue_t queue = dispatch_queue_create("my queue", 0);
//创建Timer
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//使用dispatch_source_set_timer函数设置timer参数
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
//设置回调
dispatch_source_set_event_handler(_timer, ^()
{
NSLog(@"Timer %@", [NSThread currentThread]);
});
//dispatch_source默认是Suspended状态,通过dispatch_resume函数开始它
dispatch_resume(_timer);
输出:
主线程 <NSThread: 0x711fab0>{name = (null), num = 1}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
Timer <NSThread: 0x713a380>{name = (null), num = 3}
Copyright © 2015 Powered by MWeb, 豫ICP备09002885号-5