String.format详解

String类的format()方法用于创建格式化的字符串以及连接多个字符串对象。熟悉C语言的同学应该记得C语言的sprintf()方法,两者有类似之处。format()方法有两种重载形式。
format(String format, Object… args) 新字符串使用本地语言环境,制定字符串格式和参数生成格式化的新字符串。

format(Locale locale, String format, Object… args) 使用指定的语言环境,制定字符串格式和参数生成格式化的字符串。

常规类型的格式化

转换符 说明 示例
%s 字符串类型 “mingrisoft”
%c 字符类型 ‘m’
%b 布尔类型 true
%d 整数类型(十进制) 99
%x 整数类型(十六进制) FF
%o 整数类型(八进制) 77
%f 浮点类型 99.99
%a 十六进制浮点类型 FF.35AE
%e 指数类型 9.38e+5
%g 通用浮点类型(f和e类型中较短的)
%h 散列码
%% 百分比类型
%n 换行符
%tx 日期与时间类型(x代表不同的日期与时间转换符)

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {  
String str=null;
str=String.format("Hi,%s", "王力");
System.out.println(str);
str=String.format("Hi,%s:%s.%s", "王南","王力","王张");
System.out.println(str);
System.out.printf("字母a的大写是:%c %n", 'A');
System.out.printf("3>7的结果是:%b %n", 3>7);
System.out.printf("100的一半是:%d %n", 100/2);
System.out.printf("100的16进制数是:%x %n", 100);
System.out.printf("100的8进制数是:%o %n", 100);
System.out.printf("50元的书打8.5折扣是:%f 元%n", 50*0.85);
System.out.printf("上面价格的16进制数是:%a %n", 50*0.85);
System.out.printf("上面价格的指数表示:%e %n", 50*0.85);
System.out.printf("上面价格的指数和浮点数结果的长度较短的是:%g %n", 50*0.85);
System.out.printf("上面的折扣是%d%% %n", 85);
System.out.printf("字母A的散列码是:%h %n", 'A');
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Hi,王力  
Hi,王南:王力.王张
字母a的大写是:A
3>7的结果是:false
100的一半是:50
100的16进制数是:64
100的8进制数是:144
50元的书打8.5折扣是:42.500000 元
上面价格的16进制数是:0x1.54p5
上面价格的指数表示:4.250000e+01
上面价格的指数和浮点数结果的长度较短的是:42.5000
上面的折扣是85%
字母A的散列码是:41

搭配转换符的标志,如图所示:

标志 说明 示例 结果
+ 为正数或者负数添加符号 (“%+d”,15) +15
左对齐 (“%-5d”,15) 15
0 数字前面补0 (“%04d”, 99) 0099
空格 在整数之前添加指定数量的空格 (“% 4d”, 99) 99
, 以“,”对数字分组 (“%,f”, 9999.99) 9,999.990000
( 使用括号包含负数 (“%(f”, -99.99) (99.990000)
# 如果是浮点数则包含小数点,如果是16进制或8进制则添加0x或0 (“%#x”, 99) 0x63
(“%#o”, 99) 0143
< 格式化前一个转换符所描述的参数 (“%f和%<3.2f”, 99.45) 99.450000和99.45
$ 被格式化的参数索引 (“%1$d,%2$s”, 99,”abc”) 99,abc

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {  
String str=null;
//$使用
str=String.format("格式参数$的使用:%1$d,%2$s", 99,"abc");
System.out.println(str);
//+使用
System.out.printf("显示正负数的符号:%+d与%d%n", 99,-99);
//补O使用
System.out.printf("最牛的编号是:%03d%n", 7);
//空格使用
System.out.printf("Tab键的效果是:% 8d%n", 7);
//.使用
System.out.printf("整数分组的效果是:%,d%n", 9989997);
//空格和小数点后面个数
System.out.printf("一本书的价格是:% 50.5f元%n", 49.8);
}

输出结果:

1
2
3
4
5
6
格式参数$的使用:99,abc  
显示正负数的符号:+99与-99
最牛的编号是:007
Tab键的效果是: 7
整数分组的效果是:9,989,997
一本书的价格是: 49.80000元

日期和事件字符串格式化

在程序界面中经常需要显示时间和日期,但是其显示的 格式经常不尽人意,需要编写大量的代码经过各种算法才得到理想的日期与时间格式。字符串格式中还有%tx转换符没有详细介绍,它是专门用来格式化日期和时 间的。%tx转换符中的x代表另外的处理日期和时间格式的转换符,它们的组合能够将日期和时间格式化成多种格式。

常见日期和时间组合的格式,如表所示。

转换符 说明 示例
c 包括全部日期和时间信息 星期六 十月 27 14:21:20 CST 2007
F “年-月-日”格式 2007-10-27
D “月/日/年”格式 10/27/07
r “HH:MM:SS PM”格式(12时制) 02:25:51 下午
T “HH:MM:SS”格式(24时制) 14:28:16
R “HH:MM”格式(24时制) 14:28

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {  
Date date=new Date();
//c的使用
System.out.printf("全部日期和时间信息:%tc%n",date);
//f的使用
System.out.printf("年-月-日格式:%tF%n",date);
//d的使用
System.out.printf("月/日/年格式:%tD%n",date);
//r的使用
System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);
//t的使用
System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);
//R的使用
System.out.printf("HH:MM格式(24时制):%tR",date);
}

输出结果:

1
2
3
4
5
6
全部日期和时间信息:星期一 九月 10 10:43:36 CST 2012  
年-月-日格式:2012-09-10
月/日/年格式:09/10/12
HH:MM:SS PM格式(12时制):10:43:36 上午
HH:MM:SS格式(24时制):10:43:36
HH:MM格式(24时制):10:43

定义日期格式的转换符可以使日期通过指定的转换符生成新字符串。这些日期转换符如图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {  
Date date=new Date();
//b的使用,月份简称
String str=String.format(Locale.US,"英文月份简称:%tb",date);
System.out.println(str);
System.out.printf("本地月份简称:%tb%n",date);
//B的使用,月份全称
str=String.format(Locale.US,"英文月份全称:%tB",date);
System.out.println(str);
System.out.printf("本地月份全称:%tB%n",date);
//a的使用,星期简称
str=String.format(Locale.US,"英文星期的简称:%ta",date);
System.out.println(str);
//A的使用,星期全称
System.out.printf("本地星期的简称:%tA%n",date);
//C的使用,年前两位
System.out.printf("年的前两位数字(不足两位前面补0):%tC%n",date);
//y的使用,年后两位
System.out.printf("年的后两位数字(不足两位前面补0):%ty%n",date);
//j的使用,一年的天数
System.out.printf("一年中的天数(即年的第几天):%tj%n",date);
//m的使用,月份
System.out.printf("两位数字的月份(不足两位前面补0):%tm%n",date);
//d的使用,日(二位,不够补零)
System.out.printf("两位数字的日(不足两位前面补0):%td%n",date);
//e的使用,日(一位不补零)
System.out.printf("月份的日(前面不补0):%te",date);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
英文月份简称:Sep  
本地月份简称:九月
英文月份全称:September
本地月份全称:九月
英文星期的简称:Mon
本地星期的简称:星期一
年的前两位数字(不足两位前面补0):20
年的后两位数字(不足两位前面补0):12
一年中的天数(即年的第几天):254
两位数字的月份(不足两位前面补0):09
两位数字的日(不足两位前面补0):10
月份的日(前面不补0):10

和日期格式转换符相比,时间格式的转换符要更多、更精确。它可以将时间格式化成时、分、秒甚至时毫秒等单位。格式化时间字符串的转换符如图所示。

转换符 说明 示例
H 2位数字24时制的小时(不足2位前面补0) 15
I 2位数字12时制的小时(不足2位前面补0) 03
k 2位数字24时制的小时(前面不补0) 15
l 2位数字12时制的小时(前面不补0) 3
M 2位数字的分钟(不足2位前面补0) 03
S 2位数字的秒(不足2位前面补0) 09
L 3位数字的毫秒(不足3位前面补0) 015
N 9位数字的毫秒数(不足9位前面补0) 562000000
p 小写字母的上午或下午标记 中:下午
英:pm
z 相对于GMT的RFC822时区的偏移量 +0800
Z 时区缩写字符串 CST
s 1970-1-1 00:00:00 到现在所经过的秒数 1193468128
Q 1970-1-1 00:00:00 到现在所经过的毫秒数 1193468128984

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) {  
Date date = new Date();
//H的使用
System.out.printf("2位数字24时制的小时(不足2位前面补0):%tH%n", date);
//I的使用
System.out.printf("2位数字12时制的小时(不足2位前面补0):%tI%n", date);
//k的使用
System.out.printf("2位数字24时制的小时(前面不补0):%tk%n", date);
//l的使用
System.out.printf("2位数字12时制的小时(前面不补0):%tl%n", date);
//M的使用
System.out.printf("2位数字的分钟(不足2位前面补0):%tM%n", date);
//S的使用
System.out.printf("2位数字的秒(不足2位前面补0):%tS%n", date);
//L的使用
System.out.printf("3位数字的毫秒(不足3位前面补0):%tL%n", date);
//N的使用
System.out.printf("9位数字的毫秒数(不足9位前面补0):%tN%n", date);
//p的使用
String str = String.format(Locale.US, "小写字母的上午或下午标记(英):%tp", date);
System.out.println(str);
System.out.printf("小写字母的上午或下午标记(中):%tp%n", date);
//z的使用
System.out.printf("相对于GMT的RFC822时区的偏移量:%tz%n", date);
//Z的使用
System.out.printf("时区缩写字符串:%tZ%n", date);
//s的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的秒数:%ts%n", date);
//Q的使用
System.out.printf("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ%n", date);
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2位数字24时制的小时(不足2位前面补0):11  
2位数字12时制的小时(不足2位前面补0):11
2位数字24时制的小时(前面不补0):11
2位数字12时制的小时(前面不补0):11
2位数字的分钟(不足2位前面补0):03
2位数字的秒(不足2位前面补0):52
3位数字的毫秒(不足3位前面补0):773
9位数字的毫秒数(不足9位前面补0):773000000
小写字母的上午或下午标记(英):am
小写字母的上午或下午标记(中):上午
相对于GMT的RFC822时区的偏移量:+0800
时区缩写字符串:CST
1970-1-1 00:00:00 到现在所经过的秒数:1347246232
1970-1-1 00:00:00 到现在所经过的毫秒数:1347246232773


观点仅代表自己,期待你的留言。

iOS开发MVVM理解与总结

MVC存在的问题

iOS概念对应

M(model): 普通Class,用于对基础数据类型的对象封装,不包含界面逻辑、业务逻辑与数据转换等功能。
V(view): xib或storyboard,用于显示给用户的界面。
C(controller): ViewController,用户下行数据输入与上线数据显示,view跳转,数据校验等功能。

问题

  1. 所有的逻辑代码,数据校验,UI控制,对象转换,数据缓存等服务都存在到Controller中,造成代码过于臃肿,可读性低。
  2. Controller中处理所有的任务,基本上很难多人协作完成,分工性差。
  3. Controller依赖到UIKit库,让单元测试无法覆盖,功能可靠性不可预测。
  4. 试想,iphone程序更换成macosx程序哪些能重用?功能的重用性低。

分析MVC的问题,主要还是由于Controller完成的任务过多造成,为了解决以上问题,必须让Controller进行减肥。

MVVM

iOS概念对应

M(model): 与MVC中概念一致之外,将View分为数据model与界面model。
V(view): 与MVC中概念一致之外,另外将与view连接紧密的viewcontroller划分到一起。viewcontroller只负责界面跳转、用户下行数据获取、用户上行数据绑定以及vm层的调用。
VM(view-model): 用于连接view与model层,完成界面逻辑,业务逻辑,接口调用,数据model与界面model数据转换,数据校验上行数据处理与下行数据转换,数据缓存等功能。

另外的建议

使用CocoaPods将各层分开成不同的project,由workspace融合,最终通过静态库的形式进行相互的引用。
这样做的好处有以下几点:

  1. 代码清晰,分工明确。
  2. 对于代码修改后的编译为分段进行,提高了编译速度。
  3. 对各层的代码都可以进行版本化管理,调用者的各层代码固化调用版本,对于重构代码等过程有很大的好处。
  4. 由于静态库可由其它程序平台重用(iOS与MacOSX),一次编译,多次使用。

观点仅代表自己,期待你的留言。

Docker安装问题汇总

测试环境

Centos7_x86_64

1
2
[root@localhost ~]# uname -a
Linux localhost.localdomain 3.10.0-123.el7.x86_64 #1 SMP Mon Jun 30 12:09:22 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

增加yum源

1
2
3
4
5
6
7
8
[root@localhost ~]# sudo tee /etc/yum.repos.d/docker.repo <<-'EOF'
[dockerrepo]
name=Docker Repository
baseurl=https://yum.dockerproject.org/repo/main/centos/$releasever/
enabled=1
gpgcheck=1
gpgkey=https://yum.dockerproject.org/gpg
EOF

文件冲突

Transaction check error:
file /usr/lib/systemd/system/blk-availability.service from install of device-mapper-7:1.02.107-5.el7_2.1.x86_64 conflicts with file from package lvm2-7:2.02.105-14.el7.x86_64
file /usr/sbin/blkdeactivate from install of device-mapper-7:1.02.107-5.el7_2.1.x86_64 conflicts with file from package lvm2-7:2.02.105-14.el7.x86_64
file /usr/share/man/man8/blkdeactivate.8.gz from install of device-mapper-7:1.02.107-5.el7_2.1.x86_64 conflicts with file from package lvm2-7:2.02.105-14.el7.x86_64

解决方法: 先安装lvm2

1
2
3
4
[root@localhost ~]# sudo yum install lvm2 -y
[root@localhost ~]# sudo yum install docker -y
[root@localhost ~]# docker -v
Docker version 1.10.3, build 20f81d

Docker daemon未运行

[root@localhost ~]# docker pull ubuntu
Using default tag: latest
Warning: failed to get default registry endpoint from daemon (Cannot connect to the Docker daemon. Is the docker daemon running on this host?). Using system default: https://index.docker.io/v1/
Cannot connect to the Docker daemon. Is the docker daemon running on this host?
解决方法: 重启docker服务

1
2
[root@localhost ~]# service docker restart
Redirecting to /bin/systemctl restart docker.service

Docker被墙

[root@localhost ~]# docker pull centos
Using default tag: latest
Pulling repository docker.io/library/centos
Error while pulling image: Get https://index.docker.io/v1/repositories/library/centos/images: dial tcp: lookup index.docker.io on 10.28.10.166:53: no such host

由于docker镜像站被墙,推荐使用灵雀云镜像
解决方法: pull的时候使用国内镜像地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# docker pull index.alauda.cn/tutum/centos
Using default tag: latest
latest: Pulling from tutum/centos
a3ed95caeb02: Pull complete
196355c4b639: Pull complete
edd0a8ebcd9d: Pull complete
8ba44ed17115: Pull complete
69f7e70c0063: Pull complete
54abd94c9217: Pull complete
Digest: sha256:11bc5863ca1643f1de49962c2741c3d1feca37ef258d5dd91baa2cca9a82b5b5
Status: Downloaded newer image for index.alauda.cn/tutum/centos:latest
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
index.alauda.cn/tutum/centos latest e90ef4c35b09 2 weeks ago 297.3 MB


观点仅代表自己,期待你的留言。

iOS 本地通知

为了提高用户的关注度,我们经常会推送一些新的内容给用户。ios中主要有两种推送,一种是远程通知,一种是本地通知,远程通知是和服务器端配合完成的,这里暂不说明,这篇文章主要说下本地通知。
本地通知是在ios4.0之后添加的,但是在ios8之后,在设置通知之前,需要先对通知进行注册,注册需要的通知类型,否则收不到响应类型的通知消息。

1
2
3
4
5
6
7
8
9
10
11
//ios8需要注册推送
if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){
//通知类型
UIUserNotificationType types = UIUserNotificationTypeBadge |
UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
//设置通知类型和动画
UIUserNotificationSettings *mySettings =
[UIUserNotificationSettings settingsForTypes:types categories:nil];
//注册
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
}

上边注册了Icon角标,声音,和警告通知,当程序第一次调用registerUserNotificationSettings的时候,程序会询问用户是否允许程序发送通知,在用户选择之后(不管是同意与否),程序会异步调用- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)函数。所有的注册类型都会可通过currentUserNotificationSettings变量获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
UILocalNotification *notification = [[UILocalNotification alloc] init];
if (notification) {
//设置时区
notification.timeZone=[NSTimeZone defaultTimeZone];
//设置推送的时间点
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"HH:mm:ss"];
NSDate *date = [formatter dateFromString:@"09:00:00"];
notification.fireDate=date;
//通知重复提示的单位,可以是天、周、月
notification.repeatInterval = kCFCalendarUnitDay;
//推送的内容
notification.alertBody = @"this is a notificaiton";
//推送声音
notification.soundName = UILocalNotificationDefaultSoundName;
//应用右上角红色图标数字
notification.applicationIconBadgeNumber = 1;
//自定义信息
NSDictionary *infoDict = [NSDictionary dictionaryWithObject:@"two" forKey:@"one"];
notification.userInfo = infoDict;
}

UIApplication *app = [UIApplication sharedApplication];
[app scheduleLocalNotification:notification];

本地通知是通过UILocalNotification类来完成的,首先需要通过fireDate设置通知的时间点,还可设置通知的内容,声音,角标数等。除此之外,用户还可通过userInfo设置自定义数据。具体可参考UILocalNotification官方文档
当程序正在运行时,收到通知时,会调用application:didReceiveLocalNotification方法。

1
2
3
4
5
6
7
8
9
10
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
NSDictionary *infoDict = notification.userInfo;
NSString *str = [infoDict objectForKey:@"one"];
if ([str isEqualToString:@"two"]) {
NSLog(@"--------yes equal");
} else {
NSLog(@"--------no ");
}
}

取消通知时,可以使用cancelLocalNotification取消具体某个通知或者通过cancelAllLocalNotifications取消全部通知。

1
[[UIApplication sharedApplication] cancelAllLocalNotifications];

如果我们添加了角标,在通知之后,角标会一直存在,当需要取消角标时,可利用下边语句

1
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];

从ios8开始,通知添加了通知动作事件,如果有注意到,我们上边的进行注册的时候categories赋值为nil,此变量就是用来添加动作事件的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
//ios8需要注册推送
if ([UIApplication instancesRespondToSelector:@selector(registerUserNotificationSettings:)]){
UIMutableUserNotificationAction *acceptAction =
[[UIMutableUserNotificationAction alloc] init];
acceptAction.identifier = @"accept_action"; //ID
acceptAction.title = @"Accept"; //按钮内容
acceptAction.activationMode = UIUserNotificationActivationModeBackground;
acceptAction.destructive = NO;
acceptAction.authenticationRequired = NO;

UIMutableUserNotificationAction *cancelAction =
[[UIMutableUserNotificationAction alloc] init];
cancelAction.identifier = @"cancel_action";
cancelAction.title=@"Cancel";
cancelAction.activationMode = UIUserNotificationActivationModeBackground;
cancelAction.destructive = NO;
cancelAction.authenticationRequired = NO;

// First create the category
UIMutableUserNotificationCategory *inviteCategory =
[[UIMutableUserNotificationCategory alloc] init];
inviteCategory.identifier = @"INVITE_CATEGORY";
[inviteCategory setActions:@[acceptAction, cancelAction]
forContext:UIUserNotificationActionContextDefault];
NSSet *categories = [NSSet setWithObject:inviteCategory];

//通知类型
UIUserNotificationType types = UIUserNotificationTypeBadge |
UIUserNotificationTypeSound | UIUserNotificationTypeAlert;
UIUserNotificationSettings *mySettings =
[UIUserNotificationSettings settingsForTypes:types categories:categories];
//注册
[[UIApplication sharedApplication] registerUserNotificationSettings:mySettings];
}

UILocalNotification *notification = [[UILocalNotification alloc] init];
if (notification) {
//时区
notification.timeZone=[NSTimeZone defaultTimeZone];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"ss"];
NSDate *date = [formatter dateFromString:@"10"];
notification.alertBody = @"haha";
notification.fireDate=date;
//通知重复提示的单位,可以是天、周、月
notification.repeatInterval = kCFCalendarUnitMinute;
//推送声音
notification.soundName = UILocalNotificationDefaultSoundName;
//应用右上角红色图标数字
notification.applicationIconBadgeNumber = 1;
notification.category = @"INVITE_CATEGORY";

NSDictionary *infoDict = [NSDictionary dictionaryWithObject:@"two" forKey:@"one"];
notification.userInfo = infoDict;

}

UIApplication *app = [UIApplication sharedApplication];
[app scheduleLocalNotification:notification];

以上就是一个动作的事件的注册过程,其中用到了UIMutableUserNotificationActionUIMutableUserNotificationCategory具体用法可参考官方文档
UIMutableUserNotificationAction
UIMutableUserNotificationCategory
这两个类,注册完之后,特别需要主意,要在本地通知中进行设置,否则没有效果。值为注册时category指定的ID

1
notification.category = @"INVITE_CATEGORY";

这样当我们收到通知,下拉一下就可看到动作事件,有事件,就有事件的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void (^)())completionHandler
{
if ([identifier isEqualToString:@"accept_action"]) {
NSLog(@"-----accept action");
} else if ([identifier isEqualToString:@"cancel_action"]) {
NSLog(@"-----cancel action");
}

if (completionHandler) {
completionHandler();
}
}

通过id我们就可以区分不同的动作,然后对其进行相应处理,最后调用completionHandler();


观点仅代表自己,期待你的留言。
http://zuolun.me/blog/2015/01/08/ios-ben-di-tui-song/

iOS 7 的多任务

在 iOS 7 之前,当程序置于后台之后开发者们对他们程序所能做的事情非常有限。除了 VOIP 和基于地理位置特性以外,唯一能做的地方就是使用后台任务(background tasks)让代码可以执行几分钟。如果你想下载比较大的视频文件以便离线浏览,亦或者备份用户的照片到你的服务器上,你都仅能完成一部分工作。

iOS 7 添加了两个新的 API 以便你的程序可以在后台更新界面以及内容。首先是后台获取(Background Fetch),它允许你定期地从网络获取新的内容。第二个 API 就是远程通知(Remote Notifications),这是一个当事件发生时可以让推送通知主动提醒应用的新特性,这两者都为你的应用界面保持最新提供了极大的帮助。在新的后台传输服务 (Background Transfer Service) 中执行定期的任务,也允许你在进程之外可以执行网络传输(下载和上传)工作。

后台获取 (Background Fetch) 和远程通知 (Remote Notification) 基于简单的 ApplicationDelegate 钩子,在应用程序挂起之前的 30 秒时钟时间执行工作。它们不是用于 CPU 频繁工作或者长时间运行任务,而是用来处理长时间运行的网络请求队列,例如下载一部很大的电影,或者执行快速的内容更新。

对用户来说,多任务处理有一点显而易见的改变就是新的应用切换程序 (the new app switcher),它用来呈现应用到后台时的界面快照。这些快照的存在是有一定理由的–现在你可以在后台完成工作后更新程序快照,以用来呈现新的内容。社交网络、新闻或者天气等应用现在都可以直接呈现最新的内容而不需要用户重新打开应用。我们稍后会介绍如何更新屏幕快照。

后台获取

后台获取是一种智能的轮询机制,它很适合需要经常更新内容的程序,像社交网络,新闻或天气的程序。为了在用户启动程序前提前触发后台获取,系统会根据用户行为唤醒应用程序。举个例子,如果用户经常在下午 1 点使用某个应用程序,系统会学习,适应并在使用周期前执行后台获取。为了减少电池使用,使用设备无线通信的所有应用的后台获取会被合并,如果你向系统报告新数据无法获取,iOS 会适应并使用此信息避免会继续获取。

开启后台获取的第一步是在 info plist 文件中对 UIBackgroundModes 键指定特定的值。最简单的途径是在 Xcode 5 的 project editor 中新的 Capabilities 标签页中设置,这个标签页包含了后台模式部分,可以方便配置多任务选项。

或者,你可以手动编辑这个值

1
2
3
4
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
</array>

接下来,告诉 iOS 多久进行一次数据获取

1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

return YES;
}

iOS 默认不进行后台获取,所以你需要设置一个时间间隔,否则,你的应用程序永远不能在后台被唤醒。UIApplicationBackgroundFetchIntervalMinimum 这个值要求系统尽可能频繁地去管理你的程序到底什么时候应该被唤醒,但如果你不需要这样的话,你也应该指定一个你想要的的时间间隔。例如,一个天气的应用程序,可能只需要几个小时才更新一次,iOS 将会在后台获取之间至少等待你指定的时间间隔。

如果你的应用允许用户退出登录,那么就没有获取新数据的需要了,你应该把 minimumBackgroundFetchInterval 设置为 UIApplicationBackgroundFetchIntervalNever,这样可以节省资源。

最后一步是在应用程序委托中实现下列方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (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;
}

// 解析响应/数据以决定新内容是否可用
BOOL hasNewData = ...
if (hasNewData) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}];

// 开始任务
[task resume];
}

系统唤醒应用程序后将会执行这个委托方法。需要注意的是,你只有 30 秒的时间来确定获取的新内容是否可用,然后处理新内容并更新界面。30 秒时间应该足够去从网络获取数据和获取界面的缩略图,但是最多只有 30 秒。当完成了网络请求和更新界面后,你应该执行完成的回调。

完成回调的执行有两个目的。首先,系统会估量你的进程消耗的电量,并根据你传递的 UIBackgroundFetchResult 参数记录新数据是否可用。其次,当你调用完成的处理代码时,应用的界面缩略图会被采用,并更新应用程序切换器。当用户在应用间切换时,用户将会看到新内容。这种通过 completion handler 来报告并且生成截图的方法,在新的多任务处理 API 中是很常见的。

在实际应用中,你应该将 completionHandler 传递到应用程序的子组件,然后在处理完数据和更新界面后调用。

在这里,你可能想知道 iOS 是如何在应用程序后台运行时获得界面截图的,并且想知道应用程序的生命周期与后台获取之间有什么关系。如果应用程序处于挂起状态,系统会先唤醒应用,然后再调用 application: performFetchWithCompletionHandler:。如果应用程序还没有启动,系统将会启动它,然后调用常见的委托方法,包括 application: didFinishLaunchingWithOptions:。你可以把这种应用程序运行的方式想像为用户从 Springboard 启动这个程序,区别仅仅在于界面是看不见的,在屏幕外渲染的。

大多数情况下,无论应用在后台启动或者在前台,你会执行相同的工作,但你可以通过查看 UIApplication 的 applicationState 属性来判断应用是不是从后台启动。

1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState);

return YES;
}

测试后台数据获取

有两种可以模拟后台获取的途径。最简单是从 Xcode 运行你的应用,当应用运行时,在 Xcode 的 Debug 菜单选择 Simulate Background Fetch.

第二种方法,使用 scheme 更改 Xcode 运行程序的方式。在 Xcode 菜单的 Product 选项,选择 Scheme 然后选择 Manage Schemes。在这里,你可以编辑或者添加一个新的 scheme,然后选中 Launch due to a background fetch event 。如下图:

远程通知

远程通知允许你在重要事件发生时,告知你的应用。你可能需要发送新的即时信息,突发新闻的提醒,或者用户喜爱电视的最新剧集已经可以下载以便离线观看的消息。远程通知很适合用于那些偶尔出现,但却很重要的内容,如果使用后台获取模式中在两次获取间需要等待的时间是不可接受的话,远程通知会是一个不错的选择。远程通知会比后台获取更有效率,因为应用程序只有在需要的时候才会启动。

一条远程通知实际上只是一条普通的带有 content-available 标志的推送通知。你可以发送一条带有提醒信息的推送去告诉用户有事请发生了,同时在后台对界面进行更新。但远程通知也可以做到在安静地,没有提醒消息或者任何声音的情况下,只去更新应用界面或者触发后台工作。然后你可以在完成下载或者处理完新内容后,发送一条本地通知。

静默的推送通知有速度限制,所以你可以大胆地根据应用程序的需要发送尽可能多的通知。iOS 和苹果推送服务会控制推送通知多久被递送,发送很多推送通知是没有问题的。如果你的推送通知达到了限制,推送通知可能会被延迟,直到设备下次发送保持活动状态的数据包,或者收到另外一个通知。

发送远程通知

要发送一条远程通知,需要在推送通知的有效负载(payload)设置 content-available 标志。content-available 标志和用来通知 报刊应用(Newsstand)的健值是一样的,因此,大多数推送脚本和库都已经支持远程通知。当你发送一条远程通知时,你可能还想要包含一些通知有效负载中的数据,让你应用程序可以引用事件。这可以为你节省一些网络请求,并提高应用程序的响应度。

我建议在开发的时候,使用 Nomad CLI’s Houston 工具发送推送消息,当然你也可以使用你喜欢的库或脚本。

你可以通过 nomad-cli ruby gem 来安装 Houston

1
gem install nomad-cli

然后通过包含在 Nomad 的 apn 实用工具发送一条通知:

1
2
# 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)结果和下面类似:

1
2
3
4
5
6
{
"aps" : {
"content-available" : 1
},
"content-id" : 42
}

iOS 7 添加了一个新的应用程序委托方法,当接收到一条带有 content-available 的推送通知时,下面的方法会被调用:

1
2
3
4
5
6
7
8
9
10
- (void)application:(UIApplication *)application 
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSLog(@"Remote Notification userInfo is %@", userInfo);

NSNumber *contentID = userInfo[@"content-id"];
// 根据 content ID 进行操作
completionHandler(UIBackgroundFetchResultNewData);
}

和后台抓取一样,应用程序进入后台启动,也有 30 秒的时间去获取新内容并更新界面,最后调用完成的处理代码。我们可以像后台获取那样,执行快速的网络请求,但我们可以使用新的强大的后台传输服务,处理任务队列,下面看看我们如何在任务完成后更新界面。

NSURLSession 和 后台传输服务(Background Transfer Service)

NSURLSession 是 iOS 7 添加的一个新类,它也是 Foundation networking 中的新技术。作为 NSURLConnection 的替代品,一些熟悉的概念和类都保留下来了,例如 NSURL,NSURLRequest 和 NSURLResponse。所以,你可以使用 NSURLSessionTask 这一 NSURLConnection 的替代品,来处理网络请求及响应。一共有 3 种会话任务:数据,下载和上传。每一种都向 NSURLSessionTask 添加了语法糖,根据你的需要,适当选择一种。

一个 NSURLSession 对象协调一个或多个 NSURLSessionTask 对象,并根据 NSURLSessionTask 创建的 NSURLSessionConfiguration 实现不同的功能。使用相同的配置,你也可以创建多组具有相关任务的 NSURLSession 对象。要利用后台传输服务,你将会使用 [NSURLSessionConfiguration backgroundSessionConfiguration] 来创建一个会话配置。添加到后台会话的任务在外部进程运行,即使应用程序被挂起,崩溃,或者被杀死,它依然会运行。

NSURLSessionConfiguration 允许你设置默认的 HTTP 头,配置缓存策略,限制使用蜂窝数据等等。其中一个选项是 discretionary 标志,这个标志允许系统为分配任务进行性能优化。这意味着只有当设备有足够电量时,设备才通过 Wifi 进行数据传输。如果电量低,或者只仅有一个蜂窝连接,传输任务是不会运行的。后台传输总是在 discretionary 模式下运行。

目前为止,我们大概了解了 NSURLSession,以及一个后台会话如何进行,接下来,让我们回到远程通知的例子,添加一些代码来处理后台传输服务的下载队列。当下载完成后,我们会通知用户该文件已经可以使用了。

NSURLSessionDownloadTask

首先,我们先处理一条远程通知,并把一个 NSURLSessionDownloadTask 添加到后台传输服务的队列。在 backgroundURLSession 方法中,我们根据后台会话配置,创建一个 NSURLSession 对象,并把 application delegate 作为会话的委托对象。文档不建议对于相同的标识符 (identifier) 创建多个会话对象,所以我们使用 dispatch_once 来避免潜在的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (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。你必须把这个文件移动或复制你的应用程序空间,因为当你从这个委托方法返回时,该文件将从临时存储中删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#Pragma Mark - NSURLSessionDownloadDelegate

- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location);

// 用 NSFileManager 将文件复制到应用的存储中
// ...

// 通知 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
{
}

当后台会话任务完成时,如果你的应用程序仍然在前台运行,上面的代码已经足够了。然而,在大多数情况下,你的应用程序可能是没有运行的,或者在后台被挂起。在这些情况下,你必须实现应用程序委托的两个方法,这样系统就可以唤醒你的应用程序。不同于以往的委托回调,该应用程序委托会被调用两次,因为您的会话和任务委托可能会收到一系列消息。app delegate 的:handleEventsForBackgroundURLSession: 方法会在这些 NSURLSession 委托的消息发送前被调用,然后,URLSessionDidFinishEventsForBackgroundURLSession 在随后被调用。在前面的方法中,包含了一个后台完成的回调(completionHandler),并在后面的方法中执行回调以便更新界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)                  application:(UIApplication *)application 
handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
// 你必须重新建立一个后台 seesiong 的参照
// 否则 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法会因为
// 没有 对 session 的 delegate 设定而不会被调用。参见上面的 backgroundURLSession
NSURLSession *backgroundSession = [self backgroundURLSession];

NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);

// 保存 completion handler 以在处理 session 事件后更新 UI
[self addCompletionHandler:completionHandler forSession:identifier];
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);

if (session.configuration.identifier) {
// 调用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler
[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: 方法被调用之后被调用。

配置和限制

我们简单地体验了后台传输的强大之处,但你应该深入文档,阅读 NSURLSessionConfiguration 部分,以便最好地满足你的使用场景。例如,NSURLSessionTasks 通过 NSURLSessionConfiguration 的 timeoutIntervalForResource 属性,支持资源超时特性。你可以使用这个特性指定你允许完成一个传输所需的最长时间。内容只在有限的时间可用,或者在用户只有有限 Wifi 带宽的时间内无法下载或上传资源的情况下,你也可以使用这个特性。

除了下载任务,NSURLSession 也全面支持上传任务,因此,你可能会在后台将视频上传到服务器,这保证用户不需要再像 iOS 6 那样保持应用程序前台运行。如果当传输完成时你的应用程序不需要在后台运行,一个比较好的做法是,把 NSURLSessionConfiguration 的 sessionSendsLaunchEvents 属性设置为 NO。高效利用系统资源,是一件让 iOS 和用户都高兴的事。

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

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

总结

iOS 7 中强大的多任务和网络 API 为现有应用和新应用开启了一系列全新的可能性。如果你的应用程序可以从进程外的网络传输和数据中获益,那么尽情地使用这些美妙的 API。一般情况下,你可以就像你的应用正在前台运行那样去实现后台传输,并进行适当的界面更新,而这里绝大多数的工作都已经为你完成了。


观点仅代表自己,期待你的留言。
http://objccn.io/issue-5-5/

iOS开发过程中几种消息传递机制的理解

几种消息传递机制

首先我们来看看每种机制的具体特点。在这个基础上,下一节我们会画一个流程图来帮我们在具体情况下正确选择应该使用的机制。最后,我们会介绍一些苹果框架里的例子并且解释为什么在那些用例中会选择这样的机制。

KVO

KVO 是提供对象属性被改变时的通知的机制。KVO 的实现在 Foundation 中,很多基于 Foundation 的框架都依赖它。

如果只对某个对象的值的改变感兴趣的话,就可以使用 KVO 消息传递。不过有一些前提:第一,接收者(接收对象改变的通知的对象)需要知道发送者 (值会改变的对象);第二,接收者需要知道发送者的生命周期,因为它需要在发送者被销毁前注销观察者身份。如果这两个要去符合的话,这个消息传递机制可以一对多(多个观察者可以注册观察同一个对象的变化)

如果要在 Core Data 上使用 KVO 的话,方法会有些许差别。这和 Core Data 的惰性加载 (faulting) 机制有关。一旦一个 managed object 被惰性加载处理的话,即使它的属性没有被改变,它还是会触发相应的观察者。

编者注 把属性值先取入缓存中,在对象需要的时候再进行一次访问,这在 Core Data 中是默认行为,这种技术称为 Faulting。这么做可以避免降低内存开销,但是如果你确定将访问结果对象的具体属性值时,可以禁用 Faults 以提高获取性能。关于这个技术更多的情况,请移步官方文档

通知

要在代码中的两个不相关的模块中传递消息时,通知机制是非常好的工具。通知机制广播消息,当消息内容丰富而且无需指望接收者一定要关注的话这一招特别有用。

通知可以用来发送任意消息,甚至可以包含一个 userInfo 字典。你也可以继承 NSNotification 写一个自己的通知类来自定义行为。通知的独特之处在于,发送者和接收者不需要相互知道对方,所以通知可以被用来在不同的相隔很远的模块之间传递消息。这就意味着这种消息传递是单向的,我们不能回复一个通知。

委托 (Delegation)

Delegation 在苹果的框架中广泛存在。它让我们能自定义对象的行为,并收到一些触发的事件。要使用 delegation 模式的话,发送者需要知道接收者,但是反过来没有要求。因为发送者只需要知道接收者符合一定的协议,所以它们两者结合的很松。

因为 delegate 协议可以定义任何的方法,我们可以照着自己的需求来传递消息。可以用方法参数来传递消息内容,delegate 可以通过返回值的形式来给发送者作出回应。如果只要在相对接近的两个模块间传递消息,delgation 是很灵活很直接的消息传递机制。

过度使用 delegation 也会带来风险。如果两个对象结合得很紧密,任何其中一个对象都不能单独运转,那么就不需要用 delegate 协议了。这些情况下,对象已经知道各自的类型,可以直接交流。两个比较新的例子是 UICollectionViewLayout 和 NSURLSessionConfiguration。

Block

Block 是最近才加入 Objective-C 的,首次出现在 OS X 10.6 和 iOS 4 平台上。Block 通常可以完全替代 delegation 消息传递机制的角色。不过这两种机制都有它们自己的独特需求和优势。

一个不使用 block 的理由通常是 block 会存在导致 retain 环 (retain cycles) 的风险。如果发送者需要 retain block 但又不能确保引用在什么时候被赋值为 nil, 那么所有在 block 内对 self 的引用就会发生潜在的 retain 环。

假设我们要实现一个用 block 回调而不是 delegate 机制的 table view 里的选择方法,如下所示:

1
2
3
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// 处理选择
};

这儿的问题是,self 会 retain table view,table view 为了让 block 之后可以使用而又需要 retain 这个 block。然而 table view 不能把这个引用设为 nil,因为它不知道什么时候不需要这个 block 了。如果我们不能保证打破 retain 环并且我们需要 retain 发送者,那么 block 就不是一个的好选择。

NSOperation 是使用 block 的一个好范例。因为它在一定的地方打破了 retain 环,解决了上述的问题。

1
2
3
4
5
6
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];

一眼看来好像上面的代码有一个 retain 环:self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, 而 completionBlock retain 了 self。然而,把 operation 加入 queue 中会使 operation 在某个时间被执行,然后被从 queue 中移除。(如果没被执行,问题就大了。)一旦 queue 把 operation 移除,retain 环就被打破了。

另一个例子是:我们在写一个视频编码器的类,在类里面我们会调用一个 encodeWithCompletionHandler: 的方法。为了不出问题,我们需要保证编码器对象在某个时间点会释放对 block 的引用。其代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end

@implementation Encoder

- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// 进行异步处理...
}

// 这个方法会在完成后被调用一次
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- 不要忘了这个!
}

@end

一旦任务完成,completion block 调用过了以后,我们就应该把它设为 nil。

如果一个被调用的方法需要发送一个一次性的消息作为回复,那么使用 block 是很好的选择, 因为这样做我们可以打破潜在的 retain 环。另外,如果将处理的消息和对消息的调用放在一起可以增强可读性的话,我们也很难拒绝使用 block 来进行处理。在用例之中,使用 block 来做完成的回调,错误的回调,或者类似的事情,是很常见的情况。

Target-Action

Target-Action 是回应 UI 事件时典型的消息传递方式。iOS 上的 UIControl 和 Mac 上的 NSControl/NSCell 都支持这个机制。Target-Action 在消息的发送者和接收者之间建立了一个松散的关系。消息的接收者不知道发送者,甚至消息的发送者也不知道消息的接收者会是什么。如果 target 是 nil,action 会在响应链 (responder chain) 中被传递下去,直到找到一个响应它的对象。在 iOS 中,每个控件甚至可以和多个 target-action 关联。

基于 target-action 传递机制的一个局限是,发送的消息不能携带自定义的信息。在 Mac 平台上 action 方法的第一个参数永远接收者。iOS 中,可以选择性的把发送者和触发 action 的事件作为参数。除此之外就没有别的控制 action 消息内容的方法了。

做出正确的选择

基于上述对不同消息传递机制的特点,我们画了一个流程图来帮助我们在不同情境下做出不同的选择。一句忠告:流程图的建议不代表最终答案。有些时候别的选择依然能达到应有的效果。只不过大多数情况下这张图能引导你做出正确的决定。

图中有些细节值得深究:

有个框中说到: 发送者支持 KVO。这不仅仅是说发送者会在值改变的时候发送 KVO 通知,而且说明观察者需要知道发送者的生命周期。如果发送者被存在一个 weak 属性中,那么发送者有可能会自己变成 nil,那时观察者会导致内存泄露。

一个在最后一行的框里说,消息直接响应方法调用。也就是说方法调用的接收者需要给调用者一个消息作为方法调用的直接反馈。这也就是说处理消息的代码和调用方法的代码必须在同一个地方。

最后在右下角的地方,一个选择分支这样说:发送者能确保释放对 block 的引用吗?这涉及到了我们之前讨论 block 的 API 存在潜在的 retain 环的问题。如果发送者不能保证在某个时间点会释放对 block 的引用,那么你会惹上 retain 环的麻烦。

Framework 示例

本节我们通过一些苹果框架里的例子来验证流程图的选择是否有道理,同时解释为什么苹果会选择用这些机制。

KVO

NSOperationQueue 用了 KVO 观察队列中的 operation 状态属性的改变情况 (isFinished,isExecuting,isCancelled)。当状态改变的时候,队列会收到 KVO 通知。为什么 operation 队列要用 KVO 呢?

消息的接收者(operation 队列)知道消息的发送者(operation),并 retain 它并控制后者的生命周期。另外,在这种情况下只需要单向的消息传递机制。当然如果考虑到 oepration 队列只关心那些改变 operation 的值的改变情况的话,就还不足以说服大家使用 KVO 了。但我们可以这么理解:被传递的消息可以被当成值的改变来处理。因为 state 属性在 operation 队列以外也是有用的,所以这里适合用 KVO。

当然 KVO 不是唯一的选择。我们也可以将 operation 队列作为 operation 的 delegate 来使用,operation 会调用类似 operationDidFinish: 或者 operationDidBeginExecuting: 等方法把它的 state 传递给 queue。这样就不太方便了,因为 operation 要保存 state 属性,以便于调用这些 delegate 方法。另外,由于 queue 不能主动获取 state 信息,所以 queue 也必须保存所有 operation 的 state。

Notifications

Core Data 使用 notification 传递事件(例如一个 managed object context 中的改变————NSManagedObjectContextObjectsDidChangeNotification)

发生改变时触发的 notification 是由 managed object contexts 发出的,所以我们不能假定消息的接收者知道消息的发送者。因为消息的源头不是一个 UI 事件,很多接收者可能在关注着此消息,并且消息传递是单向的,所以 notification 是唯一可行的选择。

Delegation

Table view 的 delegate 有多重功能,它可以从管理 accessory view,直到追踪在屏幕上显示的 cell。例如我们可以看看 tableView:didSelectRowAtIndexPath: 方法。为什么用 delegate 实现而不是 target-action 机制?

正如我们在上述流程图中看到的,用 target-action 时,不能传递自定义的数据。而选中 table view 的某个 cell 时,collection view 不仅需要告诉我们一个 cell 被选中了,也要通过 index path 告诉我们哪个 cell 被选中了。如果我们照着这个思路,流程图会引导我们使用 delegation 机制。

如果不在消息传递中包含选中 cell 的 index path,而是让选中项改变时我们像 table view 主动询问并获取选中 cell 的相关信息,会怎样呢?这会非常不方便,因为我们必须记住当前选中项的数据,这样才能在多选择中知道哪些 cell 是被新选中的。

同理,我们可以想象通过观察 table view 选中项的 index path 属性,当该值发生改变的时候,获得一个选中项改变的通知。不过我们会遇到上述相似问题:不做记录的话我们就不能分辨哪一个 cell 被选择或取消选择了。

Block

我们用 -[NSURLSession dataTaskWithURL:completionHandler:] 来作为一个 block API 的介绍。那么从 URL 加载部分返回给调用者是怎么传递消息的呢?首先,作为 API 的调用者,我们知道消息的发送者,但是我们并没有 retain 它。另外,这是个单向的消息传递————它直接调用 dataTaskWithURL: 的方法。如果我们对照流程图,会发现这属于 block 消息传递机制。

有其他的选项吗?当然,苹果自己的 NSURLConnection 就是最好的例子。NSURLConnection在 block 问世之前就存在了,所以它并没有用 block 来实现消息传递,而是使用 delegation 来完成。当 block 出现以后,苹果就在 OS X 10.7 和 iOS 5 平台上的 NSURLConnection 中加了 sendAsynchronousRequest:queue:completionHandler:,所以我们不再在简单的任务中使用 delegate 了。

因为 NSURLSession 是个最近在 OS X 10.9 和 iOS 7 才出现的 API,所以它们使用 block 来实现消息传递机制(NSURLSession 有一个 delegate,但是是用于其他目的)。

Target-Action

一个明显的 target-action 用例是按钮。按钮在不被按下的时候不需要发送任何的信息。为了这个目的,target-action 是 UI 中消息传递的最佳选择。

如果 target 是明确指定的,那么 action 消息会发送给指定的对象。如果 target 是 nil, action 消息会一直在响应链中被传递下去,直到找到一个能处理它的对象。在这种情况下,我们有一个完全解耦的消息传递机制:发送者不需要知道接收者,反之亦然。

Target-action 机制非常适合响应 UI 的事件。没有其他的消息传递机制能够提供相同的功能。虽然 notification 在发送者和接收者的松散关系上最接近它,但是 target-action 可以用于响应链——只有一个对象获得 action 并响应,action 在响应链中传递,直到能遇到响应这个 action 的对象。

总结

一开始接触这么多的消息传递机制的时候,我们可能有些无所适从,觉得所有的机制都可以被选用。不过一旦我们仔细分析每个机制的时候,它们各自都有特殊的要求和能力。

文中的选择流程图是帮助你清楚认识这些机制的好的开始,当然它不是所有问题的答案。如果你觉得这和你自己选择机制的方式相似或是有任何缺漏,欢迎来信指正。


观点仅代表自己,期待你的留言。
http://objccn.io/issue-7-4/

Effective Objective-C 理解消息传递机制

最简单的动态

Objective-C 是一门极其动态的语言,许多东西都可以推迟到运行时决定、修改。那么到底何为动态、何为静态?我们通过一个简单的例子对比下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/***********  例1 静态绑定   ***********/
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void saySomething(int type)
{
if (type == 0) {
printHello();
} else {
printGoodbye();
}
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/***********  例2 动态绑定   ***********/
#import <stdio.h>
void printHello() {
printf("Hello, world!\n");
}
void printGoodbye() {
printf("Goodbye, world!\n");
}
void saySomething(int type)
{
void (*func)();
if (type == 0) {
func = printHello;
} else {
func = printGoodbye;
}
func();
return 0;
}

例1的代码在编译期,编译器就已经知道了有 void printHello()、void printGoodbye() 俩函数,并且在 saySomething() 函数中,调用的函数明确,可以直接将函数名硬编码成地址,生成调用指令,这就是 静态绑定(static binding)。那么例2呢?例2的调用的是 func() 函数,而这函数实际调用的地址只能到程序运行时才能确定,这就是所谓的 动态绑定(dynamic binding)。动态绑定将函数调用从编译期推迟到了运行时。

在 Objective-C 中,向对象传递消息,就会使用这种动态绑定机制来决定需要调用的方法,这种动态特性使得 Objective-C 成为一门真正动态的语言。

objec_msgSend 函数

Objective-C 的方法调用通常都是下面这种形式

1
id returnValue = [someObject messageName:parameter];

这种方法调用其实就是消息传递,编译器看到这条消息会转换成一条标准的 C 语言函数调用

1
2
3
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);

用消息传递的话来解释就是:向 someObject 对象发送了一个名叫 messageName 的消息,这个消息携带了一个叫 parameter 的参数。这里用到了一个 objc_msgSend 函数,其函数原型如下

1
void objc_msgSend(id self, SEL cmd, ...);

这是一个可变参数的函数,第一个参数代表消息接收者,第二个代表 SEL 类型,后面的参数就是消息传递中使用的参数。

那么什么是 SEL 呢?SEL 就是代码在编译时,编译器根据方法签名来生成的一个唯一 ID。此 ID 可以用以区分不同的方法,只要 ID 一致,即看成同一个方法,ID 不同,即为不同的方法。

当进行消息传递,对象在响应消息时,是通过 SEL 在 methodlist 中查找函数指针 IMP,找到后直接通过指针调用函数,这其实就是前文介绍的 动态绑定。若是找到对应函数就跳转到实现代码,若找不到,就沿着继承链往上查找,直到找到相应的实现代码为止。若最终还是没找到实现代码,说明当前对象无法响应此消息,接下来就会执行 消息转发 操作,以试图找到一个能响应此消息的对象。

1
2
3
4
// 获取 SEL 
SEL sel = @selector(methodName);
// 获取 IMP
IMP imp = methodForSelector(sel);

消息转发

消息转发并不神奇,我们其实早已接触过,只是不知道而已

1
2
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'

这段异常代码就是由 NSObject 的 doesNotRecognizeSelector: 方法所抛出的,异常表明:消息的接收者类型为 __NSCFNumber,无法响应 lowercaseString 消息,从而转发给 NSObject 处理。

消息转发分为三大阶段

  • 第一阶段先征询消息接收者所属的类,看其是否能动态添加方法,以处理当前这个无法响应的 selector,这叫做 动态方法解析(dynamic method resolution)。如果运行期系统(runtime system) 第一阶段执行结束,接收者就无法再以动态新增方法的手段来响应消息,进入第二阶段。
  • 第二阶段看看有没有其他对象(备援接收者,replacement receiver)能处理此消息。如果有,运行期系统会把消息转发给那个对象,转发过程结束;如果没有,则启动完整的消息转发机制。
  • 第三阶段 完整的消息转发机制。运行期系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的消息。

动态方法解析

对象在收到无法响应的消息后,会调用其所属类的下列方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 如果尚未实现的方法是实例方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增实例方法用以处理selector
*/
+ (BOOL)resolveInstanceMethod:(SEL)selector;
/**
* 如果尚未实现的方法是类方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增类方法用以处理selector
*/
+ (BOOL)resolveClassMethod:(SEL)selector;

方法返回布尔类型,表示是否能新增一个方法来处理 selector,此方案通常用来实现 @dynamic 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/************** 使用 resolveInstanceMethod 实现 @dynamic 属性 **************/
id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id self, SEL _cmd, id value);
+ (BOOL)resolveInstanceMethod:(SEL)selector
{
NSString *selectorString = NSStringFromSelector(selector);
if (/* selector is from a @dynamic property */)
{
if ([selectorString hasPrefix:@"set"])
{
// 添加 setter 方法
class_addMethod(self, selector, (IMP)autoDictionarySetter, "v@:@");
}
else
{
// 添加 getter 方法
class_addMethod(self, selector, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
return [super resolveInstanceMethod:selector];
}

备援接收者

如果无法 动态解析方法,运行期系统就会询问是否能将消息转给其他接收者来处理,对应的方法为

1
2
3
4
5
6
7
8
/**
* 此方法询问是否能将消息转给其他接收者来处理
*
* @param aSelector 未处理的方法
*
* @return 如果当前接收者能找到备援对象,就将其返回;否则返回nil;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;

在对象内部,可能还有其他对象,该对象可通过此方法将能够处理 selector 的相关内部对象返回,在外界看来,就好像是该对象自己处理的似得。

完整的消息转发机制

如果前面两步都无法处理消息,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节装在里面,在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)将会把消息指派给目标对象。对应的方法为

1
2
3
4
5
6
/**
* 消息派发系统通过此方法,将消息派发给目标对象
*
* @param anInvocation 之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation;

这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。

总结


观点仅代表自己,期待你的留言。
https://www.zybuluo.com/MicroCai/note/64270

NSObject的+load与+initialize方法的区别

先来看看NSObject Class Reference里对这两个方法说明:
+(void)initialize

The runtime sends initialize to each class in a program exactly one time just before the class, or any class that inherits from it, is sent its first message from within the program. (Thus the method may never be invoked if the class is not used.) The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.

+(void)load
The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
The order of initialization is as follows:

All initializers in any framework you link to.
All +load methods in your image.
All C++ static initializers and C/C++ attribute(constructor) functions in your image.
All initializers in frameworks that link to you.
In addition:

A class’s +load method is called after all of its superclasses’ +load methods.
A category +load method is called after the class’s own +load method.
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

Apple的文档很清楚地说明了initialize和load的区别在于:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。

它们的相同点在于:方法只会被调用一次。(其实这是相对runtime来说的,后边会做进一步解释)。

文档也明确阐述了方法调用的顺序:父类(Superclass)的方法优先于子类(Subclass)的方法,类中的方法优先于类别(Category)中的方法。

总结:

- +(void)load +(void)initialize
执行时机 在程序运行后立即执行 在类的方法第一次被调时执行
若自身未定义,是否沿用父类的方法?
类别中的定义 全都执行,但后于类中的方法 覆盖类中的方法,只执行一个

观点仅代表自己,期待你的留言。

iOS中UIViewController初始化过程及LoadView默认实现

Xib或者Storyboard方式初始化UIViewController

1、系统会通过 (instancetype)initWithCoder:(NSCoder *)aDecoder创建Controller对象实例。
2、当需要将UIController的View添加到父级View时则会通过loadViewIfRequired方法来先判断self.view对象是否为nil
如果为nil则会调用 (void)loadView进行view的初始化。然后会调用viewDidLoad方法

编码的方式初始化UIViewController

[[UIViewController alloc] init],系统会调用[[UIViewController alloc] initWithNibName:(NSString )nibNameOrNil bundle:(NSBundle )nibBundleOrNil]创建实例对象

系统默认loadView实现

通过initWithNibName保存的nibName来加载对应的nib资源并赋值于self.view, 如果nibName为空,则创建一个空的UIView实例赋值。

注意:在loadView方法自定义实现中由于view未进行初始化,如果使用self.view获取值,由会再次触发调用loadView方法造成loadView的递归调用。在ios9.3环境下通过self.view进行赋值则不存在此情况。

附一张UIViewController生命周期图:


观点仅代表自己,期待你的留言。

iOS-hitTest:withEvent与pointInside:withEvent

简介

对于触摸事件的响应,首先要找到能够响应该事件的对象,iOS是用hit-testing 来找到哪个视图被触摸了(hit-test view),也就是以keyWindow为起点,hit-test view为终点,逐级调用hitTest:withEvent。

hitTest:withEvent:方法的处理流程:

先调用pointInside:withEvent:判断触摸点是否在当前视图内
1.如果返回YES,那么该视图的所有子视图调用hitTest:withEvent,调用顺序由层级低到高(top->bottom)依次调用。

2.如果返回NO,那么hitTest:withEvent返回nil,该视图的所有子视图的分支全部被忽略

如果某视图的pointInside:withEvent:返回YES,并且他的所有子视图hitTest:withEvent:都返回nil,或者该视图没有子视图,那么该视图的hitTest:withEvent:返回自己。
如果子视图的hitTest:withEvent:返回非空对象,那么当前视图的hitTest:withEvent:也返回这个对象,也就是沿原路回推,最终将hit-test view传递给keyWindow
以下视图的hitTest:withEvent:方法会返回nil,导致自身和其所有子视图不能被hit-testing发现,无法响应触摸事件:
1.隐藏(hidden=YES)的视图
2.禁止用户操作(userInteractionEnabled=NO)的视图
3.alpha<0.01的视图
4.视图超出父视图的区域


观点仅代表自己,期待你的留言。