第2章 巧用多线程提高项目性能
上篇博客被吐槽无干活,好吧,我的博客确实不是教大家很酷炫的动画,很实用的框架,我的博客讲到都是一些基础知识,是帮助大家在做完一个项目之后,反刍项目设计,项目内容,然后加以修改的东西。如果各位有什么意见,或者我写的概念中,有什么不对,或者不完善的地方,希望大家可以提出来,告诉我,我好及时修改,避免有学习的人带了错误的概念,谢谢大家,我希望技术是用来分享的,帮助更多的人,更快的提供专业技术。
本章主要介绍在iOS中多线程的使用方法,这对于合理利用cpu使用效率尤为重要。多线程实现了对CPU的并发执行,避免阻塞所造成的CPU计算时间浪费。在单核CPU的时代,实现多线程技术还局限于软件层面,这样给线程间切换带来一定的成本开销,但由于多线程的优势更大所以开发者还是会选择使用子线程处理逻辑问题,而到了多核CPU时代,由于硬件本身支持了多线程技术,就可以真正的让多线程同时地运行。
而OS X和iOS作为多线程操作系统,它们继承UNIX系统使用的POSIX线程模型。OS X和iOS都提供了一套底层为C语言的POSIX线程API来创建和管理线程。但实际上的应用开发中大可不必使用那些晦涩难懂的c语言函数,因为苹果公司已经为开发者提供了更为简单的oc的解决方案。接下来会逐一分析。
2.1 不得不提一下的NSThread
使用多线程技术,对于整个计算机科学的发展而言可谓至关重要,与其在手机内存并不是十分充裕的情况下,灵活地使用多线程,可以充分利用现有资源,解决很多复杂地逻辑问题,而在iOS系统中,苹果公司为广大开发者提供了非常丰富地多线程框架,供开发者们使用,接下来,本书将由浅入深,从最为轻量地NSThread开始,分段讲解iOS系统中的多线程技术。
2.1.1 iOS中的多线程
在iOS系统中,多线程技术也分成不同的层级,层级越高其封装度越高,使用的方便性,以及功能性也就更加完整,那么根据不同的层级,多线程技术由低到高可以分成:
1)Cocoa threads;
2)Operation objects;
3)Grand Central Dispatch (GCD)。
Cocoa threads是Cocoa框架中的范式,通常使用NSThread类,以及一些NSObject中的方法实现开辟多线程方法。Operation objects为操作队列对象主要用于构建线程池,通常使用NSOperation及其子类实现。而GCD是纯C的API,也是iOS4之后苹果主推的多线程处理方法,它的执行效率,以及管理方法都比较人性化,这些在后面将会进行详细的讲解。
2.1.2 NSThread的基本使用方法
NSThread是iOS中比较基础的多线程处理的类,该类较为轻量,非常容易使用,如下代码。
{
/**
*创建一个子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *thread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget:)
object:nil];
/**
* 线程名称
*/
thread.name = @"iOS性能优化";
/**
* 执行该线程
*/
[thread start];
/**
* 线程执行方法
*/
}
-(void)childThreadTarget:(NSThread *)thread
{
NSLog(@"线程名:%@",[[NSThread currentThread] name]);
}
打印结果为:2015-06-08 15:16:56.602 iOS性能优化[54761:847490] 线程名:iOS性能优化
这里使用多线程技术并不能让人很直观的看到其效果,因为处理方法比较简单并不会造成特别严重线程的阻塞,接下来的事例将会说明多线程的实用性。
{
/**
* 实用for循环模拟数据阻塞
* 由于处理次数过多造成后面代码无法正常执行
*/
for (int i = 0; i<MAXFLOAT; i++)
{
NSLog(@"执行次数:%d",i);
}
/**
* 初始化一个图层
* 并将图层放入窗口中
*/
UIView *view = [[UIView alloc]initWithFrame:self.window.frame];
/**
* 设置图层的背景颜色为红色
*/
view.backgroundColor = [UIColor redColor];
/**
* 将视图添加到窗口
*/
[self.window addSubview:view];
}
运行结果如图2.1所示。
图2.1 由于线程阻塞导致图层无法加载到window上
从演示图中可以清晰的看到,由于主线程中处理较为复杂的方法,导致UIView视图无法成功加载到窗口容器中,这种情况常发生在同步网络请求中,这里并不做过多介绍,但需要知道的是,在程序开发中要将数据处理的逻辑代码尽可能放到子线程中,这样可以有效提高界面的流畅度,就好比A城市要向B城市运输一些建筑材料,或者其他资源,但目前只一条高速公路,如果运输车辆较少的情况下,这条公路还是可以满足需求的,但是一旦车辆增多公路就会变得在十分拥挤从而造成堵车,这时目的城市所需要的材料无法被顺利的运输过去,这时需要做的就是为这条主路修建一些辅路,这样一些不需要立即送达的资源可以放到辅路中行驶,这将会大大减轻主路的行驶压力,使得资源可以顺利运往目的地,而这里多线程技术就是拓展这些辅路的方法,接下来在通过事例演示,看一下运行结果的变化。如下代码。
{
/**
*创建一个子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *thread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget)
object:nil];
/**
* 线程名称
*/
thread.name = @"iOS性能优化";
/**
* 执行该线程
*/
[thread start];
/**
* 初始化一个图层
* 并将图层放入窗口中
*/
UIView *view = [[UIView alloc]initWithFrame:self.window.frame];
/**
* 设置图层的背景颜色为红色
*/
view.backgroundColor = [UIColor redColor];
/**
* 将视图添加到窗口
*/
[self.window addSubview:view];
}
/**
* 线程执行方法
*/
-(void)childThreadTarget
{
/**
* 实用for循环模拟数据阻塞
* 由于处理次数过多造成后面代码无法正常执行
*/
for (int i = 0; i<MAXFLOAT; i++)
{
NSLog(@"执行次数:%d",i);
}
}
控制台打印结果:2015-06-08 15:48:42.082 Suibian[56673:870857] 执行次数:0
2015-06-08 15:48:42.084 iOS性能优化[56673:870857] 执行次数:1
2015-06-08 15:48:42.084 iOS性能优化[56673:870857] 执行次数:2
2015-06-08 15:48:42.084 iOS性能优化[56673:870857] 执行次数:3
2015-06-08 15:48:42.084 iOS性能优化[56673:870857] 执行次数:4
2015-06-08 15:48:42.084 iOS性能优化[56673:870857] 执行次数:5
2015-06-08 15:48:42.085 iOS性能优化[56673:870857] 执行次数:6
2015-06-08 15:48:42.085 iOS性能优化[56673:870857] 执行次数:7
2015-06-08 15:48:42.085 iOS性能优化[56673:870857] 执行次数:8
2015-06-08 15:48:42.085 iOS性能优化[56673:870857] 执行次数:9
2015-06-08 15:48:42.085 iOS性能优化[56673:870857] 执行次数:10
从控制台打印结果可以看出,程序运行并没有影响但for的计算,那UI是否与刚刚的结果一样,并无变化呢?如图2.2所示。
图2.2 页面加载成功
正如图2.2中运行结果一样,利用子线程处理for循环的庞大的信息处理,并不会影响主线程加载UI视图,但这里有一个建议,就是在处理UI视图的时候,不要放在子线程中执行,不然会发生很严重的bug。
虽然使用NSThread较为轻量简单,但在处理线程池,线程同步是NSThread却显得十分臃肿麻烦,这里举一个银行取款的例子,来解释线程同步的概念,如下代码。
@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
/**
* 创建实例变量总钱数
*/
int _allMoney;
/**
* 创建实例变量已取数量
*/
int _num;
}
@property (strong, nonatomic) UIWindow *window;
@end
@implementation AppDelegate
-(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
_allMoney = 100;
_num = 0;
/**
*创建一个子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *thread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget)
object:nil];
/**
* 线程名称
*/
thread.name = @"A人员";
/**
* 执行该线程
*/
[thread start];
/**
*创建一个同步子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *sameThread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget)
object:nil];
sameThread.name = @"B人员";
[sameThread start];
}
/**
* 线程执行方法
*/
-(void)childThreadTarget
{
while (TRUE) {
if(_allMoney >= 0){
_num = 100 - _allMoney;
NSLog(@"当前存款数量:%d,已取款:%d,线程名:%@",_allMoney,_num,[[NSThread currentThread] name]);
_allMoney--;
}else{
break;
}
}
}
运行结果为:2015-06-08 17:53:22.413 iOS性能优化[64395:958812] 当前票数是:100,售出:0,线程名:B人员
2015-06-08 17:53:22.415 iOS性能优化[64395:958812] 当前票数是:99,售出:1,线程名:B人员
2015-06-08 17:53:22.413 iOS性能优化[64395:958811] 当前票数是:100,售出:0,线程名:A人员
2015-06-08 17:53:22.415 iOS性能优化[64395:958812] 当前票数是:98,售出:2,线程名:B人员
2015-06-08 17:53:22.415 iOS性能优化[64395:958812] 当前票数是:97,售出:3,线程名:B人员
2015-06-08 17:53:22.416 iOS性能优化[64395:958812] 当前票数是:95,售出:5,线程名:B人员
2015-06-08 17:53:22.416 iOS性能优化[64395:958812] 当前票数是:94,售出:6,线程名:B人员
2015-06-08 17:53:22.417 iOS性能优化[64395:958812] 当前票数是:93,售出:7,线程名:B人员
2015-06-08 17:53:22.418 iOS性能优化[64395:958812] 当前票数是:92,售出:8,线程名:B人员
从运行结果可以看出,由于两个线程同时访问一个方法,一块资源,带来的数据混乱,如果发生在现实生活中将是十分恐怖的一件事情。就如果当用户A与用户B持有相同的信用卡,只不过一个是主卡一个是副卡,当两人同时取款的时候B的率先被处理了两笔交易,而此时A的取款请求才被处理,但显示给A的信息却还是100。当然没人希望被机器欺骗,这就需要开发者可以谨慎思考这个问题了,在iOS系统中,提供了一个类用了处理线程同步,称之为线程锁。这里要介绍iOS系统框架提供NSLock类,这是一个比较容易掌握的方法,那么使用这个类后之前的代码应该修改为如下代码。
@interface AppDelegate : UIResponder <UIApplicationDelegate>
{
/**
* 创建线程锁实力对象
*/
NSLock *_theLock;
/**
* 创建实例变量总钱数
*/
int _allMoney;
/**
* 创建实例变量已取数量
*/
int _num;
}
@property (strong, nonatomic) UIWindow *window;
@end
@implementation AppDelegate
-(BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.window.backgroundColor = [UIColor whiteColor];
_allMoney = 100;
_num = 0;
/**
* 初始化线程锁实例变量
*/
_theLock = [[NSLock alloc]init];
/**
*创建一个子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *thread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget)
object:nil];
/**
* 线程名称
*/
thread.name = @"A人员";
/**
* 执行该线程
*/
[thread start];
/**
*创建一个同步子线程
*:param: childThreadTarget 使用子线程执行改方法
*/
NSThread *sameThread = [[NSThread alloc]initWithTarget:self
selector:@selector(childThreadTarget)
object:nil];
sameThread.name = @"B人员";
[sameThread start];
[self.window makeKeyAndVisible];
return YES;
}
/**
* 线程执行方法
*/
-(void)childThreadTarget
{
while (TRUE) {
// 上锁
// [ticketsCondition lock];
[_theLock lock];
if(_allMoney >= 0){
_num = 100 - _allMoney;
NSLog(@"当前存款数量:%d,已取款:%d,线程名:%@",_allMoney,_num,[[NSThread currentThread] name]);
_allMoney--;
}else{
break;
}
[_theLock unlock];
}
}
运行结果为:2015-06-09 11:25:45.542 iOS性能优化[7190:70542] 当前存款数量:10,已取款:90,线程名:A人员
2015-06-09 11:25:45.542 iOS性能优化[7190:70543] 当前存款数量:9,已取款:91,线程名:B人员
2015-06-09 11:25:45.543 iOS性能优化[7190:70542] 当前存款数量:8,已取款:92,线程名:A人员
2015-06-09 11:25:45.543 iOS性能优化[7190:70543] 当前存款数量:7,已取款:93,线程名:B人员
2015-06-09 11:25:45.543 iOS性能优化[7190:70542] 当前存款数量:6,已取款:94,线程名:A人员
2015-06-09 11:25:45.543 iOS性能优化[7190:70543] 当前存款数量:5,已取款:95,线程名:B人员
2015-06-09 11:25:45.544 iOS性能优化[7190:70542] 当前存款数量:4,已取款:96,线程名:A人员
2015-06-09 11:25:45.544 iOS性能优化[7190:70543] 当前存款数量:3,已取款:97,线程名:B人员
2015-06-09 11:25:45.544 iOS性能优化[7190:70542] 当前存款数量:2,已取款:98,线程名:A人员
2015-06-09 11:25:45.545 iOS性能优化[7190:70543] 当前存款数量:1,已取款:99,线程名:B人员
2015-06-09 11:25:45.546 iOS性能优化[7190:70542] 当前存款数量:0,已取款:100,线程名:A人员
这里可以看到,使用线程锁后,A线程与B线程就像两个排队取款的人员一样,十分有序的取出自己的资源,这就是线程同步中线程锁的作用,从而合理的避免了多线程同步访问造成的数据错误,除此之外iOS系统还提供了很多线程锁的方法,这里并不一一介绍,在后面会进行详细的探讨。