0%

序言

iOS远程推送是怎么实现的?

远程推送逻辑图

1、 APP向iOS设备(iPhone手机)发送一个注册通知。然后iOS设备向APNS远程推送服务器发送APP的 Bundle ID 和设备的UDID。
2、 APNS根据上传的 Bundle ID 和 UDID 生成 Device Token 再返回给APP。
3、 APP 将 Device Token 发送给自己的服务器。
4、 当服务器发生APP感兴趣事件的时候,服务器将该消息以及对应的 Device Token 发送给APNS。
5、 APNS 再根据 Device Token 将消息发送给对应的APP(用户)。

序言

NS_DESIGNATED_INITIALIZER 是官方定义的一个宏。可以在编译的时候帮我们找出潜在的问题。
如果一个类中的初始化方法没有用 NS_DESIGNATED_INITIALIZER 修饰,说明这个方法是便利初始化方法(Convenience Initializer)。

实现

1
2
3
4
5
6
7
#ifndef NS_DESIGNATED_INITIALIZER
#if __has_attribute(objc_designated_initializer)
#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
#else
#define NS_DESIGNATED_INITIALIZER
#endif
#endif

使用

看看官方的接口是怎么使用的。

1
2
3
4
5
6
7
8
9
10
11
// UIViewController
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

// UIView
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

// UITableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier NS_AVAILABLE_IOS(3_0) NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

两个指定初始化方法分别对应纯代码方式和文件(Xib、Storyboard)方式创建。

理解

在OC中它想表达的意思大概就是:当你alloc了一个类的时候,必须直接或间接调用这个类的其中一个带有 NS_DESIGNATED_INITIALIZER 后缀的初始化方法。不然初始化不完全,编译器会报出一个警告!下图是官方给的逻辑图。

这里有两个约束
约束1、便利初始化方法只能调用本类的初始化方法,最终一定会调用本类的指定初始化方法。
约束2、子类的指定初始化方法一定会调用父类的指定初始化方法。

当存在多个初始化方法的时候,流程如下图。这里的 Secondary initializer 就是 Convenience (便利初始化方法)

这里便利初始化方法最终调用了本类的指定初始化方法,本类的指定初始化方法调用了父类的指定初始化方法。满足了约束1和约束2。

子类要先初始化父类,然后再初始化实例变量。如下图

子类的指定初始化方法调用了父类的指定初始化方法。满足了约束2。

Reference

Object Initialization
Concepts in Objective-C Programming: Object Initialization

序言

整理一些多线程相关的知识。

并行 & 并发

1、并行:并行是相对于多核而言的,几个任务同时执行。
2、并发:并发是相对于单核而言的,几个任务之间快速切换运行,看起来像是“同时”发生的一样

NSThread

优点:轻量级
缺点:需要手动管理线程活动,如生命周期、线程同步、睡眠等。
搭配runloop实现常驻线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//NSThread
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
[thread start];

- (void)threadRun {
@autoreleasepool {
NSLog(@"threadRun");
// NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timeTask) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timeTask) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}

- (void)timeTask {
NSLog(@"timeTask:%@",[NSThread currentThread]);
}

NSOperation & NSOperationQueue

NSOperation

NSOperation 是一个抽象类,只能使用它的自类来进行操作。系统为我们创建了两个子类NSInvocationOperation & NSBlockOperation。
直接使用这两个类执行任务,系统不会创建子线程,而是在当前线程执行任务。NSBlockOperation 使用 addExecutionBlock方法的任务是在多线程执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation) object:nil];
[invocationOperation start];

//NSBlockOperation
NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"blockOperation:%@",[NSThread currentThread]);
}];
//这里面的任务是多线程执行的
[blockOperation addExecutionBlock:^{
NSLog(@"addExecutionBlock:%@",[NSThread currentThread]);
}];
[blockOperation start];

- (void)invocationOperation {
NSLog(@"invocationOperation:%@",[NSThread currentThread]);
}

//invocationOperation:<NSThread: 0x60800007e1c0>{number = 1, name = main}
//blockOperation:<NSThread: 0x60800007e1c0>{number = 1, name = main}
//addExecutionBlock:<NSThread: 0x608000078800>{number = 3, name = (null)}

NSOperationQueue

//主队列,任务在主线程执行,一般用于更新UI的时候使用。
NSOperationQueue *queue = [NSOperationQueue mainQueue];
//子队列,任务在子线程执行,用于处理耗时任务。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
//NSInvocationOperation
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperation) object:nil];
//NSBlockOperation
NSBlockOperation * blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"blockOperation:%@",[NSThread currentThread]);
}];
[operationQueue addOperation:invocationOperation];
[operationQueue addOperation:blockOperation];

- (void)invocationOperation {
NSLog(@"invocationOperation:%@",[NSThread currentThread]);
}

// invocationOperation:<NSThread: 0x60c00046b940>{number = 4, name = (null)}
// blockOperation:<NSThread: 0x60800026b580>{number = 3, name = (null)}

也可以直接添加block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[operationQueue addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];
[operationQueue addOperationWithBlock:^{
NSLog(@"%@",[NSThread currentThread]);
}];

// <NSThread: 0x6080002637c0>{number = 4, name = (null)}
// <NSThread: 0x60c0002667c0>{number = 5, name = (null)}
// <NSThread: 0x60000047b2c0>{number = 3, name = (null)}

可以通过 maxConcurrentOperationCount 来控制最大并发数,当最大并发数为1时,就相当于串行队列

NSOperation添加依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
//NSBlockOperation
NSBlockOperation * blockOperation0 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"blockOperation0:%@",[NSThread currentThread]);
}];
NSBlockOperation * blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
sleep(1);
NSLog(@"blockOperation1:%@",[NSThread currentThread]);
}];

//blockOperation0 依赖 blockOperation1,只有在 blockOperation1 执行完成才能执行 blockOperation0
[blockOperation0 addDependency:blockOperation1];

[operationQueue addOperation:blockOperation0];
[operationQueue addOperation:blockOperation1];

//blockOperation1:<NSThread: 0x600000269c80>{number = 3, name = (null)}
//blockOperation0:<NSThread: 0x60c000479040>{number = 4, name = (null)}

注意:NSOperationQueue 是非线程安全的,多个线程访问统一资源时,需要加锁。

1
2
3
4
5
self.lock = [[NSLock alloc] init];

[self.lock lock];
//不能同时访问的资源
[self.lock unlock];

GCD

创建队列,然后再往队列添加任务。也可以直接使用系统创建的队列。

1
2
3
4
5
6
7
8
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", DISPATCH_QUEUE_SERIAL);
//创建并发队列
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", DISPATCH_QUEUE_CONCURRENT);
//获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
//获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

创建同步、异步任务

1
2
3
4
5
6
7
dispatch_sync(queue, ^{
//同步执行
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
//异步执行
});

GCD常用方法

dispatch_barrier

只有当添加在dispatch_barrier前面的任务完成了,才开始执行dispatch_barrier任务。
有两种方式:dispatch_barrier_syncdispatch_barrier_async

dispatch_barrier_sync
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
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"task0.....");
sleep(1);
});
dispatch_async(queue, ^{
NSLog(@"task1.....");
sleep(1);
});

dispatch_barrier_sync(queue, ^{
NSLog(@"task2.....");
sleep(5);
});

NSLog(@"test");

dispatch_async(queue, ^{
NSLog(@"task3.....");
sleep(2);
});
dispatch_async(queue, ^{
NSLog(@"task4.....");
sleep(1);
});

//task0.....
//task1.....
//task2.....
//test....
//task3.....
//task4.....

//只有当task0、task1 执行完才会执行tast2.
//task2 执行完才会往下执行。
//test 执行完后,才会把task3、task4 加进队列.
dispatch_barrier_async
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
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"task0.....");
sleep(1);
});
dispatch_async(queue, ^{
NSLog(@"task1.....");
sleep(1);
});

dispatch_barrier_async(queue, ^{
NSLog(@"task2.....");
sleep(5);
});

NSLog(@"test");

dispatch_async(queue, ^{
NSLog(@"task3.....");
sleep(2);
});
dispatch_async(queue, ^{
NSLog(@"task4.....");
sleep(1);
});

//test....
//task1.....
//task0.....
//task2.....
//task3.....
//task4.....

//test、task0、task1 并发执行。
//task0、task1 执行完才会执行tast2.
//tast2 执行完才会执行tast3、tast4.

dispatch_after
1
2
3
4
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"dispatch_after task...");
});
//dispatch_after 会在到了指定的时间之后,将才将任务加到相应的队列中。
dispatch_once

一般会以这种形式出现

1
2
3
4
5
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"dispatch_once task...");
});
//block里面的任务只会执行一次。
dispatch_apply
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_queue_t queue = dispatch_queue_create("com.vhuichen.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"%zu",index);
});
NSLog(@"end");

// 0
// 1
// 3
// 2
// 4
// 5
// 6
// 7
// 8
// 9
// end

// dispatch_apply 会等待所有的任务完成了,才会往下执行。
dispatch_semaphore

可以用于线程同步、线程加锁、控制并发线程数量

1
2
3
4
5
6
7
8
9
10
11
//实现同步请求
let semaphoreSignal = DispatchSemaphore(value: 0)
ZBDepthAPI.GET(market: market.name!, succeed: { (depthModel) in
//......
semaphoreSignal.signal()
}) { (error) in
//......
semaphoreSignal.signal()
}
semaphoreSignal.wait()

dispatch_group

当需要同时执行多个耗时任务,并且当所有任务执行完后更新UI。那么这时可以使用 dispatch_group 来实现。

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
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, global, ^{
NSLog(@"tast0.......");
});
NSLog(@"0");

dispatch_group_async(group, global, ^{
NSLog(@"tast1.......");
});
NSLog(@"1");

dispatch_group_async(group, global, ^{
NSLog(@"tast2.......");
sleep(1);
});
NSLog(@"2");

dispatch_group_enter(group);
dispatch_async(global, ^{
NSLog(@"tast00.......");
dispatch_group_leave(group);
});

// 所有任务完成的时候调用
//dispatch_group_notify(group, global, ^{
// NSLog(@"tast complete...");
//});

//会阻塞当前线程,直到 group 里面的任务全部完成
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"wait");

// 异步加入的任务会立即执行
// dispatch_group_wait会阻塞当前线程,直到 group 里面的任务全部完成
// dispatch_group_enter & dispatch_group_leave 必须成对出现,相当于 dispatch_group_async

总结

当遇到相应场景的时候,知道使用哪种方法比较合理就行了。

序言

之前在项目中使用过,写出来记录一下。

常用类 FMDatabaseQueue FMDatabase

FMDatabase

FMDatabase 可以直接操作数据库,使用时需要注意,它不是线程安全的。

FMDatabaseQueue

使用 FMDatabaseQueue 操作数据库则是线程安全的,看源码可以发现这是一个串行队列。

考虑到线程安全可以使用 FMDatabaseQueue 代替 FMDatabase

方法如下:

1
2
3
[self.databaseQueue inDatabase:^(FMDatabase * _Nonnull db) {

}];

如果要处理事务,则使用下面的方法

1
2
3
[self.databaseQueue inDeferredTransaction:^(FMDatabase * _Nonnull db, BOOL * _Nonnull rollback) {

}];

不管是使用 inDatabase 还是 inDeferredTransaction 都是同步的,所以在这里不需要任何处理。

FMDatabaseQueue 内部已经打开了数据库,所以不需要再使用 open 、close。但是当使用 FMResultSet 这个类的时候,记得一定要在block内处理完 FMResultSet 相关的逻辑,然后调用 close 方法。不要在block之外使用FMResultSet,不然会报错。

1
2
3
4
5
6
7
__block BOOL isOK = NO;
[self.databaseQueue inDatabase:^(FMDatabase * _Nonnull db) {
FMResultSet *results = [db executeQuery:sql];
isOK = results.next;
[results close];
}];
return isOK;

备注: 在block内处理完FMResultSet的相关逻辑,就算不调用 [results close] ,也不会报错,可能是由于block处理完,就自动释放了这个类。但注意不要在block外面使用FMResultSet。

Demo

Demo中目前没有加入keyid,操作不当会出现多个相同的数据,只能以后再改了。。。。
GitHub Demo

###前言
记录下来,有好几次在这里花时间了

####一、相对约束

上图表示的意思是

Superview.Trailing = Button.Trailing * (5 / 4) - 50

Superview.Trailing 表示Superview右边相对左边的距离。

####二、固有约束
Content Hugging Priority: 抗拉伸优先级,优先级越高,越难拉伸。
Content Compression Resistance Priority: 抗拉压缩先级,优先级越高,越难压缩。

序言

单元测试的基本操作。

新建单元测试文件

一般项目会默认建立一个单元测试文件,如果没有或者需要创建新的单元测试文件,可以创建一个新的文件,在选择文件类型为Unit Testing 类型。创建好文件后就可以开始了。

基本方法

setup

开始测试的时候会调用这个方法。可以在这个位置初始化一些对象、数值等。

tearDown

测试结束的时候会调用这个方法。一般在这里释放对象。

testExample

需要进行单元测试的方法,可以自定义方法名,方法名必须用test开头,返回类型是void。

testPerformanceExample

这里可以进行一下性能测试,可以测试代码的执行时间。

异步单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)testExample {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// 单元测试

XCTestExpectation *expectation = [self expectationWithDescription:@"出错了。。。。。"];
//[self expectationForNotification:(@"testNoti") object:nil handler:nil];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(5);
XCTAssertTrue(true,@"数据出错");
//测试成功
[expectation fulfill];
//[[NSNotificationCenter defaultCenter] postNotificationName:@"testNoti" object:nil];
});

[self waitForExpectationsWithTimeout:3 handler:^(NSError * _Nullable error) {
NSLog(@"%@",error);
}];
}
// 当任务中设置的睡眠时间大于等待时间的时候就会报错。
// 使用 XCTestExpectation 和 Notification 均能实现。

时间性能测试

当 Baseline *(1 - Max STDDEV)大于 Average 时,测试通过,小于则不通过。

常用断言

// 如果 expression 不为 true,则提示后面的错误信息。
XCTAssertTrue(expression, format…)
// 如果 expression 不为 nil,则提示后面的错误信息。
XCTAssertNil(expression, …)
// 如果 expression1 expression2 不相等,则提示后面的错误信息。
XCTAssertEqual(expression1, expression2, format…)

总结

单元测试不需要搭建环境,却可以为我们节省一定的时间,不需要每次调试功能都要一个一个点进去。
如果可以将单元测试集成到CI中,将更方便。

问题

测试人员在测试的时候,将系统语言设置为没有本地化的语言,然后APP显示的是中文!居然是中文,可我的“Base”语言是英文来的。

### 验证

经过测试后发现原来当将系统语言设置为没有本地化的语言的时候,APP会显示上一次的语言,而不是APP里面设置的“Base”语言。

网上搜索一下,大概得到了答案。iOS系统会从语言和地区栏里面按照优先级顺序查找,如下图,系统会检测APP有没有本地化“English”,如果有直接使用,如果没有则检查“简体中文”有没有本地化,最后检测“French”。如果都没有本地化,则使用“Base”语言。如果“Base”也没有则使用默认值(比如“key”值)。

![](http://ovsbvt5li.bkt.clouddn.com/17-11-7/49595790.jpg?imageView/2/w/300)

上图是手机里面的语言和时区界面的截图。
APP语言一共有3种,地区为中国。

解决方案

系统方法:NSLocalizedString(key, comment) 的宏定义如下

1
2
3
4
5
6
7
8
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]
#define NSLocalizedStringFromTable(key, tbl, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
[bundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \
[bundle localizedStringForKey:(key) value:(val) table:(tbl)]

只要将bundle更换为指定的语言就行了。
这里可以参考第三方库iVersion

大概步骤如下:
先获取到当前语言,然后判断APP是否有本地化该语言,如果有则直接使用,如果没有则使用APP默认语言(一般就是英语)。而不是根据语言优先级来设置语言。

写一段简单的代码,获取所有的系统语言。

1
2
3
4
//两种方法得到的结果相同
//NSArray *languages = [NSLocale preferredLanguages];
NSArray *languages = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleLanguages"];
NSLog(@"AppleLanguages = %@",languages);
1
2
3
4
5
6
//输出
AppleLanguages = (
"en-CN",
"zh-Hans-CN",
"fr-CN"
)

输出的语言里面刚好对应上面截图的三种语言,结尾的CN对应地区中国。
将地区改为美国后,输出如下:

1
2
3
4
5
AppleLanguages = (
"en-US",
"zh-Hans-US",
"fr-US"
)

然而系统创建的文件名如下

所以需要将系统创建的lproj文件夹与APP获取到的第一语言对应起来。

疑问:每一个lproj文件夹相当于一个bundle !?

代码实现

.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
// NSString+VCHLocalization.h
// Mr.Security
//
// Created by chenhui on 2017/11/8.
// Copyright © 2017年 vhuichen. All rights reserved.
//

#import <Foundation/Foundation.h>

#define kNSL(value) ((NSString *)(value)).vch_localization

@interface NSString (VCHLocalization)

- (NSString *)vch_localization;

@end

.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
//
// NSString+VCHLocalization.m
// Mr.Security
//
// Created by chenhui on 2017/11/8.
// Copyright © 2017年 vhuichen. All rights reserved.
//

#import "NSString+VCHLocalization.h"

@implementation NSString (VCHLocalization)

- (NSString *)vch_localization {
static NSBundle *bundle = nil;
if (bundle == nil) {
NSString *defaultLanguage = @"en";
NSString *currentLanguage = [[NSLocale preferredLanguages] count] ? [NSLocale preferredLanguages][0] : defaultLanguage;
NSArray *localizations = [[NSBundle mainBundle] localizations];
for (int i = 0; i < localizations.count; i++) {
NSString *localization = localizations[i];
if ([currentLanguage hasPrefix:localization]) {
defaultLanguage = localization;
break;
}
}
NSString *bundlePath = [[NSBundle mainBundle] pathForResource:defaultLanguage ofType:@"lproj"];
bundle = [NSBundle bundleWithPath:bundlePath] ? : [NSBundle mainBundle];
}
return [bundle localizedStringForKey:self value:self table:nil];
}

@end

SVN相关概念

主干(trunk)、分支(branch)、标记(tag)

trunk:有两种说法:
1、主方向开发,版本迭代,一个新的需求,一般都是放在trunk里面开发的。
2、也有一种做法是把所有开发的工作都拉到branch上去完成,也就是trunk和tag只负责版本记录,brank负责所有的开发工作。当brank完成某一部分功能的时候合并到trunk,其他人便可从trunk拉最新的代码。

branch:和trunk并行开发。有几种情况下需要创建branch:
1、当某个功能不确定能否实现或者想先看看效果的时候,可以拉一个branch,在这个branch上开发,开发完成,再考虑是否合并到trunk。
2、当trunk正在开发某个最新版本的时候,线上版本出现一个急需修复的bug,由于时间紧,不能等到新版本开发完成的时候才修复这个bug,这时候就应该在线上版本对应的tag下拉一个branch,在这个branch下修复bug,测试,然后上线。

tag:记录每一个milestone(里程碑),一般每一个线上版本都会打一个tag,一旦出现急需修复的bug,可以在对应的tag下拉一个branch进行修复。

branch是动态的,一直往前走。tag是静态的,只做版本记录,相当于只读。
至于trunk我理解的是功能的集合,多人协同开发时,每个人每当完成一个功能都将合并到trunk中,也可以将trunk中最新的代码合并到自己的branch中

Cornerstonr操作

SVN搭建、配置

参考另一篇文章:
iOS基础之搭建SVN服务器

Check Out (working copy)


选中对应的项目,点击左上角的Check Out,将项目copy到本地

Merge

选中需要Merge的项目(也就是合并后的项目),点击Merge,出现如下图所示的界面:

在Merge from里选择被合并的项目。点击 Merge Changes ,完成。

Commit

将本地代码提交带远程仓库。