利他才能利己


  • 首页

  • 标签

  • 归档

  • 搜索

NSString、NSMutableString 可变与不可变的那些事儿

发表于 2018-12-30 | 分类于 iOS |

简介

这篇文章的主要包含如下内容:

  • 可变对象和不可变对象
  • NSString 的 copy 和 mutableCopy
  • NSMutableString 的 copy 和 mutableCopy
  • property 中 copy、strong 修饰 NSString
  • property 中 copy、strong 修饰 NSMutableString

很多 iOS 开发的朋友会争论一个问题,我用 copy 和 strong 来修饰 NSString 对象都是一样的效果,在大部分情况下,这二者确实是没有区别,但是在特殊情况下,二者截然不同,所以我们必须搞清楚里面的道道。

我已经尽力简化了这篇文章的内容了,但依然需要你花个15分钟左右的时间,所以当你心情不错又没有其他事情的情况下,就可以来阅读了。

可变对象和不可变对象

在 Objective-C 中最常用来处理字符串的是 NSString 与 NSMutableString 这两个类,NSString 被创建赋值后字符串的内容与长度不能再做动态的修改,除非重新给这个字符串赋值。而 NSMutableString 被创建赋值后可以动态的修改字符串的内容。

那么简单来说,可变对象是指,对象的内容是可变的,例如 NSMutableString 对象。不可变的对象则相反,表示其内容不可变,例如 NSString 对象。

可变与不可变是针对对象来说的。在实际开发中,要根据实际的业务场景来选择使用可变还是不可变对象。今天我们只讨论 Objective-C 中 NSString 与 NSMutableString 这两个类,关于其他集合类的可变与不可变特性,后面专门再来写文章分享。

NSString 的 copy 和 mutableCopy

在 NSString.h 中,我们可以看到其定义如下:

1
@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>

NSString.h 本身实现了 NSCopying, NSMutableCopying 这两个协议,协议的定义如下内容所示:

1
2
3
4
5
6
7
8
9
10
11
@protocol NSCopying

- (id)copyWithZone:(nullable NSZone *)zone;

@end

@protocol NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

@end

也就是说,我们可以针对 NSString 对象进行 copy 和 mutableCopy 的操作,妥妥的。

举一个简单的栗子,示例代码如下:

1
2
3
4
5
6
7
8
NSString *name = @"www.";
NSLog(@"name addr: %p, name content: %@", name, name);

NSString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);

NSString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);

输出结果,如下:

1
2
3
name  addr: 0x10090ecf8, name  content: www.
name1 addr: 0x10090ecf8, name1 content: www.
name2 addr: 0x10090ecf8, name2 content: www.

从输出结果可以看出,三个对象的内容和地址都是一样的,经过 name 对象 copy 后的 name2 与 name 还是指向同一块内存地址。

在断点过程中,发现无论是 name 还是 name1、name2 对象,其都是 ConstantString,表明三者都是不可变对象,如下图所示:

从这张图也说明了一个问题,NSString 对象经过 copy 后仍然是不可变对象。

紧接着,我们再来看看 mutableCopy 的使用情况,例子如下:

1
2
3
4
5
6
7
8
9
10
11
NSString *name = @"www.";
NSLog(@"name addr: %p, name content: %@", name, name);

NSString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);

NSString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);

id name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

对象 name3 是经过 name 对象 mutableCopy 后的,这个时候因为我不确定 name3 到底是可变的还是不可变的,所以采用了 id 来修饰 name3 对象。

可以看一下输出内容:

1
2
3
4
name  addr: 0x104a6acf8, name  content: www.
name1 addr: 0x104a6acf8, name1 content: www.
name2 addr: 0x104a6acf8, name2 content: www.
name3 addr: 0x1c0052cf0, name3 content: www.

可以看出,name3 的地址变了,再看一下断点的截图:

充分说明了 name3 经过不可变的 name 进行mutableCopy 后变成了可变对象。那么可以将上面的示例代码稍作修改:

1
2
3
4
NSMutableString *name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);
[name3 appendString:@"veryitman.com"];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

从下面的输出结果也充分说明了 name3 经过不可变的 name 进行 mutableCopy 后变成了可变对象。输出结果如下:

1
name3 addr: 0x1d0058270, name3 content: www.veryitman.com

结论 1:

  • 不可变的 NSString 对象经过 copy 后,还是不可变对象。
  • 不可变的 NSString 对象经过 mutableCopy 后,变成了可变的 NSMutableString 对象。

NSMutableString 的 copy 和 mutableCopy

类 NSMutableString 继承自 NSString 的,其当然也是实现了 NSCopying, NSMutableCopying 这两个协议的。

1
@interface NSMutableString : NSString

我们还是看例子,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSMutableString *name = [NSMutableString stringWithString:@"www."];
NSLog(@"name addr: %p, name content: %@", name, name);

// 简单赋值
NSMutableString *name1 = name;
NSLog(@"name1 addr: %p, name1 content: %@", name1, name1);

// 使用 copy
id name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);

// 使用 mutableCopy
id name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);

因为事先我们不知道 NSMutableString 经过 copy 和 mutableCopy 之后到底会变成可变还是不可变,上面的例子暂时将 name2 和 name3 用 id 来表示。

断点截图如下:

结合一下输出的日志:

1
2
3
4
name  addr: 0x1d044a980, name  content: www.
name1 addr: 0x1d044a980, name1 content: www.
name2 addr: 0xa0000002e7777774, name2 content: www.
name3 addr: 0x1d044a5f0, name3 content: www.

可以看出 name2 是一个不可变的 NSString 对象, name、name1 和 name3 都是可变的 NSMutableString 对象。

也可以从另外一个角度来验证一下上面的说法,我们修改一下代码:

1
2
3
4
5
6
7
NSMutableString *name2 = [name copy];
NSLog(@"name2 addr: %p, name2 content: %@", name2, name2);
[name2 appendString:@"veryitman.com"];

NSMutableString *name3 = [name mutableCopy];
NSLog(@"name3 addr: %p, name3 content: %@", name3, name3);
[name3 appendString:@"veryitman.com"];

运行后,可以看到,代码 [name2 appendString:@"veryitman.com"] 这里会引起 Crash,报错内容如下:

1
2
3
4
5
 -[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xa0000002e7777774

*** Terminating app due to uncaught exception 'NSInvalidArgumentException'

reason: '-[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xa0000002e7777774'

也充分说明了,name2 是一个不可变的 NSString 对象。

结论 2:

  • 可变的 NSMutableString 对象经过 copy 后,会变成不可变的 NSString 对象。
  • 可变的 NSMutableString 对象经过 mutableCopy 后,仍然是可变的 NSMutableString 对象。

copy、strong 修饰 NSString

创建 Employee 文件,如下:

1
2
3
4
5
@interface Employee : NSObject

@property (nonatomic, copy) NSString *userName;

@end

其 userName 属性是 copy。

使用示例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Employee *employee = [Employee new];
employee.userName = @"John";
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// 创建一个可变对象
NSMutableString *newUserName = [[NSMutableString alloc] initWithString:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);

// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// 试图改变 newUserName 的内容,看 employee.userName 的内容是否改变
[newUserName appendString:@"_hello"];

// newUserName 的内容被改变成了 new_user_name_hello
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);

// employee.userName 的内容未变化
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// Crash,因为 employee.userName 还是不可变对象
// [(NSMutableString *)(employee.userName) appendString:@"nana"];

在上面的示例中,故意将 NSMutableString 对象 newUserName 赋值给不可变的 NSString 对象 employee.userName,看一下输出结果,如下:

1
2
3
4
5
6
7
8
9
--before-- employee.userName addr: 0x100096cf8, employee.userName content: John

newUserName addr: 0x174070a00, newUserName content: new_user_name

---after1--- employee.userName addr: 0x174023b80, employee.userName content: new_user_name

newUserName addr: 0x174070a00, newUserName content: new_user_name_hello

---after2--- employee.userName addr: 0x174023b80, employee.userName content: new_user_name

按照

1
可变的 `NSMutableString` 对象经过 `copy` 后,会变成不可变的 `NSString` 对象。

这个结论来看,employee.userName 肯定是不可变的对象,即使改变 newUserName 的内容也不会影响 employee.userName 这个对象的内容。

那么,我们将 employee.userName 的属性修饰符 copy 改为 strong,又会是什么样子呢?

我们修改两处代码

Employee.h

1
2
3
4
5
6
7
@interface Employee : NSObject

//@property (nonatomic, copy) NSString *userName;

@property (nonatomic, strong) NSString *userName;

@end

示例代码,只是打开之前会 crash 的部分

1
2
3
// employee.userName 经过 strong 修饰过后, 彻底变成了可变对象
[(NSMutableString *)(employee.userName) appendString:@"_oc"];
NSLog(@"---after3--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

看一下输出日志:

1
2
3
4
5
6
7
8
9
10
11
--before-- employee.userName addr: 0x1000a6cf8, employee.userName content: John

newUserName addr: 0x17426b280, newUserName content: new_user_name

---after1--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name

newUserName addr: 0x17426b280, newUserName content: new_user_name_hello

---after2--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name_hello

---after3--- employee.userName addr: 0x17426b280, employee.userName content: new_user_name_hello_oc

可以看到 employee.userName 最终和 newUserName 的地址、内容完全相同了,彻底变成了可变对象。

另外,如果不是将可变的 NSMutableString 对象赋值给不可变的 NSString 对象,换句话说,NSString 对 NSString 赋值,那么使用 strong 和 copy 效果都是一样的。

示例代码(无论 employee.userName 使用 strong 还是 copy,效果都是 employee.userName 不可变的):

1
2
3
4
5
6
7
8
9
10
11
Employee *employee = [Employee new];
employee.userName = @"John";
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// 创建一个不可变对象
NSString *newUserName = @"new_user_name";
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);

// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

copy、strong 修饰 NSMutableString

在 property 的修饰语中,只有 copy 修饰语而没有 mutableCopy 的修饰语。

Employee.h

1
2
3
4
5
@interface Employee : NSObject

@property (nonatomic, copy) NSMutableString *userName;

@end

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Employee *employee = [Employee new];
employee.userName = [NSMutableString stringWithString:@"John"];
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// 创建一个可变对象
NSMutableString *newUserName = [NSMutableString stringWithFormat:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);

// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// employee.userName 虽然是 NSMutableString 对象,但经过 copy 修饰过后,仍然是不可变对象
// 所以,运行到这里会引起 crash
[employee.userName appendString:@"_oc"];
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

可以看出 copy 后的的可变对象还是不可变的。

那么,我们将 employee.userName 的属性修饰符 copy 改为 strong,又会是什么样子呢?

Employee.h

1
2
3
4
5
@interface Employee : NSObject

@property (nonatomic, strong) NSString *userName;

@end

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Employee *employee = [Employee new];
employee.userName = [NSMutableString stringWithString:@"John"];
NSLog(@"--before-- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// 创建一个可变对象
NSMutableString *newUserName = [NSMutableString stringWithFormat:@"new_user_name"];
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);

// 将一个新的对像赋值给 employee.userName,此时 employee.userName 的地址肯定会变化
employee.userName = newUserName;
NSLog(@"---after1--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

// employee.userName 虽然是 NSMutableString 对象,但经过 strong 修饰过后,变成了可变对象
[employee.userName appendString:@"_hello"];
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

[newUserName appendString:@"_oc"];
// newUserName 的内容被改变成了 new_user_name_hello_oc
NSLog(@"newUserName addr: %p, newUserName content: %@", newUserName, newUserName);
// employee.userName 的内容发生了变化
NSLog(@"---after2--- employee.userName addr: %p, employee.userName content: %@", employee.userName, employee.userName);

经过 strong 修饰后,可变的 NSMutableString 对象还是可变的对象。

在这个部分的开始,说过在 property 中没有 mutableCopy 的修饰语,那么我们能否达到 mutableCopy 的效果呢?

很显然是可以的,我们可以重写属性的 set 方法,改造一下 Employee 的代码,如下:

Employee.h

1
2
3
4
5
@interface Employee : NSObject

@property (nonatomic, copy) NSMutableString *userName;

@end

Employee.m

1
2
3
4
5
6
7
8
9
10
#import "Employee.h"

@implementation Employee

- (void)setUserName:(NSMutableString *)userName
{
_userName = [userName mutableCopy];
}

@end

这样,就达到了和是 strong 修饰语一样的效果了。

大家,可以使用同样的方法来实践一下 NSArray、NSMutableArry 等集合数据的 copy 以及 mutableCopy 的效果了。

小结

  • 不可变的 NSString 对象经过 copy 后,还是不可变对象。

  • 不可变的 NSString 对象经过 mutableCopy 后,变成了可变的 NSMutableString 对象。

  • 可变的 NSMutableString 对象经过 copy 后,会变成不可变的 NSString 对象。

  • 可变的 NSMutableString 对象经过 mutableCopy 后,仍然是可变的 NSMutableString 对象。

  • 不可变的 NSString 对象在 property 中,尽量使用 copy 来修饰,因为使用 strong 修饰符可变字符串如果给不可变字符串赋值后,会导致你原本预期发生了变化,除非你有特殊的目的才使用 strong 修饰符。

  • 可变的 NSMutableString 对象在 property 中,尽量使用 strong 来修饰,除非你有特殊的目的才使用 copy 修饰符。

  • 虽然在 property 中没有 mutableCopy 修饰符,但是可以通过重写其 set 方法来达到目的。


扫码关注,你我就各多一个朋友~

OC: self

发表于 2018-12-23 | 分类于 iOS |

为了更好的说明 Objective-C 中的 self,我们先从 Java 的 this 关键字开始来引入话题。

Java 中的 this

在 Java 中 this 关键字表示当前类对象,其只能在类的非静态方法中使用,静态方法和静态的代码块中绝对不能出现 this,this 只和特定的对象关联,而不和类关联,同一个类的不同对象有不同的 this.

先看一个 Java 示例,能说明上面的问题,示例如下:

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
// 静态代码块中也不可以使用 this
// Error: non-static variable this cannot be referenced from a static context
static {
// this.eat();
}

public void play() {
System.out.println("play()");

// this 调用类方法
this.eat();

// this 调用实例方法
this.finish();
}

public static void eat() {
System.out.println("static eat()");

// 不可以在类方法中使用 this
// Error: non-static variable this cannot be referenced from a static context
// this.play();
// System.out.println(this);
}

public void finish() {
System.out.println("finish()");
}

通过实际的 Java 例子,基本表明了在静态方法和实例方法中 this 的使用场景。

Objective-C 中的 self

在 Objective-C 中,self 是一个比较特殊的对象,它既可以是实例对象也可以是类对象,有点类似于上面 Java 中的 this 关键字。

下面结合实际例子,来说明 self 这个关键字。

1、实例方法中的 self

实例方法中的 self 可以直接调用实例方法但不可以直接调用类方法,如下示例中,调用实例方法 finish没有问题,而调用类方法 eat 直接报编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)play
{
NSLog(@"---------------- '- (void)play' ------------------");
NSLog(@"self: %@, self -> %p", self, self);

// 无法使用 self 来调用类方法
// Error: No visible @interface for 'MZPerson' declares the selector 'eat'
// [self eat];

// 调用实例方法
[self finish];
}

+ (void)eat
{
NSLog(@"---------------- '+ (void)eat' ------------------");
}

- (void)finish
{
NSLog(@"--------------- '- (void)finish' ----------------");
}

我们知道,在实例方法中可以直接通过``[类 类方法]的方式来调用类方法,那么如果想在实例方法中使用self` 关键字,如何办呢?

很简单,使用 [self class] 即可。

1
2
3
4
5
6
7
- (void)play
{
NSLog(@"---------------- '- (void)play' ------------------");
NSLog(@"self: %@, self -> %p", self, self);

[[self class] eat];
}

关于 class 后续再分享给大家,这里只需要知道可以这么使用就好了。

2、类方法中的 self

这个跟 Java 的 this 有点不一样,上面的 Java 示例中我们可以看到无论是打印 this 还是使用 this 调用方法都不可以,但是在 Objective-C 中却可以使用 self,只是不能使用 self 来调用实例方法和实例变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (void)eat
{
NSLog(@"---------------- '+ (void)eat' ------------------");

// No known class method for selector 'finish'
// [self finish];

// 调用类方法
[self beat];

// 打印 self
NSLog(@"self: %@", self);
}

+ (void)beat
{
NSLog(@"---------------- '+ (void)beat' ------------------");
}

那么为什么在类方法中可以使用 self 呢?

别着急,接着往下看。

3、实例和类方法中的 self 区别

其实,在类方法中,self 表示当前类对象,在实例方法中 self 表示实例对象,这个是本质区别,务必要理解透彻。

举个例子,如下:

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
- (void)play
{
NSLog(@"---------------- '- (void)play' ------------------");
NSLog(@"self: %@, self -> %p", self, self);

// 打印对应的类地址
NSLog(@"self class: %p", [self class]);

[[self class] eat];

}

+ (void)eat
{
NSLog(@"---------------- '+ (void)eat' ------------------");

// No known class method for selector 'finish'
// [self finish];

// 打印 self 地址
NSLog(@"self: %p", self);

// 调用类方法
[self beat];
}

+ (void)beat
{
NSLog(@"---------------- '+ (void)beat' ------------------");

// 打印 self 地址
NSLog(@"self: %p", self);
}

在实例方法 play 中打印类地址,在类方法 eat 和 beat 中打印 self 的地址,输出结果是一样的,都是 0x10adb3f98 这个地址。

1
2
3
4
5
6
7
---------------- '- (void)play' ------------------
self: <MZPerson: 0x6000000d8f90>, self -> 0x6000000d8f90
self class: 0x10adb3f98
---------------- '+ (void)eat' ------------------
self: 0x10adb3f98
---------------- '+ (void)beat' ------------------
self: 0x10adb3f98

为了更好的说明,我给大家再举一个形象的例子帮助大家理解。

在 MZPerson 中声明两个方法,方法同名,一个是实例方法,另一个是类方法,如下:

1
2
3
4
5
6
7
8
9
10
11
@interface MZPerson : NSObject

- (void)play;

+ (void)play;

+ (void)eat;

- (void)finish;

@end
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
@implementation MZPerson

- (void)play
{
NSLog(@"---------------- '- (void)play' ------------------");
}

+ (void)play
{
NSLog(@"---------------- '+ (void)play' ------------------");
}

+ (void)eat
{
NSLog(@"---------------- '+ (void)eat' ------------------");

[self play];
}

- (void)finish
{
NSLog(@"---------------- '- (void)finish' ------------------");
[self play];
}

@end

在类方法 eat 中调用 [self play] 在实例方法 finish 中也调用 [self play],那么结果如何呢?

1
2
3
4
5
---------------- '- (void)finish' ---------------
---------------- '- (void)play' -----------------

---------------- '+ (void)eat' ------------------
---------------- '+ (void)play' -----------------

可以看出符合如期,类和实例方法中的 self 分别代表类本身和实例对象。

self 表示谁,在运行时是由编译器来决定的。

4、每个实例对象的 self 都是不一样的

这个跟 Java 的 this 是一样的,每个类的实例对象对应的 this 都是不一样的,self 亦如此。

下面的例子,分别创建两个 MZPerson 实例对象,然后分别调用play 方法,如下:

1
2
3
4
5
MZPerson *iperson1 = [MZPerson new];
[iperson1 play];

MZPerson *iperson2 = [MZPerson new];
[iperson2 play];

输出结果表明了上面说法的正确性。

1
2
3
4
---------------- '- (void)play' ------------------
self: <MZPerson: 0x600000576ee0>, self -> 0x600000576ee0
---------------- '- (void)play' ------------------
self: <MZPerson: 0x600000576f40>, self -> 0x600000576f40

最后

在继承关系中,使用 self 调用方法时,首先从当前类的方法列表中开始寻找,如果没有再从父类中寻找。

运行时(runtime)会使用 objc_msgSend 向对象发送消息,这个也是调用方法的本质。


扫码关注,你我就各多一个朋友~

Wireshark: 分析 TCP 四次挥手

发表于 2018-12-16 | 分类于 网络协议 |

简介

首先要明白 TCP 协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

其次,TCP 是全双工模式,需要两边的连接全部关闭,此 TCP 会话才算完全关闭,四次挥手使得 TCP 的全双工连接能够可靠的终止。

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),也叫做改进的三次握手。客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

下面根据客户端(IP: 10.4.17.176)请求服务端(IP: 116.211.186.208) 来分析四次挥手(four-way handshake)的过程。

这里需要注意一个问题,任何一方执行 close() 操作即可产生挥手操作,所以断开连接端可以是 Client 端,也可以是 Server 端。

在阅读下面内容之前,我假设你已经阅读过 Wireshark: 简单分析 TCP 三次挥手 这篇文章。

分析

使用 iOS 设备的浏览器客户端 Safari 访问 116.211.186.208 这个网站,关于如何开启 iOS 设备的虚拟网卡可以参考 初识 Wireshark 这篇文章。

针对 HTTP 的请求,可以进行 Follow,选择 TCP Stream 即可,如下图所示:

此时,可以看到四次挥手的抓包情况,如下图所示:

另外,从上图中可以看出,首先发起 Close 的是服务端。

第一次挥手,发送 FIN 和 ACK 报文,如图:

第二次挥手,客户端发送 ACK 报文给服务端,如图:

第三次挥手,客户端发送 FIN 和 ACK 报文给服务端,如图:

第四次挥手,服务端发送 ACK 报文给客户端,如图:

从图中抓包来看,seq 和 ack 的值变化如下表:

次数 seq 值 ack 值
1 140 447
2 447 141
3 447 141
4 141 448

小结

结合上面抓包的示例,小结一下四次挥手的过程。

在前面说过,断开连接端可以是 Client 端,也可以是 Server 端,我上面的例子首先发起 close 的一方是 Server 端。

第一次挥手:
服务端发送一个 [FIN+ACK] 报文,表示自己没有数据要发送了,想断开连接,并进入 fin_wait_1 状态(不能再发送数据到客户端,但能够发送控制信息 ACK 到客户端)。

第二次挥手:
客户端收到 [FIN] 报文后,客户端知道不会再有数据从服务端传来,发送 ACK 进行确认,客户端进入 close_wait 状态。此时服务端收到了客户端对 FIN 的 ACK 后,进入 fin_wait2 状态。

第三次挥手:
客户端发送 [FIN ACK] 报文给对方,表示自己没有数据要发送了,客户端进入 last_ack 状态。服务端收到了客户端的 FIN 信令后,进入 time_wait 状态,并发送 ACK 确认消息。

第四次挥手:
服务端在 time_wait 状态下,等待 2MSL(MSL是数据分节在网络中存活的最长时间) 一段时间,没有数据到来的,就认为对面已经收到了自己发送的 ACK 并正确关闭了进入 close 状态,自己也断开了到客户端的 TCP 连接,释放所有资源。当客户端收到服务端的ACK 回应后,会进入 close 状态,并关闭本端的会话接口,释放相应资源。

根据 wireshark 抓包和上面流程的分析,可以画出如下示意流程图:


扫码关注,你我就各多一个朋友~

当 NTFS 遇上 MacOS

发表于 2018-11-24 | 分类于 MacOS |

简介

自己使用的 MacBook Pro 电脑是 256G 的,这个本陪了我5个春秋,性能依旧强劲,一直舍不得抛弃换新(好吧,我承认自己穷)。

由于工作需要,里面安装了不少软件,也存储了不少资料,逐渐发现硬盘容量不足,我也一直在删除一些非必要的东西,发现还是杯水车薪,没办法,又买了一块移动硬盘,网盘用起来还是没有硬盘方便,我使用网盘的频率很低。

MacOS 挂载

我觉得移动硬盘买回来,直接插在电脑上就可以使用了,最后发现硬盘只能读不能写,卧槽!

上网查了一下,在默认情况下,NTFS 格式的硬盘插入到 MacOS 里,是只能读不能写的,需要挂载。

于是结合网上前辈的经验,弄了一个 shell 脚本,用了一段时间,发现挺好用的,但是电脑重启后,脚本就需要修改,还要查找当前硬盘到底在 disk0 还是在 disk2 上面,麻烦。

还好有一些比较成熟的工具帮我们解决问题,Paragon NTFS for MAC 就是其中一个。

这个工具是收费的,我试用了一段时间,感觉功能挺强大的,因为穷也懒得折腾破解,就找了另外一个工具 mounty,小巧可爱关键免费,在公众号上面收到的赞赏我也会 donate 给他们。

mounty 最新版本是 1.9,介绍如下:

1
A tiny tool to re-mount write-protected NTFS volumes under Mac OS X 10.9+ in read-write mode.

mounty 失效

在使用 mounty 挂载移动硬盘时, 弹框提示以下错误:

1
2
The volume My Passport is not re-mountable in read/write mode.
Probably it was not clean unmounted before.

这种错误的原因大部分是因为在 Windows 上面使用后没有安全退出(直接拨出)而造成的,网上有两种方案,分别如下:

方案 1

  1. 插入移动硬盘到 Windows 下面
  2. 打开 dos 命令行,输入 chkdsk /f, 然后回车,输入 Y
  3. 使用右下角的安全删除后再拔出移动硬盘
  4. 将移动硬盘再次插入 Mac 打开 mounty重新挂载就可以了

反正这种方案我是没有成功,报错如下:

方案 2

在 Windows 系统中,将移动硬盘插入,使用 360软件 点击 安全退出 退出硬盘,不要直接拔掉,再在 Mac 中插入使用 mounty 重新挂载就解决问题了。


扫码关注,期待与你的交流~

观察者模式

发表于 2018-10-28 | 分类于 Philosophy |

组件间通信,我会优先使用路由的通信方式(iOS中需要用到 runtime,Android 中需要用到反射和注解),组件之间按照路由协议(类似 url)实现各自的职责即可,组件间的松耦合性增强了软件设计的弹性和高可用性。

路由结合观察者模式,可以让你的组件设计更上一层楼。路由可以解决单向调用的问题,让组件之间无需知道对方是否存在。观察者模式可以让调用者(使用你框架的)可以很方便的知道组件的内部事件。

武林至尊,宝刀屠龙,号令天下,莫敢不从,倚天不出,谁与争锋?

在说观察者模式之前,先介绍一下 iOS 中的代理(delegate).

代理 delegate

在 iOS 中,代理(delegate)的本质是 protocol,类似 java 中的 Interface,一般用来处理 一对一 的关系,如下图所示:

下面的例子模拟了调用和实现过程,使用 Bank 对象和 BankDelegate 代理来模拟这种模式。Bank 有变动的时候,通过 notifyAccount 来授权 onAccountChanged 通知用户。

BankDelegate.h

1
2
3
4
5
@protocol BankDelegate <NSObject>

- (void)onAccountChanged:(NSUInteger)account;

@end

Bank.h

1
2
3
4
5
6
7
@interface Bank : NSObject

@property (nonatomic, weak) id<BankDelegate> delegate;

- (void)notifyAccount;

@end

Bank.m

1
2
3
4
5
6
7
8
9
10
@implementation Bank

- (void)notifyAccount
{
if ([self.delegate respondsToSelector:@selector(onAccountChanged:)]) {
[self.delegate onAccountChanged:100];
}
}

@end

使用者实现 delegate

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad
{
[super viewDidLoad];

Bank *bank = [Bank new];
bank.delegate = self;
[bank notifyAccount];
}

- (void)onAccountChanged:(NSUInteger)account
{
NSLog(@"Bank tell me that account is changed. %zd", account);
}

当然,delegate 也可以做到 一对多,改造一下 Bank 就可以实现。

1
2
3
4
5
6
7
@interface Bank : NSObject

- (void)addBankDelegate:(id<BankDelegate>)delegate;

- (void)notifyAccount;

@end

Bank 内部使用数组将 addBankDelegate 得到的 delegate 存起来,notifyAccount 中就可以进行通知了。

1
2
3
4
5
Bank *bank = [Bank new];
// 模拟添加多个 delegate
[bank addBankDelegate:self];
[bank addBankDelegate:self];
[bank notifyAccount];

观察者模式

理解了 delegate,观察者模式就很好理解了。

当一个对象改变状态时,它的所有依赖着都会收到通知并自动更新,这是观察者模式的常规定义。

观察者模式是一种 一对多 的设计模式,如下图所示:

继续上面的例子,使用 Bank 对象和 BankDelegate 来模拟这种模式,Bank 有变动的时候,通过 notifyAccount 来授权 onAccountChanged 通知所有注册了 BankDelegate 的用户。

Talk is cheap. Show me the code

BankDelegate.h

1
2
3
4
5
@protocol BankDelegate <NSObject>

- (void)onAccountChanged:(NSUInteger)account;

@end

Bank.h

1
2
3
4
5
6
7
8
9
@interface Bank : NSObject

- (void)addBankObserver:(id<BankDelegate>)delegate;

- (void)removeBankObserver:(id<BankDelegate>)delegate;

- (void)notifyAccount;

@end

Bank.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
- (void)notifyAccount
{
for (id<BankDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(onAccountChanged:)]) {
[delegate onAccountChanged:100];
}
}
}

- (void)addBankObserver:(id<BankDelegate>)delegate
{
if (nil == delegate) {
return;
}

[self.delegates addObject:delegate];
}

- (void)removeBankObserver:(id<BankDelegate>)delegate
{
if (self.delegates.count > 0) {
[self.delegates removeObject:delegate];
}
}

调用者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad
{
[super viewDidLoad];

[self registerObserver];
}

- (void)onAccountChanged:(NSUInteger)account
{
NSLog(@"Bank tell me that account is changed. %zd", account);
}

- (void)registerObserver
{
Bank *bank = [Bank new];
[bank addBankObserver:self];
[bank addBankObserver:self];
[bank notifyAccount];
}

看到这里,你应该发现 delegate 的设计其实就是观察者的一种设计手段而已,它本身也是观察者模式。

在 iOS 中,除了 delegate,还有很多这种设计模式的体现,如 KVO、Notification、Observer、Block 等。

发布-订阅模式

观察者模式中观察者对被观察者(Bank)是有感知的,至少需要实现对应的 BankDelegate,二者之间还是是有一定的耦合度。

那么,有没有一种方法再来降低这种耦合,让双方都不用去关心对方的存在呢?发布-订阅模式是一个不错的选择。

发布-订阅本质也是观察者模式,但是他更加的松耦合,发布者和订阅者都不用清楚对方,全部由订阅中心做处理,这样耦合度就几乎没有了。

如图展示发布-订阅模式:

在 iOS 中,Notification 就是发布-订阅模式的一种实现,NSNotificationCenter 就类似订阅中心。

1
2
3
4
5
6
7
8
// 发布
[[NSNotificationCenter defaultCenter] postNotificationName:@"name_protocol" object:nil];

// 订阅
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onUpdate:)
name:@"name_protocol"
object:nil];

争论

网上有很多人说,观察者模式和发布-订阅模式是两种不同的设计模式,它们压根就是两码事,不能混为一谈。也有很多人说,两者其实都是观察者模式,只是实现手段有点不一样罢了,本质是一样的。

江湖纷争,众说纷纭!

设计模式是一种设计思想,在观察者模式基础上你可以衍生更多的设计模式和更多的设计思想。模式的实现手段可以多样化,没有最好只有更好,就好比 MVC、MVP、MVVM 等,你说它们是设计模式也好,是设计思路也罢,关键是利用它们有没有解决业务需求,为了模式而模式的设计华而不实!

个人觉得,发布-订阅模式只是观察者模式的一种实现手段,它本质还是观察者模式。


扫码关注,期待与你的交流~

macOS 中神秘的 GCC

发表于 2018-10-13 | 分类于 C/C++ |

最近在看人工智能相关的知识,无意中发现了一个巨牛的 人工智能教程,分享一下给大家。

教程不仅是零基础,通俗易懂,而且非常风趣幽默,像看小说一样!觉得太牛了,所以分享给大家。点 这里 可以直接看教程。


这个事情,要从编译一个简单的代码开始说起。

gcc 和 g++ 的区别

gcc 是 GCC 中的 GUN C Compiler,C 编译器。

g++ 是 GCC 中的 GUN C++ Compiler,C++编译器。

就本质而言,gcc 和 g++ 并不是编译器,也不是编译器的集合,它们只是一种驱动器,根据参数中要编译的文件的类型,调用对应的 GUN 编译器而已。

gcc 与 g++ 都可以编译 C 和 C++ 文件,只是处理方式不同。 可以参考 GCC的gcc和g++区别 这篇文章。

交换变量:用指针

下面这段代码很简单,利用 C 指针实现交换两个变量的目的。

源文件:mz_swap.c

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
#include <stdio.h>

// 使用指针的方式交换两个变量
int mz_swap(int *ap, int *bp);

int main(int argc, char * argv[]) {

int xp = 901;
int yp = 902;

printf("--Before-- xp = %i, yp = %i\n", xp, yp);

mz_swap(&xp, &yp);

printf("--After--- xp = %i, yp = %i\n", xp, yp);

return 0;
}

int mz_swap(int *ap, int *bp) {

if (NULL == ap || NULL == bp) {
return -1;
}

int tp = *ap;

*ap = *bp;

*bp = tp;

return 0;
}

在 macOS 下使用 gcc 编译 mz_swap.c,产生可执行文件 exec_main.out,如下操作:

1
gcc mz_swap.c -o exec_main.out

编译成功,执行 ./exec_main.out,正常输出结果:

1
2
--Before-- xp = 901, yp = 902
--After--- xp = 902, yp = 901

采用 g++ 来编译,如下操作:

1
g++ mz_swap.c -o exec_main.out

报出如下警告,该警告的意思是在 C++ 模式下强制编译 C 文件,这里可以不予理会。

1
clang: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]

虽然有警告,但是编译可以产生可执行文件 exec_main.out 且可以正常执行的,执行结果跟上面一样。

交换变量:用引用

大家都知道,C 没有 引用 这个概念,而 C++ 是有 引用 概念的,可以在此了解 C++ 引用.

试一下使用 gcc 编译使用 引用 方式编写的 C 代码,按照说法,gcc 是无法编译通过的。

源文件:mz_swap_ref.c

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
#include <stdio.h>

// 使用引用的方式交换两个变量
int mz_swap_ref(int &ap, int &bp);

int main(int argc, char * argv[]) {

int xp = 901;
int yp = 902;

printf("--Before-- xp = %i, yp = %i\n", xp, yp);

mz_swap_ref(xp, yp);

printf("--After--- xp = %i, yp = %i\n", xp, yp);

return 0;
}

int mz_swap_ref(int &ap, int &bp) {

int tp = ap;

ap = bp;

bp = tp;

return 0;
}

编译

1
gcc mz_swap_ref.c -o exec_main.out

编译失败,错误信息如下:

1
2
3
4
5
mz_swap_ref.c:4:21: error: expected ')'
int mz_swap_ref(int &ap, int &bp);
^
mz_swap_ref.c:4:16: note: to match this '('
int mz_swap_ref(int &ap, int &bp);

既然按照 C 的编译方式不行,那就换成 g++ 来编译。

1
g++ mz_swap_ref.c -o exec_main.out

编译成功,执行可执行文件

1
./exec_main.out 
1
2
--Before-- xp = 901, yp = 902
--After--- xp = 902, yp = 901

大家可以自己动手试试,使用 clang 来编译 mz_swap_ref.c 和使用 gcc 来编译都是报错,使用 clang++ 和 g++ 编译都是没有问题的。

在博客 GCC: Homebrew 安装 GCC 和 Binutils 中,我已经分享了在 macOS 中的 gcc 和 g++ 不是 GNU 提供的,而是 Apple 自己的 clang。

这里要说明的是即使使用自己安装的 gcc(gcc-4.9) 来编译 mz_swap_ref.c 也是无法编译通过的,验证如下:

1
gcc-4.9 mz_swap_ref.c -o exec_main.out

错误信息:

1
2
3
4
5
mz_swap_ref.c:4:21: error: expected ';', ',' or ')' before '&' token
int mz_swap_ref(int &ap, int &bp);
^
mz_swap_ref.c:20:21: error: expected ';', ',' or ')' before '&' token
int mz_swap_ref(int &ap, int &bp) { ^

gcc、g++ 和 clang 的关系

在 macOS 中,gcc 以某种方式指向 llvm-gcc 编译器,g++ 亦如此。

1
In Apple's version of GCC, both cc and gcc are actually symbolic links to the llvm-gcc compiler. Similarly, c++ and g++ are links to llvm-g++.

llvm-gcc 是 c/c++/oc 的编译器,用了 gcc 前端和命令行界面的 llvm.

1
llvm-gcc is a C, C++, Objective-C and Objective-C++ compiler. llvm-g++ is a compiler driver for C++. llvm-gcc uses gcc front-end and gcc's command line interface.

接下来,我们看一下 llvm-gcc,可以使用 which llvm-gcc 看一下 llvm-gcc 所在位置是 /usr/bin/llvm-gcc,打开目录可以看出其实是一个符号链接,如下图所示:
1

1
ls -l /usr/bin/llvm-gcc
1
/usr/bin/llvm-gcc -> clang

它们是统一指向 clang 的符号链接,可以看其原始指向,llvm-gcc 指向 clang,llvm-g++ 指向 clang++。

二者都在 /usr/bin/ 目录下:
1

其实在 macOS 中 cc 编译器也指向 clang。

总结

通过上面的例子,我们至少可以学到如下几点知识:

1、C 语言规范中没有 引用 的概念,使用 C 编译器无法使其编译通过,但是使用 C++ 编译器是可以编译通过的。这是因为各自的编译器是遵循语言规范的。

2、macOS 中的 gcc 和 g++ 苹果开发者们并没有去改造和重写它们,只是分别指向 clang 和 clang++ 编译器。

3、如果不想使用 macOS 中的 gcc 和 g++,就需要自己重新安装 GNU 的编译套件,安装和使用方法已经在博客中有说明。


扫码关注,你我就各多一个朋友~

SVN 检出文件

发表于 2018-10-06 | 分类于 Tools |

需求

对于一些大工程, 整个 svn 仓库文件过大(动辄几十个G的都有),不想把整个 checkout 下来,但是又想维持整个目录结构以方便后续使用,那么可以使用 Subversion1.5 之后的 --depth 参数来只 checkout 需要的子目录.

如果只想把 svn 仓库里面某个文件 checkout 到本地, 不想 checkout 整个目录, 如何操作呢?

实施

假设需要 checkout 出来的文件是 ReadMe.md, 其 svn 地址是: http://svn.xxx.com/path/ReadMe.md,其上级目录的远程地址是 http://svn.xxx.com/path.

注意:以下出现 svn co 就是 svn checkout 的缩写.

方案一

大部分人都会在第一时间想到如下的操作:

1
svn co http://svn.xxx.com/path/ReadMe.md

实际上这样操作会报错的, 报错信息大概意思是 不可以 checkout 一个文件, 只能 checkout 目录。

所以,该方案不可行。

方案二

第一步: checkout 目录

1
svn co --depth=empty http://svn.xxx.com/path/ ./

./ 表示当前目录,--depth=empty 表示检出空目录。

第二步: 更新文件

1
svn up ReadMe.md 

这里直接 svn up 就可以将你需要的文件 checkout 出来了。

修改文件后, 也可以直接提交到 svn 仓库. 使用如下命令:

1
svn ci ReadMe.md -m "update."

方案三

使用 svn export 命令将文件导出,然后将 svn import 命令导入文件到 svn 仓库。

一般临时想 checkout 一个文件,就会使用 方案三, 比较正式的操作,我会选择 方案二,因为 方案二 这样既可在不增加磁盘大小的情况下保持 svn 仓库的目录结构,又可以任意 checkout 文件。

稀疏目录

上面的 方案二,其实就是 svn 的一种稀疏检出的操作。

Subversion 1.5 开始支持稀疏检出,允许对目录设置深度(depth)。

关于 depth 几个参数的含义:

–depth empty:只包含目录自身,不包含目录下的任何文件和子目录。

–depth files:包含目录和目录下的文件,不包含子目录。

–depth immediates:包含目录和目录下的文件及子目录。但不对子目录递归。

–depth infinity:这是默认的,包含整个目录树。

使用格式如下:

1
svn checkout URL --depth empty|files|immediates|infinity

或者

1
svn checkout URL --depth=empty|files|immediates|infinity

Cocoapods: 私有仓库

发表于 2018-09-24 | 分类于 MacOS |

使用场景

在我之前的博客中分享了几篇关于 Cocoapods 的文章(文末有链接),今天分享另外一个主题即如何创建你自己的私有仓库以及如何使用私有仓库。

为什么会有这种需求呢?

开发组内或者公司内,不希望把一些核心的公用代码给到外界使用(不开源),但是这些代码基本很稳定很成熟,可以做成组件给到其他人或者组内使用。

那么,就可以使用 Cocoapods 来创建自己的私有仓库,让大家共享代码,也是组件化的一种方案。

下面的分享全是基于实践所得,中间也遇到很多坑,都被我一一解决了,希望能帮到正在阅读文章的你。

创建仓库

这里的创建仓库,包括两个仓库,一个是代码仓库,另一个是 pod 源的仓库。

在 Github 创建 repository,如图:
1

1

创建名为 iOS_private_lib 的 repository,这个主要用来存放我们的代码。

同样道理,创建一个 Repo 用于存放私有的 SpecRepo 源,我创建的是 https://github.com/veryitman/private_spec_repo.git 这个仓库。

为了说明问题,这里我使用了 Public 来模拟 Private 仓库。在实际开发中,一般都是使用公司自己搭建的私有 git 服务器来做,我这里主要是为了说明问题。

创建代码工程

1. 先 clone 刚才创建的 repo 到本地

1
2
3
cd ~/workspace/

git clone https://github.com/veryitman/iOS_private_lib

2. 创建工程

在 ~/workspace/iOS_private_lib 下使用 pod lib create 创建工程,工程名字为 CoreHTTP,执行命令如下:

1
2
3
cd ~/workspace/iOS_private_lib

pod lib create CoreHTTP

根据提示输入对应的信息即可完成创建,如图:
1

创建成功后,包含一个 Example 例子工程,工程目录如下:
1

使用 Xcode 打开例子工程,在 CoreHTTP/Classes 中创建文件即可,如下图所示:
1

3. 编辑 podspec 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pod::Spec.new do |s|
s.name = 'CoreHTTP'
s.version = '0.1.0'
s.summary = 'CoreHTTP for create private pod, it is a demo only.'
s.description = <<-DESC
The project of CoreHTTP is a private pod, it is a demo only.
DESC

s.homepage = 'https://github.com/veryitman/iOS_private_lib.git'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'veryitman@126.com' => 'veryitman@126.com' }
s.platform = :ios, "7.0"
s.source = { :git => 'https://github.com/veryitman/iOS_private_lib.git', :tag => s.version.to_s }

s.ios.deployment_target = '8.0'

s.source_files = 'CoreHTTP/Classes/**/*'
s.requires_arc = true
end

这里需要注意,如果 s.source_files 写的不对,会报下面的错误。

1
- ERROR | [iOS] file patterns: The `source_files` pattern did not match any file.

CoreHTTP 的名字必须是你代码存放的文件夹名,而且这个文件件要与 podspec 文件在同一级文件目录下,从上面截图的目录结构可以看出。

另外一个提示的错误点,可能是因为空目录的问题造成的,如果这样的话,可以在各个文件夹下面创建 .gitkeep 文件,该文件的作用是为了提交空文件夹,因为 git 默认是不会添加空文件夹到仓库的。

创建 .gitkeep 文件的命令如下:

1
2
3
cd 文件夹下

touch .gitkeep

这里我折腾了蛮久,最后思考和参考了自己之前的开源项目管理博客,才解决这个问题。

4. 检查和验证本地 podspec 文件

1
2
3
cd CoreHTTP 

pod lib lint CoreHTTP.podspec

验证通过会显示如下图所示的成功提示
1

使用 Xcode 打开工程,删除对应以 Test开头 的相关的库和文件夹,确保 Xcode 能编译通过,选择模拟器即可。

这里一定要先清空 cocoapods 的缓存,否则后面远程验证还是报错,无法通过验证。

清空 cocoapods 的缓存,命令如下:

1
2
3
rm ~/Library/Caches/CocoaPods/Pods/External/CoreHTTP

rm ~/Library/Caches/CocoaPods/Pods/Specs/External/CoreHTTP

5. 上传工程到 Github 远程代码仓库

1
2
3
4
5
6
7
cd ~/workspace/iOS_private_lib

git add CoreHTTP/

git commit -m "Add lib."

git push

6. 给版本添加 tag

1
2
3
4
5
cd ~/workspace/iOS_private_lib

git tag 0.1.0 -m "Add lib and example project."

git push --tags

7. 进行远程验证

1
2
3
cd ~/workspace/iOS_private_lib

pod spec lint CoreHTTP/CoreHTTP.podspec --verbose --use-libraries --allow-warnings

注意:--verbose 选项是为了看编译和验证信息,--use-libraries 是为了能支持 i386 架构,如果不加此参数,私有库无法通过验证,--allow-warnings 参数是允许有警告。

验证成功的示意图:
1

提交私有 podspec

1. 向本地的 cocoapods 仓库添加私有 Spec Repo

1
2
3
cd ~/workspace/iOS_private_lib

pod repo add PrivateSpec https://github.com/veryitman/private_spec_repo.git

这里一定要注意:对应的地址是 SpecRepo 仓库地址而不是代码仓库地址。

本地存放 spec 的私有仓库名称,我这里取名为 PrivateSpec,你也可以取其他名字,但是记住在下面的步骤中要保持一致。

1
ls -alt ~/.cocoapods/repos/

可以看到已经成功添加本地 repo 中,其他 master 是 cocoapods 官方公开的源,其他为私有源。如下图所示:
1

2. 向私有的 Spec Repo 中提交 podspec

下面的 PrivateSpec 是与上面的名称对应的。

1
2
3
cd ~/workspace/iOS_private_lib

pod repo push PrivateSpec ./CoreHTTP/CoreHTTP.podspec --verbose --use-libraries --allow-warnings

这里要注意,添加私有库和之前博文 Cocoapods 管理开源项目 中提到的 pod trunk push 是不一样的操作。

使用私有库

1. 搜索库

1
pod search CoreHTTP

如果搜索不到,可以执行下面的两条命令:

1
2
3
rm ~/Library/Caches/CocoaPods/search_index.json

pod setup

然后继续搜索就可以了,搜索成功会显示如下内容:

1
2
3
4
5
6
CoreHTTP (0.1.0)
CoreHTTP for create private pod, it is a demo only.
pod 'CoreHTTP', '~> 0.1.0'
- Homepage: https://github.com/veryitman/iOS_private_lib.git
- Source: https://github.com/veryitman/iOS_private_lib.git
- Versions: 0.1.0 [PrivateSpec repo]

为了避免和其他私有库命名冲突,建议在自己的私有库前面加上公司或者其他标示,如 TepdCoreHTTP、FphCoreHTTP 等。

2. 在 Podfile 文件的顶部添加 source

1
2
source 'https://github.com/veryitman/private_spec_repo.git'
source 'https://github.com/CocoaPods/Specs.git'

注意一个是我们私有 SpecRepo 地址(非私有代码库地址),另一个是官方公有源地址。

使用私有库的工程的 Podfile 写法示例如下:

1
2
3
4
5
6
7
8
source 'https://github.com/veryitman/private_spec_repo.git'
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '7.0'

target 'TestCoreHTTP' do
pod 'CoreHTTP', '~> 0.1.0'
end

在工程中可以直接使用私有库了,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "ViewController.h"
#import <CoreHTTPHeader.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];
NSString *lib_desc = [CoreHTTPHeader libDesc];
NSLog(@"The HTTP's library desc: %@", lib_desc);
}

@end

pod 命令

1
2
3
4
5
6
7
8
//将工程添加到本地 repo 中 
pod repo add [本地私有仓库名] [远程仓库地址]

//移除本地 repo
pod repo remove [本地私有仓库名]

//查看本地 repo
pod repo
1
2
3
4
5
//只从本地验证你的pod能否通过验证
pod lib lint *.podspec

//从本地和远程验证你的pod能否通过验证
pod spec lint *.podspec

参考文档

Private Pods

我的其他文章

Mac install Cocoapods

Cocoapods 问题集锦

Cocoapods 管理开源项目

iOS 项目: 打造本地 pod 库


扫码关注,你我就各多一个朋友~

浅谈客户端模块化

发表于 2018-09-23 | 分类于 Philosophy |

回忆

大学那会喜欢跟着老师在实验室折腾,感觉每天大脑中都有新知识鱼贯而入。

当我被告知要做一个最小单片机系统的时候,兴奋而又紧张!

最小单片机系统示意图:

从画 PCB 到制板、洗板,再到选择元器件,最后组装调试和写代码,我花费了两个多月的时间,当板子上面的跑马灯亮起来的那一刻,我激动的无法言语。

大家可以看到这个最小单片机系统上面有很多组件组成,有电阻、电容、电阻、二极管和卡槽等,当然还有最核心的组件就是单片机,当时用的是51单片机。

各个元器件就好比组件,功能单一,职责明显,组件之间遵循协议构成模块,最终组合成为(最小单片机)系统。

上面的回忆是为了引出今天的分享,分享的主要内容是客户端开发中常用的模块化、组件化、插件化以及热更新等技术辞藻,并没有涉及到具体的技术问题。

模块化和组件化

从程序设计的角度出发,无论是模块化还是组件化,都是指软件架构的范畴,是一种设计思想。两者并没有严格意义上面的区分,二者的目的一致,就是将工程结构化,达到可复用可伸缩的能力,最终提供工作效率。

在说模块化和组件化之前,先解释一下什么是高内聚低耦合。

高内聚:组件内尽可能独立完成自己的功能,坚持单一职责的原则 SRP,不依赖于其他组件的代码。

低耦合:模块与模块之间尽量不要互相引用,模块之间联系越复杂耦合度越高,修改的成本就越高。

组件更加强调可替换可复用的特性,职责和功能比较单一、独立,与其他组件之间没有耦合性。

模块更加强调组合特性,更加偏重于业务,比如一个社区项目,登录注册、论坛和个人中心都是模块,这些业务模块又是有很多个组件组合而成。

下图展示的是一个简易的论坛系统示例图,如下:

可以看出,各个组件可以单独使用到其他模块当中,各个模块之间相对独立,只要定义好模块之间的通信协议,就可以做到并行开发,各个模块甚至可以复用到其他系统之中。

通信机制

这里说的通信机制并不是指 HTTP 或 TCP 的通信方式,而是指组件与组件之间,模块和模块之间的交互方式。

正常情况下,写业务功能的代码,在不同组件之间需要相互调用,这样就需要 import 其他组件,无形之中就增加了组件之间的耦合度。

有开发经验的同学,可能会用到类似 java 的反射机制,或者一些动态语言的运行时机制如 Objective-C Runtime,对它们不进行 import,而是动态的解析代码,达到组件之间或者是类之间相互调用的目的。

现在提倡也是比较热门的技术是通过路由通信机制,来进行组件之间的通信。这里举一个实际使用场景。

用户安装了我们的 APP,运营同事在某个节日来临之际做一个促活跃的活动,期望用户点在打开推送通知的时候,打开 APP 后可以直接跳转到对应的活动详情页面。大致流程如下:

这是一个再常规不过的需求了,相信经历过产品开发的朋友都见过这种需求。

很显然,我们可以使用路由的通信机制来完成这个需求,开发的流程图大致如下:

这里关键的核心得益于 iOS 和 Android 平台的 scheme 机制,对于 scheme,通俗的讲就是一种可以跨进程或者进程内的通信协议,例如可以在 APP 中打开某个应用商店。

例如下面的 URL:

1
bbs://page/activity/activity_detail?id=8978&user_id=67890432

其中,bbs 就是 scheme,可以看到该 URL 完全可以被各自平台来解析。

iOS 和 Android 平台各有很多开源的路由方案,实现手法各有差异,但是思想是一样的,建议大家去了解和学习。

不过,现在你只要知道,路由的通信机制大大降低了组件之间的耦合性就够了。

插件化

插件这个词,大家应该很熟悉,例如 Chrome 浏览器中可以安装各种小工具,这些小工具就是插件,还有各种开发使用的 IDE 也支持插件安装,便于提高我们的工作效率。Chrome 和 IDE 被称之为宿主,插件寄生于他们。

支付宝和微信里面的小程序也可以看成是一个个插件,但是他们不能脱离支付宝和微信独立运行。

可以发现这些插件即使被卸载或者被删除,并不会让 Chrome 或者 IDE 受到影响,换句话说,插件让宿主锦上添花。

这种插件思想当然可以运用到 APP 中来,试想一下,如果某个 APP 在线上经过动态下载就具有了一个强大或者好用的功能,岂不美哉?!

插件化,将 APP 拆分成很多模块,这些模块包括一个宿主和多个插件,宿主提供插件的管理和通信协议及规范,每个模块都是一个的库或者功能包。

插件化是一种编程和解决问题的思想,没有统一的定义。在 Android 上面运用比较多,iOS 上面很少,并不是 iOS 没有这样的技术,主要是因为苹果审核等各方面的限制。在 iOS8 上的 App Extension 功能,也可以看做是插件化了。

在 Android 平台中,插件化已经不是很新鲜的技术了,VirtualAPK、Atlas、Replugin 等框架相继开源,插件化技术已经进入了比较成熟的阶段了。

插件化的编程思想和实现,在不同的平台有所差异,即使在同一个平台上面都会有不同的实现手段,建议选择一个开源方案去了解其原理,然后试着动手去实现一个,千里之行始于足下!

热更新

“不好了,昨天有很多用户反馈我们的 APP 出现闪退。“ 小王一大早的开始撕喊,坐在他旁边的开发大神们顿时微笑凝固,马上去后台看上报的崩溃日志,紧接着开始复现和解决问题,最终得出结论,需要重新提审 APP,并周知渠道部门做好更新准备。

试想一下,如果一个游戏几G的大小,你让用户为了你的一个小失误来整包更新游戏,用户和你估计都要疯了。

最近几年,随着业务发展的需求,热更新技术也是得到了突飞猛进的发展。类似于上面的场景,能在用户神不知鬼不觉的情况下使用热更新的技术解决崩溃的问题,岂不是两全其美。

Android 的热更新技术如火如荼的发展着,而苹果这边严厉禁止热更新,一旦检查到立即会责令你修改或者下架 APP。我们还是从技术的角度来看这个问题,学习和了解一下对应的技术总归没有错,这里 有 iOS 上面的热更新方案,另外 Android热修复方案比较 介绍了很多 Android 热更新的开源的方案,可以了解学习一下。

站在跨平台的角度,我个人比较推荐使用 Lua 实现热更新,Lua 不仅简单高效,而且可以很好的和 C/C++ 结合在一起,而 Android 上面通过 JNI 又能与 C/C++ 通信,iOS 上面就更加不用说了。这只是我个人的一点看法,不喜勿喷。


扫码关注,你我就各多一个朋友~

HTTP 演进史

发表于 2018-09-16 | 分类于 网络协议 |

精通 HTTP 吗?

在面试相关开发岗位的同学中,我经常会问一些跟编程语言不相关的东西,比如下面这些问题:

1. 你平时是怎么学习的?学习渠道有哪些?
2. 项目组中,你认为最重要的是什么?
3. 你在项目组中遇到的最大困难(非编程问题)是什么,你是怎么解决的?
4. 项目执行过程中,其他同事遇到困难,你是如何做的?

这些问题可以让面试者自由发挥的题目,并没有固定的标准的答案。这样的问题问出来有诸多好处,首先可以让面试者稳定一下心态不至于太紧张,其次可以看看他的语言组织能力和表达能力,从中也能看出他有没有过实际的项目经验。

很多人的简历上面写的很完美,做过诸多项目,精通某某语言的编程,精通各种网络协议,熟悉各大操作系统等等,当深入问他们熟悉的知识时,发现他们的回答却含糊不清。不排除世界上有这样的全才,但是肯定不会被我们轻易遇到,至少我觉得自己还没有达到那个能力。

如果你还没有但又想精通各种知识,就慢慢去学习并精通相关的知识,循序渐进,相信自己会做到的。好了,不扯了,今天聊聊 HTTP 的发展历史。

我敢保证有绝大部分的人不知道 HTTP/2,虽然他早就来临.

HTTP 0.x 到 HTTP/2

HTTP/1 是 HTTP 1.0 和 HTTP 1.1 的统称,分别指 HTTP 协议的版本是 1.0 和 1.1.

1960年美国人 Ted Nelson 构思了一种通过计算机处理文本信息的方法,并称之为超文本(hypertext), 这成为了 HTTP 超文本传输协议标准架构的发展根基,所以他被称之为 HTTP 之父。

1989年3月在 CERN(欧洲核子研究组织)工作的 Tim Berners-Lee(蒂姆.伯纳斯.李) 提交了一个提案 Information Management: A Proposal,该提案描绘了其对万维网(World Wide Web)最初的设想即能让远隔两地的人们共享知识,Tim 是 万维网之父,成就和贡献也是不计其数。

直到1990年 HTTP 才算是问世,姑且称之为 HTTP 0.x 版本,也有些地方称之为 HTTP 0.9 版本,总之这不是一个正式版。1996年5月,HTTP 1.0 作为 HTTP 的第一个正式版本正式公布,并记载于 RFC1945 中。

随着人们对互联网的认知和技术的发展(尤其是浏览器等相关的技术),发现 HTTP 1.0 缺点还是比较多,无法满足快速的互联网发展需求。于是在1997年1月发布了 HTTP 1.1 版本,并记载于 RFC2068 中,后来在1999年6月,对 RFC2068 又做了补充和修正,称之为 RFC2616, 至此 HTTP 1.1 开始一统天下,直到现在绝大数开发者使用的都是 HTTP 1.1 版本。

另外一个版本 HTTP/2 标准于2015年5月以 RFC 7540 正式发表,多数主流浏览器已经在2015年底支持了该协议,时隔多年,HTTP/2 才问世。

苹果从 iOS9 开始支持 HTTP2.0,对 iOS 开发人员来说从 iOS9 开始 NSURLSession 可以支持 HTTP2.0 了。目前 Android 绝大部分系统不支持 HTTP/2,Android 原生 HttpURLConnection 框架并不支持 HTTP/2,但可以选用三方开源库 okhttp 作为网络框架,Android 5.0+(系统版本)设备才支持 HTTP/2。所以想全面使用 HTTP/2 还是要付出一定的代价,不过随着技术的革新,相信 HTTP/2 将会大放光彩。

HTTP 不断的改进

用一张图来描述 HTTP 的不断改进和演变史,如下图所示:

下面具体看看这些协议版本的演进过程中,究竟是新增和改进了哪些东西?

HTTP 0.9

HTTP 0.9 并非出自某个标准化组织,只是 Tim 的个人作品而已,比较简单。可以参阅这篇文章 The Original HTTP as defined in 1991 ,该版本规定了 HTTP 使用 TCP/IP 连接,HTTP 请求只有一个请求行,只有一个 GET 方法加上请求的URI。HTTP 响应则直接返回 HTML 文本,没有状态码,所以也没有办法区分错误消息和正常的文本。可以看出,HTTP/0.9并不完善,也不是那么完美,但是这的确是 Tim 坚持下来的结晶。

HTTP 1.0

HTTP 1.0 增加了请求头域和响应头域,增加了 HEAD 和 POST 方法,响应对象不再局限于 HTML 文本,支持长连接和缓存机制等等。

HTTP 1.0 具体特点

1.可以发送更多格式的内容,如图像、视频、二进制文件,不仅仅局限于文字了。

2.增加了 POST 和 HEAD 方法。

3.改变了 HTTP 请求和回应的格式。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据,即增加了请求头信息,响应数据不再局限于 HTML 文本。

4.新增状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等功能。

5.增加了长连接的支持,不过 HTTP 1.0 需要在 request 中增加 ”Connection: keep-alive“ header 才能够支持,后面要说到的 HTTP 1.1 默认就支持了。

可以看出,HTTP 1.0在 HTTP 0.9 的基础上做了大量的扩充和改进。

HTTP 1.0 主要缺点

1.每次请求都需要建立 TCP 连接,即每对 Request/Response 都使用一个新的连接。

2.不支持断点续传。

HTTP 1.1

每个新版本的诞生基本都是对上一个版本的补充和改进,就像我们的软件开发一样,协议也需要迭代。

HTTP 1.1 具体特点

1.加强和优化了持久连接即长连接。

HTTP 1.1 支持长连接(Persistent Connection)和请求的流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP 1.1 中默认开启 Connection: keep-alive,一定程度上弥补了 HTTP 1.0 每次请求都要创建连接的缺点。

2.增强了缓存机制。

引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等可供选择的缓存头。

3.请求头引入了 range 头域。

它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

4.将 Content-length 字段的作用进行扩充,即声明本次回应的数据长度(一个TCP连接现在可以传送多个回应,势必就要有一种机制,区分数据包是属于哪一个回应的)。

5.采用分块传输编码。

对于一些很耗时的动态操作,服务器需要等到所有操作完成,才能发送数据,显然这样的效率不高。更好的处理方法是,产生一块数据,就发送一块,采用流模式(stream)取代缓存模式(buffer)。

6.新增了许多动词方法:PUT、PATCH、HEAD、OPTIONS、DELETE。另外,客户端请求的头信息新增了 Host 字段,用来指定服务器的域名。

7.新增了 24 个错误状态响应码。

HTTP 1.0 主要缺点

HTTP 1.1 虽然增加了很多功能,在一定程度上已经很强大了,但是他自身也是有缺点的,换句话说有优化的空间,主要问题如下:

1.队头堵塞(Head-of-line blocking),各个请求到达的服务器的速度是不同的,如果先发的请求先到达可能会发生阻塞,剩下所有的工作都会被阻塞在那次请求应答之后,这样就降低了带宽。

为了避免这个问题,有两种方法:一是减少请求数,二是同时多开持久连接。

2.臃肿的消息头部

3.不支持服务端推送,例如要求使用 HTTP 协议做一个服务端数据变动页面立即改变的组件就不好做,可用轮询的方式进行,这样就会对带宽影响较大。

SPDY

在说到 HTTP/2 之前,必须要先说说 SPDY。SPDY 是由 Google 公司推出的,SPDY 的推出是为了解决 HTTP 1.1 中存在的一些问题的。

1.多路复用(multiplexing)

多路复用通过多个请求 stream 共享一个 TCP 连接的方式,解决了HOL blocking 的问题,降低了延迟同时提高了带宽的利用率。

2.可以对请求设置优先级

多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。

3.对请求头的压缩

选择合适的压缩算法可以减小包的大小和数量,这样在一定程度上可以提高网络使用效率。

4.为了数据传输的安全性,强制使用基于 HTTPS 的加密协议传输。

5.支持服务端推送。

称之为 Server Push 功能,服务器在还没有收到客户端的请求,服务器就可以把各种资源推送给客户端。

比如,客户端只请求了 index.html,但是服务器把 index.html、x.css、x.jpg 等资源全部发送给客户端。这样的话,只需要一轮 HTTP 通信,客户端就得到了全部资源,提高了通信的性能,也提高了用户体验。

HTTP/2

本质上来讲,HTTP/2 是建立在 SPDY 基础之上的,借鉴了很多 SPDY 的设计思想和策略。 HTTP 具有 SPDY 的优点,同时自己也有与 SPDY 的不同点,主要如下:

1.HTTP/2 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS。这一点 HTTP/2 相对人性化一些。

2.HTTP/2 会对请求头和响应头做压缩以提升请求性能,HTTP/2 消息头的压缩算法不同于 SPDY。 HTTP/2 采用 HPACK,SPDY 采用的 DEFLATE 算法。

HTTP/2 支持所有的 HTTP 1.1 的核心特征,其提供了 HTTP 语义的传输优化,并且在各方面做到更高效。HTTP/2 的头信息是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为帧(frame),即头信息帧和数据帧。对 HTTP/2 感兴趣的可以看一下 HTTP2 协议初识 这篇文章。

推荐文章

  • ATS: HTTP/HTTPS 协议介绍

  • ATS: HTTPS 认证


没有人能照顾你一辈子,你自己必须慢慢强大起来~

<1…101112…20>

193 日志
16 分类
163 标签
© 2024 veryitman