Geometry.h的一些方法

这些是在CGGeometry.h里的

CGPoint、CGSize、CGRect、CGRectEdge实际上都是结构体
struct CGPoint {
CGFloat x;
CGFloat y;
};
typedef struct CGPoint CGPoint;

struct CGSize {
CGFloat width;
CGFloat height;
};
typedef struct CGSize CGSize;

struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;

enum CGRectEdge {
CGRectMinXEdge, CGRectMinYEdge, CGRectMaxXEdge, CGRectMaxYEdge
};
typedef enum CGRectEdge CGRectEdge;

创建CGPoint、CGSize、CGRect

CGPoint CGPointMake(CGFloat x, CGFloat y);
CGSize CGSizeMake(CGFloat width, CGFloat height);
CGRect CGRectMake(CGFloat x, CGFloat y, CGFloat width,CGFloat height);

得到关于CGRect的数据
CGFloat CGRectGetMinX(CGRect rect);
CGFloat CGRectGetMidX(CGRect rect);
CGFloat CGRectGetMaxX(CGRect rect);
CGFloat CGRectGetMinY(CGRect rect);
CGFloat CGRectGetMidY(CGRect rect);
CGFloat CGRectGetMaxY(CGRect rect);
CGFloat CGRectGetWidth(CGRect rect);
CGFloat CGRectGetHeight(CGRect rect);

1345337307_3611

bool CGPointEqualToPoint(CGPoint point1, CGPoint point2);//判断点是否相等
bool CGSizeEqualToSize(CGSize size1, CGSize size2);//判断大小是否相等
bool CGRectEqualToRect(CGRect rect1, CGRect rect2);//判断矩形框是否相等

CGRect CGRectStandardize(CGRect rect); //返回一个CGRect
//CGRectMake(1, 1, 1, 1)返回(1, 1, 1, 1)
//CGRectMake(1, 1, 1, -1)返回(1, 0, 1, 1)
//CGRectMake(1, 1, -1, 1)返回(0, 1, 1, 1)
//CGRectMake(1, 1, -1, -1)返回(0, 0, 1, 1)
//只有当width或height小于零时才有改变

bool CGRectIsEmpty(CGRect rect);//判断是否为空 既width或height为0
bool CGRectIsNull(CGRect rect) //判断是否为空 Null一般时执行某个方法后的返回值(例如两个不相交的CGRect执行相交方法(在下面)返回值为Null)
bool CGRectIsInfinite(CGRect rect) //判断是否为无穷大

CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy)
//返回一个CGRect,x为原本的x-dx y为原本的y-dy width为原本的width-2dx height为原本的height-2dy

CGRect CGRectIntegral(CGRect rect) //情况与CGRectStandardize类似
CGRect CGRectUnion(CGRect r1, CGRect r2)//两个CGRect的合集
CGRect CGRectIntersection(CGRect r1, CGRect r2) //两个CGRect的交集
CGRect CGRectOffset(CGRect rect, CGFloat dx, CGFloat dy); //CGRect向x或y方向便宜 x>0向右偏 x<0向左 y>0向下偏 y<0向上
void CGRectDivide(CGRect rect, CGRect *slice, CGRect *remainder,CGFloat amount, CGRectEdge edge);

bool CGRectContainsPoint(CGRect rect, CGPoint point) //判断point是否在rect内
bool CGRectContainsRect(CGRect rect1, CGRect rect2) //判断rect1是否包含rect2
bool CGRectIntersectsRect(CGRect rect1, CGRect rect2) //判断rect1和rect2是否相交

CFDictionaryRefCGPointCreateDictionaryRepresentation(CGPoint point) //把点转换为不可变字典
bool CGPointMakeWithDictionaryRepresentation(CFDictionaryRef dict,CGPoint *point); //把字典转换为点,存在point里,成功返回true 其他false

CFDictionaryRef CGSizeCreateDictionaryRepresentation(CGSize size); //把CGSize转换为不可变字典
bool CGSizeMakeWithDictionaryRepresentation(CFDictionaryRef dict,CGSize *size); //把字典转换为CGSize,存在size里,成功返回true 其他false

CFDictionaryRef CGRectCreateDictionaryRepresentation(CGRect); //把CGRect转换为不可变字典
bool CGRectMakeWithDictionaryRepresentation(CFDictionaryRef dict,CGRect *rect); //把字典转换为CGSize,存在rect里,成功返回true 其他false

这些是在UIGeometry.h里的
NSString *NSStringFromCGPoint(CGPoint point); //把一个点转换字符串,下面类似
NSString *NSStringFromCGSize(CGSize size);
NSString *NSStringFromCGRect(CGRect rect);

CGPoint CGPointFromString(NSString *string); //把字符串转换为点 字符串为@"{2,3}"的形式
CGSize CGSizeFromString(NSString *string); //把字符串转换为CGSize 字符串为@"{3,4}"的形式
CGRect CGRectFromString(NSString *string); //把字符串转换为CGRect 字符串为@"{{3,7},{3,4}}"的形式

转载:http://blog.csdn.net/xingboss3/article/details/7882163

08/14/2015 15:06 下午 posted in  apple

国人为什么这么轻视技术?

到处都是“过来人”的谆谆教导我们不要过分重视技术。技术的确不是创业成功的唯一原因,也的确是不能太把技术的重要性过分强调。

国人为什么这么轻视技术?

  网上搜索一下,到处都是“过来人”的谆谆教导我们不要过分重视技术。技术的确不是创业成功的唯一原因,也的确是不能太把技术的重要性过分强调。假如目前国内是一种技术至上的现状,技术人员的地位虚高,那么这些言论的确有助于业内整体的协调发展。但是现状是,目前国内普遍的认识并没有很抬高技术的地位。恰恰相反,技术被过低的估计了其应有的价值,网上强调技术重要性的文章几乎没有。技术人员的地位已经是低得不能再低,在许多公司内部,已经到处都是对技术人员的轻蔑的眼光。在重要的技术几乎都由国外主导,而目前中国已经几乎丧失了任何技术主导权的今天,还在一直继续强调切不可技术主导,将会给国家的IT产业的自强发展造成严重的影响。

  比如今天看到的这篇关于创业团队的思考:切不可技术主导,又是一篇典型文章。其实整篇看下来,作者说的有一定道理,只不过他其实考虑的只是有关Web 2.0网站的创业,却又强扣了一顶技术无用论的大帽子。他的文章标题假如是:“关于Web 2.0网站创业的思考:切不可技术主导”的话,倒也较少可辩驳之处。可惜他文章标题的范围说的是整个创业团队,那这篇文章就完全是在误导人了。

  IT业技术主导创业成功的例子比比皆是,著名的有:

  Borland公司。就只有两个人,谈不上管理,没有资金,没有关系,没有背景,也没有市场,要不是Anders Hejlesberg拥有高超的技术,写出了当时世界上最块的Borland Turbo Pascal编译器,他们怎么成功?他们的创业团队是靠什么主导的?

  ID公司。也只有两穷小子,John Carmark和John Romero。要不是John Carmark利用天才的3D技术写出了Doom,他们怎么成功?他们的创业团队是靠什么主导的?

  微软公司。还是只有两个人。要不是Bill和Allen利用高超的技术,在4K内存的Atali 4004计算机上写出了Basic语言的解释器,他们怎么成功?他们的创业团队是靠什么主导的?

  另外Apple,Oracle,Intel等等著名的公司都有类似的经历。

  你也许会说这些都是以前的例子,现在不会有这种情况。OK,那么现在最红火的Google,他们的几个创始人都是技术沙文主义者,要不是他们能创造出可以支持几亿人同时访问只用毫秒级时间的搜索引擎,他们能成功么?另外,Linux,Firefox,MySQL,JBoss这些东西,哪个创业团队不是靠技术主导成功的?

  最后,我还想知道,对一个创业团队来说,如果你不靠技术主导?那么靠什么主导?

  靠市场主导?刚创业就有市场?你用什么东西占领市场?

  靠管理主导?Come on,我们正在谈论的是“创业”团队,你认为哪个创业团队会有几百号人让你来管理?

  靠Idea?有这种想法的人一般是空想家。就像不会在人群拥挤的大街上捡到一百万一样,容易实现的Idea早被n多人发现并且做了。哪有什么你可以想到别人没有想到的点子。除非你想到并且可以做到的点子别人无法做到。那你在资金,关系,地位什么都没有的情况下,靠什么来拉高这个门槛让别人够不着?我能想到的只有技术。

  靠关系?如果你是国家主席的儿子,那么没什么好谈的了。这篇文章不是给你看的。

  靠钻法律空子,政策空子?这也是需要高超的技术的,你至少在法律政策上要很熟悉。

  偷钱,抢银行?对不起,我认为这还是需要技术,同样是技术主导。
转自:
http://www.testage.net/html/01/n-158101.html

08/10/2015 09:57 上午 posted in  杂七杂八

理解 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 是开源的

  1. ObjC Runtime 的代码是开源的,可以从这个站点下载: opensource.apple.com。

  2. 这个是所有开源代码的链接: http://www.opensource.apple.com/source/

  3. 这个是ObjC rumtime 的源代码: http://www.opensource.apple.com/source/objc4/

  4. 应该代表的是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 术语

再往下深谈之前咱先介绍几个术语。

  1. Runtimes
    目前说来Runtime有两种,一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。
  2. 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

这段代码是这样执行的:

  1. [MyObject alloc] 先被执行。但是由于 MyObject 这个类没有 +alloc 这个方法,于是去父类 NSObject 查找。
  2. 检测 NSObject 是否响应 +alloc 方法,发现响应,于是检测 MyObject 类,根据其所需的内存空间大小开始分配内存空间,然后把 isa 指针指向 MyObject 类。那么 +alloc 就被加进 cache 列表里面了。
  3. 完了执行 -init 方法,因为 MyObject 响应该方法,直接加入 cache。
  4. 执行 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 里面找到了,就调用它对应的函数指针。如果找不到,那就会执行一些其他的东西。步骤如下:

  1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
  2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
  3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
  4. 如果 cache 找不到就找一下方法分发表。
  5. 如果还找不到就要开始消息转发逻辑了。

在编译的时候,你定义的方法比如:

-(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:

Objective-C Runtime Programming Guide

Objective-C Runtime Reference

08/07/2015 08:37 上午 posted in  apple

iOS开发runtime:类与对象

要深入理解runtime,首先要从最基本的类与对象开始:

###runtime中的类和对象
首先,我们从*/usr/include/objc/objc.h* 和 runtime.h 中找到对 classobject 的定义:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

由此可见,Class是一个指向objc_class结构体的指针,而id是一个指向objc_object结构体的指针,其中的成员isa是一个指向objec_class结构体的指针。

下面我们来看看关于objc_class的定义:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;// 指向metaclass

#if !__OBJC2__
    Class super_class;      // 指向父类                                  
    const char *name;       // 类名                                
    long version;           // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取                            
    long info;              // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;                                   
    long instance_size;     // 该类的实例变量大小(包括从父类继承下来的实例变量);
    struct objc_ivar_list *ivars;           // 用于存储每个成员变量的地址                      
    struct objc_method_list **methodLists;   // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
    struct objc_cache *cache;        // 指向最近使用的方法的指针,用于提升效率;
    struct objc_protocol_list *protocols     // 存储该类声明遵守的协议                
#endif

} OBJC2_UNAVAILABLE;

可见,类与对象的区别仅仅在于类比对象的结构体中多了众多的成员,它们都可以当做一个objec_object来对待,也就是说类和对象都是对象,为了区别概念,这里引入一个术语:类对象(class object)实例对象(instance object),这样我们就可以区别对象和类了(可别混淆了哦)。

下面详细介绍一下objec_class中各成员:

isa:objec_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(还记得“-”开头的方法吗?);此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。

super_class: 指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为NULL。

到这里我们可以看清楚OC中类与对象的继承层次关系:

注意点,所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。

当我们调用某个对象的实例方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过class的super_class指针找到父类的类对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根class;

当我们调用某个某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其中methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass对象结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查找,直至根metaclass;

经过以上介绍,相信你已经对OC中对象与类的结构层次有了更深刻的认识。下面介绍如何利用runtime机制。

##runtime的简单使用

runtime机制为我们提供了一系列的方法让我们可以在程序运行时动态修改类、对象中的所有属性、方法。

下面就介绍运行时一种很常见的使用方式,字典转模型。当然,你可能会说,“我用KVO直接 setValuesForKeysWithDictionary:传入一个字典一样可以快速将字典转模型啊”,但是这种方法有它的弊端,只有遍历某个模型中所有的成员变量,然后通过成员变量从字典中取出对应的值并赋值最为稳妥,由于篇幅有限,这里暂且不讨论那么多,你权且当作多认识一种数据转模型的方式,以及初步认识一下runtime的强大。

1、假设我定义了一个类(随便写的,不要纠结名字,.m文件啥也没写);

@interface Lender : NSObject{
    CGFloat height;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, assign) int no;

@end

2、在其它文件使用这个类,注意:要使用运行时,必须先包含

#import <objc/message.h>

下面,我将会通过一小段代码来获取到这个类中所有的成员变量

unsigned int outCount = 0;
    Ivar *vars = class_copyIvarList([Lender class], &outCount); // 获取到所有的变量列表
    
    // 遍历所有的成员变量
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = vars[i]; // 取出第i个位置的成员变量
        
        const char *propertyName = ivar_getName(ivar); // 通过变量获取变量名
        const char *propertyType = ivar_getTypeEncoding(ivar); // 获取变量编码类型
        printf("---%s--%s\n", propertyName, propertyType);

    }

打印结果:

---height--f
---_name--@"NSString"
---_age--@"NSNumber"
---_no--i

可见,通过这几句简单的代码就可以获取到某个类中所有变量的名称和类型,然后通过object_setIvar()方法为具体某个对象的某个成员变量赋值。

EG:http://justinyan.me/post/1624

08/06/2015 22:00 下午 posted in  apple

萌音FM

07/28/2015 12:00 下午 posted in  Application

iOS项目的目录结构和开发流程

原文出处: Limboy

网上相关的资源不多,开源的且质量还不错的iOS项目也是少之又少,最近正好跟同事合作了一个iOS项目,来说说自己的一些想法。

##目录结构


AppDelegate

Models

Macro

General

Helpers

Vendors

Sections

Resources

一个合理的目录结构首先应该是清晰的,让人一眼看上去就能大概了解目录的职责,且容易应对新的变化。

###AppDelegate

这个目录下放的是AppDelegate.h(.m)文件,是整个应用的入口文件,所以单独拿出来。

###Models

这个目录下放一些与数据相关的Model文件,里面大概是这样:

Models

    |-
 BaseModel.h

    |-
 BaseModel.m

    |-
 CollectionModel.h

    |-
 CollectionModel.m

    ...

###Macro

这个目录下放了整个应用会用到的宏定义,里面大概是这样:

Macro

    |-
 AppMacro.h

    |-
 NotificationMacro.h

    |-
 VendorMacro.h

    |-
 UtilsMacro.h

    ...

AppMacro.h 里放app相关的宏定义,如:

//
 表情相关

#define
 EMOTION_CACHE_PATH @"cachedemotions"

#define
 EMOTION_RECENT_USED @"recentusedemotions"

#define
 EMOTION_CATEGORIES @"categoryemotions"

#define
 EMOTION_TOPICS @"emotiontopics"

 

//
 收藏相关

#define
 COLLECT_CACHE_PATH @"collected"

 

//
 配图相关

#define
 WATERFALL_ITEM_HEIGHT_MAX 300

#define
 WATERFALL_ITEM_WIDTH 146

NotificationMacro.h 里放的是通知相关的宏定义。
UtilsMacro.h 里放的是一些方便使用的宏定义,如:

define
 UIColorFromRGB(r,g,b) [UIColor \

colorWithRed:r/255.0
 \
green:g/255.0
 \
blue:b/255.0
 alpha:1]
#define
 NSStringFromInt(intValue) [NSString stringWithFormat:@"%d",intValue]

VendorMacro.h 里放一些第三方常量,如:

#define
 UMENG_KEY @"xxxxx"

#define
 UMENG_CHANNEL_ID @"xxx"

如果有新的类型的宏定义,可以再新建一个相关的Macro.h。

General

这个目录放会被重用的Views/Classes和Categories。里面大概是这样:

General

    |-
 Views

        |-
 TPKScollView

        |-
 TPKPullToRefresh

        ...

    |-
 Classes

        |-
 TPKBaseViewController

        |-
 TPKHorizontalView

        ...

    |
 - Categories

        |-
 UIViewController+Sizzle

        |-
 UIImageView+Downloader

        ...

这里的TPK是项目的首字母缩写。

###Helpers

这个目录放一些助手类,文件名与功能挂钩。里面大概是这样:


Helpers

    |-
 TPKShareHelper

    |-
 TPDBHelper

    |-
 TPKEmotionHelper

    ...

助手类的主要作用是帮助Controller瘦身,也可以提供一定程度的复用。

###Vendors

这个目录放第三方的类库/SDK,如UMeng、WeiboSDK、WeixinSDK等等。

###Sections

这个目录下面的文件对应的是app的具体单元,如导航、瀑布流等等。里面大概是这样:


Sections

    |-
 Menu

    |-
 Setting

    |-
 Collection

    ...

###Resources

这个目录下放的是app会用到的一些资源,主要是图片。

##Cocoapods

业务无关的类库可以通过 Cocoapods 来方便地管理,如SDWebImage, Reachability等等。还有一些是多个应用都会用到的基础模块,比如HBAPI、HBSNS 、HBFoundation(HB为公司名首字母)等等,可以建一个私有的git repo,然后加到podfile中,这样如果HBAPI有更新,只需要pod update一下就行了。

顺便说一下HBFoundation,这个git仓库中可以放一些自己写的所有应用基本上都会用到的小模块。如很多app都会有隔一段时间跳出一个求好评的alertView,就可以写一个HBRating类,这样需要使用该功能的app只需加上一句:[HBRating checkIfShouldPopupWithAppID:(NSInteger)appID]就行了。又比如app都有接受push notification的需求,可以写一个HBAPNS类,等等。

##开发流程

在拿到设计图后,就可以针对设计图抽离出可复用的Classes/Views/Helpers,考虑一下某个效果的具体实现,使用合适的设计模式来避免大量的if/else嵌套,等等。不要一下子就钻到Sections中去实现页面效果和功能,初看起来可能会快一点,但只要有点复杂度的项目,这种做法到后来只会吃尽苦头,代码会变的越来越难维护。所以前期一定要做好充足的准备工作。

07/11/2015 20:52 下午 posted in  Program

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那样离开正在运行的应用程序。如果当传输完成时你的应用程序不需要在后台运行,一个比较好的做法是,把NSURLSessionConfigurationsessionSendsLaunchEvents属性设置为NO。高效利用系统资源,是一件让iOS和用户都高兴的事。

最后,我们来说一说使用后台会话的几个限制。作为一个必须实现的委托,您不能对NSURLSession使用简单的基于块的回调方法。后台启动应用程序,是相对耗费较多资源的,所以总是采用HTTP重定向。后台传输服务只支持HTTP和HTTPS,你不能使用自定义的协议。系统会根据可用的资源进行优化,在任何时候你都不能强制传输任务在后台进行。

另外,要注意,在后台会话中,NSURLSessionDataTasks 是完全不支持的,你应该只出于短期的,小请求为目的使用这些任务,而不是用来下载或上传。

##总结

iOS7中新添加的多任务处理和网络的APIs十分强大,它们为现有和新的应用程序开辟了一系列可能。如果你的应用程序可以从进程外的网络传输和数据中获益,那么尽情地使用这些美妙的APIs!一般情况下,实现后台传输,可以假装你的应用程序正在前台运行,并进行适当的界面更新,而这大部分的工作已经为你完成了。

  • 使用适当的新API,为你的应用程序提供内容服务。
  • 尽可能早地有效率调用完成处理代码。
  • 让完成的处理代码为应用程序更新界面快照。
07/07/2015 15:47 下午 posted in  apple

iOS7的视图控制器转场动画简介

##自定义动画

iOS 7 对我来说最激动人心的特性就是新的视图控制器切换API(View Controller Transitioning API)。 iOS 7 之前,View Controller之间切换,我需要创建自定义的transitions。 而且这些方法都支持不完整,让人头疼。在transitions中增加交互功能就更难了。

在开始这篇文章之前,我要提醒一下:这是一个新的API,我们尽最大努力让它可以实用,但是并不能保证是最佳。可能需要至少一个月后才能确定,这篇文章不是最佳的实用方案,这里只是一个对新功能的探索。如果有更好的使用这个API的方法,请联系我们,这样就可以修正这篇文章。

在开始介绍这个API之前,我们需要知道导航控制器的默认行为在iOS7下已经改变了:导航控制器下,切换2个view controller的动画有一点细微的改变,变得更有交互性。例如,当你希望弹出一个view controller时,可以从屏幕左边开始拖动,把整个内容拖动到屏幕右边。

让我们仔细看一下这个API,我发现这个被重度使用的接口是协议并不是一个实体。虽然一上来看上去有一点怪,但是我喜欢这个API,它给了我们更多的灵活性。我们从简单开始:用自定义动画代替原有的view controller的push动画(这里是sample project 在github)。我们首先需要实现这个新的 UINavigationControllerDelegate 方法:

- (id<UIViewControllerAnimatedTransitioning>)
               navigationController:(UINavigationController *)navigationController
    animationControllerForOperation:(UINavigationControllerOperation)operation
                 fromViewController:(UIViewController*)fromVC
                   toViewController:(UIViewController*)toVC
{
    if (operation == UINavigationControllerOperationPush) {
        return self.animator;
    }
    return nil;
}

我们可以观察一下这种类型的操作(push 和 pop)返回一个不同的 animator。如果我们分享代码的话,这个可能是一个对象。我们可能需要把这个变量通过property保存下来。我们也可以为不同的操作创建不同的对象,这里有很高的灵活性。

让这个动画运行起来,我们创建一个自定义对象实现 UIViewControllerContextTransitioning 协议。

@interface Animator : NSObject <UIViewControllerAnimatedTransitioning>
 
@end

这个协议要求我们实现2个方法,其中一个是描述动画的执行时间

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.25;
}

另一个是描述动画的执行。

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0;
 
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        fromViewController.view.transform = CGAffineTransformMakeScale(0.1, 0.1);
        toViewController.view.alpha = 1;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
 
    }];
 
}

这里你可以看到这个协议是怎么用的:没有提供实体的对象参数,而是通过这个类型 id 得到transitionContext 唯一的最重要的东西就是在完成动画之后要调用 completeTransition 这个告诉 transitionContext 我们已经完成动画并且相应的更新了 view controller的状态。其他代码是标准的,我们通过transitionContext得到2个UIViewController,然后使用简单的 UIView 动画,这里我们很简单的做了一个zooming的动画

注意,我们只是写了push的自定义动画,当view controller pop时,iOS系统还是会使用默认的滑动动画。而且,实现这个方法后。导航栏也不能交互了(就是从左到右拖动实现pop view controller)。下面完善它

##交互动画

让之前的动画变得能够交互起来非常简单。我们需要实现另一个UINavigationControllerDelegate

- (id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController*)navigationController
                          interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController
{
    return self.interactionController;
}

注意,如果在一个不能交互的动画中,这里会返回nil。(译注:当不能交互时 self.interactionController 为 nil)

interactionController是UIPercentDrivenInteractionTransition的实例,没有必要更多的设置。我们通过创建拖动手势(UIPanGestureRecognizer)来实现:

if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
    if (location.x >  CGRectGetMidX(view.bounds)) {
        navigationControllerDelegate.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self performSegueWithIdentifier:PushSegueIdentifier sender:self];
    }
}

只有当用户在屏幕右边操作时,我们才设置动画是可以交互的(通过设置interactionController 属性)。然后我们调用performSegueWithIdentifier(或是不用storyboards,直接push view controller) 在这个手势变化中,我们调用interactionController 的一个方法 updateInteractiveTransition:

else if (panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
    CGFloat d = (translation.x / CGRectGetWidth(view.bounds)) * -1;
    [interactionController updateInteractiveTransition:d];
}

这里根据拖动的距离设置百分比,非常cool的事情是交互控制器(interactionController)和 动画控制器(animation controller)相互协作。而且因为是普通的 UIView 动画,它控制着动画的进程。我们不需要处理他们之前的事情, 所有的事情都在背后默默的自动搞定了。

最后,当手势停止或是取消掉,我们需要调用interaction controller相应的方法

else if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
    if ([panGestureRecognizer velocityInView:view].x < 0) {
        [interactionController finishInteractiveTransition];
    } else {
        [interactionController cancelInteractiveTransition];
    }
    navigationControllerDelegate.interactionController = nil;
}

当切换动画完毕时,设定interactionController为nil非常重要。如果下一个动画是非交互的,我们不希望得到一个奇怪的 interactionController

现在我们已经有一个完整的自定义的可交互的过度变换(transition)了。通过普通的拖动手势和一个UIKit提供的实体对象,几行代码就搞定了。对于大多数的自定义交互过度变换,你可以在这里停下来,用上面提到的方法做任何你想做得动画 或是交互。

##GPUImage自定义动画

我们现在已经能够实现一个完整的自定义动画了,可以不用UIView 甚至Core Animation,做自己喜欢的动画。一开始,我用Core Image实现了一个项目Letterpress-style。但是在我的旧iPhone4上面只能跑到大约9FPS,这个和我所期望的60FPS差距太大了。

但是当我使用GPUImage后,实现一个非常漂亮的自定义动画效果变得非常简单。我们希望这个动画能够做到像素级的消融在2个view controller切换的时候。这个是通过分别对2个view controller 截屏,然后应用GPUImage的图片滤镜实现的。

首先,我们创建一个自定义类,实现animation 和 interactive transition 协议。

@interface GPUImageAnimator : NSObject
  <UIViewControllerAnimatedTransitioning,
   UIViewControllerInteractiveTransitioning>
 
@property (nonatomic) BOOL interactive;
@property (nonatomic) CGFloat progress;
 
- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;
 
@end

为了让这个动画跑的飞快,我们只把图片传给GPU一次,然后把所有的图像处理绘制交给GPU,而不是传给CPU(GPU和CPU之间的数据传输非常慢)。通过GPUImageView,我们可以用OpenGL绘制动画效果(不需要手动编写底层的OpenGL代码,我们可以继续编写上层代码)

创建这样的滤镜链非常方便。这里可以看一下下面的例子。有一点挑战的是实现动态的滤镜。GPUImage不能给我们直接提供动画效果。这里我们通过在每一帧的时候更新滤镜来实现动画的绘制。我们使用CADisplayLink类来做这个。

self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(frame:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

在frame:方法中,我们根据时间更新动画进度,然后更新滤镜

- (void)frame:(CADisplayLink*)link
{
    self.progress = MAX(0, MIN((link.timestamp - self.startTime) / duration, 1));
    self.blend.mix = self.progress;
    self.sourcePixellateFilter.fractionalWidthOfAPixel = self.progress *0.1;
    self.targetPixellateFilter.fractionalWidthOfAPixel = (1- self.progress)*0.1;
    [self triggerRenderOfNextFrame];
}

以上就是我们所有要讲得了。在交互变换中,我们需要确保我们的进度是根据手势识别设置的,而不是根据时间。但是剩下的代码几乎都一样了。

这个真的太强大了,你可以使用GPUImage提供的任何滤镜或是自己写的OpenGL代码来实现上面的效果。

##小结

我们这里仅仅提到了导航控制器下面的2个 view controller 之间的动画,事实上你可以做相同的事情在tabbar controller 或是自定义的container view controller。而且 UICollectionViewController 现在已经可以在layout上面自动实现交互动画了。他们都是使用相同的机制。这个真的太强大了。

当我和Orta提到这个API时,他指出他已经使用这个功能创建了一些轻量级的view controller。不要在每一个view controller 保存管理动画的代码,而是创建一个新的view controller,然后实现2个view controlller视图切换时的自定义的动画效果。

转载于:View Controller Transitions

07/07/2015 15:38 下午

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加载系统的相互关联的组件:NSURLRequestNSURLResponseNSURLProtocolNSURLCacheNSHTTPCookieStorageNSURLCredentialStorage,以及和它同名的NSURLConnection

NSURLRequest对象被传递给一个NSURLConnection对象。委托(遵守从前的非正式协议)作为一个NSURLResponse异步响应,任何相关的NSData从服务器发送。

一个请求发送到服务器前,共享的高速缓存先被访问,然后根据策略(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仍然可以很好地作为更高级别的网络功能的一个基础。

本文转载:From NSURLConnection to NSURLSession 作者:Mattt Thompson

07/07/2015 15:08 下午 posted in  apple

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+WebCachacategory,方便使用。纪录一下 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+WebCacheMKAnnotationView+WebCache,方便使用。
SDWebImagePrefetcher 可以预先下载图片,方便后续使用。
##SDWebImage库的作用:
通过对UIImageView的类别扩展来实现异步加载替换图片的工作。
主要用到的对象:
1、UIImageView (WebCache)类别,入口封装,实现读取图片完成后的回调
2、SDWebImageManager,对图片进行管理的中转站,记录那些图片正在读取。
向下层读取Cache(调用SDImageCache),或者向网络读取对象(调用SDWebImageDownloader) 。
实现SDImageCacheSDWebImageDownloader的回调。
3、SDImageCache,根据URL的MD5摘要对图片进行存储和读取(实现存在内存中或者存在硬盘上两种实现)
实现图片和内存清理工作。
4、SDWebImageDownloader,根据URL向网络读取数据(实现部分读取和全部读取后再通知回调两种方式)
其他类:
SDWebImageDecoder,异步对图像进行了一次解压⋯⋯
目前不明白为什么要做这么道工序。(现在清楚了,功能解释见下文)
##有趣的点:

  1. SDImageCache是怎么做数据管理的?

SDImageCache分两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面的相当是个缓存器,以Key-Value的形式存储图片。当内存不够的时候会清除所有缓存图片。
用搜索文件系统的方式做管理,文件替换方式是以时间为单位,剔除时间大于一周的图片文件。
SDWebImageManagerSDImageCache要资源时,先搜索内存层面的数据,如果有直接返回,没有的话去访问磁盘,将图片从磁盘读取出来,然后做Decoder,将图片对象放到内存层面做备份,再返回调用层。
2. 为啥必须做Decoder?

通过这个博客:http://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/
现在明白了,由于UIImage的imageWithData函数是每次画图的时候才将Data解压成ARGB的图像,
所以在每次画图的时候,会有一个解压操作,这样效率很低,但是只有瞬时的内存需求。
为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另外一张图片上,这样这张新图片就不再需要重复解压了。
这种做法是典型的空间换时间的做法。

06/30/2015 10:24 上午 posted in  apple