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

理解 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

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

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

Widget的简单开发

今天简单对原有的工程添加了 Widget支持,这里只简单记录下中间遇到的问题。

Widget支持8.0之后的SDK API调用,
Widget只能给现有的工程进行扩展设计,目前还无法进行单独的开发。
Widget由于不具备UI层的一些特性,故一些UI的API不支持,比如以下代码不支持

[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
  Widget仅仅是对原有工程的一个扩展,具有自己独有的场景才可使用,并非滥用

Widget使用的时候如果需要调用基本工程中的数据,根据苹果的安全原则,必须进行跨程序协作才可调用,需要开启工程中Capabilties->App Groups
当然打开基本工程需要使用appextension://123进行调用

PS:最终贴一副最终的效果图,由于特有的场景和背景,这里可能需要UI进行设计

屏幕快照 2015-06-25 22.01.36

06/25/2015 20:52 下午 posted in  apple

NSURLProtocol和NSRunLoop的那些坑

最近用AFNetworking替换掉了工程里的ASIHttpRequest,结果陆续碰到很多问题:

·如何统一地添加全局的HTTP头(不仅仅是UA而已)

·如何优雅地进行流量统计

·对特定的地址进行CDN加速(URL到IP的替换)

·怎么实现HTTP的同步请求

前三个需求对于ASIHttpReqeust来说都不是问题,只需要在几个统一的点进行修改即可。而使用AFNetworking后就没有那么容易了:一方面AFNetworking中生成NSURLRequest的点比较多,并没有一个统一的路径。其次工程中会有部分直接使用NSURLConnecion的场景,无法统一。经cyzju提醒发现了NSURLProtocol这个大杀器,可惜对应的文档过于简略,唯一比较详细的介绍就只有RW的这篇教程而已,掉了很多坑,值得记上一笔。

NSURLProtocol

##概念

NSURLProtocol也是苹果众多黑魔法中的一种,使用它可以轻松地重定义整个URL Loading System。当你注册自定义NSURLProtocol后,就有机会对所有的请求进行统一的处理,基于这一点它可以让你

·自定义请求和响应

·提供自定义的全局缓存支持

·重定向网络请求

·提供HTTP Mocking (方便前期测试)

·其他一些全局的网络请求修改需求

使用方法

继承NSURLPorotocl,并注册你的NSURLProtocol

[NSURLProtocol registerClass:[YXURLProtocol class]];

当NSURLConnection准备发起请求时,它会遍历所有已注册的NSURLProtocol,询问它们能否处理当前请求。所以你需要尽早注册这个Protocol。

实现NSURLProtocol的相关方法

当遍历到我们自定义的NSURLProtocol时,系统先会调用canInitWithRequest:这个方法。顾名思义,这是整个流程的入口,只有这个方法返回YES我们才能够继续后续的处理。我们可以在这个方法的实现里面进行请求的过滤,筛选出需要进行处理的请求。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    if ([NSURLProtocol propertyForKey:YXURLProtocolHandled inRequest:request])
    {
        return NO;
    }
    NSString *scheme = [[request URL] scheme];
    NSDictionary *dict = [request allHTTPHeaderFields];
    return [dict objectForKey:@"custom_header"] == nil &&
    ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
     [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
}

当筛选出需要处理的请求后,就可以进行后续的处理,需要至少实现如下4个方法

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a
                       toRequest:(NSURLRequest *)b
{
    return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    [YXURLProtocol applyCustomHeaders:mutableReqeust];
    [NSURLProtocol setProperty:@(YES)
                        forKey:YXURLProtocolHandled
                     inRequest:mutableReqeust];
     
    self.connection = [NSURLConnection connectionWithRequest:mutableReqeust
                                                    delegate:self];
}
- (void)stopLoading
{
    [self.connection cancel];
    self.connection = nil;
}

·canonicalRequestForRequest: 返回规范化后的request,一般就只是返回当前request即可。

·requestIsCacheEquivalent:toRequest: 用于判断你的自定义reqeust是否相同,这里返回默认实现即可。它的主要应用场景是某些直接使用缓存而非再次请求网络的地方。

·startLoading和stopLoading 实现请求和取消流程。

实现NSURLConnectionDelegate和NSURLConnectionDataDelegate

因为在第二步中我们接管了整个请求过程,所以需要实现相应的协议并使用NSURLProtocolClient将消息回传给URL Loading System。在我们的场景中推荐实现所有协议。

- (void)connection:(NSURLConnection *)connection
  didFailWithError:(NSError *)error
{
    [self.client URLProtocol:self
            didFailWithError:error];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
    if (response != nil) 
    {
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
    return request;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection
{
    return YES;
}
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    [self.client URLProtocol:self
didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge 
{
    [self.client URLProtocol:self
didCancelAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
    [self.client URLProtocol:self
          didReceiveResponse:response
          cacheStoragePolicy:[[self request] cachePolicy]];
}
- (void)connection:(NSURLConnection *)connection
    didReceiveData:(NSData *)data
{
    [self.client URLProtocol:self
                 didLoadData:data];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
                  willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
    return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    [self.client URLProtocolDidFinishLoading:self];
}

在每个delgate的实现中我都刨去了工程中的特定实现(流量统计),只保留了需要实现的最小Protocol集合。

NSURLProtocol那些坑

从上面的介绍来看,NSURLProtocol还是比较简单,但是实际使用的过程中却容易掉进各种坑,一方面是文档不够详尽,另一方面也是对于苹果这套URL Loading Sytem并不熟悉,不能将整个调用过程有机地统一。

·坑1:企图在canonicalRequestForRequest:进行request的自定义操作,导致各种递归调用导致连接超时。这个API的表述其实很暧昧:

It is up to each concrete protocol implementation to define what “canonical” means. A protocol should guarantee that the same input request always yields the same canonical form.

所谓的canonical form到底是什么呢?而围观了包括NSEtcHosts和RNCachingURLProtocol在内的实现,它们都是直接返回当前request。在这个方法内进行request的修改非常容易导致递归调用(即使通过setProperty:forKey:inRequest:对请求打了标记)

·坑2:没有实现足够的回调方法导致各种奇葩问题。如connection:willSendRequest:redirectResponse: 内如果没有通过[self client]回传消息,那么需要重定向的网页就会出现问题:host不对或者造成跨域调用导致资源无法加载。

同步AFNetworking请求

虽然Mattt各种鄙视同步做网络请求,但是我们不可否认某些场景下使用同步调用会带来不少便利。一种比较简单的实现是使用信号量做同步:

@implementation AFHTTPRequestOperation (YX)
- (void)yxStartSynchronous
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
        dispatch_semaphore_signal(semaphore);
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        dispatch_semaphore_signal(semaphore);
    }];
    [self start];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
@end

但是这样带来的问题是在UI线程调用同步请求就会导致线程堵死崩溃(好吧,就不应该允许UI线程上这么做)。一种改进的方法是使用NSRunLoop

即:

while (_shouldBlock)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate distantFuture]];
   }

但是这种写法是有大坑的:如果当前NSRunLoop并没有任何NSTimer或Input Source,runMode:beforeDate:方法将立刻返回NO,于是造成死循环,占用大量CPU,进而导致NSURLConnection请求超时。 规避的方法是往RunLoop中添加NSTimer或者空NSPort使得NSRunLoop挂起而不占用CPU。(ASIHttpRequest就是在当前RunLoop中添加了0.25秒触发一次的刷新Timer)

If no input sources or timers are attached to the run loop, this method exits immediately and returns NO; otherwise, it returns after either the first input source is processed or limitDate is reached. Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. OS X may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

06/13/2015 08:21 上午 posted in  apple

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,这个模式等效于NSDefaultRunLoopModeNSEventTrackingRunLoopMode的结合。(参考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
上面讲的NSRunLoopCommonModesTimer中有一个问题,这个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)

internalleeway参数分别表示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}

转载:iOS: NSTimer使用小记

06/12/2015 19:29 下午 posted in  apple

iOS 常用数学函数


   1、 三角函数 
  double sin (double);正弦 
  double cos (double);余弦 
  double tan (double);正切 
  2 、反三角函数 
  double asin (double); 结果介于[-PI/2, PI/2] 
  double acos (double); 结果介于[0, PI] 
  double atan (double); 反正切(主值), 结果介于[-PI/2, PI/2] 
  double atan2 (double, double); 反正切(整圆值), 结果介于[-PI, PI] 
  3 、双曲三角函数 
  double sinh (double); 
  double cosh (double); 
  double tanh (double); 
  4 、指数与对数 
  double exp (double);求取自然数e的幂 
  double sqrt (double);开平方 
  double log (double); 以e为底的对数 
  double log10 (double);以10为底的对数 
  double pow(double x, double y);计算以x为底数的y次幂 
  float powf(float x, float y); 功能与pow一致,只是输入与输出皆为浮点数 
  5 、取整 
  double ceil (double); 取上整 
  double floor (double); 取下整 
  6 、绝对值 
  double fabs (double);求绝对值 
  double cabs(struct complex znum) ;求复数的绝对值 
  7 、标准化浮点数 
  double frexp (double f, int *p); 标准化浮点数, f = x * 2^p, 已知f求x, p ( x介于[0.5, 1] ) 
  double ldexp (double x, int p); 与frexp相反, 已知x, p求f 
  8 、取整与取余 
  double modf (double, double*); 将参数的整数部分通过指针回传, 返回小数部分 
  double fmod (double, double); 返回两参数相除的余数 
  9 、其他 
  double hypot(double x, double y);已知直角三角形两个直角边长度,求斜边长度 
  double ldexp(double x, int exponent);计算x*(2的exponent次幂) 
  double poly(double x, int degree, double coeffs [] );计算多项式 
  nt matherr(struct exception *e);数学错误计算处理程序

06/12/2015 19:25 下午 posted in  apple