Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5

##前言

Enum,也就是枚举,从C语言开始就有了,C++、Java、Objective-C、Swift这些语言,当然都有对应的枚举类型,功能可能有多有少,但是最核心的还是一个—规范的定义代码中的状态、选项等“常量”。

Item 5 - Use Enumerations for States, Options, and Status Codes

本节的内容就是如何正确的使用枚举。

##状态与选项的区别(states and options)

在用enum之前,我个人觉得,区分一下状态和选项的概念还是很必要的。

状态,同时只能有一种,如“OK”,“Error”,不可能同时是OK和Error。
选项,同时可以有一种或一种以上,如App可以同时支持横屏和竖屏,横屏竖屏在这个时候就是“屏幕方向”的两种不同的选项。

接下来,我们看看如何用枚举定义状态和选项。

##enum与状态(states)

不好的做法
经常看到这样的写法:

#define STATE_OK 0
#define STATE_ERROR 1
#define STATE_UNKNOW 2
//直接用int型变量接收
int STATE = STATE_UNKNOW;

这样做有如下“不恰当”:

  • 宏定义没有类型约束,只是单纯的替换。
  • 无法限制状态的所有情况,如,认为的将STATE赋值成3,程序可能就会出错,找不到匹配的状态,因为编译器不会对“STATE = 3;”提出警告。

正确的做法

typedef enum _TTGState {
    TTGStateOK  = 0,
    TTGStateError,
    TTGStateUnknow
} TTGState;
//指明枚举类型
TTGState state = TTGStateOK;

用的时候就如下:

- (void)dealWithState:(TTGState)state {
    switch (state) {
        case TTGStateOK:
            //...
            break;
        case TTGStateError:
            //...
            break;
        case TTGStateUnknow:
            //...
            break;
    }
}

##enum与选项 (options)

选项,就是说一个“选项变量”的类型要能够同时表示一个或多个组合的选择,如下例子:

//方向,可同时支持一个或多个方向
typedef enum _TTGDirection {
    TTGDirectionNone = 0,
    TTGDirectionTop = 1 << 0,
    TTGDirectionLeft = 1 << 1,
    TTGDirectionRight = 1 << 2,
    TTGDirectionBottom = 1 << 3
} TTGDirection;

看,这里的选项是用位运算的方式定义的,这样的好处就是,我们的选项变量可以如下表示:

//用“或”运算同时赋值多个选项
TTGDirection direction = TTGDirectionTop | TTGDirectionLeft | TTGDirectionBottom;
//用“与”运算取出对应位
if (direction & TTGDirectionTop) {
    NSLog(@"top");
}
if (direction & TTGDirectionLeft) {
    NSLog(@"left");
}
if (direction & TTGDirectionRight) {
    NSLog(@"right");
}
if (direction & TTGDirectionBottom) {
    NSLog(@"bottom");
}

direction变量的实际内存如下:

这样,用位运算,就可以同时支持多个值。

enum在Objective-C中的“升级版”

一般来说,我们不能指定枚举变量的实际类型是什么,就是说,我们不知道枚举最后是int型,还是其他的什么类型。但是从C++ 11开始,我们可以为枚举指定其实际的存储类型,如下语法:

enum TTGState : NSInteger {/*...*/};

但是,我们在定义枚举的时候如何保证兼容性呢?Foundation框架已经为我们提供了更加“统一、便捷”的枚举定义方法,我们重新定义上面的例子:

//NS_ENUM,定义状态等普通枚举
typedef NS_ENUM(NSUInteger, TTGState) {
    TTGStateOK = 0,
    TTGStateError,
    TTGStateUnknow
};
//NS_OPTIONS,定义选项
typedef NS_OPTIONS(NSUInteger, TTGDirection) {
    TTGDirectionNone = 0,
    TTGDirectionTop = 1 << 0,
    TTGDirectionLeft = 1 << 1,
    TTGDirectionRight = 1 << 2,
    TTGDirectionBottom = 1 << 3
};

所以,在开发Mac、iOS程序中,最好所有的枚举都用“NS_ENUM”和“NS_OPTIONS”定义,保证统一。

##总结

充分的用好枚举,可以增强代码的可读性,减少各种“错误”,让代码更加的规范。

04/28/2017 16:46 下午 posted in  apple

iOS中的唯一标识

##IDFA(广告标识符)-identifierForldentifier
依赖:AdSupport.framework
系统支持:iOS6及以上系统
获取方式:[ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString
定义: 由数字和字母组成的用来标识唯一设备的字符串。
特点

每个设备只有一个IDFA,不同APP在同一设备上获取IDFA的结果是一样的
设备重启不会产生新的IDFA
但IDFA存在重新生成的情况:
用户完全重置系统(设置程序 -> 通用 -> 还原 -> 还原位置与隐私)
用户明确还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符)
注意:Appstore禁止不使用广告而采集IDFA的app上架。请参考:
如何防止应用因获取IDFA被AppStore拒绝

##IDFV-identifierForVendor
依赖:UIKit.framework
系统支持:iOS6及以上系统
获取方式:[UIDevice currentDevice].identifierForVendor.UUIDString
定义:由数字和字母组成的用来标识唯一设备的字符串。
特点: 根据vendor的值,如果vendor相同,则返回同一字符串;如果vendor不同,则返回不同的字符串。
vendor解释:英文解释为卖家,小贩。根据xcode文档解释,正常情况下,会根据App Store提供的数据进行判断。但是如果app不是通过app store进行安装的(如企业应用或开发调试阶段),那么会根据bundle ID判断。
判断准则

如:com.example.app1和com.example.app2,只有最后的后缀不同,所以会产生相同的vendor ID

详细参考:iOS唯一标示符引导
:https://possiblemobile.com/2013/04/unique-identifiers/

简书:iOS怎样获取设备唯一标识符

10/27/2016 14:22 下午 posted in  apple

iOS中的宏定义

转载:http://www.uml.org.cn/c++/200902104.asp

1、先来几个常用的:

// 是否高清屏
#define isRetina ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 960), [[UIScreen mainScreen] currentMode].size) : NO)
// 是否模拟器
#define isSimulator (NSNotFound != [[[UIDevice currentDevice] model] rangeOfString:@"Simulator"].location)
// 是否iPad
#define isPad (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
// 是否iPad
#define someThing (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)? ipad: iphone

2、基本的使用:

//定义π值 3.1415926  
#define PI 3.1415926   
//则在程序用可以如下使用     
double i=2*PI*3;   
//效果相当于  double i=2*3.1415926*3;  

//预处理命令可以定义任何符合格式的形式,例如判断年份是否闰年
#define  IS_LEAP_YEAR  year%4==0&&year%100!=0||year%400==0  
//使用时则可以直接  
if(IS_LEAP_YEAR)  
 
//或者可以定义一个参数    
#define  IS_LEAP_YEAR(y)  y%4==0&&y%100!=0||y%400==0  
//使用时则可以直接   
int ys=2012;   
if(IS_LEAP_YEAR(ys))     
  
//通常预处理程序定义在一行 如果好分行 比如说太长需要换行  需要使用“/”符号 表示还有下一行,多行分列也是如此,例:  
#Define  IS_LEAP_YEAR  year%4==0&&year%100!=0/  
           ||year%400==0   
//宏定义参数后边放一个# 那么在调用该宏时,预处理程序将根据宏参数创建C风格的常量字符串 例:  
#define STR(x) # x  
//将会使得 随后调用的    

NSLOG(STR(Programming in Objective-c./n));  
//显示结果为 Programming in Objective-c./n

3、关于#与##的操作符:

  1. 宏定义中字符串化操作符#:
    #的功能是将其后面的宏参数进行字符串化操作,意思就是对它所应用的宏变量通过替换后在其左右各加上一个双引号。例如
#define WARN_IF(EXPR)\
do {\
if (EXPR)\
fprintf(stderr, "Warning: " #EXPR "\n");\
} while(0)

上面代码中的反斜线\主要用来转译换行符,即屏蔽换行符。

那么如下的代码调用:
WARN_IF(divider == 0);

将被解析为:
do {\
if (divider == 0)\
fprintf(stderr, "Warning: " "divider == 0" "\n");\
} while(0);

注意能够字符串化操作的必须是宏参数,不是随随便便的某个子串(token)都行的。

  1. 宏定义中的连接符##:
    连接符##用来将两个token连接为一个token,但它不可以位于第一个token之前or最后一个token之后。注意这里连接的对象只要是token就行,而不一定是宏参数,但是##又必须位于宏定义中才有效,因其为编译期概念(比较绕)。
#define LINK_MULTIPLE(a, b, c, d) a##_##b##_##c##_##d
typedef struct _record_type LINK_MULTIPLE(name, company, position, salary);
/*
* 上面的代码将被替换为
* typedef struct _record_type name_company_position_salary;
*/

又如下面的例子:
#define PARSER(N) printf("token" #N " = %d\n", token##N)

int token64 = 64;

如下调用宏:
PARSER(64);

将被解析为:
printf("token" "64" " = %d\n", token64);

在obj-c中,如果我有如下定义:
#define _X(A, B) (A#B)
#define _XX(A, B) _X([NSString stringWithFormat:@"%@_c", A], B)
gcc将报错!
正确的写法为:
#define _XX(A, B) _X(([NSString stringWithFormat:@"%@_c", A]), B)

4、再来个宏定义 object-c 单例

#define GTMOBJECT_SINGLETON_BOILERPLATE(_object_name_, _shared_obj_name_)
static _object_name_ *z##_shared_obj_name_ = nil; 
+ (_object_name_ *)_shared_obj_name_ {            
@synchronized(self) {                           
if (z##_shared_obj_name_ == nil) {            
/* Note that ‘self’ may not be the same as _object_name_ */                              
/* first assignment done in allocWithZone but we must reassign in case init fails */     
z##_shared_obj_name_ = [[self alloc] init];                                              
_GTMDevAssert((z##_shared_obj_name_ != nil), @”didn’t catch singleton allocation”);      
}                                             
}                                               
return z##_shared_obj_name_;                    
}                                                 

+ (id)allocWithZone:(NSZone *)zone {              
@synchronized(self) {                           
if (z##_shared_obj_name_ == nil) {            
z##_shared_obj_name_ = [super allocWithZone:zone];
return z##_shared_obj_name_;                
}                                             
}                                               
 
/* We can’t return the shared instance, because it’s been init’d */
_GTMDevAssert(NO, @”use the singleton API, not alloc+init”);       
return nil;                                     
}                                                 

- (id)retain {                                    
return self;                                    
}                                                 

- (NSUInteger)retainCount {                       
return NSUIntegerMax;                           
}                                                 

- (void)release {                                 
}                                                 

- (id)autorelease {                               
return self;                                    
}                                                 

- (id)copyWithZone:(NSZone *)zone {               
return self;                                    
}

5、条件编译:

#if !defined(FCDebug) || FCDebug == 0
#define FCLOG(...) do {} while (0)
#define FCLOGINFO(...) do {} while (0)
#define FCLOGERROR(...) do {} while (0)
    
#elif FCDebug == 1
#define FCLOG(...) NSLog(__VA_ARGS__)
#define FCLOGERROR(...) NSLog(__VA_ARGS__)
#define FCLOGINFO(...) do {} while (0)
    
#elif FCDebug > 1
#define FCLOG(...) NSLog(__VA_ARGS__)
#define FCLOGERROR(...) NSLog(__VA_ARGS__)
#define FCLOGINFO(...) NSLog(__VA_ARGS__)
#endif

6、参照C语言的预处理命令简介 :

#define              定义一个预处理宏
#undef               取消宏的定义
#include            包含文件命令
#include_next   与#include相似, 但它有着特殊的用途
#if                      编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef                判断某个宏是否被定义, 若已定义, 执行随后的语句
#ifndef             与#ifdef相反, 判断某个宏是否未被定义
#elif                  若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if
#else                与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else
#endif              #if, #ifdef, #ifndef这些条件命令的结束标志.
defined            与#if, #elif配合使用, 判断某个宏是否被定义
#line                标志该语句所在的行号
#                      将宏参数替代为以参数值为内容的字符窜常量
##                   将两个相邻的标记(token)连接为一个单独的标记
#pragma        说明编译器信息#warning       显示编译警告信息
#error            显示编译错误信息
#define SAFE_RELEASE(obj) if(obj){[obj release];obj=nil;} 释放

07/05/2016 12:40 下午 posted in  apple

苹果APNs’ device token特性和过期更新

APNs全名是Apple Push Notification Service。用iPhone的应该都习惯了,每次安装完一个新应用启动后,几乎都会弹出个警告框,“XXX应用”想要给您发送推送通知。这个警告框的权限申请就是为了APNs推送,用户授权后,应用提供商就可以通过APNs给用户推送消息。
APNs的工作机制简单来说可以分为两步,第一步是注册推送服务从APNs获取device token来告知应用提供商服务端,第二步是应用提供商服务端通过APNs给设备推送消息,device token是作为设备的唯一标示。

上图就是device token生成的一个过程。我们以第一次安装启动360儿童卫士应用为例,首先应用会弹出个警告框,请求用户允许发送推送通知,用户允许后–>儿童卫士会向系统注册推送服务,系统接到注册请求后就会自动连接APNs服务器请求获取设备令牌(即device token)–>APNs服务器生成包含device id的device token并下发给设备–>儿童卫士接受到device token,保存在本地同时发送给儿童卫士服务器,到此第一步就完成了。

上图就是推送消息的示图了,设备通过device token和APNs服务器保持连接状态。还以360儿童卫士为例,当孩子到家了,儿童卫士服务器就需要发到达提醒给家长。这时儿童卫士服务器就会通过device token作为目的设备标示来推送加密的到达提醒消息给APNs,APNs解密后再根据device token推送给指定设备。这样,一次推送就完成了。
了解了APNs工作机制,很明显能够看到device token在其中起了至关重要的串联指向作用。如果device token错误或缺失,推送就无法送达目标设备了。所以测试也罢开发也好,都很有必要了解一下device token的一些特性:

  1. 每个device token都是唯一的,只会对应一台设备。
  2. device token与设备系统相关(注意不是和设备绑定的!详解见后文),同一设备系统上不同应用获取的token是同一个。
  3. 应用卸载重新安装,获取到的device token不会变化,而且不会再弹出推送权限申请的弹窗,会自动继承前一次安装的设置信息。这个特性容易引发一些安全问题,用户卸载重新安装一个应用后,还没有登录应用,就可能接到上次登录帐号的推送消息了。我使用iPhone QQ和Skype都碰到过这种情况。客户端没有办法处理这个问题,因为被卸载时客户端是没法做出反应来通知服务器的。苹果有一个feedback的机制可以解决这个问题,苹果为每个应用程序维护了一个不断更新的推送失败的设备列表。服务端可以去定期检查并更新推送设备列表,这样能解决大部分问题,也能减少不必要的报文开销。
  4. 第三点客户端不能处理,但退出登录通知服务器就是客户端的工作了。用户退出登录客户端时,客户端应该告知服务器,停止对这个设备继续推送用户退出登录帐号的消息了。这点应该不算device token的特性了,是一个标准处理方法。
    相信很多人都有这样一个疑问,作为一个设备推送的唯一标示,device token是否会变化或者过期呢?苹果在这点上有些含糊其辞,只是在官方文档上建议开发者在每次启动应用时应该都向APNs获取device token并上传给服务器。从这句话来看,device token是会变化的,不然不用每次启动都去获取。

因为苹果官方没有给出明确的device token变化的情况,所以以下列举的都是一些前人总结的经验,主要援引了stackoverflow上关于这个问题一个回答,回答者称是和苹果的一个工程师交流及自己实验得出的结果。

  1. 升级系统device token有可能变化,确认的是升级到iOS5会变化,猜测是升级大的系统版本后device token会变化。
  2. 抹掉所有内容和设置,reset设备后,device token会变化。
  3. 恢复一个非本机的备份后,device token会变化。
  4. device token会过期,这个众说纷纭,有说是半年的,有说一年,有说两年的,不过会过期应该是确凿的。
  5. 备份或者恢复本机的备份,device token不会变化。
    所以保险起见,按照苹果的每次启动应用时检查device token并发送到服务器是比较稳妥的做法。
06/07/2016 10:28 上午 posted in  apple

HTTP 常用 Header 讲解

1、HTTP请求协议名://主机名:端口号/资源URI

*******************************
GET /index.html HTTP/1.1
Host: localhost:8088
connection: Keep-Alive

******************************/

2、request headerHost, 请求头,标名请求主机器名,可为IP也可为域名,http1.1后强制使用,用此请求信息,可在服务端做WEB虚拟机,实现一机多WEB服务
Content-Length,请求、响应体的数据字节大小
Accept-Encoding,请求头,可接受的文本压缩算法,如: gzip, deflate
Accept-Language,请求头,支持语言,客户端浏览器的设置,如:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
User-Agent,请求头,浏览器信息,如:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:12.0) Gecko/20100101 Firefox/12.0,细心会注册到IE也会用Mozilla,这是一个历史问题,早期WEB服务器貌似有问题,只支持Mozilla,微软IE做为后起之秀只能伪装成Mozilla
Cookie,请求头,服务器或客户端在上次设置的COOKIE,包括作用域名(.360buy.com),过期时间,键与值。大部分WEB服务器都会在第一次访问时在响应头上加Set-Cookie,如:BAIDUID=49415814CDBBB4CE65EC50EE4BB65E9A:FG=1; expires=Wed, 07-Nov-42 07:03:34 GMT; path=/; domain=.baidu.com
Referer,从一个连接打开一个新页面,新页面的请求一般会加此信息,标名是从哪里跳过来的,所有的页面的打开历史链就可被挖掘出来,有利于分析用户行为与CPS分成
3、reponse headerContent-Type, 响应的数据类型:text/html;charset=gbk
Content-Length,响应的数据体大小
Content-Encoding, 如果为文本、HTML信息,则使用的编码方式
Date, 当前服务器日期
Server, 服务器名
Set-Cookie,第一次访问或服务设置COOKIE时,响应头里会有此信息,如,BAIDUID=49415814CDBBB4CE65EC50EE4BB65E9A:FG=1; expires=Wed, 07-Nov-42 07:03:34 GMT; path=/; domain=.baidu.com
4、 Cache-Control , Expires控制缓存的两个响应头,如果都出现在响应头里,按Cache-Controler计算
Cache-Control,为响应头信息,取值为:
Public,当前系统任何登录的用户都可使用
Private,当前系统登录的此用户进行缓存
no-cache,不做缓存
max-age,缓存指定秒数,如Cache-control: max-age=5,表示当访问此网页后的5秒内再次访问不会去服务器
Expires,为响应头,Http1.1以上版本,与Max-Age一样,用来控制缓存
5、 Last-Modified, If-Modified-Since
Last-Modified, 为响应头,标名本资料上一次的修改时间
If-Modified-Since,为请求头,把上一次请求的Last-Modified日期信息为值进行请求,如果服务器判断Last-Modified时间与服务器一致则直接返回304,浏览器使用本地缓存进行显示。一般用来节省带宽,加速请求与显示。
6、ETag + If-Match同样是缓存策略,做为以上的补充
ETag,为响应头,在 http1.1中规定为一个字串,具体格式未定义,用来校验客户端缓存
If-Match,为请求头信息,把上一次请求响应的Etag带上进行请求,服务端的处理方法比较灵活,做为Cache-Control,Expires,Last-Modified的补充,可不以时间为参考的缓存策略。
Apache默认对html的Etag取值为INode+Mtime+Size
如:Etag”2e681a-6-5d044840″1
用途:
• a,仅仅改变的修改时间,但内容未做修改
• b,修改非常频繁,一秒内修改千次,但Cache-Control与Last-Modified,只能控制在秒级,这是对控制力度的进一步提升
• c,某些服务器不能精确的得到文件的最后修改时间,个例,我们使用的服务器都已支持,所以所用不多。
7、Connection Keep-AliveHTTP协议采用TCP协议,每次页面资源请求都被规定为一次连接,而每次连接的TCP三次握手关闭时的四次通信与端口滞留等待防止数据包未传送,
而每个TCP都是一个打开文件IO句柄数,Unix/Linux又对这个做了严格的限制。
一个网页,大量资源文件(html\css\javascript\image)需要加载量时需要大数据量的TCP连接,为了减少socket连接数提供了KeepAlive,使一个tcp连接可重复使用。
事实也证明用Keep-Alive速度也更快(但移动客户端接口开发会关掉此属性)。
8、Range: 10-100取信息的一部分,断点下载时常用
9、返回状态码
200,返回成功
501,服务器内容错误
304,使用本地缓firebug
404,资源没有找到
10、http 协议监察工具:
Firebox:httpfox、live http header,firebug
IE:httpwatch、iehttpheader

05/23/2016 08:58 上午 posted in  apple

Block在Objective-C中的声明

From:How Do I Declare A Block in Objective-C?

简单翻译一下:

作为本地变量的声明方法:

returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

属性的声明:

@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

作为方法的参数声明:

- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;

作为参数被方法调用的声明:

[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

作为结构体的声明方法:

typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

参考:Apple官方的Block编程指南

05/04/2016 10:56 上午 posted in  apple

NSDecimalNumber的用法

加减乘除 解决字符串转float类型不准确 结尾无法去0处理 用于货币处理

    //货币算法 使用NSDecimalNumber 来进行精准计算
    //float  double  在计算时会产生误差
     
    //加法
     
    //声明两个  NSDecimalNumber
    NSDecimalNumber *jiafa1 = [NSDecimalNumber decimalNumberWithString:@"55.55555"];
     
    NSDecimalNumber *jiafa2 = [NSDecimalNumber decimalNumberWithString:@"0.11111"];
     
     
    //加法运算函数  decimalNumberByAdding
    NSDecimalNumber *jiafa = [jiafa1 decimalNumberByAdding:jiafa2];
     
    NSLog(@"加法 %@", jiafa);
     
     
    //减法
     
    //声明两个  NSDecimalNumber
    NSDecimalNumber *jianfa1 = [NSDecimalNumber decimalNumberWithString:@"55.55555"];
     
    NSDecimalNumber *jianfa2 = [NSDecimalNumber decimalNumberWithString:@"0.11111"];
     
     
    //减法运算函数  decimalNumberByAdding
    NSDecimalNumber *jianfa = [jianfa1 decimalNumberBySubtracting:jianfa2];
     
    NSLog(@"减法 %@", jianfa);
     
     
    //乘法
     
    //声明两个  NSDecimalNumber
    NSDecimalNumber *chengfa1 = [NSDecimalNumber decimalNumberWithString:@"55.55555"];
     
    NSDecimalNumber *chengfa2 = [NSDecimalNumber decimalNumberWithString:@"0.11111"];
     
     
    //乘法运算函数  decimalNumberByAdding
    NSDecimalNumber *chengfa = [chengfa1 decimalNumberByMultiplyingBy:chengfa2];
     
    NSLog(@"乘法 %@", chengfa);
     
     
    //除法
     
    //声明两个  NSDecimalNumber
    NSDecimalNumber *chufa1 = [NSDecimalNumber decimalNumberWithString:@"55"];
     
    NSDecimalNumber *chufa2 = [NSDecimalNumber decimalNumberWithString:@"3"];
     
     
    //除法运算函数  decimalNumberByAdding
    NSDecimalNumber *chufa = [chufa1 decimalNumberByDividingBy:chufa2];

04/01/2016 13:23 下午 posted in  apple

iOS中的唯一标识

在2013年3月21日苹果已经通知开发者,从2013年5月1日起,访问UIDID的应用将不再能通过审核,替代的方案是开发者应该使用“在iOS 6中介绍的Vendor或Advertising标示符”。

unique Identifier即将退出,苹果给了我们Vendor和Advertising identifier两个选择,但应该用哪一个?文档并没有给出确切答案,具体使用哪个完全由你根据自己app的目的来决定。下面我将列出iOS中目前支持的,以及被废弃的唯一标示符方法,并对其做出相应的解释,希望可以帮你做出正确的确定。

##CFUUID
从iOS2.0开始,CFUUID就已经出现了。它是CoreFoundatio包的一部分,因此API属于C语言风格。CFUUIDCreate 方法用来创建CFUUIDRef,并且可以获得一个相应的NSString,如下代码:

CFUUIDRef cfuuid = CFUUIDCreate(kCFAllocatorDefault);NSString cfuuidString = (NSString)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuid));

获得的这个CFUUID值系统并没有存储。每次调用CFUUIDCreate,系统都会返回一个新的唯一标示符。如果你希望存储这个标示符,那么需要自己将其存储到NSUserDefaults, Keychain, Pasteboard或其它地方。

##NSUUID
NSUUID在iOS 6中才出现,这跟CFUUID几乎完全一样,只不过它是Objective-C接口。+ (id)UUID 是一个类方法,调用该方法可以获得一个UUID。通过下面的代码可以获得一个UUID字符串:

NSString *uuid = [[NSUUID UUID] UUIDString];

跟CFUUID一样,这个值系统也不会存储,每次调用的时候都会获得一个新的唯一标示符。如果要存储的话,你需要自己存储。在我读取NSUUID时,注意到获取到的这个值跟CFUUID完全一样(不过也可能不一样):

示例: 68753A44-4D6F-1226-9C60-0050E4C00067

##广告标示符(IDFA-identifierForIdentifier)
这是iOS 6中另外一个新的方法,advertisingIdentifier是新框架AdSupport.framework的一部分。ASIdentifierManager单例提供了一个方法advertisingIdentifier,通过调用该方法会返回一个上面提到的NSUUID实例。

NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];

跟CFUUID和NSUUID不一样,广告标示符是由系统存储着的。不过即使这是由系统存储的,但是有几种情况下,会重新生成广告标示符。如果用户完全重置系统((设置程序 -> 通用 -> 还原 -> 还原位置与隐私) ,这个广告标示符会重新生成。另外如果用户明确的还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符) ,那么广告标示符也会重新生成。关于广告标示符的还原,有一点需要注意:如果程序在后台运行,此时用户“还原广告标示符”,然后再回到程序中,此时获取广告标示符并不会立即获得还原后的标示符。必须要终止程序,然后再重新启动程序,才能获得还原后的广告标示符。之所以会这样,我猜测是由于ASIdentifierManager是一个单例。

针对广告标示符用户有一个可控的开关“限制广告跟踪”。Nick Arnott的文章中已经指出了。将这个开关打开,实际上什么也没有做,不过这是希望限制你访问广告标示符。这个开关是一个简单的boolean标志,当将广告标示符发到任意的服务器端时,你最好判断一下这个值,然后再做决定。

示例: 1E2DFA89-496A-47FD-9941-DF1FC4E6484A

##Vindor标示符 (IDFV-identifierForVendor)
这种叫法也是在iOS 6中新增的,不过获取这个IDFV的新方法被添加在已有的UIDevice类中。跟advertisingIdentifier一样,该方法返回的是一个NSUUID对象。

NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

苹果官方的文档中对identifierForVendor有如下这样的一段描述 :

The value of this property is the same for apps that come from the same vendor running on the same device. A different value is returned for apps on the same device that come from different vendors, and for apps on different devices regardless of vendor.

如果满足这样的条件,那么获取到的这个属性值就不会变:相同的一个程序里面-相同的vindor-相同的设备。如果是这样的情况,那么这个值是不会相同的:相同的程序-相同的设备-不同的vindor,或者是相同的程序-不同的设备-无论是否相同的vindor。

看完上面的内容,我有这样的一个疑问“vendor是什么”。我首先想到的是苹果开发者账号。但事实证明这是错误的。接着我想可能是有一个AppIdentifierPrefix东西,跟钥匙串访问一样,可以在多个程序间共享。同样,这个想法也是的。最后证明,vendor非常简单:一个Vendor是CFBundleIdentifier(反转DNS格式)的前两部分。例如,com.doubleencore.app1 和 com.doubleencore.app2 得到的identifierForVendor是相同的,因为它们的CFBundleIdentifier 前两部分是相同的。不过这样获得的identifierForVendor则完全不同:com.massivelyoverrated 或 net.doubleencore。

在这里,还需要注意的一点就是:如果用户卸载了同一个vendor对应的所有程序,然后在重新安装同一个vendor提供的程序,此时identifierForVendor会被重置。

示例: 599F9C00-92DC-4B5C-9464-7971F01F8370

03/04/2016 13:44 下午 posted in  apple

详解键值观察(KVO)及其实现机理

From:https://mikeash.com/pyblog/friday-qa-2009-01-23.html
##一,前言

Objective-C 中的键(key)-值(value)观察(KVO)并不是什么新鲜事物,它来源于设计模式中的观察者模式,其基本思想就是:

一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。

在 Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键)。下面将一一讲述这些,并会深入 Objective-C 内部一窥键值观察是如何实现的。

本文源码下载:点此下载

##二,运用键值观察

###1,注册与解除注册

如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察对象)上调用名为 NSKeyValueObserverRegistration 的 category 方法将观察者对象与被观察者对象注册与解除注册:

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

这两个方法的定义在 Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均实现了以上方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。在该头文件中,我们还可以看到 NSObject 还实现了 NSKeyValueObserverNotification 的 category 方法(更多类似方法,请查看该头文件):

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

这两个方法在手动实现键值观察时会用到,暂且不提。

值得注意的是:不要忘记解除注册,否则会导致资源泄露。

###2,设置属性

将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过 key-path 来设置:

[target setAge:30]; 
[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

###3,处理变更通知

观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;

在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。

###4,下面来看看一个完整的使用示例:

观察者类:

// Observer.h
@interface Observer : NSObject
@end

// Observer.m
#import "Observer.h"
#import <objc/runtime.h>
#import "Target.h"

@implementation Observer

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

@end

注意:在实现处理变更通知方法 observeValueForKeyPath 时,要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。

使用示例:

Observer * observer = [[[Observer alloc] init] autorelease];

Target * target = [[[Target alloc] init] autorelease];
[target addObserver:observer
         forKeyPath:@"age"
            options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
            context:[Target class]];

[target setAge:30];
//[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];

[target removeObserver:observer forKeyPath:@"age"];

在这里 observer 观察 target 的 age 属性变化,运行结果如下:

  >> class: Target, Age changed

  old age is 10

  new age is 30

##三,手动实现键值观察

上面的 Target 应该怎么实现呢?首先来看手动实现。

@interface Target : NSObject
{
    int age;
}

// for manual KVO - age
- (int) age;
- (void) setAge:(int)theAge;

@end

@implementation Target

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

// for manual KVO - age
- (int) age
{
    return age;
}

- (void) setAge:(int)theAge
{
    [self willChangeValueForKey:@"age"];
    age = theAge;
    [self didChangeValueForKey:@"age"];
}

+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        return NO;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

@end

首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;

其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

##四,自动实现键值观察

自动实现键值观察就非常简单了,只要使用了属性即可。

@interface Target : NSObject// for automatic KVO - age
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age

- (id) init
{
    self = [super init];
    if (nil != self)
    {
        age = 10;
    }
    
    return self;
}

@end
 

##五,键值观察依赖键

有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。

###1,观察依赖键

观察依赖键的方式与前面描述的一样,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加处理变更通知的代码:

#import "TargetWrapper.h"

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object 
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if ([keyPath isEqualToString:@"age"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Age changed", className);

        NSLog(@" old age is %@", [change objectForKey:@"old"]);
        NSLog(@" new age is %@", [change objectForKey:@"new"]);
    }
    else if ([keyPath isEqualToString:@"information"])
    {
        Class classInfo = (Class)context;
        NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                  encoding:NSUTF8StringEncoding];
        NSLog(@" >> class: %@, Information changed", className);
        NSLog(@" old information is %@", [change objectForKey:@"old"]);
        NSLog(@" new information is %@", [change objectForKey:@"new"]);
    }
    else
    {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

###2,实现依赖键

在这里,观察的是 TargetWrapper 类的 information 属性,该属性是依赖于 Target 类的 age 和 grade 属性。为此,我在 Target 中添加了 grade 属性:

@interface Target : NSObject
@property (nonatomic, readwrite) int grade;
@property (nonatomic, readwrite) int age;
@end

@implementation Target
@synthesize age; // for automatic KVO - age
@synthesize grade;
@end
下面来看看 TragetWrapper 中的依赖键属性是如何实现的。

@class Target;

@interface TargetWrapper : NSObject
{
@private
    Target * _target;
}

@property(nonatomic, assign) NSString * information;
@property(nonatomic, retain) Target * target;

-(id) init:(Target *)aTarget;

@end

#import "TargetWrapper.h"
#import "Target.h"

@implementation TargetWrapper

@synthesize target = _target;

-(id) init:(Target *)aTarget
{
    self = [super init];
    if (nil != self) {
        _target = [aTarget retain];
    }
    
    return self;
}

-(void) dealloc
{
    self.target = nil;
    [super dealloc];
}

- (NSString *)information
{
    return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
}

- (void)setInformation:(NSString *)theInformation
{
    NSArray * array = [theInformation componentsSeparatedByString:@"#"];
    [_target setGrade:[[array objectAtIndex:0] intValue]];
    [_target setAge:[[array objectAtIndex:1] intValue]];
}

+ (NSSet *)keyPathsForValuesAffectingInformation
{
    NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
    return keyPaths;
}

//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
//{
//    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
//    NSArray * moreKeyPaths = nil;
//    
//    if ([key isEqualToString:@"information"])
//    {
//        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
//    }
//    
//    if (moreKeyPaths)
//    {
//        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
//    }
//    
//    return keyPaths;
//}

@end

首先,要手动实现属性 information 的 setter/getter 方法,在其中使用 Target 的属性来完成其 setter 和 getter。

其次,要实现 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法来告诉系统 information 属性依赖于哪些其他属性,这两个方法都返回一个key-path 的集合。在这里要多说几句,如果选择实现 keyPathsForValuesAffectingValueForKey,要先获取 super 返回的结果 set,然后判断 key 是不是目标 key,如果是就将依赖属性的 key-path 结合追加到 super 返回的结果 set 中,否则直接返回 super的结果。

在这里,information 属性依赖于 target 的 age 和 grade 属性,target 的 age/grade 属性任一发生变化,information 的观察者都会得到通知。

###3,使用示例:

Observer * observer = [[[Observer alloc] init] autorelease];
Target * target = [[[Target alloc] init] autorelease];

TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
[wrapper addObserver:observer
          forKeyPath:@"information"
             options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
             context:[TargetWrapper class]];

[target setAge:30];
[target setGrade:1];
[wrapper removeObserver:observer forKeyPath:@"information"];

输出结果:

 >> class: TargetWrapper, Information changed

  old information is 0#10

  new information is 0#30

  >> class: TargetWrapper, Information changed

  old information is 0#30

  new information is 1#30

 

##六,键值观察是如何实现的

###1,实现机理

键值观察用处很多,Core Binding 背后的实现就有它的身影,那键值观察背后的实现又如何呢?想一想在上面的自动实现方式中,我们并不需要在被观察对象 Target 中添加额外的代码,就能获得键值观察的功能,这很好很强大,这是怎么做到的呢?答案就是 Objective C 强大的 runtime 动态能力,下面我们一起来窥探下其内部实现过程。

当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。

如果你对类和对象的关系不太明白,请阅读《深入浅出Cocoa之类与对象》;如果你对如何动态创建类不太明白,请阅读《深入浅出Cocoa 之动态创建类》。

苹果官方文档说得很简洁:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called isa-swizzling.

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

###2,代码分析

由于派生类中被重写的 class 对我们撒谎(它说它就是起初的基类),我们只有通过调用 runtime 函数才能揭开派生类的真面目。 下面来看 Mike Ash 的代码:

首先是带有 x, y, z 三个属性的观察目标 Foo:

@interface Foo : NSObject
{
    int x;
    int y;
    int z;
}

@property int x;
@property int y;
@property int z;

@end

@implementation Foo
@synthesize x, y, z;
@end

下面是检验代码:

#import <objc/runtime.h>

static NSArray * ClassMethodNames(Class c)
{
    NSMutableArray * array = [NSMutableArray array];
    
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(c, &methodCount);
    unsigned int i;
    for(i = 0; i < methodCount; i++) {
        [array addObject: NSStringFromSelector(method_getName(methodList[i]))];
    }

    free(methodList);
    
    return array;
}

static void PrintDescription(NSString * name, id obj)
{
    NSString * str = [NSString stringWithFormat:
                      @"\n\t%@: %@\n\tNSObject class %s\n\tlibobjc class %s\n\timplements methods <%@>",
                      name,
                      obj,
                      class_getName([obj class]),
                      class_getName(obj->isa),
                      [ClassMethodNames(obj->isa) componentsJoinedByString:@", "]];
    NSLog(@"%@", str);
}

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        // Deep into KVO: kesalin@gmail.com
        //
        Foo * anything = [[Foo alloc] init];
        Foo * x = [[Foo alloc] init];
        Foo * y = [[Foo alloc] init];
        Foo * xy = [[Foo alloc] init];
        Foo * control = [[Foo alloc] init];
        
        [x addObserver:anything forKeyPath:@"x" options:0 context:NULL];
        [y addObserver:anything forKeyPath:@"y" options:0 context:NULL];
        
        [xy addObserver:anything forKeyPath:@"x" options:0 context:NULL];
        [xy addObserver:anything forKeyPath:@"y" options:0 context:NULL];
        
        PrintDescription(@"control", control);
        PrintDescription(@"x", x);
        PrintDescription(@"y", y);
        PrintDescription(@"xy", xy);
        
        NSLog(@"\n\tUsing NSObject methods, normal setX: is %p, overridden setX: is %p\n",
               [control methodForSelector:@selector(setX:)],
               [x methodForSelector:@selector(setX:)]);
        NSLog(@"\n\tUsing libobjc functions, normal setX: is %p, overridden setX: is %p\n",
               method_getImplementation(class_getInstanceMethod(object_getClass(control),
                                                                @selector(setX:))),
               method_getImplementation(class_getInstanceMethod(object_getClass(x),
                                                                @selector(setX:))));
    }

    return 0;
}

在上面的代码中,辅助函数 ClassMethodNames 使用 runtime 函数来获取类的方法列表,PrintDescription 打印对象的信息,包括通过 -class 获取的类名, isa 指针指向的类的名字以及其中方法列表。

在这里,我创建了四个对象,x 对象的 x 属性被观察,y 对象的 y 属性被观察,xy 对象的 x 和 y 属性均被观察,参照对象 control 没有属性被观察。在代码的最后部分,分别通过两种方式(对象方法和 runtime 方法)打印出参数对象 control 和被观察对象 x 对象的 setX 方面的实现地址,来对比显示正常情况下 setter 实现以及派生类中重写的 setter 实现。

编译运行,输出如下:

control: <Foo: 0x10010c980>

NSObject class Foo

libobjc class Foo

implements methods <x, setX:, y, setY:, z, setZ:>

x: <Foo: 0x10010c920>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

y: <Foo: 0x10010c940>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

xy: <Foo: 0x10010c960>

NSObject class Foo

libobjc class NSKVONotifying_Foo

implements methods <setY:, setX:, class, dealloc, _isKVOA>

Using NSObject methods, normal setX: is 0x100001df0, overridden setX: is 0x100001df0

Using libobjc functions, normal setX: is 0x100001df0, overridden setX: is 0x7fff8458e025

从上面的输出可以看到,如果使用对象的 -class 方面输出类名始终为:Foo,这是因为新诞生的派生类重写了 -class 方法声称它就是起初的基类,只有使用 runtime 函数 object_getClass 才能一睹芳容:NSKVONotifying_Foo。注意看:x,y 以及 xy 三个被观察对象真正的类型都是 NSKVONotifying_Foo,而且该类实现了:setY:, setX:, class, dealloc, _isKVOA 这些方法。其中 setX:, setY:, class 和 dealloc 前面已经讲到过,私有方法 _isKVOA 估计是用来标示该类是一个 KVO 机制声称的类。在这里 Objective C 做了一些优化,它对所有被观察对象只生成一个派生类,该派生类实现所有被观察对象的 setter 方法,这样就减少了派生类的数量,提供了效率。所有 NSKVONotifying_Foo 这个派生类重写了 setX,setY方法(留意:没有必要重写 setZ 方法)。

接着来看最后两行输出,地址 0x100001df0 是 Foo 类中的实现,而地址是 0x7fff8458e025 是派生类 NSKVONotifying_Foo 类中的实现。那后面那个地址到底是什么呢?可以通过 GDB 的 info 命令加 symbol 参数来查看该地址的信息:

 (gdb) info symbol 0x7fff8458e025

_NSSetIntValueAndNotify in section LC_SEGMENT.__TEXT.__text of /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

看起来它是 Foundation 框架提供的私有函数:_NSSetIntValueAndNotify。更进一步,我们来看看 Foundation 到底提供了哪些用于 KVO 的辅助函数。打开 terminal,使用 nm -a 命令查看 Foundation 中的信息:

nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

其中查找到我们关注的函数:

00000000000233e7 t __NSSetDoubleValueAndNotify
00000000000f32ba t __NSSetFloatValueAndNotify
0000000000025025 t __NSSetIntValueAndNotify
000000000007fbb5 t __NSSetLongLongValueAndNotify
00000000000f33e8 t __NSSetLongValueAndNotify
000000000002d36c t __NSSetObjectValueAndNotify
0000000000024dc5 t __NSSetPointValueAndNotify
00000000000f39ba t __NSSetRangeValueAndNotify
00000000000f3aeb t __NSSetRectValueAndNotify
00000000000f3512 t __NSSetShortValueAndNotify
00000000000f3c2f t __NSSetSizeValueAndNotify
00000000000f363b t __NSSetUnsignedCharValueAndNotify
000000000006e91f t __NSSetUnsignedIntValueAndNotify
0000000000034b5b t __NSSetUnsignedLongLongValueAndNotify
00000000000f3766 t __NSSetUnsignedLongValueAndNotify
00000000000f3890 t __NSSetUnsignedShortValueAndNotify
00000000000f3060 t __NSSetValueAndNotifyForKeyInIvar
00000000000f30d7 t __NSSetValueAndNotifyForUndefinedKey

Foundation 提供了大部分基础数据类型的辅助函数(Objective C中的 Boolean 只是 unsigned char 的 typedef,所以包括了,但没有 C++中的 bool),此外还包括一些常见的 Cocoa 结构体如 Point, Range, Rect, Size,这表明这些结构体也可以用于自动键值观察,但要注意除此之外的结构体就不能用于自动键值观察了。对于所有 Objective C 对象对应的是 __NSSetObjectValueAndNotify 方法。

###七,总结

KVO 并不是什么新事物,换汤不换药,它只是观察者模式在 Objective C 中的一种运用,这是 KVO 的指导思想所在。其他语言实现中也有“KVO”,如 WPF 中的 binding。而在 Objective C 中又是通过强大的 runtime 来实现自动键值观察的。至此,对 KVO 的使用以及注意事项,内部实现都介绍完毕,对 KVO 的理解又深入一层了。Objective 中的 KVO 虽然可以用,但却非完美,有兴趣的了解朋友请查看《KVO 的缺陷》 以及改良实现 MAKVONotificationCenter

###八,引用

Key-value observing:官方文档

Key-Value Observing Done Right : 官方 KVO 实现的缺陷

MAKVONotificationCenter : 一个改良的 Notification 实现,托管在 GitHub 上

02/29/2016 13:58 下午 posted in  apple

美味不用等4.0版本数据管理类设计简介

Controller-->Manager: Request
Note left of Manager: 多个管理类进行数据处理
Manager->Controller:

Note right of Manager: HTTP,Cache,数据处理
Manager->Controller: Model
Controller->View: Data

一直想总结一下自己对一个项目工程架构的经验,在我看来无论是MVC,还是MVVM,其实都是为了将业务数据与具体的UI分离开,这个UI 不单单是具体的View也是UIController,要达到数据的低耦合,降低各模块的依赖,必须要使用一个中间层来管理和数据处理,一直以来我一直使用MVC的基础设计结构,但是我所理解的与之稍有出入。

MVC的概念指的是模型-控制器-视图,而在我看来,如果单一的使用控制器来管理诸多业务,类似网络请求,缓存,通知等,会造成控制器的臃肿和请求的散乱不便于管理,所以我对M的理解更倾向于Manager,当然在某种程度上也可以理解成为管理类和模型类的结合。管理类负责各工具类的业务调用和对结果数据的封装整理,存储,和下发给控制器,这里工具类是最基础的框架封装,比如工程中的对AFNetworking网络框架的的二次封装NWOperationManager,

##Manager管理类

数据请求管理结构

  1. 封装网络请求工具类|NWOperationManager
    这里我使用的是AFNetworking作为网络请求框架,继承它进行一些数据的基础封装。

Screen Shot 2016-02-16 at 15.28.45

注:目前该框架使用的是2.6.3,所以依然继承的是AFHTTPRequestOperationManager

其中回调的block为:

//Block
typedef void (^OperationCompleteBlock)(AFHTTPRequestOperation *operation, NWResult *result);

NWResult是针对服务器返回的数据结构的一个最基础的封装:

Screen Shot 2016-02-16 at 15.31.30

  1. 缓存工具类|NWCache
    NWOperationManager是一个网络请求基础工具类,它实现了网络请求的GET,POST等请求功能,同时也实现了缓存管理,当然如果和服务器进行配合可以较好的实现缓存详情参考:
    iOS网络缓存扫盲篇,但是目前服务器还未有该功能设计,而且由于该工程在最初的版本的时候是没有缓存设计的,是后来功能需求增加,所以需要一个缓存工具来实现,NWCache缓存设计的初步思想如下:
st=>start: 发起数据请求
e=>end: 数据
op=>operation: 本地缓存
op1=>operation: 发起网络请求
op2=>operation: 存储数据
cond=>condition: 缓存有效?

st->op->cond
cond(yes)->e(left)

cond(no)->op1->e
op1->op2(left)->e

NWCache类包含缓存的存储有效时间,和存储路径等作为最底层的工具封装。在网络模块中缓存的实现是请求工具类中处理的,管理类中只负责具体请求的缓存要求,
在请求工具类中其中一个基础接口是:

- (AFHTTPRequestOperation *)requestURL:(NSString *)URLString
                               inQueue:(dispatch_queue_t)queue
                               inGroup:(dispatch_group_t)group
                            HttpMethod:(HttpMethod)method
                            parameters:(NSDictionary *)parameters
                    cacheBodyWithBlock:(void (^)(id<NWCacheArgumentProtocol> cacheArgumentProtocol))block
                operationCompleteBlock:(OperationCompleteBlock)completeBlock;

NWCacheArgumentProtocol 是一个缓存的配置协议:

@protocol NWCacheArgumentProtocol <NSObject>
- (void)cacheResponseWithIgnoreCache:(BOOL)ignoreCache
                      supportOffLine:(BOOL)supportOffLine
                  cacheTimeInSeconds:(NSInteger)cacheTimeInSeconds;
@end
@interface NWCacheArgument : NSObject<NWCacheArgumentProtocol>
@property (nonatomic, strong, readonly) NSString *key;
/** 忽略缓存直接请求*/
@property (nonatomic, assign) BOOL ignoreCache;
/** 是否支持离线缓存,默认不支持*/
@property (nonatomic, assign) BOOL supportOffLine;
@property (nonatomic, assign) NSInteger cacheTimeInSeconds;

- (id)initWithKey:(NSString *)key;
@end

数据管理类中某一个请求是这样的:

- (void)fetchReferencesShopsWithNextCursor:(NSInteger)nextCursor
                                  useCache:(BOOL)useCache
                               expiredTime:(NSInteger)expiredTime
                             completeBlock:(CompleteBlock)completeBlock

可以看到该方法中有两个参数是有关缓存的,是否缓存和过期时间,在具体实现的时候有一个方法来处理这两个参数

#pragma mark private base method
- (void)httpRequestMethod:(HttpMethod)method
                      url:(NSString *)url
               parameters:(NSDictionary *)parameters
                 useCache:(BOOL)useCache
              expiredTime:(NSInteger)expiredTime
     requestCompleteBlock:(RequestCompleteBlock)requestCompleteBlock{
    AFHTTPRequestOperation *opreration = nil;
    NSMutableDictionary *param = [self commonArgumentWithUserParameters:parameters];
    DDLogInfo(@"%@:%@",url,param);
    opreration = [[NWOperationManager sharedClient] requestURL:url
                                                       inQueue:self.httpRequest_queue_t
                                                       inGroup:self.httpRequest_group_t
                                                    HttpMethod:method
                                                    parameters:param
                                            cacheBodyWithBlock:^(id<NWCacheArgumentProtocol> cacheArgumentProtocol) {
                                                [cacheArgumentProtocol cacheResponseWithIgnoreCache:!useCache supportOffLine:YES cacheTimeInSeconds:expiredTime];
                                            } operationCompleteBlock:^(AFHTTPRequestOperation *operation, NWResult *result) {
                                                [self requestURI:url operationCompleteBlock:requestCompleteBlock result:result];
                                                DDLogInfo(@"%@:%@",url,result.response);
                                            }];
}

cacheBodyWithBlock该Block就是配置该请求的缓存设置的,

该设计在没有大改早先控制器方法调用的基础上只是修改了 管理类的请求工具调用和增加缓存工具类就实现了自定义的网络请求缓存,目前还能够满足需求。

当然在具体实现中遇到的一些问题是要避免的,首先在缓存数据的时候是有一个Key是很关键的,该key由接口和识别参数,是该参数是需要注意分页中的页码和诸如经纬度等,避免出错。

总之数据管理类的功能是管理和处理数据请求的,解放控制器,让控制器不再进行数据逻辑处理,控制器只负责简单的数据请求,它只要数据不必管这个数据是来自缓存还是来此云端。数据管请求工具返回的数据是简单封装的原始数据(NWResult),然后数据管理进行数据的再封装转为model返回给控制器,当要进行UI展示的时候将数据传输给视图View,这里的数据不建议是model传输,因为要保证解耦,不能让视图与model过多的联系,所以建议以原始数据的形式进行传输。

02/16/2016 13:54 下午 posted in  apple