本机架设一个RTMP直播流服务器

为了演示直播推流工程, 我们来自己快速架设一个简易的RTMP服务器,用OBS推流,并在播放器中播放,基于Nginx和rtmp模块。
步骤很简单:

  • 安装Homebrew包管理器,以便安装Nginx。
  • 配置服务器的rtmp端口和路径。
  • 用OBS来推流,安装VLC播放器来播放rtmp串流。

详细步骤如下:
1.①如果你安装过cocoapods,就应该会安装好Homebrew,那就跳过此步。如果不确定是否已经安装,可以在终端用man homebrew命令来确定,如果回馈一堆使用说明,那就是安装好了。 否则请用以下命令安装:

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

卸载Homebrew:

1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/uninstall)"

②下载Homebrew上的Nginx项目到本地:

1
brew tap homebrew/nginx

③安装rtmp模块:

brew install nginx-full --with-rtmp-module

④启动Nginx服务器:

nginx (终端)

⑤验证启动(在浏览器中打开以下网址)

http://localhost:8080

如果看到 Welcome to nginx字样,说明服务器启动成功了,成功了一半,oh,yeah!

注 第一个步骤内可能有权限不够,可以按照命令失败的提示修正一下。或者按照提示google 下都有答案

2.①显示nginx的的使用说明

1
brew info nginx-full

②修改 nginx.conf, 一般在

1
/usr/local/etc/nginx/nginx.conf

③配置rtmp:
用任意的文本编辑器打开

1
/usr/local/etc/nginx/nginx.conf

在文件的末尾加上以下并保存:

1
2
3
4
5
6
7
8
rtmp {
server {
listen 1935;
application rtmplive {
live on;
}
}
}

这是一个最小化配置的,在端口1935监听的rtmp服务,名叫rtmplive,启用了直播(live on)。
详见
④重启服务器使配置生效:

1
/usr/local/Cellar/nginx-full/1.10.2/bin/nginx -s reload

这其中的1.10.2是当前的nginx版本. 可以用以下命令查看当前的版本:

1
nginx -v

3.①安装OBS Mac版本,在 设置 > 串流 > 自定义流媒体服务器 > URL 中填入

1
rtmp://192.168.166.172:1935/rtmplive/

确定后退出设置。设置直播场景后,点击“开始串流”,这时OBS状态栏应该可以看到连接状态和推流速率。
②百度一下VLC,安装Mac版本。
③菜单File > Open Network… > 输入:

1
rtmp://192.168.166.172:1935/rtmplive/

把这里的192.168.166.172替换成你自己电脑对外的IP,也就是其他电脑可以访问的IP。

不仅OBS可以用来推流, iPhone/安卓机也可以利用这个自建服务器来推流了。

测试环境搭建

转自这里

//2017-5-4 SRS 服务器搭建

下载

1
2
3
$ git clone https://github.com/ossrs/srs
$ cd srs/trunk
mac 下 和centos 下

编译

1
2
// 支持 hls,flv,rtmp转码,http-server,http-api
$ ./configure --with-ssl --with-http-api --with-hls --with-http-server --with-nginx --with-ffmpeg --with-transcode && make

配置:

我用的是2.0 版本
参考https://github.com/ossrs/srs/wiki/v2_CN_Home
在查看
启动 :
先编辑配置文件,按照每个的单独的配置文件集成到conf/srs.conf
从hls.conf 和http.live.flv.conf 上获取配置数据.

1
2
3
4
5
检查配置文件
./objs/srs -t -c conf/xx.conf
启动
./etc/init.d/srs start

查看当前服务端stream列表

1
http:xxx:9090/api/v1/streams/
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
{
"code": 0,
"server": 12323,
"streams": [
{
"id": 12325,
"name": "h5",
"vhost": 12324,
"app": "live",
"live_ms": 1482371950909,
"clients": 1,
"send_bytes": 12576,
"recv_bytes": 29070803,
"kbps": {
"recv_30s": 0,
"send_30s": 0
},
"publish": {
"active": true, // 表示是否在线
"cid": 166
},
"video": {
"codec": "H264",
"profile": "High(422)",
"level": "Other"
},
"audio": null
}
]
}

iOS-Block的详解

iOS-Block的详解
From: http://www.jianshu.com/p/007632bbb01d

学习Block的感悟

Block

一. iOS代码块Block

1.1 概述

代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,Block是一种特殊的数据类型,其可以正常定义变量、作为参数、作为返回值,特殊地,Block还可以保存一段代码,在需要的时候调用,目前Block已经广泛应用于iOS开发中,常用于GCD、动画、排序及各类回调

注: Block的声明与赋值只是保存了一段代码段,必须调用才能执行内部代码

1.2 Block变量的声明、赋值与调用
#1.2.1 Block变量的声明
Block变量的声明格式为: 返回值类型(^Block名字)(参数列表);

// 声明一个无返回值,参数为两个字符串对象,叫做aBlock的Block
void(^aBlock)(NSString *x, NSString *y);

// 形参变量名称可以省略,只留有变量类型即可
void(^aBlock)(NSString *, NSString *);
#1.2.2 Block变量的赋值
Block变量的赋值格式为: Block变量 = ^(参数列表){函数体};

aBlock = ^(NSString *x, NSString *y){
    NSLog(@"%@ love %@", x, y);
};

注: Block变量的赋值格式可以是: Block变量 = ^返回值类型(参数列表){函数体};,不过通常情况下都将返回值类型省略,
因为编译器可以从存储代码块的变量中确定返回值的类型
#1.2.3 声明Block变量的同时进行赋值
int(^myBlock)(int) = ^(int num){
    return num * 7;
};

// 如果没有参数列表,在赋值时参数列表可以省略
void(^aVoidBlock)() = ^{
    NSLog(@"I am a aVoidBlock");
};
#1.2.4 Block变量的调用
// 调用后控制台输出"Li Lei love Han Meimei"
aBlock(@"Li Lei",@"Han Meimei");

// 调用后控制台输出"result = 63"
NSLog(@"result = %d", myBlock(9));

// 调用后控制台输出"I am a aVoidBlock"
aVoidBlock();

二. 使用typedef定义Block类型

在实际使用Block的过程中,我们可能需要重复地声明多个相同返回值相同参数列表的Block变量,如果总是重复地编写一长串代码来声明变量会非常繁琐,所以我们可以使用typedef来定义Block类型

// 定义一种无返回值无参数列表的Block类型
typedef void(^SayHello)();

// 我们可以像OC中声明变量一样使用Block类型SayHello来声明变量
SayHello hello = ^(){
    NSLog(@"hello");
};

// 调用后控制台输出"hello"
hello();

三. Block作为函数参数

3.1 Block作为C函数参数
// 1.定义一个形参为Block的C函数
void useBlockForC(int(^aBlock)(int, int))
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 2.声明并赋值定义一个Block变量
int(^addBlock)(int, int) = ^(int x, int y){
    return x+y;
};

// 3.以Block作为函数参数,把Block像对象一样传递
useBlockForC(addBlock);

// 将第2点和第3点合并一起,以内联定义的Block作为函数参数
useBlockForC(^(int x, int y) {
    return x+y;
});
3.2 Block作为OC函数参数
// 1.定义一个形参为Block的OC函数
- (void)useBlockForOC:(int(^)(int, int))aBlock
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 2.声明并赋值定义一个Block变量
int(^addBlock)(int, int) = ^(int x, int y){
    return x+y;
};

// 3.以Block作为函数参数,把Block像对象一样传递
[self useBlockForOC:addBlock];

// 将第2点和第3点合并一起,以内联定义的Block作为函数参数
[self useBlockForOC:^(int x, int y){
    return x+y;
}];
3.3 使用typedef简化Block
// 1.使用typedef定义Block类型
typedef int(^MyBlock)(int, int);

// 2.定义一个形参为Block的OC函数
- (void)useBlockForOC:(MyBlock)aBlock
{
    NSLog(@"result = %d", aBlock(300,200));
}

// 3.声明并赋值定义一个Block变量
MyBlock addBlock = ^(int x, int y){
    return x+y;
};

// 4.以Block作为函数参数,把Block像对象一样传递
[self useBlockForOC:addBlock];

// 将第3点和第4点合并一起,以内联定义的Block作为函数参数
[self useBlockForOC:^(int x, int y){
    return x+y;
}];

四 Block内访问局部变量

4.1 在Block中可以访问局部变量
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
4.2 在声明Block之后、调用Block之前对局部变量进行修改,在调用Block时局部变量值是修改之前的旧值
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 100"
myBlock();
4.3 在Block中不可以直接修改局部变量
// 声明局部变量global
int global = 100;

void(^myBlock)() = ^{
    global ++; // 这句报错
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();

五. Block内访问__block修饰的局部变量

5.1 在局部变量前使用下划线下划线block修饰,在声明Block之后、调用Block之前对局部变量进行修改,在调用Block时局部变量值是修改之后的新值
// 声明局部变量global
__block int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
5.2 在局部变量前使用下划线下划线block修饰,在Block中可以直接修改局部变量
// 声明局部变量global
__block int global = 100;

void(^myBlock)() = ^{
    global ++; // 这句正确
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

六. Block内访问全局变量

6.1 在Block中可以访问全局变量
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
6.2 在声明Block之后、调用Block之前对全局变量进行修改,在调用Block时全局变量值是修改之后的新值
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
6.3 在Block中可以直接修改全局变量
// 声明全局变量global
int global = 100;

void(^myBlock)() = ^{
    global ++;
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

七. Block内访问静态变量

7.1 在Block中可以访问静态变量
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 100"
myBlock();
7.2 在声明Block之后、调用Block之前对静态变量进行修改,在调用Block时静态变量值是修改之后的新值
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    NSLog(@"global = %d", global);
};
global = 101;
// 调用后控制台输出"global = 101"
myBlock();
7.3 在Block中可以直接修改静态变量
// 声明静态变量global
static int global = 100;

void(^myBlock)() = ^{
    global ++;
    NSLog(@"global = %d", global);
};
// 调用后控制台输出"global = 101"
myBlock();

8. Block在MRC及ARC下的内存管理

8.1 Block在MRC下的内存管理
默认情况下,Block的内存存储在栈中,不需要开发人员对其进行内存管理

// 放Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

在Block的内存存储在栈中时,如果在Block中引用了外面的对象,不会对所引用的对象进行任何操作

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

[p release]; // Person对象在这里可以正常被释放

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,这时需要开发人员对其进行release操作来管理内存

void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行一次retain操作,即使在Block自身调用了release操作之后,Block也不会对所引用的对象进行一次release操作,这时会造成内存泄漏

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

[p release]; // Person对象在这里无法正常被释放,因为其在Block中被进行了一次retain操作

如果对Block进行一次copy操作,那么Block的内存会被移动到堆中,在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行一次retain操作,为了不对所引用的对象进行一次retain操作,可以在对象的前面使用下划线下划线block来修饰

__block Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

Block_copy(myBlock);

// do something ...

Block_release(myBlock);

[p release]; // Person对象在这里可以正常被释放

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用

  • 情况一

    @interface Person : NSObject

    @property (nonatomic, copy) void(^myBlock)();

    @end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);
    [super dealloc];
}

@end


Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

[p release]; // 因为myBlock作为Person的属性,采用copy修饰符修饰(这样才能保证Block在堆里面,以免Block在栈中被系统释放),所以Block会对Person对象进行一次retain操作,导致循环引用无法释放
  • 情况二

    @interface Person : NSObject

    @property (nonatomic, copy) void(^myBlock)();

    • (void)resetBlock;

    @end

@implementation Person

- (void)resetBlock
{
    self.myBlock = ^{
        NSLog(@"------%@", self);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);

    [super dealloc];
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];
[p release]; // Person对象在这里无法正常释放,虽然表面看起来一个alloc对应一个release符合内存管理规则,但是实际在resetBlock方法实现中,Block内部对self进行了一次retain操作,导致循环引用无法释放

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,解决循环引用的办法是在对象的前面使用下划线下划线block来修饰,以避免Block对对象进行retain操作

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);
    [super dealloc];
}

@end


__block Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

[p release]; // Person对象在这里可以正常被释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    // 这里为了通用一点,可以使用__block typeof(self) p = self;
    __block Person *p = self;
    self.myBlock = ^{
        NSLog(@"------%@", p);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");

    Block_release(_myBlock);

    [super dealloc];
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];
[p release]; // Person对象在这里可以正常被释放

九. Block在ARC下的内存管理

9.1 在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,程序员只需要避免循环引用即可
// 放Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

在Block的内存存储在堆中时,如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,所以不会造成内存泄漏

Person *p = [[Person alloc] init];

void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();

// Person对象在这里可以正常被释放
9.2
如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];

p.myBlock = ^{
    NSLog(@"------%@", p);
};
p.myBlock();

// 因为myBlock作为Person的属性,采用copy修饰符修饰(这样才能保证Block在堆里面,以免Block在栈中被系统释放),所以Block会对Person对象进行一次强引用,导致循环引用无法释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    self.myBlock = ^{
        NSLog(@"------%@", self);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];

// Person对象在这里无法正常释放,在resetBlock方法实现中,Block内部对self进行了一次强引用,导致循环引用

如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,解决循环引用的办法是使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样避免了Block对对象进行强引用

情况一

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

@end


@implementation Person

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
__weak typeof(p) weakP = p;

p.myBlock = ^{
    NSLog(@"------%@", weakP);
};
p.myBlock();

// Person对象在这里可以正常被释放

情况二

@interface Person : NSObject

@property (nonatomic, copy) void(^myBlock)();

- (void)resetBlock;

@end


@implementation Person

- (void)resetBlock
{
    // 这里为了通用一点,可以使用__weak typeof(self) weakP = self;
    __weak Person *weakP = self;
    self.myBlock = ^{
        NSLog(@"------%@", weakP);
    };
}

- (void)dealloc
{
    NSLog(@"Person dealloc");
}

@end


Person *p = [[Person alloc] init];
[p resetBlock];

// Person对象在这里可以正常被释放

十. Block在ARC下的内存管理的官方案例

在MRC中,我们从当前控制器采用模态视图方式present进入MyViewController控制器,在Block中会对myViewController进行一次retain操作,造成循环引用

MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
   [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];

在MRC中解决循环引用的办法即在变量前使用下划线下划线block修饰,禁止Block对所引用的对象进行retain操作

__block MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{
   [myController release];
}];

但是上述方法在ARC下行不通,因为下划线下划线block在ARC中并不能禁止Block对所引用的对象进行强引用,解决办法可以是在Block中将myController置空(为了可以修改myController,还是需要使用下划线下划线block对变量进行修饰)

__block MyViewController *myController = [[MyViewController alloc] init];
// ...
myController.completionHandler =  ^(NSInteger result) {
    [myController dismissViewControllerAnimated:YES completion:nil];
    myController = nil;
};
[self presentViewController:myController animated:YES completion:^{}];

上述方法确实可以解决循环引用,但是在ARC中还有更优雅的解决办法,新创建一个弱指针来指向该对象,并将该弱指针放在Block中使用,这样Block便不会造成循环引用

MyViewController *myController = [[MyViewController alloc] init];
// ...
__weak MyViewController *weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    [weakMyController dismissViewControllerAnimated:YES completion:nil];
};
[self presentViewController:myController animated:YES completion:^{}];

虽然解决了循环引用,但是也容易涉及到另一个问题,因为Block是通过弱引用指向了myController对象,那么有可能在调用Block之前myController对象便已经被释放了,所以我们需要在Block内部再定义一个强指针来指向myController对象

MyViewController *myController = [[MyViewController alloc] init];
// ...
__weak MyViewController *weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController)
    {
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
    }
    else
    {
        // Probably nothing...
    }
};
[self presentViewController:myController animated:YES completion:^{}];

这里需要补充一下,在Block内部定义的变量,会在作用域结束时自动释放,Block对其并没有强引用关系,且在ARC中只需要避免循环引用即可,如果只是Block单方面地对外部变量进行强引用,并不会造成内存泄漏

注: 关于下划线下划线block关键字在MRC和ARC下的不同

__block在MRC下有两个作用
1. 允许在Block中访问和修改局部变量 
2. 禁止Block对所引用的对象进行隐式retain操作

__block在ARC下只有一个作用
1. 允许在Block中访问和修改局部变量

小笨狼

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

一. 概况

这里引用一下前文中对 PhotoKit 基本构成的介绍:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHAssetCollection: PHCollection 的子类,表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHFetchResult: 表示一系列的资源结果集合,也可以是相册的集合,从?PHCollection 的类方法中获得
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

这里还有一个额外的概念?PHCollectionList,表示一组?PHCollection,它本身也是一个?PHCollection,因此?PHCollection 作为一个集合,可以包含其他集合,这使到 PhotoKit 的组成比 ALAssetLibrary 要复杂一些。另外与 ALAssetLibrary 相似,一个 PHAsset 可以同时属于多个不同的 PHAssetCollection,最常见的例子就是刚刚拍摄的照片,至少同时属于“最近添加”、“相机胶卷”以及“照片 - 精选”这三个 PHAssetCollection。关于这几个概念的关系如下图:

二. ?PhotoKit 的机制

  1. 获取资源

在 ALAssetLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据,并且数据是从 ALAssetLibrary(照片库) - ALAssetGroup(相册)- ALAsset(资源)这一路径逐层获取,即使有直接从 ALAssetLibrary 这一层获取 ALAsset 的接口,本质上也是枚举 ALAssetLibrary 所得,并不是直接获取,这样的好处很明显,就是非常符合实际应用中资源的显示路径:照片库 - 相册 - 图片或视频,但由于采用枚举的方式获取资源,效率低而且不灵活。

而在 PhotoKit 中,则是采用“获取”的方式拉取资源,这些获取的手段,都是一系列形如 class func fetchXXX(…, options: PHFetchOptions) -> PHFetchResult 的类方法,具体使用哪个类方法,则视乎需要获取的是相册、时刻还是资源,这类方法中的 option 充当了过滤器的作用,可以过滤相册的类型,日期,名称等,从而直接获取对应的资源而不需要枚举。例如在前文中列举个的几个小例子:

1
2
3
4
5
6
7
8
9
10
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];

如前面提到过的那样,从?PHAssetCollection 获取中获取到的可以是相册也可以是资源,但无论是哪种内容,都统一使用?PHFetchResult 对象封装起来,因此虽然 PHAssetCollection 获取到的结果可能是多样的,但通过?PHFetchResult 就可以使用统一的方法去处理这些内容(即遍历 PHFetchResult)。例如扩展上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 这时 smartAlbums 中保存的应该是各个智能相册对应的 PHAssetCollection
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个相册(PHAssetCollection)
PHCollection *collection = fetchResult[i];
if ([collection isKindOfClass:[PHAssetCollection class]]) {
PHAssetCollection *assetCollection = (PHAssetCollection *)collection;
// 从每一个智能相册中获取到的 PHFetchResult 中包含的才是真正的资源(PHAsset)
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:assetCollection options:fetchOptions];
else {
NSAssert(NO, @"Fetch collection not PHCollection: %@", collection);
}
}
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 这时 assetsFetchResults 中包含的,应该就是各个资源(PHAsset)
for (NSInteger i = 0; i < fetchResult.count; i++) {
// 获取一个资源(PHAsset)
PHAsset *asset = fetchResult[i];
}

?2. 获取图像的方式与坑点

经过了上面的步骤,已经可以了解到如何在 PhotoKit 中获取到代表资源的 PHAsset 了,但与 ALAssetLibrary 中从 ALAsset 中直接获取图像的方式不同,PhotoKit 无法直接从 PHAsset 的实例中获取图像,而是引入了一个管理器?PHImageManager 获取图像。PHImageManager 是通过请求的方式拉取图像,并可以控制请求得到的图像的尺寸、剪裁方式、质量,缓存以及请求本身的管理(发出请求、取消请求)等。而请求图像的方法是 ?PHImageManager 的一个实例方法:

1
2
3
4
5
- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset
targetSize:(CGSize)targetSize
contentMode:(PHImageContentMode)contentMode
options:(nullable PHImageRequestOptions *)options
resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler;

这个方法中的参数坑点不少,下面逐个参数列举一下其作用及坑点:

  • asset,图像对应的 PHAsset。
  • targetSize,需要获取的图像的尺寸,如果输入的尺寸大于资源原图的尺寸,则只返回原图。需要注意在 PHImageManager 中,所有的尺寸都是用 Pixel 作为单位(Note that all sizes are in pixels),因此这里想要获得正确大小的图像,需要把输入的尺寸转换为 Pixel。如果需要返回原图尺寸,可以传入 PhotoKit 中预先定义好的常量?PHImageManagerMaximumSize,表示返回可选范围内的最大的尺寸,即原图尺寸。
  • contentMode,图像的剪裁方式,与?UIView 的 contentMode 参数相似,控制照片应该以按比例缩放还是按比例填充的方式放到最终展示的容器内。注意如果 targetSize 传入?PHImageManagerMaximumSize,则 contentMode 无论传入什么值都会被视为?PHImageContentModeDefault。
  • options,一个?PHImageRequestOptions 的实例,可以控制的内容相当丰富,包括图像的质量、版本,也会有参数控制图像的剪裁,下面再展开说明。
  • resultHandler,请求结束后被调用的 block,返回一个包含资源对于图像的 UIImage 和包含图像信息的一个 NSDictionary,在整个请求的周期中,这个 block 可能会被多次调用,关于这点连同 options 参数在下面展开说明。

(1)PHImageRequestOptions 与 iCloud 照片库

PHImageRequestOptions 中包含了一系列控制请求图像的属性。

resizeMode 属性控制图像的剪裁,不知道为什么 PhotoKit 会在请求图像方法(requestImageForAsset)中已经有控制图像剪裁的参数后(contentMode),还在 options 中加入控制剪裁的属性,但如果两个地方所控制的剪裁结果有所冲突,PhotoKit 会以 resizeMode 的结果为准。另外,resizeMode 也有控制图像质量的作用。如?resizeMode?设置为?PHImageRequestOptionsResizeModeExact 则返回图像必须和目标大小相匹配,并且图像质量也为高质量图像,而设置为 PHImageRequestOptionsResizeModeFast 则请求的效率更高,但返回的图像可能和目标大小不一样并且质量较低。

在 PhotoKit 中,对 iCloud 照片库有很好的支持,如果用户开启了 iCloud 照片库,并且选择了“优化 iPhone/iPad 储存空间”,或者选择了“下载并保留原件”但原件还没有加载好的时候,PhotoKit 也会预先拿到这些非本地图像的 PHAsset,但是由于本地并没有原图,所以如果产生了请求高清图的请求,PHotoKit 会尝试从 iCloud 下载图片,而这个行为最终的表现,会被?PHImageRequestOptions 中的值所影响。PHImageRequestOptions 中常常会用的几个属性如下:

networkAccessAllowed 参数控制是否允许网络请求,默认为 NO,如果不允许网络请求,那么就没有然后了,当然也拉取不到 iCloud 的图像原件。deliveryMode 则用于控制请求的图片质量。synchronous 控制是否为同步请求,默认为 NO,如果?synchronous 为 YES,即同步请求时,deliveryMode 会被视为 PHImageRequestOptionsDeliveryModeHighQualityFormat,即自动返回高质量的图片,因此不建议使用同步请求,否则如果界面需要等待返回的图像才能进一步作出反应,则反应时长会很长。

还有一个与 iCloud 密切相关的属性?progressHandler,当图像需要从 iCloud 下载时,这个 block 会被自动调用,block 中会返回图像下载的进度,图像的信息,出错信息。开发者可以利用这些信息反馈给用户当前图像的下载进度以及状况,但需要注意?progressHandler 不在主线程上执行,因此在其中需要操作 UI,则需要手工放到主线程执行。

上面有提到,requestImageForAsset 中的参数?resultHandler 可能会被多次调用,这种情况就是图像需要从 iCloud 中下载的情况。在?requestImageForAsset 返回的内容中,一开始的那一次请求中会返回一个小尺寸的图像版本,当高清图像还在下载时,开发者可以首先给用户展示这个低清的图像版本,然后 block 在多次调用后,最终会返回高清的原图。至于当前返回的图像是哪个版本的图像,可以通过 block 返回的 NSDictionary info 中获知,PHImageResultIsDegradedKey 表示当前返回的 UIImage 是低清图。如果需要判断是否已经获得高清图,可以这样判断:

1
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];

另外,当我们使用?requestImageForAsset 发出对图像的请求时,如果在同一个 PHImageManager 中同时对同一个资源发出图像请求,请求的进度是可以共享的,因此我们可以利用这个特性,把 PHImageManager 以单例的形式使用,这样在切换界面时也不用担心无法传递图像的下载进度。例如,在图像的列表页面触发了下载图像,当我们离开列表页面进入预览大图界面时,并不用担心会重新图像会重新下载,只要没有手工取消图像下载,进入预览大图界面下载图像会自动继续从上次的进度下载图像。

如果希望取消下载图像,则可以使用?PHImageManager 的 ?cancelImageRequest 方法,它传入的是请求图像的请求 ID,这个 ID 可以从?requestImageForAsset 的返回值中获得,也可以从前面提到的包含图像信息的?NSDictionary info 中获得,当然前提是这个这个接收取消请求的 PHImageManager 与刚刚发出请求的 PHImageManager 是同一个实例,如上面所述使用单例是最为简单有效的方式。

最后,还要介绍一个?PHImageRequestOptions 的属性 versions,这个属性是指获取的图像是否需要包含系统相册“编辑”功能处理过的信息(如滤镜,旋转等),这一点比 ALAssetLibrary 要灵活很多,ALAssetLibrary 中并不能灵活地控制获取的图像是否带有“编辑”处理过的效果,例如在 ALAsset 中获取原图的接口 fullResolutionImage 获取到的是不带“编辑”效果的图像,要想获取带有“编辑”效果的图像,只能自行处理获取这些滤镜效果,并手工叠加上去。在我们的 UI 框架 QMUI 中就有对获取原图作出这样的封装,整个过程也较为繁琐,而框架中处理 PhotoKit 的部分则灵活很多,这也体现了 PhotoKit 相比 ALAssetLibrary 的最主要特点——复杂但灵活。文章的第三部分也会详细列出如何处理这个问题。

(2)获取图像的优化

PHImageManager 提供了一个子类?PHImageCachingManager 用于处理图像的缓存,但是这个子类并不只是图像本身的缓存,而是更加实用——处理图像的整个加载过程的缓存。例如要在一个?collectionView 上展示图像列表这类大量的资源图像的缩略图时,可以利用 PHImageCachingManager?预先将一些图像加载到内存中,这对优化 collectionView 滚动时的表现很有帮助。然而,这只是官方说法,实际上由于加载图像的过程并不确定,每个业务加载图像的实际需求都可能不一样,因此?PHImageCachingManager 也采用比较松散的方法去控制这些缓存,其中的关键方法:

1
- (void)startCachingImagesForAssets:(NSArray<PHAsset *> *)assets targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options;

需要传入一组 PHAsset,以及 targetSize,contentMode,以及一个?PHImageRequestOptions,如上面所述,这些参数之间的有着互相影响的作用,因此实际上不同的场景对于每个参数要求都不一样,而这些参数的最佳取值也只能通过实际在场景中测试所得。因此,比起使用?PHImageCachingManager,我总结了一些更为简易可行的缓存方法:

  • 获取图片时尽量获取预览图,不要直接显示原件,建议获取与设备屏幕同样大小的图像即可,实际上系统相册预览大图时使用的也是预览图,这也是系统相册加载速度快的原因。
  • 获取图片使用异步请求,如上面所述,当请求为异步时返回图像的 block 会被多次调用,先返回低清图,再返回高清图,这样一来可以大大减少 UI 的等待时间。
  • 获取到高清图后可以缓存下来,简单地使用变量缓存即可,尽量在获取到高清图后避免再次发起请求获取图像。因为即使图像原件已经下载下来,重新请求高清图时因为图片的尺寸比较大,因此系统生成图像和剪裁图像也会花费一些时间。
  • 预先加载图像,如像预览大图这类情景中,用户同时只会看到一张大图,因此在观看某一张图片时,预先请求其邻近两张图片,对于加快 UI 的响应很有帮助

经过实际测试,如果请求的是缩略图(即尺寸小的图像),那么即使请求的图像很多,仍不会产生任何不流畅的表现,但如果请求的是高清大图,那么即使只是同时请求几张图都会产生不流畅的状况。如上面提到过的那样,这些的状况的出现很可能是请求大图时由图片元数据产生图像,以及剪裁图像的过程耗时较多。所以按实际表现来看,即使 PhotoKit 有自己的缓存策略,仍然很难避免这部分耗时。因此上面几点优化获取图像的策略重点也是放在减少图像大小,异步请求以及做缓存几个方面。

iOS 开发之照片框架详解之二 —— PhotoKit 详解(下)

三. 常用方法的封装

虽然 PhotoKit 的功能强大很多,但基于兼容 iOS 8.0 以下版本的考虑,暂时可能仍无法抛弃 ALAssetLibrary,这时候一个比较好的方案是基于 ALAssetLibrary 和 PhotoKit 封装出一系列模拟系统 Asset 类的自定义类,然后在其中封装好兼容 ALAssetLibrary 和 PhotoKit 的方法。

这里列举了四种常用的封装好的方法:原图,缩略图,预览图,方向,下面直接上代码,代码中有相关注释解释其中的要点。其中下面的代码中常常出现的 [[QMUIAssetsManager sharedInstance] phCachingImageManager] 是 QMUI 框架中封装的类以及单例方法,表示产生一个 PHCachingImageManager 的单例,这样做的好处是 PHCachingImageManager 需要占用较多的资源,因此使用单例可以避免无谓的资源消耗,另外请求图像等方法需要基于用一个 PHCachingImageManager 实例才能进行进度续传,管理请求等操作。

  1. 原图

由于原图的尺寸通常会比较大,因此建议使用异步拉取,但这里仍同时列举同步拉取的方法。这里需要留意如前文中所述,ALAssetRepresentation 中获取原图的接口 fullResolutionImage 所得到的图像并没有带上系统相册“编辑”(选中,滤镜等)的效果,需要额外获取这些效果并手工叠加到图像上。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Asset 的原图(包含系统相册“编辑”功能处理后的效果)
- (UIImage *)originImage;
/**
* 异步请求 Asset 的原图,包含了系统照片“编辑”功能处理后的效果(剪裁,旋转和滤镜等),可能会有网络请求
*
* @param completion 完成请求后调用的 block,参数中包含了请求的原图以及图片信息,在 iOS 8.0 或以上版本中,
* 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
* 获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil
* @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
* @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

.m 文件

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
- (UIImage *)originImage {
if (_originImage) {
return _originImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:PHImageManagerMaximumSize
contentMode:PHImageContentModeDefault
options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullResolutionImageRef = [_alAssetRepresentation fullResolutionImage];
// 通过 fullResolutionImage 获取到的的高清图实际上并不带上在照片应用中使用“编辑”处理的效果,需要额外在 AlAssetRepresentation 中获取这些信息
NSString *adjustment = [[_alAssetRepresentation metadata] objectForKey:@"AdjustmentXMP"];
if (adjustment) {
// 如果有在照片应用中使用“编辑”效果,则需要获取这些编辑后的滤镜,手工叠加到原图中
NSData *xmpData = [adjustment dataUsingEncoding:NSUTF8StringEncoding];
CIImage *tempImage = [CIImage imageWithCGImage:fullResolutionImageRef];
NSError *error;
NSArray *filterArray = [CIFilter filterArrayFromSerializedXMP:xmpData
inputImageExtent:tempImage.extent
error:&error];
CIContext *context = [CIContext contextWithOptions:nil];
if (filterArray && !error) {
for (CIFilter *filter in filterArray) {
[filter setValue:tempImage forKey:kCIInputImageKey];
tempImage = [filter outputImage];
}
fullResolutionImageRef = [context createCGImage:tempImage fromRect:[tempImage extent]];
}
}
// 生成最终返回的 UIImage,同时把图片的 orientation 也补充上去
resultImage = [UIImage imageWithCGImage:fullResolutionImageRef scale:[_alAssetRepresentation scale] orientation:(UIImageOrientation)[_alAssetRepresentation orientation]];
}
_originImage = resultImage;
return resultImage;
}
- (NSInteger)requestOriginImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_originImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_originImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _originImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_originImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self originImage], nil);
}
return 0;
}
}
  1. 缩略图

相对于在拉取原图时 ALAssetLibrary 的部分需要手工叠加系统相册的“编辑”效果,拉取缩略图则简单一些,因为系统接口拉取到的缩略图已经带上“编辑”的效果了。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Asset 的缩略图
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
*
* @return Asset 的缩略图
*/
- (UIImage *)thumbnailWithSize:(CGSize)size;
/**
* 异步请求 Asset 的缩略图,不会产生网络请求
*
* @param size 指定返回的缩略图的大小,仅在 iOS 8.0 及以上的版本有效,其他版本则调用 ALAsset 的接口由系统返回一个合适当前平台的图片
* @param completion 完成请求后调用的 block,参数中包含了请求的缩略图以及图片信息,在 iOS 8.0 或以上版本中,这个 block 会被多次调用,
* 其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,获取到高清图后 QMUIAsset 会缓存起这张高清图,
* 这时 block 中的第二个参数(图片信息)返回的为 nil。
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion;

.m 文件

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
- (UIImage *)thumbnailWithSize:(CGSize)size {
if (_thumbnailImage) {
return _thumbnailImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *phImageRequestOptions = [[PHImageRequestOptions alloc] init];
phImageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale)
contentMode:PHImageContentModeAspectFill options:phImageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef thumbnailImageRef = [_alAsset thumbnail];
if (thumbnailImageRef) {
resultImage = [UIImage imageWithCGImage:thumbnailImageRef];
}
}
_thumbnailImage = resultImage;
return resultImage;
}
- (NSInteger)requestThumbnailImageWithSize:(CGSize)size completion:(void (^)(UIImage *, NSDictionary *))completion {
if (_usePhotoKit) {
if (_thumbnailImage) {
if (completion) {
completion(_thumbnailImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;
// 在 PHImageManager 中,targetSize 等 size 都是使用 px 作为单位,因此需要对targetSize 中对传入的 Size 进行处理,宽高各自乘以 ScreenScale,从而得到正确的图片
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(size.width * ScreenScale, size.height * ScreenScale) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _thumbnailImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_thumbnailImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self thumbnailWithSize:size], nil);
}
return 0;
}
}
  1. 预览图

与上面的方法类似,不再展开说明。

.h 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Asset 的预览图
*
* @warning 仿照 ALAssetsLibrary 的做法输出与当前设备屏幕大小相同尺寸的图片,如果图片原图小于当前设备屏幕的尺寸,则只输出原图大小的图片
* @return Asset 的全屏图
*/
- (UIImage *)previewImage;
/**
* 异步请求 Asset 的预览图,可能会有网络请求
*
* @param completion 完成请求后调用的 block,参数中包含了请求的预览图以及图片信息,在 iOS 8.0 或以上版本中,
* 这个 block 会被多次调用,其中第一次调用获取到的尺寸很小的低清图,然后不断调用,直接获取到高清图,
* 获取到高清图后 QMUIAsset 会缓存起这张高清图,这时 block 中的第二个参数(图片信息)返回的为 nil
* @param phProgressHandler 处理请求进度的 handler,不在主线程上执行,在 block 中修改 UI 时注意需要手工放到主线程处理。
*
* @wraning iOS 8.0 以下中并没有异步请求预览图的接口,因此实际上为同步请求,这时 block 中的第二个参数(图片信息)返回的为 nil
*
* @return 返回请求图片的请求 id
*/
- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler;

__.m 文件

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
- (UIImage *)previewImage {
if (_previewImage) {
return _previewImage;
}
__block UIImage *resultImage;
if (_usePhotoKit) {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.synchronous = YES;
[[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset
targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT)
contentMode:PHImageContentModeAspectFill
options:imageRequestOptions
resultHandler:^(UIImage *result, NSDictionary *info) {
resultImage = result;
}];
} else {
CGImageRef fullScreenImageRef = [_alAssetRepresentation fullScreenImage];
resultImage = [UIImage imageWithCGImage:fullScreenImageRef];
}
_previewImage = resultImage;
return resultImage;
}
- (NSInteger)requestPreviewImageWithCompletion:(void (^)(UIImage *, NSDictionary *))completion withProgressHandler:(PHAssetImageProgressHandler)phProgressHandler {
if (_usePhotoKit) {
if (_previewImage) {
// 如果已经有缓存的图片则直接拿缓存的图片
if (completion) {
completion(_previewImage, nil);
}
return 0;
} else {
PHImageRequestOptions *imageRequestOptions = [[PHImageRequestOptions alloc] init];
imageRequestOptions.networkAccessAllowed = YES; // 允许访问网络
imageRequestOptions.progressHandler = phProgressHandler;
return [[[QMUIAssetsManager sharedInstance] phCachingImageManager] requestImageForAsset:_phAsset targetSize:CGSizeMake(SCREEN_WIDTH, SCREEN_HEIGHT) contentMode:PHImageContentModeAspectFill options:imageRequestOptions resultHandler:^(UIImage *result, NSDictionary *info) {
// 排除取消,错误,低清图三种情况,即已经获取到了高清图时,把这张高清图缓存到 _previewImage 中
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined) {
_previewImage = result;
}
if (completion) {
completion(result, info);
}
}];
}
} else {
if (completion) {
completion([self previewImage], nil);
}
return 0;
}
}
  1. 方向(imageOrientation)

比较奇怪的是,无论在 PhotoKit 或者是 ALAssetLibrary 中,要想获取到准确的图像方向,只能通过某些 key 检索所得。

1
2
3
4
.h 文件
1
- (UIImageOrientation)imageOrientation;

.m 文件

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    - (UIImageOrientation)imageOrientation {
    UIImageOrientation orientation;
    if (_usePhotoKit) {
    if (!_phAssetInfo) {
    // PHAsset 的 UIImageOrientation 需要调用过 requestImageDataForAsset 才能获取
    [self requestPhAssetInfo];
    }
    // 从 PhAssetInfo 中获取 UIImageOrientation 对应的字段
    orientation = (UIImageOrientation)[_phAssetInfo[@"orientation"] integerValue];
    } else {
    orientation = (UIImageOrientation)[[_alAsset valueForProperty:@"ALAssetPropertyOrientation"] integerValue];
    }
    return orientation;
    }

转自http://kayosite.com/ios-development-and-detail-of-photo-framework-part-three.html

iOS 开发之照片框架详解

iOS 开发之照片框架详解

一. 概要

在 iOS 设备中,照片和视频是相当重要的一部分。最近刚好在制作一个自定义的 iOS 图片选择器,顺便整理一下 iOS 中对照片框架的使用方法。在 iOS 8 出现之前,开发者只能使用 AssetsLibrary 框架来访问设备的照片库,这是一个有点跟不上 iOS 应用发展步伐以及代码设计原则但确实强大的框架,考虑到 iOS7 仍占有不少的渗透率,因此 AssetsLibrary 也是本文重点介绍的部分。而在 iOS8 出现之后,苹果提供了一个名为 PhotoKit 的框架,一个可以让应用更好地与设备照片库对接的框架,文末也会介绍一下这个框架。

另外值得强调的是,在 iOS 中,照片库并不只是照片的集合,同时也包含了视频。在 AssetsLibrary 中两者都有相同类型的对象去描述,只是类型不同而已。文中为了方便,大部分时候会使用「资源」代表 iOS 中的「照片和视频」。

二. AssetsLibrary 组成介绍

  • AssetsLibrary 的组成比较符合照片库本身的组成,照片库中的完整照片库对象、相册、相片都能在 AssetsLibrary 中找到一一对应的组成,这使到 AssetsLibrary 的使用变得直观而方便。

  • AssetsLibrary: 代表整个设备中的资源库(照片库),通过 AssetsLibrary 可以获取和包括设备中的照片和视频

  • ALAssetsGroup: 映射照片库中的一个相册,通过 ALAssetsGroup 可以获取某个相册的信息,相册下的资源,同时也可以对某个相册添加资源。
  • ALAsset: 映射照片库中的一个照片或视频,通过 ALAsset 可以获取某个照片或视频的详细信息,或者保存照片和视频。
  • ALAssetRepresentation: ALAssetRepresentation 是对 ALAsset 的封装(但不是其子类),可以更方便地获取 ALAsset 中的资源信息,每个 ALAsset 都有至少有一个 ALAssetRepresentation 对象,可以通过 defaultRepresentation 获取。而例如使用系统相机应用拍摄的 RAW + JPEG 照片,则会有两个 ALAssetRepresentation,一个封装了照片的 RAW 信息,另一个则封装了照片的 JPEG 信息。

    三. AssetsLibrary 的基本使用

    AssetsLibrary 的功能很多,基本可以分为对资源的获取/保存两个部分,保存的部分相对简单,API 也比较少,因此这里不作详细介绍。获取资源的 API 则比较丰富了,一个常见的使用大量 AssetsLibrary API 的例子就是图片选择器(ALAsset Picker)。要制作一个图片选择器,思路应该是获取照片库-列出所有相册-展示相册中的所有图片-预览图片大图。
1
2
3
4
5
6
7
8
9
10
NSString *tipTextWhenNoPhotosAuthorization; // 提示语
// 获取当前应用对照片的访问授权状态
ALAuthorizationStatus authorizationStatus = [ALAssetsLibrary authorizationStatus];
// 如果没有获取访问授权,或者访问授权状态已经被明确禁止,则显示提示语,引导用户开启授权
if (authorizationStatus == ALAuthorizationStatusRestricted || authorizationStatus == ALAuthorizationStatusDenied) {
NSDictionary *mainInfoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *appName = [mainInfoDictionary objectForKey:@"CFBundleDisplayName"];
tipTextWhenNoPhotosAuthorization = [NSString stringWithFormat:@"请在设备的\"设置-隐私-照片\"选项中,允许%@访问你的手机相册", appName];
// 展示提示语
}

如果已经获取授权,则可以获取相册列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_assetsLibrary = [[ALAssetsLibrary alloc] init];
_albumsArray = [[NSMutableArray alloc] init];
[_assetsLibrary enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
if (group) {
[group setAssetsFilter:[ALAssetsFilter allPhotos]];
if (group.numberOfAssets > 0) {
// 把相册储存到数组中,方便后面展示相册时使用
[_albumsArray addObject:group];
}
} else {
if ([_albumsArray count] > 0) {
// 把所有的相册储存完毕,可以展示相册列表
} else {
// 没有任何有资源的相册,输出提示
}
}
} failureBlock:^(NSError *error) {
NSLog(@"Asset group not found!\n");
}];

上面的代码中,遍历出所有的相册列表,并把相册中资源数不为空的相册 ALAssetGroup 对象的引用储存到一个数组中。这里需要强调几点:

  • iOS 中允许相册为空,即相册中没有任何资源,如果不希望获取空相册,则需要像上面的代码中那样手动过滤
  • ALAssetsGroup 有一个 setAssetsFilter 的方法,可以传入一个过滤器,控制只获取相册中的照片或只获取视频。一旦设置过滤,ALAssetsGroup 中资源列表和资源数量的获取也会被自动更新。
  • 整个 AssetsLibrary 中对相册、资源的获取和保存都是使用异步处理(Asynchronous),这是考虑到资源文件体积相当比较大(还可能很大)。例如上面的遍历相册操作,相册的结果使用 block 输出,如果相册遍历完毕,则最后一次输出的 block 中的 group 参数值为 nil。而 stop 参数则是用于手工停止遍历,只要把 *stop 置 YES,则会停止下一次的遍历。关于这一点常常会引起误会,所以需要注意。

现在,已经可以获取相册了,接下来是获取相册中的资源:

1
2
3
4
5
6
7
8
_imagesAssetArray = [[NSMutableArray alloc] init];
[assetsGroup enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// resultnil,即遍历相片或视频完毕,可以展示资源列表
}
}];

跟遍历相册的过程类似,遍历相片也是使用一系列的异步方法,其中上面的方法所输出的 block 中,除了 result 参数表示资源信息,stop 用于手工停止遍历外,还提供了一个 index 参数,这个参数表示资源的索引。一般来说,展示资源列表都会使用缩略图(result.thumbnail),因此即使资源很多,遍历资源的速度也会相当快。但如果确实需要加载资源的高清图或者其他耗时的处理,则可以利用上面的 index 参数和 stop 参数做一个分段拉取资源。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NSUInteger _currentIndex; // 当前 index,每次拉取资源时从这个值开始
_targetIndex = 50;
_currentIndex = 0;
- (void)loadAssetWithAssetsGroup:(assetsGroup *)assetsGroup {
[assetsGroup enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:_currentIndex] options:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) {
_currentIndex = index;
if (index > _targetIndex) {
// 拉取资源的索引如果比目标值大,则停止拉取
*stop = YES;
} else {
if (result) {
[_imagesAssetArray addObject:result];
} else {
// result 为 nil,即遍历相片或视频完毕
}
}
}];
}
// 之前拉取的数据已经显示完毕,需要展示新数据,重新调用 loadAssetWithAssetsGroup 方法,并根据需要更新 _targetIndex 的值

最后一步是获取图片详细信息,例如:

1
2
3
4
// 获取资源图片的详细资源信息,其中 imageAsset 是某个资源的 ALAsset 对象
ALAssetRepresentation *representation = [imageAsset defaultRepresentation];
// 获取资源图片的 fullScreenImage
UIImage *contentImage = [UIImage imageWithCGImage:[representation fullScreenImage]];

对于一个 ALAssetRepresentation,里面包含了图片的多个版本。最常用的是 fullResolutionImage 和 fullScreenImage。fullResolutionImage 是图片的原图,通过 fullResolutionImage 获取的图片没有任何处理,包括通过系统相册中“编辑”功能处理后的信息也没有被包含其中,因此需要展示“编辑”功能处理后的信息,使用 fullResolutionImage 就比较不方便,另外 fullResolutionImage 的拉取也会比较慢,在多张 fullResolutionImage 中切换时能明显感觉到图片的加载过程。因此这里建议获取图片的 fullScreenImage,它是图片的全屏图版本,这个版本包含了通过系统相册中“编辑”功能处理后的信息,同时也是一张缩略图,但图片的失真很少,缺点是图片的尺寸是一个适应屏幕大小的版本,因此展示图片时需要作出额外处理,但考虑到加载速度非常快的原因(在多张图片之间切换感受不到图片加载耗时),仍建议使用 fullScreenImage。

系统相册的处理过程大概也是如上,可以看出,在整个过程中并没有使用到图片的 fullResolutionImage,从相册列表展示到最终查看资源,都是使用缩略图,这也是 iOS 相册加载快的一个重要原因。

三. AssetsLibrary 的坑点

作为一套老框架,AssetsLibrary 不但有坑,而且还不少,除了上面提到的资源异步拉取时需要注意的事项,下面几点也是值得注意的:

  1. AssetsLibrary 实例需要强引用
    实例一个 AssetsLibrary 后,如上面所示,我们可以通过一系列枚举方法获取到需要的相册和资源,并把其储存到数组中,方便用于展示。但是,当我们把这些获取到的相册和资源储存到数组时,实际上只是在数组中储存了这些相册和资源在 AssetsLibrary 中的引用(指针),因而无论把相册和资源储存数组后如何利用这些数据,都首先需要确保 AssetsLibrary 没有被 ARC 释放,否则把数据从数组中取出来时,会发现对应的引用数据已经丢失(参见下图)。这一点较为容易被忽略,因此建议在使用 AssetsLibrary 的 viewController 中,把 AssetsLibrary 作为一个强持有的 property 或私有变量,避免在枚举出 AssetsLibrary 中所需要的数据后,AssetsLibrary 就被 ARC 释放了。
  1. AssetsLibrary 遵循写入优先原则
    写入优先也就是說,在利用 AssetsLibrary 读取资源的过程中,有任何其它的进程(不一定是同一个 App)在保存资源时,就会收到 ALAssetsLibraryChangedNotification,让用户自行中断读取操作。最常见的就是读取 fullResolutionImage 时,用进程在写入,由于读取 fullResolutionImage 耗时较长,很容易就会 exception。
  2. 开启 Photo Stream 容易导致 exception

本质上,这跟上面的 AssetsLibrary 遵循写入优先原则是同一个问题。如果用户开启了共享照片流(Photo Stream),共享照片流会以 mstreamd 的方式“偷偷”执行,当有人把相片写入 Camera Roll 时,它就会自动保存到 Photo Stream Album 中,如果用户刚好在读取,那就跟上面说的一样产生 exception 了。由于共享照片流是用户决定是否要开启的,所以开发者无法改变,但是可以通过下面的接口在需要保护的时刻关闭监听共享照片流产生的频繁通知信息。

1
[ALAssetsLibrary disableSharedPhotoStreamsSupport];

四. PhotoKit 简介

PhotoKit 是一套比 AssetsLibrary 更完整也更高效的库,对资源的处理跟 AssetsLibrary 也有很大的不同。
首先简单介绍几个概念:

  • PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源
  • PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值
  • PHFetchResult: 表示一系列的资源集合,也可以是相册的集合
  • PHAssetCollection: 表示一个相册或者一个时刻,或者是一个「智能相册(系统提供的特定的一系列相册,例如:最近删除,视频列表,收藏等等,如下图所示)
  • PHImageManager: 用于处理资源的加载,加载图片的过程带有缓存处理,可以通过传入一个 PHImageRequestOptions 控制资源的输出尺寸等规格
  • PHImageRequestOptions: 如上面所说,控制加载图片时的一系列参数

再列出几个代码片段,展示如何获取相册以及某个相册下资源的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 列出所有相册智能相册
PHFetchResult *smartAlbums = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
// 列出所有用户创建的相册
PHFetchResult *topLevelUserCollections = [PHCollectionList fetchTopLevelUserCollectionsWithOptions:nil];
// 获取所有资源的集合,并按资源的创建时间排序
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *assetsFetchResults = [PHAsset fetchAssetsWithOptions:options];
// 在资源的集合中获取第一个集合,并获取其中的图片
PHCachingImageManager *imageManager = [[PHCachingImageManager alloc] init];
PHAsset *asset = assetsFetchResults[0];
[imageManager requestImageForAsset:asset
targetSize:SomeSize
contentMode:PHImageContentModeAspectFill
options:nil
resultHandler:^(UIImage *result, NSDictionary *info) {
// 得到一张 UIImage,展示到界面上
}];

结合上面几个代码片段上看,PhotoKit 相对 AssetsLibrary 主要有三点重要的改进:

  • 从 AssetsLibrary 中获取数据,无论是相册,还是资源,本质上都是使用枚举的方式,遍历照片库取得相应的数据。而 PhotoKit 则是通过传入参数,直接获取相应的数据,因而效率会提高不少。
  • 在 AssetsLibrary 中,相册和资源是对应不同的对象(ALAssetGroup 和 ALAsset),因此获取相册和获取资源是两个完全没有关联的接口。而 PhotoKit 中则有 PHFetchResult 这个可以统一储存相册或资源的对象,因此处理相册和资源时也会比较方便。
  • PhotoKit 返回资源结果时,同时返回了资源的元数据,获取元数据在 AssetsLibrary 中是很难办到的一件事。同时通过 PHAsset,开发者还能直接获取资源是否被收藏(favorite)和隐藏(hidden),拍摄图片时是否开启了 HDR 或全景模式,甚至能通过一张连拍图片获取到连拍图片中的其他图片。这也是文章开头说的,PhotoKit 能更好地与设备照片库接入的一个重要因素。

转自http://kayosite.com/ios-development-and-detail-of-photo-framework.html

IOS 图标以及启动界面

From: https://my.oschina.net/hqc17/blog/735282

在苹果应用的开发后期,我们需要对应用设置显示的图标以及启动界面(启动界面也可以省略),Xcode是一个非常方便的工具,到了7.3版本,已经在这块非常成熟了,不再需要plist,只需xcasset文件就可以,也不需要再关心具体文件的命名了。

在开始之前,我们先要了解需要用到的分辨率,以下归纳的分辨率是universal的,不是iPhone-only或者iPad-only,如果只需要其中一种可自行调整。

29pt: 1x=2929,2x=5858,3x=87*87

40pt: 1x=4040,2x=8080,3x=120*120

50pt: 1x=5050,2x=100100

57pt: 1x=5757,2x=114114

60pt: 2x=120120,3x=180180

72pt: 1x=7272,2x=144144

76pt: 1x=7676,2x=152152

83.5pt: 2x=167*167

其中,iphone使用到的是29,57,58,80,87,114,120,180;

ipad使用到的是29,40,50,58,72,76,80,100,144,152,167。

在开始设置之前,请确保已经将图标生成对应的像素大小,便于调用。

portrait:

1x=320*480

2x=640*960

Retina 4=640*1136

Retina HD 4.7=750*1334

Retina HD 5.5=1242*2208

landscape:

1x=1024*768

2x=2048*1536

1x=1024*748(Without Status Bar)

2x=2048*1496(Without Status Bar)

Retina HD 5.5=2208*1242

其中,iphone使用到的landscape只有Retina HD 5.5,portrait全用;

ipad使用到的landscape除了Retina HD 5.5外都有,portrait只有1x和2x。

在开始设置之前,请确保已经将启动图像生成对应的像素大小,便于调用。

裁剪好图片资源后,我们接下来对xcasset文件操作,添加图片资源。

![][1]

在工程设置general里头,找到这个选项,如果是don’t use asset catalogs,就单击选取,migrate到xcasset里,而后会在左边的树状结构出现image.xcassets![][2]

单击后,在这空白处右键,选择App Icons & Launch Images,会看到New iOS App Icon(新建图标)以及New iOS Launch Image(新建启动图像)

我们先来看Icon的制作,很简单,单击AppIcon后,右侧出现如图所示:

将我们之前做好的图标,按照我上述总结的表,一个个对应拖进去就可以了,不需要其他操作。拖完后Xcode自动会重命名一份到工程里,不用担心删掉制作的Icon或者移动文件后会出问题。同时,在同级下的Contents.json会记录相应的文件信息。这样就做好图标了。

LaunchImage的制作类似,但是需要注意,Icon是必须都要添加,而启动界面只需要添加需要的即可。单击LaunchImage如图所示:

对于很多游戏,我们需要横屏显示,而iPad的设置非常方便,只需要将对应的Landscape图拖到对应的框内即可,而iPhone非常坑,只有一个Landscape,还是Retina HD 5.5的,这是6plus用的,其他的都需要通过portrait设置。我第一次设置的时候以为苹果会自动把高清的切成低分辨的,结果打开是黑屏。查阅后才明白原来是要设置portrait,而为了横屏显示,portrait图片需要调整,将本来正常显示的landscape图像转动90度后在拖入,这样读取的时候横过来显示,就是正常的。

CGRectContainsPoint 用法

CGRectContainsPoint 用法

UILongPressGestureRecognizer -> longPressed
长按手势重心 判断是否被view 包含

CGPoint point = [longPressed locationInView:self]; // 计算相对坐标(触摸手势的重心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
判断给定的点是否被一个CGRect包含,可以用CGRectContainsPoint函数
BOOL contains = CGRectContainsPoint(CGRect rect, CGPoint point);
判断一个CGRect是否包含再另一个CGRect里面,常用与测试给定的对象之间是否又重叠
BOOL contains = CGRectContainsRect(CGRect rect1, CGRect rect2);
判断两个结构体是否有交错.可以用CGRectIntersectsRect
BOOL contains = CGRectIntersectsRect(CGRect rect1, CGRect rect2);
float float_ = CGRectGetMaxX(CGRect rect);返回矩形右边缘的坐标
CGRectGetMaxY返回矩形顶部的坐标
CGRectGetMidX返回矩形中心X的坐标
CGRectGetMidY返回矩形中心Y的坐标
CGRectGetMinX返回矩形左边缘的坐标
CGRectGetMinY返回矩形底部的坐标
CGRectContainsPoint 看参数说明,一个点是否包含在矩形中,所以参数为一个点一个矩形

IOS 常用正则

在开发过程中,有时需要对用户输入的类型做判断,最常见是在注册页面即用户名和密码,直接上代码

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#pragma - mark 只能为中文
-(BOOL)onlyInputChineseCharacters:(NSString*)string{
NSString *inputString = @"[\u4e00-\u9fa5]+";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",inputString];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 只能为数字
- (BOOL)onlyInputTheNumber:(NSString*)string{
NSString *numString =@"[0-9]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",numString];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 只能为小写
- (BOOL)onlyInputLowercaseLetter:(NSString*)string{
NSString *regex =@"[a-z]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 只能为大写
- (BOOL)onlyInputACapital:(NSString*)string{
NSString *regex =@"[A-Z]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许大小写
- (BOOL)InputCapitalAndLowercaseLetter:(NSString*)string{
NSString *regex =@"[a-zA-Z]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许含大小写或数字(不限字数)
- (BOOL)inputLettersOrNumbers:(NSString*)string{
NSString *regex =@"[a-zA-Z0-9]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许含大小写或数字(限字数)
-(BOOL)inputNumberOrLetters:(NSString*)name {
NSString *userNameRegex = @"^[A-Za-z0-9]{6,20}+$";
NSPredicate *userNamePredicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",userNameRegex];
BOOL inputString = [userNamePredicate evaluateWithObject:name];
return inputString;
}
#pragma - mark 允许汉字或数字(不限字数)
- (BOOL)inputChineseOrNumbers:(NSString*)string{
NSString *regex =@"[\u4e00-\u9fa5]+[0-9]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许汉字或数字(限字数)
- (BOOL)inputChineseOrNumbersLimit:(NSString*)string{
NSString *regex =@"[\u4e00-\u9fa5][0-9]{6,20}+$";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许汉字,大小写或数字(不限字数)
- (BOOL)inputChineseOrLettersAndNumbersNum:(NSString*)string{
NSString *regex =@"[\u4e00-\u9fa5]+[A-Za-z0-9]*";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}
#pragma - mark 允许汉字,大小写或数字(限字数)
- (BOOL)inputChineseOrLettersNumberslimit:(NSString*)string{
NSString *regex =@"[\u4e00-\u9fa5]+[A-Za-z0-9]{6,20}+$";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
BOOL inputString = [predicate evaluateWithObject:string];
return inputString;
}

转:http://blog.csdn.net/h643342713/article/details/53966446

Aria2 安装

From: https://www.hi-linux.com/posts/49089.html

关于Aria2

Aria2是一个基于命令行的开源下载工具,支持多协议、多来源(HTTP/HTTPS、FTP、BitTorrent、Metalink协议等)、多线程的下载。它比axel优秀的地方在于完全支持BitTorrent协议,同时可以作为BitTorrent客户端来下载种子文件,支持Metalink协议,远程控制(通过web端)下载进程。

主要优势如下

高速,自动多线程下载;
断点续传;
轻量占用内存非常少,通常情况平均4~9MB内存占用(官方介绍);
多平台。支援 Win/Linux/OSX/Android 等操作系统下的部署;
模块化。分段下载引擎,文件整合速度快;
支持RPC界面远程;
全面支持BitTorrent协议;

Aria2官方项目页面:https://aria2.github.io/

安装Aria2

包安装

CentOS

默认Repo里没有Aria2,我们需要添加第三方的yum源。

安装rpmforge源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ wget http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.3-1.el6.rf.x86_64.rpm
$ rpm -ivh rpmforge-release-0.5.3-1.el6.rf.x86_64.rpm
安装Aria2
1
$ yum -y install aria2
:rpmforge源中的版本是1.16.4,版本相对是比较低!
Ubuntu
$ sudo apt-get install aria2
MAC OS
$ brew install aria2

编译安装Aria2

依赖环境

Aria2 1.17.1以上版本要求gcc >= 4.8.3 or clang >= 3.4

安装clang

$ yum install clang #epel源

安装GCC

通过SCL安装GCC

CentOS 6

https://copr.fedoraproject.org/coprs/rhscl/devtoolset-3/

1
2
3
4
5
6
7
$ wget https://copr.fedoraproject.org/coprs/rhscl/devtoolset-3/repo/epel-6/rhscl-devtoolset-3-epel-6.repo -O /etc/yum.repos.d/rhscl-devtoolset-3-epel-6.repo
$ yum install devtoolset-3-gcc devtoolset-3-gcc-c++ devtoolset-3-binutils devtoolset-3-gcc-gfortran
$ scl enable devtoolset-3 bash #启用SCL环境中新版本GCC
$ gcc --version

编译Aria2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ wget https://github.com/aria2/aria2/releases/download/release-1.22.0/aria2-1.22.0.tar.gz
$ tar xzvf aria2-1.22.0.tar.gz
$ cd aria2-1.22.0
$ ./configure
$ make
$ make install
$ man aria2c //查看aria2c manual
验证Aria2版本
$ aria2c --version
aria2 版本 1.22.0
Copyright (C) 2006, 2015 Tatsuhiro Tsujikawa

本程序为自由软件;您可自由再版或修改它,惟须遵守 GNU 通用公共许可证,

第 2 版或更新版本(依您所愿)的条款,以自由软件基金会发布的版本为准。

我们本着希望有用的态度发行此软件,但 从未做出任何保证,甚至不暗示对

于适销性或对某一特定用途的适用性的保证。参见 GNU 通用公共许可证以获取

更多信息。

配置

已开启的特性: BitTorrent, Firefox3 Cookie, GZip, HTTPS, Message Digest, Metalink, XML-RPC

哈希算法: sha-1, sha-224, sha-256, sha-384, sha-512, md5, adler32

库: zlib/1.2.3 libxml2/2.7.6 sqlite3/3.6.20 OpenSSL/1.0.1e

编译器: gcc 4.9.2 20150212 (Red Hat 4.9.2-6)

built by x86_64-pc-linux-gnu

on May 6 2016 14:31:52

系统: Linux 2.6.32-573.12.1.el6.x86_64 #1 SMP Tue Dec 15 21:19:08 UTC 2015 x86_64

配置Aria2

创建配置文件

1
2
3
$ mkdir /etc/aria2/
$ vim /etc/aria2/aria2.conf
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#用户名
#rpc-user=user
#密码
#rpc-passwd=passwd
#上面的认证方式不建议使用,建议使用下面的token方式
#设置加密的密钥
#rpc-secret=token
#允许rpc
enable-rpc=true
#允许所有来源, web界面跨域权限需要
rpc-allow-origin-all=true
#允许外部访问,false的话只监听本地端口
rpc-listen-all=true
#RPC端口, 仅当默认端口被占用时修改
rpc-listen-port=6800
#最大同时下载数(任务数), 路由建议值: 3
max-concurrent-downloads=5
#断点续传
continue=true
#同服务器连接数
max-connection-per-server=5
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=10M
#单文件最大线程数, 路由建议值: 5
split=10
#下载速度限制
max-overall-download-limit=0
#单文件速度限制
max-download-limit=0
#上传速度限制
max-overall-upload-limit=0
#单文件速度限制
max-upload-limit=0
#断开速度过慢的连接
#lowest-speed-limit=0
#验证用,需要1.16.1之后的release版本
#referer=*
#文件保存路径, 默认为当前启动位置
dir=/root/downloads
#文件缓存, 使用内置的文件缓存, 如果你不相信Linux内核文件缓存和磁盘内置缓存时使用, 需要1.16及以上版本
#disk-cache=0
#另一种Linux文件缓存方式, 使用前确保您使用的内核支持此选项, 需要1.15及以上版本(?)
#enable-mmap=true
#文件预分配, 能有效降低文件碎片, 提高磁盘性能. 缺点是预分配时间较长
#所需时间 none < falloc ? trunc << prealloc, falloc和trunc需要文件系统和内核支持
file-allocation=prealloc

注意将配置表中保存路径一项dir=/root/downloads替换为自己的保存位置。(Windows下类似这样dir=F:\SoftWare)

Aria2的使用

配置完成后,就可以开始使用了。

Aria2有两种模式

命令直接调用

直接在命令行下载$ aria2c "download.url", 下载完成后自动退出,就和wget 的工作方式一样。

Aria2命令行使用

使用Aria2下载文件,只需在命令后附加地址即可。如

$ aria2c http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.22.6.tar.bz2

分段下载

利用Aria2的分段下载功能可以加快文件的下载速度,对于下载大文件时特别有用。为了使用aria2的分段下载功能,你需要在命令中指定-s选项。如

$ aria2c -s 2 http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.22.6.tar.bz2

这将使用2连接来下载该文件。-s后面的参数值介于1~5之间,你可以根据实际情况选择。

断点续传

在命令中使用-c选项可以断点续传文件。如

$ aria2c -c http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.22.6.tar.bz2

下载torrent文件

你也可以使用Aria2下载BitTorrent文件。如

$ aria2c -o gutsy.torrent http://cdimage.ubuntu.com/daily-live/current/gutsy-desktop-i386.iso.torrent

后台下载

1
2
3
$ aria2c -D url
$ aria2c --deamon=true url

验证文件

$ aria2c --checksum=md5=别人提供的md5

BT下载

1
2
3
4
$ aria2c /tmp/CentOS-6.3-i386-bin-DVD1to2.torrent
$ aria2c http://mirrors.163.com/centos/6.6/isos/x86_64/CentOS-6.6-x86_64-minimal.torrent

列出种子内容

$ aria2c -S .torrent

下载种子内特定编号的文件

$ aria2c --select-file=1,4-7 .torrent

此处下载编号为1,4,5,6,7的文件

设置bt端口

$ aria2c --listen-port=1234 .torrent

设置dht端口

$ aria2c --dht-listen-port=1234 .torrent

下载需要引用页的文件

$ aria2c --referer=referurl url

限速下载

$ aria2c --max-download-limit=500k url //单个文件

$ aria2c --max-overall-download-limit=500k url //全局

下载需要Cookie验证的文件

$ aria2c --header='Cookie:cookie名称=cookie内容' url

$ aria2c --load-cookies=cookie文件 url

Metalink

$ aria2c http://example.org/mylinux.metalink

批量下载文本中所有URL

$ aria2c -i uris.txt

注意:当源地址存在诸如&,*等shell的特殊字符,请使用单引号或双引号把URI包含起来。

RPC Server模式(推荐)

Aria2作为后台常驻程序,监测rpc端口的活动情况,添加并下载文件。完成后继续在后台运行。

涉及到命令输入,力求简化,第二种模式明显更省事。

启动Aria2 RPC模式

命令行启动

$ aria2c --enable-rpc --rpc-listen-all --rpc-allow-origin-all -c --dir /root/downloads -D (-D daemon模式,用于后台执行)

配置文件启动(推荐)

$ aria2c --conf-path=<Path>

是指配置文件所在的绝对路径。默认位置是:$HOME/.aria2/aria2.conf

依照上述配置一路下来,具体是

$ aria2c --conf-path="/etc/aria2.conf" -D #(-D daemon模式,用于后台执行)

这时正确无误的话,Aria2就启动了。

启动脚本

为方便管理,创建一个管理脚本。

$ vi /etc/init.d/aria2

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/bin/bash
#
# aria2 - this script starts and stops the aria2 daemon
#
# chkconfig: - 85 15
# description: Aria2 - Download Manager
# processname: aria2c
# config: /etc/aria2/aria2.conf
# pidfile:
# Source function library.
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0
aria2c="/usr/bin/aria2c"
ARIA2C_CONF_FILE="/etc/aria2/aria2.conf"
options=" --conf-path=$ARIA2C_CONF_FILE -D "
RETVAL=0
start() {
# code here to start the program
echo -n "Starting aria2c daemon."
${aria2c} ${options}
RETVAL=$?
echo
}
stop() {
echo -n "Shutting down aria2c daemon."
/usr/bin/killall aria2c
RETVAL=$?
echo
}
status() {
ID=$(/bin/ps -ef | grep 'aria2c' | grep -v 'grep' | awk '{print $2}')
if [[ "x$ID" != "x" ]]; then
echo "Aria2 is running."
else
echo "Aria2 is not running."
fi
}
restart() {
stop
sleep 3
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status
;;
restart)
restart
;;
*)
echo "Usage: service aria2c {start|stop|restart}"
RETVAL=1
esac
exit $RETVAL

添加可执行权限

$ chmod +x /etc/init.d/aria2

启动Aria2

$ /etc/init.d/aria2 start

搭配Aria2 Web UI

Aria2不带GUI界面。了解下载进度会有不便,日常使用需搭配Web UI工具方便查看。

webui-aria2

$ git clone https://github.com/ziahamza/webui-aria2

$ python -m SimpleHTTPServer 9999

访问这台机器的9999端口就可以了,这里为了方便用python做为WEB服务器,其它任意一种WEB服务器都是可以的。

如果你不想搭建可使用http://ziahamza.github.io/webui-aria2/,配置数据是存在本地浏览器的,不需要注册。

注意:需要根据情况设置一下Aria2 RPC的地址,一般为Aria2后台进程运行的ip:port,例如192.168.119.100:6800。

YAAW

$ git clone https://github.com/binux/yaaw

$ python -m SimpleHTTPServer 9999 #也可以使用Apache

访问这台机器的9999端口就可以了,这里为了方便用python做为WEB服务器,其它任意一种WEB服务器都是可以的。

YAAW也有线版本

http://aria2c.com/
http://binux.github.io/yaaw/demo/

注意:需要根据情况设置一下Aria2 RPC的地址,一般为Aria2后台进程运行的ip:port,例如192.168.119.100:6800。

Windows下图形版本

Aria2c Remote Control

http://sourceforge.net/projects/aria2cremote/

给jsonrpc加上验证

使用token验证(建议使用)

需要1.18.4以上版本,帐号密码方式将在后续版本中停用!

配置文件

token验证

rpc-secret=secret

命令行

使用--rpc-secret=xxxxxx选项

启用验证后,使用http://token:secret@hostname:port/jsonrpc的地址格式设置secret。

使用密码验证

需要1.15.2以上,1.18.6以下版本
1.18.4新增了--rpc-secret ,设置的RPC授权令牌, 取代--rpc-user--rpc-passwd选项

配置文件

#用户名

rpc-user=username

#密码

rpc-passwd=passwd

命令行

使用--rpc-user=user --rpc-passwd=pwd选项

启用验证后,使用http://username:passwd@hostname:port/jsonrpc的地址格式设置密码。

对于RPC模式来说, 界面和后端是分离的, 只要给后端设置密码即可. 前端认证什么的是毫无意义的。

其它相关

YAAW搭配脚本

迅雷离线(需会员账号)

Chrome Extension: ThunderLixianAssistant
UserScript: ThunderLixianExporter

旋风离线

UserScript: XuanFengEx
UserScript: LixianExporter

百度网盘

Chrome Extension: BaiduExporter
Firefox Addons: BaiduExporter
UserScript: BaiduPanDownloadHelper

115网盘

Chrome Extension: 115exporter

其他脚本

Chrome Extension

添加到aria2
Chrome Download Helper

参考文档

http://www.google.com
http://scateu.me/2015/02/12/aria2c-flashgot-firefox-jsonrpc.html
http://skypegnu1.blog.51cto.com/8991766/1637168
http://aria2c.com/usage.html
http://azeril.me/blog/Aria2.html


Linux ./configure && make && make install 编译安装和卸载

正常的编译安装/卸载:

源码的安装一般由3个步骤组成:配置(configure)、编译(make)、安装(make install)。

configure文件是一个可执行的脚本文件,它有很多选项,在待安装的源码目录下使用命令./configure –help可以输出详细的选项列表。

其中–prefix选项是配置安装目录,如果不配置该选项,安装后可执行文件默认放在/usr /local/bin,库文件默认放在/usr/local/lib,配置文件默认放在/usr/local/etc,其它的资源文件放在/usr /local/share,比较凌乱。

如果配置了–prefix,如:
$ ./configure –prefix=/usr/local/test

安装后的所有资源文件都会被放在/usr/local/test目录中,不会分散到其他目录。

使用–prefix选项的另一个好处是方便卸载软件或移植软件;当某个安装的软件不再需要时,只须简单的删除该安装目录,就可以把软件卸载得干干净净;而移植软件只需拷贝整个目录到另外一个机器即可(相同的操作系统下)。

当然要卸载程序,也可以在原来的make目录下用一次make uninstall,但前提是Makefile文件有uninstall命令(nodejs的源码包里有uninstall命令,测试版本v0.10.35)。

关于卸载:

如果没有配置–prefix选项,源码包也没有提供make uninstall,则可以通过以下方式可以完整卸载:

找一个临时目录重新安装一遍,如:
$ ./configure –prefix=/tmp/to_remove && make install

然后遍历/tmp/to_remove的文件,删除对应安装位置的文件即可(因为/tmp/to_remove里的目录结构就是没有配置–prefix选项时的目录结构)。

原文地址:
http://www.cnblogs.com/zhangbo127/p/4556008.html

Masonry 使用方法

一个约束优先级解说
http://www.cnblogs.com/siasyl/p/6775055.html


1
2
3
4
5
6
7
8
9
10
11
如果想要约束变换之后实现动画效果,则需要执行如下操作
// 通知需要更新约束,但是不立即执行
[self setNeedsUpdateConstraints];
// 立即更新约束,以执行动态变换
// update constraints now so we can animate the change
[self updateConstraintsIfNeeded];
// 执行动画效果, 设置动画时间
[UIView animateWithDuration:0.4 animations:^{
[self layoutIfNeeded];
}];


Masonry 动画 更新或者重置动画
然后用UIView 动画

1
2
3
4
5
6
7
UIView.animate(withDuration: 0.5, animations: {
self.controlBar.layoutIfNeeded()
self.topbar.layoutIfNeeded()
}) { (bool) in
self.topbar.isHidden = isHidden
self.controlBar.isHidden = isHidden
}

Masonry的更新约束 mas_updateConstraints 更新约束 但是约束的相对实例不能变
mas_makeConstraints 删除以前的约束用新的约束


要求:

当键盘挡住输入框时,输入框自动向上弹到键盘上方。

实现:

这里需要使用到Masonry的另外一个方法mas_updateConstraints。这个方法用于更新控件约束。

具体的实现方式可以下载Demo来看,这里只贴出键盘弹出时的处理代码:

swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardChangeFrame(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardHid(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardChangeFrame(_ sender:Notification){
var keyHeight:CGFloat = 246
var info = (sender as NSNotification).userInfo //as? NSDictionary
let keyboardSize = (info?[UIKeyboardFrameBeginUserInfoKey] as AnyObject).cgRectValue //UIKeyboardFrameEndUserInfoKey UIKeyboardFrameBeginUserInfoKey
let keyboardDuration = (info?[UIKeyboardAnimationDurationUserInfoKey] as AnyObject).doubleValue ?? 0
if keyboardSize != nil{
keyHeight = (keyboardSize!.height)
}
self.searchAssociative.mas_updateConstraints { (make) in
make?.bottom.mas_equalTo()(self.view)?.offset()(-keyHeight)
}
UIView.animate(withDuration: keyboardDuration) {
self.view.layoutIfNeeded()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)keyboardWillChangeFrameNotification:(NSNotification *)notification {
// 获取键盘基本信息(动画时长与键盘高度)
NSDictionary *userInfo = [notification userInfo];
CGRect rect = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGFloat keyboardHeight = CGRectGetHeight(rect);
CGFloat keyboardDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
// 修改下边距约束
[_textField mas_updateConstraints:^(MASConstraintMaker *make) {
make.bottom.mas_equalTo(-keyboardHeight);
}];
// 更新约束
[UIView animateWithDuration:keyboardDuration animations:^{
[self.view layoutIfNeeded];
}];
}

由于业务需要,有时候需要动态加载Cell,考虑一些方案后采取把Cell高度设置为0.01的方法。但是约束为-10 的话会有错误信息(据说ios7 会崩溃) ,解决方法是降低优先级

1
2
3
4
5
self.titleLabel.mas_makeConstraints({ (make) in
make?.top.mas_equalTo()(self.imageView.mas_bottom)?.offset()(interval_W_10)?.priority()(499)
make?.left.right().mas_equalTo()(self.imageView)
make?.bottom.mas_equalTo()(self)?.offset()(-interval_W_10)?.priority()(499)
})

From: http://www.jianshu.com/p/a24dd8638d28

个人喜欢用纯代码写东西,其中用到最多的就是Masonry,我整理一些使用过程中一些点,方便以后使用.(基本的语法就不说了)

首先说几点:

  1. 我一般将数值类型的约束用mas_equalTo,而相对于某个控件,或者某个控件的某个约束,我会使用equalTo,如:
    make.size.mas_equalTo(CGSizeMake(100, 100));
    make.center.equalTo(weakSelf.view);
  2. setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用layoutSubviews。
    layoutIfNeeded:告知页面布局立刻更新。所以一般都会和setNeedsLayout一起使用。如果希望立刻生成新的frame需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。
    layoutSubviews:系统重写布局
    setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始
    updateConstraintsIfNeeded:告知立刻更新约束
    updateConstraints:系统更新约束
  3. - (void)updateViewConstraints ViewController的View在更新视图布局时,会先调用ViewController的updateViewConstraints 方法。我们可以通过重写这个方法去更新当前View的内部布局,而不用再继承这个View去重写-updateConstraints方法。我们在重写这个方法时,务必要调用 super 或者 调用当前View的 -updateConstraints 方法。

    // 防止block中的循环引用
    __weak typeof(self) weakSelf = self;
    UIView view = [UIView new];
    view.backgroundColor = [UIColor brownColor];
    [self.view addSubview:view];
    //使用mas_makeConstraints添加约束
    [view mas_makeConstraints:^(MASConstraintMaker
    make) {

    // 添加大小约束(make就是要添加约束的控件view)
    make.size.mas_equalTo(CGSizeMake(200, 200));

    // 添加居中约束(居中方式与self相同)
    make.center.equalTo(weakSelf.view);
    }];

    UIView* blackView = [UIView new];
    blackView.backgroundColor = [UIColor blackColor];
    [self.view addSubview:blackView];

    [blackView mas_makeConstraints:^(MASConstraintMaker *make) {
    //添加约束大小
    make.size.mas_equalTo(CGSizeMake(100, 100));
    //在 左,上 添加约束 (左、上约束都是20)
    make.left.and.top.mas_equalTo(20);
    }];

    UIView* grayView = [UIView new];
    grayView.backgroundColor = [UIColor lightGrayColor];
    [self.view addSubview:grayView];

    [grayView mas_makeConstraints:^(MASConstraintMaker *make) {
    // 大小、上边距约束与黑色view相同
    make.size.and.top.equalTo(blackView);
    // 添加右边距约束(这里的间距是有方向性的,左、上边距约束为正数,右、下边距约束为负数)
    make.right.mas_equalTo(-20);
    }];

    • (void)dealloc {
      [[NSNotificationCenter defaultCenter] removeObserver:self];
      }

    • (void)viewDidLoad {
      [super viewDidLoad];
      // Do any additional setup after loading the view.
      __weak typeof(self) weakSelf = self;
      _textField = [UITextField new];
      _textField.backgroundColor = [UIColor redColor];
      [self.view addSubview:_textField];

      [_textField mas_makeConstraints:^(MASConstraintMaker *make) {
      //left,right,centerx,y 不能共存只能有其二
      make.left.mas_equalTo(20);
      // make.right.mas_equalTo(-60);
      make.centerX.equalTo(weakSelf.view);
      make.height.mas_equalTo(40);
      make.bottom.mas_equalTo(0);
      }];

      // 注册键盘通知
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil];
      [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
      }

    • (void)keyboardWillChangeFrameNotification:(NSNotification *)notification {

      // 获取键盘基本信息(动画时长与键盘高度)
      NSDictionary userInfo = [notification userInfo];
      CGRect rect = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
      CGFloat keyboardHeight = CGRectGetHeight(rect);
      CGFloat keyboardDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
      // 修改下边距约束
      [_textField mas_updateConstraints:^(MASConstraintMaker
      make) {

      make.bottom.mas_equalTo(-keyboardHeight);
      

      }];

      // 更新约束
      [UIView animateWithDuration:keyboardDuration animations:^{
      [self.view layoutIfNeeded];
      }];
      }

    • (void)keyboardWillHideNotification:(NSNotification *)notification {

      // 获得键盘动画时长
      NSDictionary *userInfo = [notification userInfo];
      CGFloat keyboardDuration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];

      // 修改为以前的约束(距下边距0)
      [_textField mas_updateConstraints:^(MASConstraintMaker *make) {

      make.bottom.mas_equalTo(0);
      

      }];

      // 更新约束
      [UIView animateWithDuration:keyboardDuration animations:^{

      [self.view layoutIfNeeded];
      

      }];
      }

    • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event {
      [super touchesBegan:touches withEvent:event];
      [self.view endEditing:YES];
      }

方法一:

array 的 mas_distributeViewsAlongAxis withFixedSpacing 变化的是控件 长度或宽度

定义一个存放三个控件的数组NSArray *array;
array = @[greenView,redView,blueView];

注意:

数组里面的元素不能小于2个,要不会报错 views to distribute need to bigger than one

直接调用下面的方法:

- (void)getHorizontalone
{
//方法一,array 的 mas_distributeViewsAlongAxis
/**
 *  多个控件固定间隔的等间隔排列,变化的是控件的长度或者宽度值
 *
 *  @param axisType        轴线方向
 *  @param fixedSpacing    间隔大小
 *  @param leadSpacing     头部间隔
 *  @param tailSpacing     尾部间隔
 */
//    MASAxisTypeHorizontal  水平
//    MASAxisTypeVertical    垂直

[arrayList mas_distributeViewsAlongAxis:MASAxisTypeHorizontal
                       withFixedSpacing:20
                            leadSpacing:5
                            tailSpacing:5];
[arrayList mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(60);
    make.height.mas_equalTo(100);
}];

}

方法二:

array de mas_distributeViewsAlongAxis withFixedItemLength 控件size不变,变化的是间隙

- (void)getVertical
{
/**
 *  多个固定大小的控件的等间隔排列,变化的是间隔的空隙
 *
 *  @param axisType        轴线方向
 *  @param fixedItemLength 每个控件的固定长度或者宽度值
 *  @param leadSpacing     头部间隔
 *  @param tailSpacing     尾部间隔
 */
[arrayList mas_distributeViewsAlongAxis:MASAxisTypeVertical
                    withFixedItemLength:60
                            leadSpacing:40
                            tailSpacing:10];
[arrayList mas_makeConstraints:^(MASConstraintMaker *make) {
    //        make.top.mas_equalTo(100);
    //        make.height.mas_equalTo(100);
    make.left.mas_equalTo(20);
    make.right.mas_equalTo(-20);
}];

}

以上俩方法都在NSArray+MASAdditions

方法三:直接设置multiplier实现等间距

 for (NSUInteger i = 0; i < 4; i++) {
    UIView *itemView = [self getItemViewWithIndex:i];
    [_containerView addSubview:itemView];

    [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.width.and.height.equalTo(@(ITEM_SIZE));
        make.centerY.equalTo(_containerView.mas_centerY);
        make.centerX.equalTo(_containerView.mas_right).multipliedBy(((CGFloat)i + 1) / ((CGFloat)ITEM_COUNT + 1));
    }];
}

方法四: 利用透明等宽度的SpaceView实现等间距

  UIView *lastSpaceView       = [UIView new];
lastSpaceView.backgroundColor = [UIColor greenColor];
[_containerView1 addSubview:lastSpaceView];

[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.and.top.and.bottom.equalTo(_containerView1);
}];

for (NSUInteger i = 0; i < ITEM_COUNT; i++) {
    UIView *itemView = [self getItemViewWithIndex:i];
    [_containerView1 addSubview:itemView];

    [itemView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.height.and.width.equalTo(@(ITEM_SIZE));
        make.left.equalTo(lastSpaceView.mas_right);
        make.centerY.equalTo(_containerView1.mas_centerY);
    }];

    UIView *spaceView         = [UIView new];
    spaceView.backgroundColor = [UIColor greenColor];
    [_containerView1 addSubview:spaceView];

    [spaceView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(itemView.mas_right).with.priorityHigh(); // 降低优先级,防止宽度不够出现约束冲突
        make.top.and.bottom.equalTo(_containerView1);
        make.width.equalTo(lastSpaceView.mas_width);
    }];

    lastSpaceView = spaceView;
}

[lastSpaceView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.right.equalTo(_containerView1.mas_right);
}];

和面方法4一样,利用spaceView来实现

  UIView* bgView       = [[UIView alloc]init];
bgView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:bgView];

[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.and.right.mas_equalTo(0);
    make.top.mas_equalTo(@100);
    make.height.mas_equalTo(@100);
}];

listText = @[@"北京",@"地大吴波啊",@"你大爷",@"我们的爱哎哎"];
UIView *lastSpaceView = nil;
for(int i = 0 ; i < listText.count;  i ++)
{
    UILabel* label = [UILabel new];
    label.text     = listText[i];
    label.backgroundColor = RANDOMCOLOR;
    [bgView addSubview:label];

    UIView* lineView         = [UIView new];
    lineView.backgroundColor = [UIColor redColor];
    [bgView addSubview:lineView];

    [label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.bottom.mas_equalTo(0);
        if (lastSpaceView)
        {
            NSLog(@"存在 lastView");
            make.left.equalTo(lastSpaceView.mas_right).mas_offset(@20);
        }else
        {
            NSLog(@"不存在存在 lastView");
            make.left.equalTo(bgView.mas_left);
        }
        make.height.equalTo(bgView);
    }];

    lastSpaceView = label;

    [lineView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.and.bottom.mas_equalTo(0);
        make.width.mas_equalTo(1);
        make.left.mas_equalTo(label.mas_right).mas_offset(@10);
    }];
}

UIView* bgView = [UIView new];
bgView.backgroundColor = [UIColor purpleColor];
[self.view addSubview:bgView];

UILabel* titleLab        = [UILabel new];
titleLab.backgroundColor = [UIColor redColor];
titleLab.textAlignment   = NSTextAlignmentCenter;
titleLab.font            = [UIFont systemFontOfSize:15.f];
titleLab.text            = @"曹操——《短歌行》";
[bgView addSubview:titleLab];

UILabel* contentLab        = [UILabel new];
contentLab.numberOfLines   = 0 ;
contentLab.textAlignment   = NSTextAlignmentCenter;
contentLab.backgroundColor = [UIColor brownColor];
contentLab.font            = [UIFont systemFontOfSize:13.f];
contentLab.text            = @" 对酒当歌,人生几何? 譬如朝露,去日苦多。\n 慨当以慷,忧思难忘。 何以解忧?唯有杜康。\n 青青子衿,悠悠我心。 但为君故,沉吟至今。\n 呦呦鹿鸣,食野之苹。 我有嘉宾,鼓瑟吹笙。\n 明明如月,何时可掇? 忧从中来,不可断绝。\n 越陌度阡,枉用相存。 契阔谈宴,心念旧恩。\n 月明星稀,乌鹊南飞。 绕树三匝,何枝可依?\n 山不厌高,海不厌深。 周公吐哺,天下归心。";

[bgView addSubview:contentLab];
//思路: 父视图的上间距等于title的上间距,父视图的下间距等于content的下间距
__weak typeof(self) weakSelf = self;
[bgView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.mas_offset(@30);
    make.right.mas_offset(@-30);
    make.centerY.equalTo(weakSelf.view);
}];

[titleLab mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.top.right.mas_equalTo(@0);
}];

[contentLab mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.right.mas_equalTo(@0);
    make.top.equalTo(titleLab.mas_bottom).mas_offset(@10);
    make.bottom.equalTo(bgView);
}];
以后慢慢更新,记录方便以后使用

文/栋飞

//一些扒的别人的记录

自适应布局允许将宽度或高度设置为固定值.如果你想要给视图一个最小或最大值,你可以这样:

//width >= 200 && width <= 400 make.width.greaterThanOrEqualTo(@200); make.width.lessThanOrEqualTo(@400)

约束的优先级

.priority允许你指定一个精确的优先级,数值越大优先级越高.最高1000.
.priorityHigh等价于 UILayoutPriorityDefaultHigh .优先级值为 750.
.priorityMedium介于高优先级和低优先级之间,优先级值在 250~750之间.
.priorityLow等价于 UILayoutPriorityDefaultLow , 优先级值为 250.

优先级可以在约束的尾部添加:

make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow();
make.top.equalTo(label.mas_top).with.priority(600);

center 中心

//使 centerX和 centerY = button1
make.center.equalTo(button1)

//使 centerX = superview.centerX - 5, centerY = superview.centerY + 10 make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))

指定宽度为父视图的 1/4.

make.width.equalTo(superview).multipliedBy(0.25);


Masonry框架初次使用遇到的坑 frame为0
From: http://www.jianshu.com/p/ad9c075a7547

实现自适应布局的一个非常方便的方法就是使用Masonry框架,然而使用Masonry布局的时候,并不能立刻反应到frame的改变上,比如:

UIView *parent = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIImageView *child = [UIView alloc] init];
[parent addSubview:child];
[child mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(20,20));
        make.top.left.mas_equalTo(50);
    }];
NSLog(@"%@",redView);

打印结果:
** <UIImageView: 0x7fb222605550; frame = (0 0; 0 0); layer = <CALayer: 0x7fb22260b3a0>>**

可以发现,虽然使用Masonry进行布局和约束,但是子视图childframe仍然为(0, 0, 0 ,0).

而这时候如果有需求要设置child的形状为圆形,就得知道它的frame,像下面这样写肯定不会设置成功的:

[child mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(20,20));
        make.top.left.mas_equalTo(50);
    }];
 child.layer.cornerRadius = child.bounds.size.width/2;
 child.layer.masksToBounds = YES; //设置头像为圆形

因为这时候的frame还是0。我想会不会是因为block中的处理是放在另一个线程中异步进行的,block还没执行完就已经走到了下面使用frame的代码,(一阵狂喜,好聪明。。。),所以马上把代码改写:

[child mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(20,20));
        make.top.left.mas_equalTo(50);
        child.layer.cornerRadius = child.bounds.size.width/2;
        child.layer.masksToBounds = YES; //设置头像为圆形
    }];

然而并没有什么卵用。。。

没办法只能问谷歌了,然后找到了Masonry约束下获取frame的方法:

使用masonry的实质还是调用了ios7以后的autolayout,如果要更新frame,需要调用layoutIfNeeded函数进行布局,然后所约束的控件才会按照约束条件,生成当前布局相应的framebounds。这样就可以利用这两个属性来进行图片圆角剪裁。而调用layoutIfNeeded的目的是让系统调用layoutSubviews方法,我们也可以直接在这个方法里获取frame,因为这时候开始layout subviews,Masonry已经计算出了真实的frame。

下面附上关于autolayout更新几个方法的区别:

setNeedsLayout:告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用layoutSubviews。

layoutIfNeeded:告知页面布局立刻更新。所以一般都会和setNeedsLayout一起使用。如果希望立刻生成新的frame需要调用此方法,利用这点,动画可以在更新布局后直接使用这个方法让动画生效。

layoutSubviews:系统重写布局

setNeedsUpdateConstraints:告知需要更新约束,但是不会立刻开始

updateConstraintsIfNeeded:告知立刻更新约束

updateConstraints:系统更新约束


From: http://www.cnblogs.com/gfxxbk/p/5827301.html
1、Masonry概述

  • 目前最流行的Autolayout第三方框架

  用优雅的代码方式编写Autolayout

  省去了苹果官方恶心的Autolayout代码

  大大提高了开发效率

2、常用方法

  • 这个方法只会添加新的约束

    1
    2
    3
    [blueView mas_makeConstraints:^(MASConstraintMaker *make) {
    }];
  • 这个方法会将以前的所有约束删掉,添加新的约束

    1
    2
    3
    [blueView mas_remakeConstraints:^(MASConstraintMaker *make) {
    }];
  • 这个方法将会覆盖以前的某些特定的约束

    1
    2
    3
    4
    [blueView mas_updateConstraints:^(MASConstraintMaker *make) {
    }];

3、约束类型

  • 尺寸:

  width(宽)\height(高)\size(大小)

// 宽度约束
make.width.mas_equalTo(100);
// 高度约束
make.height.mas_equalTo(100);

//  大小约束(与上面两句等价)
make.size.mas_equalTo(CGSizeMake(100, 100));
  • 边界:

  left\leading(左边界)\right\trailing(右边界)\top(顶部边界)\bottom(底部边界)  

// 左边(leading类似)
 make.left.mas_equalTo(self.view).offset(50); 
// 右边(trailing类似)
 make.right.equalTo(self.view).offset(-20);
 // 顶部
 make.top.equalTo(self.view).offset(20);
 // 底部
 make.bottom.mas_equalTo(self.view).offset(-50);
  • 中心点:

  center\centerX\centerY

    // 居中(水平+垂直)
    // 尺寸是父控件的一半
    [blueView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(self.view).multipliedBy(0.5);
        make.center.mas_equalTo(self.view); // 与下面两句代码等价
//        make.centerX.mas_equalTo(self.view);
//        make.centerY.mas_equalTo(self.view);
    }];
  • 内边距实现边界约束:

  edges

// UIEdgeInsets 内边距

make.edges.mas_equalTo(self.view).insets(UIEdgeInsetsMake(50, 50, 50, 50));

4、mas_前缀修饰与不修饰的区别    

  • mas_equalTo和equalTo

  默认情况下:

   mas_equalTo有自动包装功能,比如自动将20包装为@20

   equalTo没有自动包装功能

  mas_equalTo的功能强于 > equalTo,可以一直使用mas_equalTo

  • mas_width和width

  默认情况下:

   width是make对象的一个属性,用来添加宽度约束用的,表示对宽度进行约束

   mas_width是一个属性值,用来当做equalTo的参数,表示某个控件的宽度属性

  mas_height、mas_centerX以此类推

  • 消除区别办法

  如果添加了下面的宏,那么 mas_equalTo 和 equalTo 就没有区别

  #define MAS_SHORTHAND_GLOBALS // 注意:这个宏一定要添加到#import “Masonry.h”前面

  如果添加了下面的宏,mas_width也可以写成width

  #define MAS_SHORTHAND

//define this constant if you want to use Masonry without the 'mas_' prefix
#define MAS_SHORTHAND

//define this constant if you want to enable auto-boxing for default syntax
#define MAS_SHORTHAND_GLOBALS

#import "Masonry.h"

- (void)viewDidLoad {
    [super viewDidLoad];

    // 蓝色控件
    UIView *blueView = [[UIView alloc] init];
    blueView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:blueView];

    // 红色控件
    UIView *redView = [[UIView alloc] init];
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];

    // 添加约束
    CGFloat margin = 20;
    CGFloat height = 50;
    [blueView makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view.left).offset(margin);
        make.right.equalTo(redView.left).offset(-margin);
        make.bottom.equalTo(self.view.bottom).offset(-margin);
        make.height.equalTo(height);
        make.top.equalTo(redView.top);
        make.bottom.equalTo(redView.bottom);
        make.width.equalTo(redView.width);
    }];

    [redView makeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(self.view.right).offset(-margin);
    }];
} 

5、可有可无的用法

  以下方法都仅仅是为了提高可读性,可有可无

  • with

    • (MASConstraint*)with {
      return self;
      }

  使用情况示例代码

  // 尺寸限制:100x100
  // 位置:粘着父控件右下角,间距是20
  [blueView mas_makeConstraints:^(MASConstraintMaker *make) {
      // 宽度约束
      make.width.equalTo(@100);
      // 高度约束
      make.height.equalTo(@100);
      // 右边
      make.right.equalTo(self.view.mas_right).with.offset(-20);
      // 顶部
      make.top.equalTo(self.view.mas_top).with.offset(20);
  }];
  • and

    • (MASConstraint*)and {
      return self;
      }

  使用情况示例代码

// 尺寸限制:100x100
// 位置:粘着父控件右下角,间距是20

[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
    // 宽度高度约束
    make.width.and.height.mas_equalTo(100);
    // 右边
    make.right.equalTo(self.view).offset(-20);
    // 顶部
    make.top.equalTo(self.view).offset(20);

}];

From: http://liuyanwei.jumppo.com/2015/06/14/ios-library-masonry.html

Masonry是一个轻量级的布局框架 拥有自己的描述语法 采用更优雅的链式语法封装自动布局 简洁明了 并具有高可读性 而且同时支持 iOS 和 Max OS X。 Masonry是一个用代码写iOS或os界面的库,可以代替Auto layout。 Masonry的github地址:https://github.com/SnapKit/Masonry

本章内容

    • Masonry配置
    • Masonry使用
    • Masonry实例

Masonry配置

    • 推荐使用pods方式引入类库,pod ‘Masonry’,若不知道pod如何使用,情况我的另一篇文章: 提高iOS开发效率的工具
    • 引入头文件 #import “Masonry.h”

Masonry使用讲解

  1. mas_makeConstraints 是给view添加约束,约束有几种,分别是边距,宽,高,左上右下距离,基准线。添加过约束后可以有修正,修正 有offset(位移)修正和multipliedBy(倍率)修正

  2. 语法一般是 make.equalTo or make.greaterThanOrEqualTo or make.lessThanOrEqualTo + 倍数和位移修正

  3. 注意点1: 使用 mas_makeConstraints方法的元素必须事先添加到父元素的中,例如[self.view addSubview:view];

  4. 注意点2: mas_equalTo 和 equalTo 区别:mas_equalTo 比equalTo多了类型转换操作,一般来说,大多数时候两个方法都是 通用的,但是对于数值元素使用mas_equalTo。对于对象或是多个属性的处理,使用equalTo。特别是多个属性时,必须使用equalTo,例如 make.left.and.right.equalTo(self.view);

  5. 注意点3: 注意到方法with和and,这连个方法其实没有做任何操作,方法只是返回对象本身,这这个方法的左右完全是为了方法写的时候的可读性 。make.left.and.right.equalTo(self.view);和make.left.right.equalTo(self.view);是完全一样的,但是明显的加了and方法的语句可读性 更好点。

Masonry初级使用例子

// exp1: 中心点与self.view相同,宽度为400*400
-(void)exp1{

    UIView *view = [UIView new];
    [view setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {

         make.center.equalTo(self.view);
         make.size.mas_equalTo(CGSizeMake(400,400));
    }];

}


//exp2: 上下左右边距都为10
-(void)exp2{

    UIView *view = [UIView new];
    [view setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {

        make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));

        //  make.left.equalTo(self.view).with.offset(10);
        //  make.right.equalTo(self.view).with.offset(-10);
        //  make.top.equalTo(self.view).with.offset(10);
        //  make.bottom.equalTo(self.view).with.offset(-10);
    }];

}

//exp3 让两个高度为150的view垂直居中且等宽且等间隔排列 间隔为10
-(void)exp3{

    UIView *view1 = [UIView new];
    [view1 setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:view1];

    UIView *view2 = [UIView new];
    [view2 setBackgroundColor:[UIColor redColor]];
    [self.view addSubview:view2];

    [view1 mas_makeConstraints:^(MASConstraintMaker *make) {

        make.centerY.mas_equalTo(self.view.mas_centerY);
        make.height.mas_equalTo(150);
        make.width.mas_equalTo(view2.mas_width);
        make.left.mas_equalTo(self.view.mas_left).with.offset(10);
        make.right.mas_equalTo(view2.mas_left).offset(-10);

    }];
    [view2 mas_makeConstraints:^(MASConstraintMaker *make) {

        make.centerY.mas_equalTo(self.view.mas_centerY);
        make.height.mas_equalTo(150);
        make.width.mas_equalTo(view1.mas_width);
        make.left.mas_equalTo(view1.mas_right).with.offset(10);
        make.right.equalTo(self.view.mas_right).offset(-10);

    }];

}

Masonry例子-计算器布局

![][2]

   [2]: http://liuyanwei.jumppo.com/assets/uploads/masonry_1.png


//高级布局练习 iOS自带计算器布局
-(void)exp4{


    //申明区域,displayView是显示区域,keyboardView是键盘区域
    UIView *displayView = [UIView new];
    [displayView setBackgroundColor:[UIColor blackColor]];
    [self.view addSubview:displayView];

    UIView *keyboardView = [UIView new];
    [self.view addSubview:keyboardView];

    //先按1:3分割 displView(显示结果区域)和 keyboardView(键盘区域)
    [displayView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view.mas_top);
        make.left.and.right.equalTo(self.view);
        make.height.equalTo(keyboardView).multipliedBy(0.3f);
    }];

    [keyboardView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(displayView.mas_bottom);
        make.bottom.equalTo(self.view.mas_bottom);
        make.left.and.right.equalTo(self.view);

    }];

    //设置显示位置的数字为0
    UILabel *displayNum = [[UILabel alloc]init];
    [displayView addSubview:displayNum];
    displayNum.text = @"0";
    displayNum.font = [UIFont fontWithName:@"HeiTi SC" size:70];
    displayNum.textColor = [UIColor whiteColor];
    displayNum.textAlignment = NSTextAlignmentRight;
    [displayNum mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.and.right.equalTo(displayView).with.offset(-10);
        make.bottom.equalTo(displayView).with.offset(-10);
    }];


    //定义键盘键名称,?号代表合并的单元格
    NSArray *keys = @[@"AC",@"+/-",@"%",@"÷"
                     ,@"7",@"8",@"9",@"x"
                     ,@"4",@"5",@"6",@"-"
                     ,@"1",@"2",@"3",@"+"
                     ,@"0",@"?",@".",@"="];


    int indexOfKeys = 0;
    for (NSString *key in keys){
        //循环所有键
        indexOfKeys++;
        int rowNum = indexOfKeys %4 ==0? indexOfKeys/4:indexOfKeys/4 +1;
        int colNum = indexOfKeys %4 ==0? 4 :indexOfKeys %4;
        NSLog(@"index is:%d and row:%d,col:%d",indexOfKeys,rowNum,colNum);

        //键样式
        UIButton *keyView = [UIButton buttonWithType:UIButtonTypeCustom];
        [keyboardView addSubview:keyView];
        [keyView setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
        [keyView setTitle:key forState:UIControlStateNormal];
        [keyView.layer setBorderWidth:1];
        [keyView.layer setBorderColor:[[UIColor blackColor]CGColor]];
        [keyView.titleLabel setFont:[UIFont fontWithName:@"Arial-BoldItalicMT" size:30]];

        //键约束
        [keyView mas_makeConstraints:^(MASConstraintMaker *make) {

            //处理 0 合并单元格
            if([key isEqualToString:@"0"] || [key isEqualToString:@"?"] ){

                if([key isEqualToString:@"0"]){
                    [keyView mas_makeConstraints:^(MASConstraintMaker *make) {
                        make.height.equalTo(keyboardView.mas_height).with.multipliedBy(.2f);
                        make.width.equalTo(keyboardView.mas_width).multipliedBy(.5);
                        make.left.equalTo(keyboardView.mas_left);
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.9f);
                    }];
                }if([key isEqualToString:@"?"]){
                    [keyView removeFromSuperview];
                }

            }
            //正常的单元格
            else{
                make.width.equalTo(keyboardView.mas_width).with.multipliedBy(.25f);
                make.height.equalTo(keyboardView.mas_height).with.multipliedBy(.2f);

                //按照行和列添加约束,这里添加行约束
                switch (rowNum) {
                    case 1:
                    {
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.1f);
                        keyView.backgroundColor = [UIColor colorWithRed:205 green:205 blue:205 alpha:1];

                    }
                        break;
                    case 2:
                    {
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.3f);
                    }
                        break;
                    case 3:
                    {
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.5f);
                    }
                        break;
                    case 4:
                    {
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.7f);
                    }
                        break;
                    case 5:
                    {
                        make.baseline.equalTo(keyboardView.mas_baseline).with.multipliedBy(.9f);
                    }
                        break;
                    default:
                        break;
                }
                //按照行和列添加约束,这里添加列约束
                switch (colNum) {
                    case 1:
                    {
                        make.left.equalTo(keyboardView.mas_left);

                    }
                        break;
                    case 2:
                    {
                        make.right.equalTo(keyboardView.mas_centerX);

                    }
                        break;
                    case 3:
                    {
                        make.left.equalTo(keyboardView.mas_centerX);
                    }
                        break;
                    case 4:
                    {
                        make.right.equalTo(keyboardView.mas_right);
                        [keyView setBackgroundColor:[UIColor colorWithRed:243 green:127 blue:38 alpha:1]];
                    }
                        break;
                    default:
                        break;
                }
            }
        }];
    }

}

本例子使用的baseline去控制高度位置,这似乎不是太准,如果想要精准控制高度位置,可以使用一行一行添加的方法,每次当前行的top去equelTo上一行的bottom。 给个提示:

for(遍历所有行)
    for(遍历所以列)
    //当前行约束根据上一行去设置
    ......
  • 下一个例子中,使用上面类似的方法

Masonry高级使用例子2

根据设计图,使用masonry布局:

代码下载

步骤1
 -(void)createUI{

    UIView *titleView = [UIView new];
    titleView.backgroundColor = [UIColor redColor];
    UIView *caredView = [UIView new];
    [self.view addSubview:caredView];
    UIView *brifeView = [UIView new];
    [self.view addSubview:brifeView];

    //self.view
    self.view.backgroundColor = [UIColor colorWithWhite:0.965 alpha:1.000];

    //thrm
    UIImageView *plantThrm = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"defalutPlantReferenceIcon"]];
    [self.view addSubview:plantThrm];
    [plantThrm mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.and.top.equalTo(self.view).with.offset(10);
    }];

    //title
       [self.view addSubview:titleView];
       UIImageView *bgTitleView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"bg-plant-reference-title"]];
    [titleView addSubview:bgTitleView];
    [titleView mas_makeConstraints:^(MASConstraintMaker *make) {

        make.right.equalTo(self.view.mas_right);
        make.left.equalTo(plantThrm.mas_right).with.offset(20);
        make.centerY.equalTo(plantThrm.mas_centerY);
   }];

    [bgTitleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(titleView);
    }];

    UILabel *title = [[UILabel alloc]init];
    title.textColor =  [UIColor whiteColor];
    title.font = [UIFont fontWithName:@"Heiti SC" size:26];
    title.text = _reference.name;
    [titleView addSubview:title];
    [title mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(titleView.mas_left).offset(10);
        make.width.equalTo(titleView.mas_width);
        make.centerY.equalTo(titleView.mas_centerY);
    }];

    //植物养护
    UILabel *caredTitle = [[UILabel alloc]init];
    caredTitle.textColor =  [UIColor colorWithRed:0.172 green:0.171 blue:0.219 alpha:1.000];
    caredTitle.font = [UIFont fontWithName:@"Heiti SC" size:10];
    caredTitle.text = @"植物养护";
    [self.view addSubview:caredTitle];
    [caredTitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(plantThrm.mas_bottom).with.offset(20);
        make.left.and.right.equalTo(self.view).with.offset(10);
        make.height.mas_equalTo(10);
    }];



    //将图层的边框设置为圆脚
    caredView.layer.cornerRadius = 5;
    caredView.layer.masksToBounds = YES;
    //给图层添加一个有色边框
    caredView.layer.borderWidth = 1;
    caredView.layer.borderColor = [[UIColor colorWithWhite:0.521 alpha:1.000] CGColor];
    caredView.backgroundColor = [UIColor whiteColor];


    [caredView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(caredTitle.mas_bottom).with.offset(5);
        make.left.equalTo(self.view.mas_left).with.offset(10);
        make.right.equalTo(self.view.mas_right).with.offset(-10);
        make.height.equalTo(brifeView);
    }];


    //植物简介
    UILabel *brifeTitle = [[UILabel alloc]init];
    brifeTitle.textColor =  [UIColor colorWithRed:0.172 green:0.171 blue:0.219 alpha:1.000];
    brifeTitle.font = [UIFont fontWithName:@"Heiti SC" size:10];
    brifeTitle.text = @"植物简介";
    [self.view addSubview:brifeTitle];
    [brifeTitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(caredView.mas_bottom).with.offset(20);
        make.left.and.right.equalTo(self.view).with.offset(10);
        make.height.mas_equalTo(10);
    }];


    //将图层的边框设置为圆脚
    brifeView.layer.cornerRadius = 5;
    brifeView.layer.masksToBounds = YES;
    //给图层添加一个有色边框
    brifeView.layer.borderWidth = 1;
    brifeView.layer.borderColor = [[UIColor colorWithWhite:0.521 alpha:1.000] CGColor];
    brifeView.backgroundColor = [UIColor whiteColor];


    [brifeView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(brifeTitle.mas_bottom).with.offset(5);
        make.left.equalTo(self.view.mas_left).with.offset(10);
        make.right.equalTo(self.view.mas_right).with.offset(-10);
        make.bottom.equalTo(self.view.mas_bottom).with.offset(-10);
        make.height.equalTo(caredView);
    }];


}

完成之后如下图 步骤1

步骤2,在上面的基础上,增加植物养护部分ui构造的代码,思想是,先构造出四行,然后根据每行单独构造出行样式。
//把块拆分为四行
-(void)createIndexUIWithView:(UIView *)view{

    //拆分四行
    UIView *row1 = [UIView new];
    UIView *row2 = [UIView new];
    UIView *row3 = [UIView new];
    UIView *row4 = [UIView new];
    [view addSubview:row1];
    [view addSubview:row2];
    [view addSubview:row3];
    [view addSubview:row4];

    [row1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.and.left.equalTo(view);
        make.height.equalTo(view.mas_height).multipliedBy(0.25);
        make.top.equalTo(view.mas_top);
    }];
    [row2 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.and.left.equalTo(view);
        make.top.equalTo(row1.mas_bottom);
        make.height.equalTo(view.mas_height).multipliedBy(0.25);
    }];
    [row3 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(view.mas_right);
        make.top.equalTo(row2.mas_bottom);
        make.height.equalTo(view.mas_height).multipliedBy(0.25);
        make.left.equalTo(view.mas_left);
    }];
    [row4 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.right.and.left.equalTo(view);
        make.top.equalTo(row3.mas_bottom);
        make.height.equalTo(view.mas_height).multipliedBy(0.25);
    }];

    [self createIndexRowUI:PlantReferenceWaterIndex withUIView:row1];
    [self createIndexRowUI:PlantReferenceSumIndex withUIView:row2];
    [self createIndexRowUI:PlantReferenceTemperatureIndex withUIView:row3];
    [self createIndexRowUI:PlantReferenceElectrolyteIndex withUIView:row4];
}


//构造每行的UI
-(void)createIndexRowUI:(PlantReferenceIndex) index withUIView:(UIView *)view{

    //index标题
    UILabel *indexTitle = [UILabel new];

    indexTitle.font = [UIFont fontWithName:@"HeiTi SC" size:14];
    indexTitle.textColor = [UIColor colorWithWhite:0.326 alpha:1.000];

    [view addSubview:indexTitle];
    [indexTitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(view.mas_left).with.offset(20);
        make.centerY.equalTo(view.mas_centerY);
    }];


    switch (index) {


        case PlantReferenceWaterIndex:
        {
            indexTitle.text = @"水分";
            UIImageView * current;
            for(int i=1;i<=5;i++){
                if(i<_reference.waterIndex){
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_water_light"]];
                }else{
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_water_dark"]];
                }
                [view addSubview:current];

                //间距12%,左边留空30%
                [current mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.left.equalTo(view.mas_right).with.multipliedBy(0.12*(i-1) +0.3);
                    make.centerY.equalTo(view.mas_centerY);
                }];
            }

        }
              break;
        case PlantReferenceSumIndex:
        {
            indexTitle.text = @"光照";
            UIImageView * current;
            for(int i=1;i<=5;i++){
                if(i<_reference.temperatureIndex){
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_summer_light"]];
                }else{
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_summer_dark"]];
                }
                [view addSubview:current];

                //间距12%,左边留空30%
                [current mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.left.equalTo(view.mas_right).with.multipliedBy(0.12*(i-1) +0.3);
                    make.centerY.equalTo(view.mas_centerY);
                }];
            }

        }
              break;
        case PlantReferenceTemperatureIndex:
        {
            indexTitle.text = @"温度";
            UIImageView * current;
            for(int i=1;i<=5;i++){
                if(i<_reference.sumIndex){
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_temperature_light"]];
                }else{
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_temperature_dark"]];
                }
                [view addSubview:current];

                //间距12%,左边留空30%
                [current mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.left.equalTo(view.mas_right).with.multipliedBy(0.12*(i-1) +0.3);
                    make.centerY.equalTo(view.mas_centerY);
                }];
            }

        }
              break;
        case PlantReferenceElectrolyteIndex:
        {
            indexTitle.text = @"肥料";
            UIImageView * current;
            for(int i=1;i<=5;i++){
                if(i<_reference.electrolyteIndex){
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_electolyte_light"]];
                }else{
                    current = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_electolyte_dark"]];
                }
                [view addSubview:current];

                //间距12%,左边留空30%
                [current mas_makeConstraints:^(MASConstraintMaker *make) {
                    make.left.equalTo(view.mas_right).with.multipliedBy(0.12*(i-1) +0.3);
                    make.centerY.equalTo(view.mas_centerY);
                }];
            }

        }
            break;

        default:
            break;
    }


}


//在步骤1createui的基础上,做了一些微调。
-(void)createUI{

    self.title = _reference.name;

    UIView *titleView = [UIView new];
    UIView *caredView = [UIView new];
    [self.view addSubview:caredView];
    UITextView *brifeView = [UITextView new];
    [self.view addSubview:brifeView];

    //self.view
    self.view.backgroundColor = [UIColor colorWithWhite:0.965 alpha:1.000];

    //thrm
    UIImageView *plantThrm = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"defalutPlantReferenceIcon"]];
    [self.view addSubview:plantThrm];
    [plantThrm mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.and.top.equalTo(self.view).with.offset(10);
    }];

    //title
       [self.view addSubview:titleView];
       UIImageView *bgTitleView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"bg-plant-reference-title"]];
    [titleView addSubview:bgTitleView];
    [titleView mas_makeConstraints:^(MASConstraintMaker *make) {

        make.right.equalTo(self.view.mas_right);
        make.left.equalTo(plantThrm.mas_right).with.offset(20);
        make.centerY.equalTo(plantThrm.mas_centerY);
   }];

    [bgTitleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(titleView);
    }];

    UILabel *title = [[UILabel alloc]init];
    title.textColor =  [UIColor whiteColor];
    title.font = [UIFont fontWithName:@"Heiti SC" size:26];
    title.text = _reference.name;
    [titleView addSubview:title];
    [title mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(titleView.mas_left).offset(10);
        make.width.equalTo(titleView.mas_width);
        make.centerY.equalTo(titleView.mas_centerY);
    }];

    //植物养护
    UILabel *caredTitle = [[UILabel alloc]init];
    caredTitle.textColor =  [UIColor colorWithRed:0.172 green:0.171 blue:0.219 alpha:1.000];
    caredTitle.font = [UIFont fontWithName:@"Heiti SC" size:10];
    caredTitle.text = @"植物养护";
    [self.view addSubview:caredTitle];
    [caredTitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(plantThrm.mas_bottom).with.offset(20);
        make.left.and.right.equalTo(self.view).with.offset(10);
        make.height.mas_equalTo(10);
    }];
    //植物养护 数据
    [self createIndexUIWithView:caredView];





    //将图层的边框设置为圆脚
    caredView.layer.cornerRadius = 5;
    caredView.layer.masksToBounds = YES;
    //给图层添加一个有色边框
    caredView.layer.borderWidth = 1;
    caredView.layer.borderColor = [[UIColor colorWithWhite:0.521 alpha:1.000] CGColor];
    caredView.backgroundColor = [UIColor whiteColor];


    [caredView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(caredTitle.mas_bottom).with.offset(5);
        make.left.equalTo(self.view.mas_left).with.offset(10);
        make.right.equalTo(self.view.mas_right).with.offset(-10);
        make.height.equalTo(brifeView);
    }];


    //植物简介
    UILabel *brifeTitle = [[UILabel alloc]init];
    brifeTitle.textColor =  [UIColor colorWithRed:0.172 green:0.171 blue:0.219 alpha:1.000];
    brifeTitle.font = [UIFont fontWithName:@"Heiti SC" size:10];
    brifeTitle.text = @"植物简介";
    [self.view addSubview:brifeTitle];
    [brifeTitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(caredView.mas_bottom).with.offset(20);
        make.left.and.right.equalTo(self.view).with.offset(10);
        make.height.mas_equalTo(10);
    }];



    //将图层的边框设置为圆脚
    brifeView.layer.cornerRadius = 5;
    brifeView.layer.masksToBounds = YES;
    //给图层添加一个有色边框
    brifeView.layer.borderWidth = 1;
    brifeView.layer.borderColor = [[UIColor colorWithWhite:0.447 alpha:1.000] CGColor];
    brifeView.backgroundColor = [UIColor whiteColor];

    //文字样式
//    brifeView.textColor = [UIColor colorWithWhite:0.352 alpha:1.000];
//    brifeView.font = [UIFont fontWithName:@"HeiTi SC" size:12];
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
    paragraphStyle.lineHeightMultiple = 20.f;
    paragraphStyle.maximumLineHeight = 25.f;
    paragraphStyle.minimumLineHeight = 15.f;
    paragraphStyle.alignment = NSTextAlignmentJustified;
    NSDictionary *attributes = @{ NSFontAttributeName:[UIFont systemFontOfSize:12], NSParagraphStyleAttributeName:paragraphStyle, NSForegroundColorAttributeName:[UIColor colorWithWhite:0.447 alpha:1.000]};
    //植物简介数据
    //brifeView.text = _reference.brief;
    brifeView.attributedText = [[NSAttributedString alloc] initWithString: _reference.brief attributes:attributes];



    [brifeView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(brifeTitle.mas_bottom).with.offset(5);
        make.left.equalTo(self.view.mas_left).with.offset(10);
        make.right.equalTo(self.view.mas_right).with.offset(-10);
        make.bottom.equalTo(self.view.mas_bottom).with.offset(-10);
        make.height.equalTo(caredView);
    }];


}

}


采坑: 一个UIScrollView 上add 一些view
view 布局用masonry,
如下, 高度 和底部 有一个算多余约束 无报错, 但是UIScrollView, contentOffset 页面已跳转就会变为0,0

1
2
3
4
5
6
7
self.hotVideoListTab.mas_makeConstraints { (make) in
make?.left.mas_equalTo()(self.scrollView)
make?.top.mas_equalTo()(self.scrollView)
// make?.bottom.mas_equalTo()(self.scrollView)
make?.height.mas_equalTo()(self.scrollView)
make?.width.mas_equalTo()(stageWidth)
}

Objective-C编码规范

Objective-C-Coding-Guidelines-In-Chinese

Objective-C编码规范,内容来自苹果、谷歌的文档翻译,『博爱』的编码经验和对其它资料的总结。
详情可前往:开源项目『BABaseProject』

转载请注明出处。

##概要

Objective-C是一门面向对象的动态编程语言,主要用于编写iOS和Mac应用程序。关于Objective-C的编码规范,苹果和谷歌都已经有很好的总结:

本文主要整合了对上述文档的翻译、作者自己的编程经验和其他的相关资料,为公司总结出一份通用的编码规范。

##代码格式

###使用空格而不是制表符Tab

不要在工程里使用Tab键,使用空格来进行缩进。在Xcode > Preferences > Text Editing将Tab和自动缩进都设置为4个空格。(Google的标准是使用两个空格来缩进,但这里还是推荐使用Xcode默认的设置。

###每一行的最大长度

同样的,在Xcode > Preferences > Text Editing > Page guide at column:中将最大行长设置为80,过长的一行代码将会导致可读性问题。

###函数的书写

一个典型的Objective-C函数应该是这样的:

1
2
3
- (void)writeVideoFrameWithData:(NSData *)frameData timeStamp:(int)timeStamp {
...
}

-(void)之间应该有一个空格,第一个大括号{的位置在函数所在行的末尾,同样应该有一个空格。(我司的C语言规范要求是第一个大括号单独占一行,但考虑到OC较长的函数名和苹果SDK代码的风格,还是将大括号放在行末。

如果一个函数有特别多的参数或者名称很长,应该将其按照:来对齐分行显示:

1
2
3
4
5
6
7
8
9
10
11
-(id)initWithModel:(IPCModle)model
ConnectType:(IPCConnectType)connectType
Resolution:(IPCResolution)resolution
AuthName:(NSString *)authName
Password:(NSString *)password
MAC:(NSString *)mac
AzIp:(NSString *)az_ip
AzDns:(NSString *)az_dns
Token:(NSString *)token
Email:(NSString *)email
Delegate:(id<IPCConnectHandlerDelegate>)delegate;

在分行时,如果第一段名称过短,后续名称可以以Tab的长度(4个空格)为单位进行缩进:

1
2
3
4
5
6
- (void)short:(GTMFoo *)theFoo
longKeyword:(NSRect)theRect
evenLongerKeyword:(float)theInterval
error:(NSError **)theError {
...
}

###函数调用

函数调用的格式和书写差不多,可以按照函数的长短来选择写在一行或者分成多行:

1
2
3
4
5
6
7
8
9
10
11
12
13
//写在一行
[myObject doFooWith:arg1 name:arg2 error:arg3];
//分行写,按照':'对齐
[myObject doFooWith:arg1
name:arg2
error:arg3];
//第一段名称过短的话后续可以进行缩进
[myObj short:arg1
longKeyword:arg2
evenLongerKeyword:arg3
error:arg4];

以下写法是错误的:

1
2
3
4
5
6
7
8
9
10
//错误,要么写在一行,要么全部分行
[myObject doFooWith:arg1 name:arg2
error:arg3];
[myObject doFooWith:arg1
name:arg2 error:arg3];
//错误,按照':'来对齐,而不是关键字
[myObject doFooWith:arg1
name:arg2
error:arg3];

###@public和@private标记符

@public和@private标记符应该以一个空格来进行缩进:

1
2
3
4
5
6
7
@interface MyClass : NSObject {
@public
...
@private
...
}
@end

###协议(Protocols)

在书写协议的时候注意用<>括起来的协议和类型名之间是没有空格的,比如IPCConnectHandler()<IPCPreconnectorDelegate>,这个规则适用所有书写协议的地方,包括函数声明、类声明、实例变量等等:

1
2
3
4
5
6
7
@interface MyProtocoledClass : NSObject<NSWindowDelegate> {
@private
id<MyFancyDelegate> _delegate;
}
- (void)setDelegate:(id<MyFancyDelegate>)aDelegate;
@end

###闭包(Blocks)

根据block的长度,有不同的书写规则:

  • 较短的block可以写在一行内。
  • 如果分行显示的话,block的右括号}应该和调用block那行代码的第一个非空字符对齐。
  • block内的代码采用4个空格的缩进。
  • 如果block过于庞大,应该单独声明成一个变量来使用。
  • ^(之间,^{之间都没有空格,参数列表的右括号){之间有一个空格。
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
//较短的block写在一行内
[operation setCompletionBlock:^{ [self onOperationDone]; }];
//分行书写的block,内部使用4空格缩进
[operation setCompletionBlock:^{
[self.delegate newDataAvailable];
}];
//使用C语言API调用的block遵循同样的书写规则
dispatch_async(_fileIOQueue, ^{
NSString* path = [self sessionFilePath];
if (path) {
// ...
}
});
//较长的block关键字可以缩进后在新行书写,注意block的右括号'}'和调用block那行代码的第一个非空字符对齐
[[SessionService sharedService]
loadWindowWithCompletionBlock:^(SessionWindow *window) {
if (window) {
[self windowDidLoad:window];
} else {
[self errorLoadingWindow];
}
}];
//较长的block参数列表同样可以缩进后在新行书写
[[SessionService sharedService]
loadWindowWithCompletionBlock:
^(SessionWindow *window) {
if (window) {
[self windowDidLoad:window];
} else {
[self errorLoadingWindow];
}
}];
//庞大的block应该单独定义成变量使用
void (^largeBlock)(void) = ^{
// ...
};
[_operationQueue addOperationWithBlock:largeBlock];
//在一个调用中使用多个block,注意到他们不是像函数那样通过':'对齐的,而是同时进行了4个空格的缩进
[myObject doSomethingWith:arg1
firstBlock:^(Foo *a) {
// ...
}
secondBlock:^(Bar *b) {
// ...
}];

###数据结构的语法糖

应该使用可读性更好的语法糖来构造NSArrayNSDictionary等数据结构,避免使用冗长的alloc,init方法。

如果构造代码写在一行,需要在括号两端留有一个空格,使得被构造的元素于与构造语法区分开来:

1
2
3
4
5
6
7
//正确,在语法糖的"[]"或者"{}"两端留有空格
NSArray *array = @[ [foo description], @"Another String", [bar description] ];
NSDictionary *dict = @{ NSForegroundColorAttributeName : [NSColor redColor] };
//不正确,不留有空格降低了可读性
NSArray* array = @[[foo description], [bar description]];
NSDictionary* dict = @{NSForegroundColorAttributeName: [NSColor redColor]};

如果构造代码不写在一行内,构造元素需要使用两个空格来进行缩进,右括号]或者}写在新的一行,并且与调用语法糖那行代码的第一个非空字符对齐:

1
2
3
4
5
6
7
8
9
10
11
NSArray *array = @[
@"This",
@"is",
@"an",
@"array"
];
NSDictionary *dictionary = @{
NSFontAttributeName : [NSFont fontWithName:@"Helvetica-Bold" size:12],
NSForegroundColorAttributeName : fontColor
};

构造字典时,字典的Key和Value与中间的冒号:都要留有一个空格,多行书写时,也可以将Value对齐:

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
//正确,冒号':'前后留有一个空格
NSDictionary *option1 = @{
NSFontAttributeName : [NSFont fontWithName:@"Helvetica-Bold" size:12],
NSForegroundColorAttributeName : fontColor
};
//正确,按照Value来对齐
NSDictionary *option2 = @{
NSFontAttributeName : [NSFont fontWithName:@"Arial" size:12],
NSForegroundColorAttributeName : fontColor
};
//错误,冒号前应该有一个空格
NSDictionary *wrong = @{
AKey: @"b",
BLongerKey: @"c",
};
//错误,每一个元素要么单独成为一行,要么全部写在一行内
NSDictionary *alsoWrong= @{ AKey : @"a",
BLongerKey : @"b" };
//错误,在冒号前只能有一个空格,冒号后才可以考虑按照Value对齐
NSDictionary *stillWrong = @{
AKey : @"b",
BLongerKey : @"c",
};

##命名规范

###基本原则

####清晰

命名应该尽可能的清晰和简洁,但在Objective-C中,清晰比简洁更重要。由于Xcode强大的自动补全功能,我们不必担心名称过长的问题。

1
2
3
4
5
6
7
8
9
10
11
//清晰
insertObject:atIndex:
//不清晰,insert的对象类型和at的位置属性没有说明
insert:at:
//清晰
removeObjectAtIndex:
//不清晰,remove的对象类型没有说明,参数的作用没有说明
remove:

不要使用单词的简写,拼写出完整的单词:

1
2
3
4
5
6
7
//清晰
destinationSelection
setBackgroundColor:
//不清晰,不要使用简写
destSel
setBkgdColor:

然而,有部分单词简写在Objective-C编码过程中是非常常用的,以至于成为了一种规范,这些简写可以在代码中直接使用,下面列举了部分:

1
2
3
4
5
6
7
8
9
10
alloc == Allocate max == Maximum
alt == Alternate min == Minimum
app == Application msg == Message
calc == Calculate nib == Interface Builder archive
dealloc == Deallocate pboard == Pasteboard
func == Function rect == Rectangle
horiz == Horizontal Rep == Representation (used in class name such as NSBitmapImageRep).
info == Information temp == Temporary
init == Initialize vert == Vertical
int == Integer

命名方法或者函数时要避免歧义

1
2
3
4
5
//有歧义,是返回sendPort还是send一个Port?
sendPort
//有歧义,是返回一个名字属性的值还是display一个name的动作?
displayName

####一致性

整个工程的命名风格要保持一致性,最好和苹果SDK的代码保持统一。不同类中完成相似功能的方法应该叫一样的名字,比如我们总是用count来返回集合的个数,不能在A类中使用count而在B类中使用getNumber

###使用前缀

如果代码需要打包成Framework给别的工程使用,或者工程项目非常庞大,需要拆分成不同的模块,使用命名前缀是非常有用的。

  • 前缀由大写的字母缩写组成,比如Cocoa中NS前缀代表Founation框架中的类,IB则代表Interface Builder框架。

  • 可以在为类、协议、函数、常量以及typedef宏命名的时候使用前缀,但注意不要为成员变量或者方法使用前缀,因为他们本身就包含在类的命名空间内。

  • 命名前缀的时候不要和苹果SDK框架冲突。

###命名类和协议(Class&Protocol)

类名以大写字母开头,应该包含一个名词来表示它代表的对象类型,同时可以加上必要的前缀,比如NSString, NSDate, NSScanner, NSApplication等等。

而协议名称应该清晰地表示它所执行的行为,而且要和类名区别开来,所以通常使用ing词尾来命名一个协议,比如NSCopying,NSLocking

有些协议本身包含了很多不相关的功能,主要用来为某一特定类服务,这时候可以直接用类名来命名这个协议,比如NSObject协议,它包含了id对象在生存周期内的一系列方法。

###命名头文件(Headers)

源码的头文件名应该清晰地暗示它的功能和包含的内容:

  • 如果头文件内只定义了单个类或者协议,直接用类名或者协议名来命名头文件,比如NSLocale.h定义了NSLocale类。

  • 如果头文件内定义了一系列的类、协议、类别,使用其中最主要的类名来命名头文件,比如NSString.h定义了NSStringNSMutableString

  • 每一个Framework都应该有一个和框架同名的头文件,包含了框架中所有公共类头文件的引用,比如Foundation.h

  • Framework中有时候会实现在别的框架中类的类别扩展,这样的文件通常使用被扩展的框架名+Additions的方式来命名,比如NSBundleAdditions.h

###命名方法(Methods)

Objective-C的方法名通常都比较长,这是为了让程序有更好地可读性,按苹果的说法“好的方法名应当可以以一个句子的形式朗读出来”

方法一般以小写字母打头,每一个后续的单词首字母大写,方法名中不应该有标点符号(包括下划线),有两个例外:

  • 可以用一些通用的大写字母缩写打头方法,比如PDF,TIFF等。
  • 可以用带下划线的前缀来命名私有方法或者类别中的方法。

如果方法表示让对象执行一个动作,使用动词打头来命名,注意不要使用dodoes这种多余的关键字,动词本身的暗示就足够了:

1
2
3
//动词打头的方法表示让对象执行一个动作
- (void)invokeWithTarget:(id)target;
- (void)selectTabViewItem:(NSTabViewItem *)tabViewItem;

如果方法是为了获取对象的一个属性值,直接用属性名称来命名这个方法,注意不要添加get或者其他的动词前缀:

1
2
3
4
5
6
//正确,使用属性名来命名方法
- (NSSize)cellSize;
//错误,添加了多余的动词前缀
- (NSSize)calcCellSize;
- (NSSize)getCellSize;

对于有多个参数的方法,务必在每一个参数前都添加关键词,关键词应当清晰说明参数的作用:

1
2
3
4
5
6
7
8
9
10
11
//正确,保证每个参数都有关键词修饰
- (void)sendAction:(SEL)aSelector toObject:(id)anObject forAllCells:(BOOL)flag;
//错误,遗漏关键词
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
//正确
- (id)viewWithTag:(NSInteger)aTag;
//错误,关键词的作用不清晰
- (id)taggedView:(int)aTag;

不要用and来连接两个参数,通常and用来表示方法执行了两个相对独立的操作(从设计上来说,这时候应该拆分成两个独立的方法):

1
2
3
4
5
//错误,不要使用"and"来连接参数
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//正确,使用"and"来表示两个相对独立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;

方法的参数命名也有一些需要注意的地方:

  • 和方法名类似,参数的第一个字母小写,后面的每一个单词首字母大写
  • 不要再方法名中使用类似pointer,ptr这样的字眼去表示指针,参数本身的类型足以说明
  • 不要使用只有一两个字母的参数名
  • 不要使用简写,拼出完整的单词

下面列举了一些常用参数名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...action:(SEL)aSelector
...alignment:(int)mode
...atIndex:(int)index
...content:(NSRect)aRect
...doubleValue:(double)aDouble
...floatValue:(float)aFloat
...font:(NSFont *)fontObj
...frame:(NSRect)frameRect
...intValue:(int)anInt
...keyEquivalent:(NSString *)charCode
...length:(int)numBytes
...point:(NSPoint)aPoint
...stringValue:(NSString *)aString
...tag:(int)anInt
...target:(id)anObject
...title:(NSString *)aString

###存取方法(Accessor Methods)

存取方法是指用来获取和设置类属性值的方法,属性的不同类型,对应着不同的存取方法规范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//属性是一个名词时的存取方法范式
- (type)noun;
- (void)setNoun:(type)aNoun;
//栗子
- (NSString *)title;
- (void)setTitle:(NSString *)aTitle;
//属性是一个形容词时存取方法的范式
- (BOOL)isAdjective;
- (void)setAdjective:(BOOL)flag;
//栗子
- (BOOL)isEditable;
- (void)setEditable:(BOOL)flag;
//属性是一个动词时存取方法的范式
- (BOOL)verbObject;
- (void)setVerbObject:(BOOL)flag;
//栗子
- (BOOL)showsAlpha;
- (void)setShowsAlpha:(BOOL)flag;

命名存取方法时不要将动词转化为被动形式来使用:

1
2
3
4
5
6
7
//正确
- (void)setAcceptsGlyphInfo:(BOOL)flag;
- (BOOL)acceptsGlyphInfo;
//错误,不要使用动词的被动形式
- (void)setGlyphInfoAccepted:(BOOL)flag;
- (BOOL)glyphInfoAccepted;

可以使用can,should,will等词来协助表达存取方法的意思,但不要使用do,和does

1
2
3
4
5
6
7
8
9
//正确
- (void)setCanHide:(BOOL)flag;
- (BOOL)canHide;
- (void)setShouldCloseDocument:(BOOL)flag;
- (BOOL)shouldCloseDocument;
//错误,不要使用"do"或者"does"
- (void)setDoesAcceptGlyphInfo:(BOOL)flag;
- (BOOL)doesAcceptGlyphInfo;

为什么Objective-C中不适用get前缀来表示属性获取方法?因为get在Objective-C中通常只用来表示从函数指针返回值的函数:

1
2
//三个参数都是作为函数的返回值来使用的,这样的函数名可以使用"get"前缀
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase;

###命名委托(Delegate)

当特定的事件发生时,对象会触发它注册的委托方法。委托是Objective-C中常用的传递消息的方式。委托有它固定的命名范式。

一个委托方法的第一个参数是触发它的对象,第一个关键词是触发对象的类名,除非委托方法只有一个名为sender的参数:

1
2
3
4
5
6
//第一个关键词为触发委托的类名
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(int)row;
- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename;
//当只有一个"sender"参数时可以省略类名
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender;

根据委托方法触发的时机和目的,使用should,will,did等关键词

1
2
3
4
5
- (void)browserDidScroll:(NSBrowser *)sender;
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;、
- (BOOL)windowShouldClose:(id)sender;

###集合操作类方法(Collection Methods)

有些对象管理着一系列其它对象或者元素的集合,需要使用类似“增删查改”的方法来对集合进行操作,这些方法的命名范式一般为:

1
2
3
4
5
6
7
8
9
//集合操作范式
- (void)addElement:(elementType)anObj;
- (void)removeElement:(elementType)anObj;
- (NSArray *)elements;
//栗子
- (void)addLayoutManager:(NSLayoutManager *)obj;
- (void)removeLayoutManager:(NSLayoutManager *)obj;
- (NSArray *)layoutManagers;

注意,如果返回的集合是无序的,使用NSSet来代替NSArray。如果需要将元素插入到特定的位置,使用类似于这样的命名:

1
2
- (void)insertLayoutManager:(NSLayoutManager *)obj atIndex:(int)index;
- (void)removeLayoutManagerAtIndex:(int)index;

如果管理的集合元素中有指向管理对象的指针,要设置成weak类型以防止引用循环。

下面是SDK中NSWindow类的集合操作方法:

1
2
3
4
5
- (void)addChildWindow:(NSWindow *)childWin ordered:(NSWindowOrderingMode)place;
- (void)removeChildWindow:(NSWindow *)childWin;
- (NSArray *)childWindows;
- (NSWindow *)parentWindow;
- (void)setParentWindow:(NSWindow *)window;

###命名函数(Functions)

在很多场合仍然需要用到函数,比如说如果一个对象是一个单例,那么应该使用函数来代替类方法执行相关操作。

函数的命名和方法有一些不同,主要是:

  • 函数名称一般带有缩写前缀,表示方法所在的框架。
  • 前缀后的单词以“驼峰”表示法显示,第一个单词首字母大写。

函数名的第一个单词通常是一个动词,表示方法执行的操作:

1
2
NSHighlightRect
NSDeallocateObject

如果函数返回其参数的某个属性,省略动词:

1
2
unsigned int NSEventMaskFromType(NSEventType type)
float NSHeight(NSRect aRect)

如果函数通过指针参数来返回值,需要在函数名中使用Get

1
const char *NSGetSizeAndAlignment(const char *typePtr, unsigned int *sizep, unsigned int *alignp)

函数的返回类型是BOOL时的命名:

1
BOOL NSDecimalIsNotANumber(const NSDecimal *decimal)

###命名属性和实例变量(Properties&Instance Variables)

属性和对象的存取方法相关联,属性的第一个字母小写,后续单词首字母大写,不必添加前缀。属性按功能命名成名词或者动词:

1
2
3
4
5
//名词属性
@property (strong) NSString *title;
//动词属性
@property (assign) BOOL showsAlpha;

属性也可以命名成形容词,这时候通常会指定一个带有is前缀的get方法来提高可读性:

1
@property (assign, getter=isEditable) BOOL editable;

命名实例变量,在变量名前加上_前缀(有些有历史的代码会将_放在后面),其它和命名属性一样:

1
2
3
@implementation MyClass {
BOOL _showsTitle;
}

一般来说,类需要对使用者隐藏数据存储的细节,所以不要将实例方法定义成公共可访问的接口,可以使用@private@protected前缀。

按苹果的说法,不建议在除了initdealloc方法以外的地方直接访问实例变量,但很多人认为直接访问会让代码更加清晰可读,只在需要计算或者执行操作的时候才使用存取方法访问,我就是这种习惯,所以这里不作要求。

###命名常量(Constants)

如果要定义一组相关的常量,尽量使用枚举类型(enumerations),枚举类型的命名规则和函数的命名规则相同。
建议使用 NS_ENUMNS_OPTIONS 宏来定义枚举类型,参见官方的 Adopting Modern Objective-C 一文:

1
2
3
4
5
6
7
//定义一个枚举
typedef NS_ENUM(NSInteger, NSMatrixMode) {
NSRadioModeMatrix,
NSHighlightModeMatrix,
NSListModeMatrix,
NSTrackModeMatrix
};

定义bit map:

1
2
3
4
5
6
7
typedef NS_OPTIONS(NSUInteger, NSWindowMask) {
NSBorderlessWindowMask = 0,
NSTitledWindowMask = 1 << 0,
NSClosableWindowMask = 1 << 1,
NSMiniaturizableWindowMask = 1 << 2,
NSResizableWindowMask = 1 << 3
};

使用const定义浮点型或者单个的整数型常量,如果要定义一组相关的整数常量,应该优先使用枚举。常量的命名规范和函数相同:

1
const float NSLightGray;

不要使用#define宏来定义常量,如果是整型常量,尽量使用枚举,浮点型常量,使用const定义。#define通常用来给编译器决定是否编译某块代码,比如常用的:

1
#ifdef DEBUG

注意到一般由编译器定义的宏会在前后都有一个__,比如__MACH__

###命名通知(Notifications)

通知常用于在模块间传递消息,所以通知要尽可能地表示出发生的事件,通知的命名范式是:

[触发通知的类名] + [Did | Will] + [动作] + Notification

栗子:

1
2
3
4
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification
NSColorPanelColorDidChangeNotification

##注释

读没有注释代码的痛苦你我都体会过,好的注释不仅能让人轻松读懂你的程序,还能提升代码的逼格。注意注释是为了让别人看懂,而不是仅仅你自己。

###文件注释

每一个文件都必须写文件注释,文件注释通常包含

  • 文件所在模块
  • 作者信息
  • 历史版本信息
  • 版权信息
  • 文件包含的内容,作用

一段良好文件注释的栗子:

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
/*******************************************************************************
Copyright (C), 2011-2013, Andrew Min Chang
File name: AMCCommonLib.h
Author: Andrew Chang (Zhang Min)
E-mail: LaplaceZhang@126.com
Description:
This file provide some covenient tool in calling library tools. One can easily include
library headers he wants by declaring the corresponding macros.
I hope this file is not only a header, but also a useful Linux library note.
History:
2012-??-??: On about come date around middle of Year 2012, file created as "commonLib.h"
2012-08-20: Add shared memory library; add message queue.
2012-08-21: Add socket library (local)
2012-08-22: Add math library
2012-08-23: Add socket library (internet)
2012-08-24: Add daemon function
2012-10-10: Change file name as "AMCCommonLib.h"
2012-12-04: Add UDP support in AMC socket library
2013-01-07: Add basic data type such as "sint8_t"
2013-01-18: Add CFG_LIB_STR_NUM.
2013-01-22: Add CFG_LIB_TIMER.
2013-01-22: Remove CFG_LIB_DATA_TYPE because there is already AMCDataTypes.h
Copyright information:
This file was intended to be under GPL protocol. However, I may use this library
in my work as I am an employee. And my company may require me to keep it secret.
Therefore, this file is neither open source nor under GPL control.
********************************************************************************/

文件注释的格式通常不作要求,能清晰易读就可以了,但在整个工程中风格要统一。

###代码注释

好的代码应该是“自解释”(self-documenting)的,但仍然需要详细的注释来说明参数的意义、返回值、功能以及可能的副作用。

方法、函数、类、协议、类别的定义都需要注释,推荐采用Apple的标准注释风格,好处是可以在引用的地方alt+点击自动弹出注释,非常方便。

有很多可以自动生成注释格式的插件,推荐使用VVDocumenter

Screenshot

一些良好的注释:

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
/**
* Create a new preconnector to replace the old one with given mac address.
* NOTICE: We DO NOT stop the old preconnector, so handle it by yourself.
*
* @param type Connect type the preconnector use.
* @param macAddress Preconnector's mac address.
*/
- (void)refreshConnectorWithConnectType:(IPCConnectType)type Mac:(NSString *)macAddress;
/**
* Stop current preconnecting when application is going to background.
*/
-(void)stopRunning;
/**
* Get the COPY of cloud device with a given mac address.
*
* @param macAddress Mac address of the device.
*
* @return Instance of IPCCloudDevice.
*/
-(IPCCloudDevice *)getCloudDeviceWithMac:(NSString *)macAddress;
// A delegate for NSApplication to handle notifications about app
// launch and shutdown. Owned by the main app controller.
@interface MyAppDelegate : NSObject {
...
}
@end

协议、委托的注释要明确说明其被触发的条件:

1
2
/** Delegate - Sent when failed to init connection, like p2p failed. */
-(void)initConnectionDidFailed:(IPCConnectHandler *)handler;

如果在注释中要引用参数名或者方法函数名,使用||将参数或者方法括起来以避免歧义:

1
2
3
// Sometimes we need |count| to be less than zero.
// Remember to call |StringWithoutSpaces("foo bar baz")|

定义在头文件里的接口方法、属性必须要有注释!

##编码风格

每个人都有自己的编码风格,这里总结了一些比较好的Cocoa编程风格和注意点。

###不要使用new方法

尽管很多时候能用new代替alloc init方法,但这可能会导致调试内存时出现不可预料的问题。Cocoa的规范就是使用alloc init方法,使用new会让一些读者困惑。

###Public API要尽量简洁

共有接口要设计的简洁,满足核心的功能需求就可以了。不要设计很少会被用到,但是参数极其复杂的API。如果要定义复杂的方法,使用类别或者类扩展。

####import和#include

#import是Cocoa中常用的引用头文件的方式,它能自动防止重复引用文件,什么时候使用#import,什么时候使用#include呢?

  • 当引用的是一个Objective-C或者Objective-C++的头文件时,使用#import
  • 当引用的是一个C或者C++的头文件时,使用#include,这时必须要保证被引用的文件提供了保护域(#define guard)。

栗子:

1
2
3
4
#import <Cocoa/Cocoa.h>
#include <CoreFoundation/CoreFoundation.h>
#import "GTMFoo.h"
#include "base/basictypes.h"

为什么不全部使用#import呢?主要是为了保证代码在不同平台间共享时不出现问题。

###引用框架的根头文件

上面提到过,每一个框架都会有一个和框架同名的头文件,它包含了框架内接口的所有引用,在使用框架的时候,应该直接引用这个根头文件,而不是其它子模块的头文件,即使是你只用到了其中的一小部分,编译器会自动完成优化的。

1
2
3
4
5
6
//正确,引用根头文件
#import <Foundation/Foundation.h>
//错误,不要单独引用框架内的其它头文件
#import <Foundation/NSArray.h>
#import <Foundation/NSString.h>

###BOOL的使用

BOOL在Objective-C中被定义为signed char类型,这意味着一个BOOL类型的变量不仅仅可以表示YES(1)和NO(0)两个值,所以永远不要将BOOL类型变量直接和YES比较:

1
2
3
4
5
6
7
8
9
//错误,无法确定|great|的值是否是YES(1),不要将BOOL值直接与YES比较
BOOL great = [foo isGreat];
if (great == YES)
// ...be great!
//正确
BOOL great = [foo isGreat];
if (great)
// ...be great!

同样的,也不要将其它类型的值作为BOOL来返回,这种情况下,BOOL变量只会取值的最后一个字节来赋值,这样很可能会取到0(NO)。但是,一些逻辑操作符比如&&,||,!的返回是可以直接赋给BOOL的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//错误,不要将其它类型转化为BOOL返回
- (BOOL)isBold {
return [self fontTraits] & NSFontBoldTrait;
}
- (BOOL)isValid {
return [self stringValue];
}
//正确
- (BOOL)isBold {
return ([self fontTraits] & NSFontBoldTrait) ? YES : NO;
}
//正确,逻辑操作符可以直接转化为BOOL
- (BOOL)isValid {
return [self stringValue] != nil;
}
- (BOOL)isEnabled {
return [self isValid] && [self isBold];
}

另外BOOL类型可以和_Bool,bool相互转化,但是不能Boolean转化。

###使用ARC

除非想要兼容一些古董级的机器和操作系统,我们没有理由放弃使用ARC。在最新版的Xcode(6.2)中,ARC是自动打开的,所以直接使用就好了。

###在init和dealloc中不要用存取方法访问实例变量

init``dealloc方法被执行时,类的运行时环境不是处于正常状态的,使用存取方法访问变量可能会导致不可预料的结果,因此应当在这两个方法内直接访问实例变量。

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
//正确,直接访问实例变量
- (instancetype)init {
self = [super init];
if (self) {
_bar = [[NSMutableString alloc] init];
}
return self;
}
- (void)dealloc {
[_bar release];
[super dealloc];
}
//错误,不要通过存取方法访问
- (instancetype)init {
self = [super init];
if (self) {
self.bar = [NSMutableString string];
}
return self;
}
- (void)dealloc {
self.bar = nil;
[super dealloc];
}

###按照定义的顺序释放资源

在类或者Controller的生命周期结束时,往往需要做一些扫尾工作,比如释放资源,停止线程等,这些扫尾工作的释放顺序应当与它们的初始化或者定义的顺序保持一致。这样做是为了方便调试时寻找错误,也能防止遗漏。

###保证NSString在赋值时被复制

NSString非常常用,在它被传递或者赋值时应当保证是以复制(copy)的方式进行的,这样可以防止在不知情的情况下String的值被其它对象修改。

1
2
3
- (void)setFoo:(NSString *)aFoo {
_foo = [aFoo copy];
}

###使用NSNumber的语法糖

使用带有@符号的语法糖来生成NSNumber对象能使代码更简洁:

1
2
3
4
5
6
NSNumber *fortyTwo = @42;
NSNumber *piOverTwo = @(M_PI / 2);
enum {
kMyEnum = 2;
};
NSNumber *myEnum = @(kMyEnum);

###nil检查

因为在Objective-C中向nil对象发送命令是不会抛出异常或者导致崩溃的,只是完全的“什么都不干”,所以,只在程序中使用nil来做逻辑上的检查。

另外,不要使用诸如nil == Object或者Object == nil的形式来判断。

1
2
3
4
5
6
7
8
9
//正确,直接判断
if (!objc) {
...
}
//错误,不要使用nil == Object的形式
if (nil == objc) {
...
}

###属性的线程安全

定义一个属性时,编译器会自动生成线程安全的存取方法(Atomic),但这样会大大降低性能,特别是对于那些需要频繁存取的属性来说,是极大的浪费。所以如果定义的属性不需要线程保护,记得手动添加属性关键字nonatomic来取消编译器的优化。

###点分语法的使用

不要用点分语法来调用方法,只用来访问属性。这样是为了防止代码可读性问题。

1
2
3
4
5
6
7
8
//正确,使用点分语法访问属性
NSString *oldName = myObject.name;
myObject.name = @"Alice";
//错误,不要用点分语法调用方法
NSArray *array = [NSArray arrayWithObject:@"hello"];
NSUInteger numberOfItems = array.count;
array.release;

###Delegate要使用弱引用

一个类的Delegate对象通常还引用着类本身,这样很容易造成引用循环的问题,所以类的Delegate属性要设置为弱引用。

1
2
/** delegate */
@property (nonatomic, weak) id <IPCConnectHandlerDelegate> delegate;