今天去面试的是一家规模不太大的公司,首先是笔试,是12道题目。题目标红了,答案的话是我从网上查来的,大家觉得不标准的可以自行百度。
(1) 应用程序启动时的顺序
首先回顾一下应用程序的启动过程
①.先加载Main函数
②.在Main函数里的 UIApplicationMain方法中,创建Application对象 创建Application的Delegate对象
③.创建主循环,代理对象开始监听事件
④.启动完毕会调用 didFinishLaunching方法,并在这个方法中创建UIWindow
⑤.设置UIWindow的根控制器是谁
⑥.如果有storyboard,会根据info.plist中找到应用程序的入口storyboard并加载箭头所指的控制器
⑦.显示窗口
本文考虑的时步骤③之后到步骤⑦结束时将要调用的方法
其中有AppDelegate,ViewController,MainView(控制器的View),ChildView(子控件的View)的18个方法
AppDelegate中的:
1.application:didFinishLaunchingWithOptions:
2.applicationDidBecomeActive:
ViewController中的:
3.loadView
4.viewDidLoad
5.load
6.initialize
7.viewWillAppear
8.viewWillLayoutSubviews
9.viewDidLayoutSubviews
10.viewDidAppear
MainView(控制器的View)中的:
11.initWithCoder(如果没有storyboard就会调用initWithFrame,这里两种方法视为一种)
12.awakeFromNib
13.layoutSubviews
14.drawRect
ChildView(子控件View)中的:
15.initWithCoder(如果没有storyboard就会调用initWithFrame,这里两种方法视为一种)
16.awakeFromNib
17.layoutSubviews
18.drawRect
(2) 堆和栈的区别
按管理方式分
对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
按分配方式分
堆是动态分配和回收内存的,没有静态分配的堆
栈有两种分配方式:静态分配和动态分配
静态分配是系统编译器完成的,比如局部变量的分配
动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动态分配也由系统编译器进行释放,不需要程序员手动管理
一位网友用10个字总结了堆和栈的区别
栈是吃了吐 堆是吃了拉
(3) 线程和进程的区别
1.1 程序:
由源代码生成的可执行应用。(例如:QQ.APP)
1.2 进程:
一个正在运行的程序可以看做一个进程。(例如:正在运行的QQ就是一个进程),进程拥有独立运行所需的全部资源。
1.3 线程:
程序中独立运行的代码段。(例如:接收QQ消息的代码)
一个进程是由一或多个线程组成。进程只负责资源的调度和分配,线程才是程序真正的执行单元,负责代码的执行。
2. 单线程与多线程有什么区别
2.1单线程
每个正在运行的程序(即进程),至少包含一个线程,这个线程叫主线程。
主线程在程序启动时被创建,用于执行main函数。
只有一个主线程的程序,称作单线程程序。
主线程负责执行程序的所有代码(UI展现以及刷新,网络请求,本地存储等等)。这些代码只能顺序执行,无法并发执行。
2.2多线程
拥有多个线程的程序,称作多线程程序。
iOS允许用户自己开辟新的线程,相对于主线程来讲,这些线程,称作子线程。
可以根据需要开辟若干子线程
子线程和主线程是 都是 独立的运行单元,各自的执行互不影响,因此能够并发执行。
2.3区别
单线程程序:只有一个线程,代码顺序执行,容易出现代码阻塞(页面假死)。
多线程程序:有多个线程,线程间独立运行,能有效的避免代码阻塞,并且提高程序的运行性能。
注意:iOS中关于UI的添加和刷新必须在主线程中操作。
(4) 如何开启多线程
在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项。当然也会给出几种多线程的案例,在实际使用中感受它们的区别。还有一点需要说明的是,这篇文章将会使用 Swift 和 Objective-c 两种语言讲解,双语幼儿园。OK,let's begin!
概述
这篇文章中,我不会说多线程是什么、线程和进程的区别、多线程有什么用,当然我也不会说什么是串行、什么是并行等问题,这些我们应该都知道的。
在 iOS 中其实目前有 4 套多线程方案,他们分别是:
Pthreads
NSThread
GCD
NSOperation& NSOperationQueue
所以接下来,我会一一讲解这些方案的使用方法和一些案例。在将这些内容的时候,我也会顺带说一些多线程周边产品。比如:线程同步、 延时执行、 单例模式 等等。
Pthreads
其实这个方案不用说的,只是拿来充个数,为了让大家了解一下就好了。百度百科里是这么说的:
POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。
简单地说,这是一套在很多操作系统上都通用的多线程API,所以移植性很强(然并卵),当然在 iOS 中也是可以的。不过这是基于 c语言 的框架,使用起来这酸爽!感受一下:
OBJECTIVE-C
当然第一步要包含头文件
#import<pthread.h>
然后创建线程,并执行任务
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
pthread_t thread;
//创建一个线程并自动执行
pthread_create(&thread, NULL, start,NULL);
}
void*start(void *data) {
NSLog(@"%@", [NSThreadcurrentThread]);
return NULL;
}
打印输出:
2015-07-2723:57:21.689 testThread[10616:2644653] <NSThread: 0x7fbb48d33690>{number= 2, name = (null)}
看代码就会发现他需要 c语言函数,这是比较蛋疼的,更蛋疼的是你需要手动处理线程的各个状态的转换即管理生命周期,比如,这段代码虽然创建了一个线程,但并没有销毁。
SWIFT
很遗憾,在我目前的swift1.2 中无法执行这套方法,原因是这个函数需要传入一个函数指针CFunctionPointer<T> 类型,但是目前 swift 无法将方法转换成此类型。听说 swift 2.0 引入一个新特性 @convention(c), 可以完成 Swift 方法转换成 c 语言指针的。在这里可以看到
那么,Pthreads方案的多线程我就介绍这么多,毕竟做 iOS 开发几乎不可能用到。但是如果你感兴趣的话,或者说想要自己实现一套多线程方案,从底层开始定制,那么可以去搜一下相关资料。
NSThread
这套方案是经过苹果封装后的,并且完全面向对象的。所以你可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理,所以这套方案也是偶尔用用,比如 [NSThread currentThread],它可以获取当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。下面来看看它的一些用法。
创建并启动
先创建线程类,再启动
OBJECTIVE-C
// 创建
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run:) object:nil];
// 启动
[thread start];
SWIFT
//创建
let thread = NSThread(target: self, selector:"run:", object: nil)
//启动
thread.start()
创建并自动启动
OBJECTIVE-C
[NSThreaddetachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
SWIFT
NSThread.detachNewThreadSelector("run:", toTarget: self,withObject: nil)
使用NSObject 的方法创建并自动启动
OBJECTIVE-C
[selfperformSelectorInBackground:@selector(run:) withObject:nil];
SWIFT
很遗憾 too! 苹果认为 performSelector: 不安全,所以在 Swift 去掉了这个方法。
Note: TheperformSelector: method and related selector-invoking methods are not importedin Swift because they are inherently unsafe.
其他方法
除了创建启动外,NSThread还以很多方法,下面我列举一些常见的方法,当然我列举的并不完整,更多方法大家可以去类的定义里去看。
OBJECTIVE-C
//取消线程
-(void)cancel;
//启动线程
-(void)start;
//判断某个线程的状态的属性
@property(readonly, getter=isExecuting) BOOL executing;
@property(readonly, getter=isFinished) BOOL finished;
@property(readonly, getter=isCancelled) BOOL cancelled;
//设置和获取线程名字
-(void)setName:(NSString*)n;
-(NSString*)name;
//获取当前线程信息
+(NSThread *)currentThread;
//获取主线程信息
+(NSThread *)mainThread;
//使当前线程暂停一段时间,或者暂停到某个时刻
+(void)sleepForTimeInterval:(NSTimeInterval)time;
+(void)sleepUntilDate:(NSDate *)date;
SWIFT
Swift的方法名字和OC的方法名都一样,我就不浪费空间列举出来了。
其实,NSThread用起来也挺简单的,因为它就那几种方法。同时,我们也只有在一些非常简单的场景才会用 NSThread, 毕竟它还不够智能,不能优雅地处理多线程中的其他高级概念。所以接下来要说的内容才是重点。
GCD
GrandCentral Dispatch,听名字就霸气。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 c语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。所以基本上大家都使用 GCD 这套方案,老少咸宜,实在是居家旅行、杀人灭口,必备良药。不好意思,有点中二,咱们继续。
任务和队列
在 GCD 中,加入了两个非常重要的概念:任务 和 队列。
任务:即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式:同步执行 和 异步执行,他们之间的区别是 是否会创建新的线程。
同步执行:只要是同步执行的任务,都会在当前线程执行,不会另开线程。
异步执行:只要是异步执行的任务,都会另开线程,在别的线程执行。
更新:
这里说的并不准确,同步(sync) 和 异步(async) 的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕!
如果是 同步(sync)操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
队列:用于存放任务。一共有两种队列, 串行队列 和 并行队列。
串行队列 中的任务会根据队列的定义 FIFO 的执行,一个接一个的先进先出的进行执行。
更新:放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。
并行队列 中的任务 根据同步或异步有不同的执行方式。
更新:放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。
虽然很绕,但请看下表:
同步执行 异步执行
串行队列 当前线程,一个一个执行 其他线程,一个一个执行
并行队列 当前线程,一个一个执行 开很多线程,一起执行
创建队列
主队列:这是一个特殊的 串行队列。什么是主队列,大家都知道吧,它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。
//OBJECTIVE-C
dispatch_queue_t queue =ispatch_get_main_queue();
//SWIFT
let queue = ispatch_get_main_queue()
自己创建的队列:凡是自己创建的队列都是 串行队列。 其中第一个参数是标识符,用于 DEBUG 的时候标识唯一的队列,可以为空。大家可以看xcode的文档查看参数意义。
更新:自己可以创建 串行队列, 也可以创建 并行队列。看下面的代码(代码已更新),它有两个参数,第一个上面已经说了,第二个才是最重要的。
第二个参数用来表示创建的队列是串行的还是并行的,传入 DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。
//OBJECTIVE-C
//串行队列
dispatch_queue_t queue =dispatch_queue_create("tk.bourne.testQueue", NULL);
dispatch_queue_t queue =dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
//并行队列
dispatch_queue_t queue =dispatch_queue_create("tk.bourne.testQueue",DISPATCH_QUEUE_CONCURRENT);
//SWIFT
//串行队列
let queue =dispatch_queue_create("tk.bourne.testQueue", nil);
let queue =dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL)
//并行队列
let queue =dispatch_queue_create("tk.bourne.testQueue",DISPATCH_QUEUE_CONCURRENT)
全局并行队列:这应该是唯一一个并行队列, 只要是并行任务一般都加入到这个队列。这是系统提供的一个并发队列。
//OBJECTIVE-C
dispatch_queue_t queue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//SWIFT
let queue =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
创建任务
同步任务: 不会另开线程 改:会阻塞当前线程 (SYNC)
OBJECTIVE-C
dispatch_sync(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThreadcurrentThread]);
});
SWIFT
dispatch_sync(<#queue#>, { () ->Void in
//code here
println(NSThread.currentThread())
})
异步任务:会另开线程 改:不会阻塞当前线程 (ASYNC)
OBJECTIVE-C
dispatch_async(<#queue#>, ^{
//code here
NSLog(@"%@", [NSThreadcurrentThread]);
});
SWIFT
dispatch_async(<#queue#>, { () ->Void in
//code here
println(NSThread.currentThread())
})
更新:
为了更好的理解同步和异步,和各种队列的使用,下面看两个示例:
示例一:
以下代码在主线程调用,结果是什么?
NSLog("之前 - %@", NSThread.currentThread())
dispatch_sync(dispatch_get_main_queue(),{ () -> Void in
NSLog("sync - %@",NSThread.currentThread())
})
NSLog("之后 - %@", NSThread.currentThread())
答案:
只会打印第一句:之前 - <NSThread: 0x7fb3a9e16470>{number = 1, name = main} ,然后主线程就卡死了,你可以在界面上放一个按钮,你就会发现点不了了。
解释:
同步任务会阻塞当前线程,然后把 Block 中的任务放到指定的队列中执行,只有等到 Block 中的任务完成后才会让当前线程继续往下运行。
那么这里的步骤就是:打印完第一句后,dispatch_sync 立即阻塞当前的主线程,然后把 Block 中的任务放到 main_queue 中,可是 main_queue 中的任务会被取出来放到主线程中执行,但主线程这个时候已经被阻塞了,所以 Block 中的任务就不能完成,它不完成,dispatch_sync 就会一直阻塞主线程,这就是死锁现象。导致主线程一直卡死。
示例二:
以下代码会产生什么结果?
let queue= dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL)
NSLog("之前 - %@", NSThread.currentThread())
dispatch_async(queue,{ () -> Void in
NSLog("sync之前- %@", NSThread.currentThread())
dispatch_sync(queue, { () -> Void in
NSLog("sync - %@",NSThread.currentThread())
})
NSLog("sync之后- %@", NSThread.currentThread())
})
NSLog("之后 - %@", NSThread.currentThread())
**答案:**
2015-07-3002:06:51.058 test[33329:8793087] 之前 - <NSThread:0x7fe32050dbb0>{number = 1, name = main}
2015-07-3002:06:51.059 test[33329:8793356] sync之前 - <NSThread:0x7fe32062e9f0>{number = 2, name = (null)}
2015-07-3002:06:51.059 test[33329:8793087] 之后 - <NSThread:0x7fe32050dbb0>{number = 1, name = main}
很明显 `sync- %@` 和 `sync之后 - %@` 没有被打印出来!这是为什么呢?我们再来分析一下:
>**分析:**
我们按执行顺序一步步来哦:
1. 使用 `DISPATCH_QUEUE_SERIAL` 这个参数,创建了一个 **串行队列**。
2. 打印出 `之前 - %@` 这句。
3.`dispatch_async` 异步执行,所以当前线程不会被阻塞,于是有了两条线程,一条当前线程继续往下打印出 `之后 - %@`这句, 另一台执行 Block 中的内容打印 `sync之前 - %@` 这句。因为这两条是并行的,所以打印的先后顺序无所谓。
4. 注意,高潮来了。现在的情况和上一个例子一样了。`dispatch_sync`同步执行,于是它所在的线程会被阻塞,一直等到 `sync`里的任务执行完才会继续往下。于是 `sync` 就高兴的把自己Block 中的任务放到 `queue` 中,可谁想`queue` 是一个串行队列,一次执行一个任务,所以 `sync` 的 Block 必须等到前一个任务执行完毕,可万万没想到的是 `queue` 正在执行的任务就是被 `sync` 阻塞了的那个。于是又发生了死锁。所以 `sync` 所在的线程被卡死了。剩下的两句代码自然不会打印。
### 队列组
队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。下面是使用方法,这是一个很实用的功能。
######OBJECTIVE-C
```objective-c
//1.创建队列组
dispatch_group_tgroup = dispatch_group_create();
//2.创建队列
dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.多次使用队列组的方法执行任务, 只有异步方法
//3.1.执行3次循环
dispatch_group_async(group,queue, ^{
for (NSInteger i = 0; i < 3; i++) {
NSLog(@"group-01 - %@",[NSThread currentThread]);
}
});
//3.2.主队列执行8次循环
dispatch_group_async(group,dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 8; i++) {
NSLog(@"group-02 - %@",[NSThread currentThread]);
}
});
//3.3.执行5次循环
dispatch_group_async(group,queue, ^{
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"group-03 - %@",[NSThread currentThread]);
}
});
//4.都完成后会自动通知
dispatch_group_notify(group,dispatch_get_main_queue(), ^{
NSLog(@"完成 -%@", [NSThread currentThread]);
});
SWIFT
//1.创建队列组
let group= dispatch_group_create()
//2.创建队列
let queue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
//3.多次使用队列组的方法执行任务, 只有异步方法
//3.1.执行3次循环
dispatch_group_async(group,queue) { () -> Void in
for _ in 0..<3 {
NSLog("group-01 - %@",NSThread.currentThread())
}
}
//3.2.主队列执行8次循环
dispatch_group_async(group,dispatch_get_main_queue()) { () -> Void in
for _ in 0..<8 {
NSLog("group-02 - %@", NSThread.currentThread())
}
}
//3.3.执行5次循环
dispatch_group_async(group,queue) { () -> Void in
for _ in 0..<5 {
NSLog("group-03 - %@",NSThread.currentThread())
}
}
//4.都完成后会自动通知
dispatch_group_notify(group,dispatch_get_main_queue()) { () -> Void in
NSLog("完成 -%@", NSThread.currentThread())
}
打印结果
2015-07-2803:40:34.277 test[12540:3319271] group-03 - <NSThread:0x7f9772536f00>{number = 3, name = (null)}
2015-07-2803:40:34.277 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number= 1, name = main}
2015-07-2803:40:34.277 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.277 test[12540:3319271] group-03 - <NSThread:0x7f9772536f00>{number = 3, name = (null)}
2015-07-2803:40:34.278 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.278 test[12540:3319271] group-03 - <NSThread:0x7f9772536f00>{number = 3, name = (null)}
2015-07-2803:40:34.278 test[12540:3319271] group-03 - <NSThread:0x7f9772536f00>{number = 3, name = (null)}
2015-07-2803:40:34.278 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.277 test[12540:3319273] group-01 - <NSThread: 0x7f977272e8d0>{number= 2, name = (null)}
2015-07-2803:40:34.278 test[12540:3319271] group-03 - <NSThread:0x7f9772536f00>{number = 3, name = (null)}
2015-07-2803:40:34.278 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.278 test[12540:3319273] group-01 - <NSThread:0x7f977272e8d0>{number = 2, name = (null)}
2015-07-2803:40:34.278 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.278 test[12540:3319273] group-01 - <NSThread:0x7f977272e8d0>{number = 2, name = (null)}
2015-07-2803:40:34.279 test[12540:3319146] group-02 - <NSThread:0x7f977240ba60>{number = 1, name = main}
2015-07-2803:40:34.279 test[12540:3319146] group-02 - <NSThread: 0x7f977240ba60>{number= 1, name = main}
2015-07-2803:40:34.279 test[12540:3319146] 完成 - <NSThread:0x7f977240ba60>{number = 1, name = main}
这些就是 GCD 的基本功能,但是它的能力远不止这些,等讲完 NSOperation 后,我们再来看看它的一些其他方面用途。而且,只要你想象力够丰富,你可以组合出更好的用法。
更新:关于GCD,还有两个需要说的:
func dispatch_barrier_async(_queue: dispatch_queue_t, _ block: dispatch_block_t):
这个方法重点是你传入的 queue,当你传入的 queue 是通过 DISPATCH_QUEUE_CONCURRENT 参数自己创建的 queue 时,这个方法会阻塞这个 queue(注意是阻塞 queue ,而不是阻塞当前线程),一直等到这个 queue 中排在它前面的任务都执行完成后才会开始执行自己,自己执行完毕后,再会取消阻塞,使这个 queue 中排在它后面的任务继续执行。
如果你传入的是其他的 queue, 那么它就和 dispatch_async 一样了。
funcdispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t):
这个方法的使用和上一个一样,传入 自定义的并发队列(DISPATCH_QUEUE_CONCURRENT),它和上一个方法一样的阻塞queue,不同的是 这个方法还会 阻塞当前线程。
如果你传入的是其他的 queue, 那么它就和 dispatch_sync 一样了。
NSOperation和NSOperationQueue
NSOperation是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。操作步骤也很好理解:
将要执行的任务封装到一个 NSOperation 对象中。
将此任务添加到一个NSOperationQueue 对象中。
然后系统就会自动在执行任务。至于同步还是异步、串行还是并行请继续往下看:
添加任务
值得说明的是,NSOperation只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。
NSInvocationOperation: 需要传入一个方法名。
OBJECTIVE-C
//1.创建NSInvocationOperation对象
NSInvocationOperation *operation =[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run)object:nil];
//2.开始执行
[operation start];
SWIFT
在 Swift 构建的和谐社会里,是容不下 NSInvocationOperation 这种不是类型安全的败类的。苹果如是说。这里有相关解释
NSBlockOperation
OBJECTIVE-C
//1.创建NSBlockOperation对象
NSBlockOperation *operation =[NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThreadcurrentThread]);
}];
//2.开始任务
[operation start];
SWIFT
//1.创建NSBlockOperation对象
let operation = NSBlockOperation { () ->Void in
println(NSThread.currentThread())
}
//2.开始任务
operation.start()
之前说过这样的任务,默认会在当前线程执行。但是 NSBlockOperation 还有一个方法:addExecutionBlock: ,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务 会并发执行,它会 在主线程和其它的多个线程 执行这些任务,注意下面的打印结果:
OBJECTIVE-C
//1.创建NSBlockOperation对象
NSBlockOperation *operation =[NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThreadcurrentThread]);
}];
//添加多个Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//2.开始任务
[operation start];
SWIFT
//1.创建NSBlockOperation对象
let operation = NSBlockOperation { ()-> Void in
NSLog("%@",NSThread.currentThread())
}
//2.添加多个Block
for i in 0..<5 {
operation.addExecutionBlock { ()-> Void in
NSLog("第%ld次 - %@", i,NSThread.currentThread())
}
}
//2.开始任务
operation.start()
打印输出
2015-07-2817:50:16.585 test[17527:4095467] 第2次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}
2015-07-2817:50:16.585 test[17527:4095666] 第1次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}
2015-07-2817:50:16.585 test[17527:4095665] <NSThread: 0x7ff5c961b610>{number = 3,name = (null)}
2015-07-2817:50:16.585 test[17527:4095662] 第0次 - <NSThread: 0x7ff5c948d310>{number = 2, name = (null)}
2015-07-2817:50:16.586 test[17527:4095666] 第3次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}
2015-07-2817:50:16.586 test[17527:4095467] 第4次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}
NOTE:addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错:
‘***-[NSBlockOperation addExecutionBlock:]: blocks cannot be added after theoperation has started executing or finished'
NOTE:大家可能发现了一个问题,为什么我在 Swift 里打印输出使用 NSLog() 而不是 println() 呢?原因是使用 print() / println() 输出的话,它会简单地使用流(stream) 的概念,学过 C++ 的都知道。它会把需要输出的每个字符一个一个的输出到控制台。普通使用并没有问题,可是当多线程同步输出的时候问题就来了,由于很多 println() 同时打印,就会导致控制台上的字符混乱的堆在一起,而NSLog() 就没有这个问题。到底是什么样子的呢?你可以把上面 NSLog() 改为 println() ,然后一试便知。 更多 NSLog() 与 println() 的区别看这里
自定义Operation
除了上面的两种 Operation以外,我们还可以自定义 Operation。自定义Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现 cancel() 在内的各种方法。所以这个功能提供给高级玩家,我在这里就不说了,等我需要用到时在研究它,到时候可能会再做更新。
创建队列
看过上面的内容就知道,我们可以调用一个 NSOperation 对象的 start() 方法来启动这个任务,但是这样做他们默认是同步执行 的。就算是 addExecutionBlock 方法,也会在 当前线程和其他线程 中执行,也就是说还是会占用当前线程。这是就要用到队列 NSOperationQueue 了。而且,按类型来说的话一共有两种类型:主队列、其他队列。只要添加到队列,会自动调用任务的 start() 方法
主队列
细心的同学就会发现,每套多线程方案都会有一个主线程(当然啦,说的是iOS中,像 pthread 这种多系统的方案并没有,因为 UI线程 理论需要每种操作系统自己定制)。这是一个特殊的线程,必须串行。所以添加到主队列的任务都会一个接一个地排着队在主线程处理。
//OBJECTIVE-C
NSOperationQueue*queue = [NSOperationQueue mainQueue];
//SWIFT
let queue= NSOperationQueue.mainQueue()
其他队列
因为主队列比较特殊,所以会单独有一个类方法来获得主队列。那么通过初始化产生的队列就是其他队列了,因为只有这两种队列,除了主队列,其他队列就不需要名字了。
注意:其他队列的任务会在其他线程并行执行。
OBJECTIVE-C
//1.创建一个其他队列
NSOperationQueue*queue = [[NSOperationQueue alloc] init];
//2.创建NSBlockOperation对象
NSBlockOperation*operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThreadcurrentThread]);
}];
//3.添加多个Block
for(NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//4.队列添加任务
[queue addOperation:operation];
SWIFT
//1.创建其他队列
let queue= NSOperationQueue()
//2.创建NSBlockOperation对象
letoperation = NSBlockOperation { () -> Void in
NSLog("%@",NSThread.currentThread())
}
//3.添加多个Block
for i in0..<5 {
operation.addExecutionBlock { () -> Voidin
NSLog("第%ld次 - %@", i, NSThread.currentThread())
}
}
//4.队列添加任务
queue.addOperation(operation)
打印输出
2015-07-2820:26:28.463 test[18622:4443534] <NSThread: 0x7fd022c3ac10>{number = 5,name = (null)}
2015-07-2820:26:28.463 test[18622:4443536] 第2次 - <NSThread: 0x7fd022e36d50>{number = 2, name = (null)}
2015-07-2820:26:28.463 test[18622:4443535] 第0次 - <NSThread: 0x7fd022f237f0>{number = 4, name = (null)}
2015-07-2820:26:28.463 test[18622:4443533] 第1次 - <NSThread: 0x7fd022d372b0>{number = 3, name = (null)}
2015-07-2820:26:28.463 test[18622:4443534] 第3次 - <NSThread: 0x7fd022c3ac10>{number = 5, name = (null)}
2015-07-2820:26:28.463 test[18622:4443536] 第4次 - <NSThread: 0x7fd022e36d50>{number = 2, name = (null)}
OK, 这时应该发问了,大家将 NSOperationQueue 与 GCD的队列 相比较就会发现,这里没有串行队列,那如果我想要10个任务在其他线程串行的执行怎么办?
这就是苹果封装的妙处,你不用管串行、并行、同步、异步这些名词。NSOperationQueue 有一个参数maxConcurrentOperationCount 最大并发数,用来设置最多可以让多少个任务同时执行。当你把它设置为 1 的时候,他不就是串行了嘛!
NSOperationQueue还有一个添加任务的方法,- (void)addOperationWithBlock:(void(^)(void))block; ,这是不是和 GCD 差不多?这样就可以添加一个任务到队列中了,十分方便。
NSOperation有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A:从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:
OBJECTIVE-C
//1.任务一:下载图片
NSBlockOperation*operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 -%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任务二:打水印
NSBlockOperation*operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任务三:上传图片
NSBlockOperation*operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 -%@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.设置依赖
[operation2addDependency:operation1]; //任务二依赖任务一
[operation3addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue*queue = [[NSOperationQueue alloc] init];
[queueaddOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
SWIFT
//1.任务一:下载图片
letoperation1 = NSBlockOperation { () -> Void in
NSLog("下载图片 -%@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//2.任务二:打水印
letoperation2 = NSBlockOperation { () -> Void in
NSLog("打水印 - %@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//3.任务三:上传图片
letoperation3 = NSBlockOperation { () -> Void in
NSLog("上传图片 -%@", NSThread.currentThread())
NSThread.sleepForTimeInterval(1.0)
}
//4.设置依赖
operation2.addDependency(operation1) //任务二依赖任务一
operation3.addDependency(operation2) //任务三依赖任务二
//5.创建队列并加入任务
let queue= NSOperationQueue()
queue.addOperations([operation3,operation2, operation1], waitUntilFinished: false)
打印结果
2015-07-2821:24:28.622 test[19392:4637517] 下载图片 - <NSThread:0x7fc10ad4d970>{number = 2, name = (null)}
2015-07-2821:24:29.622 test[19392:4637515] 打水印 - <NSThread:0x7fc10af20ef0>{number = 3, name = (null)}
2015-07-2821:24:30.627 test[19392:4637515] 上传图片 - <NSThread:0x7fc10af20ef0>{number = 3, name = (null)}
注意:不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。
可以使用removeDependency 来解除依赖关系。
可以在不同的队列之间依赖,反正就是这个依赖是添加到任务身上的,和队列没关系。
其他方法
以上就是一些主要方法, 下面还有一些常用方法需要大家注意:
NSOperation
BOOLexecuting; //判断任务是否正在执行
BOOLfinished; //判断任务是否完成
void(^completionBlock)(void); //用来设置完成后需要执行的操作
-(void)cancel; //取消任务
-(void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕
NSOperationQueue
NSUIntegeroperationCount; //获取队列的任务数
-(void)cancelAllOperations; //取消队列中所有的任务
-(void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕
[queuesetSuspended:YES]; // 暂停queue
[queuesetSuspended:NO]; // 继续queue
好啦,到这里差不多就讲完了。当然,我讲的并不完整,可能有一些知识我并没有讲到,但作为常用方法,这些已经足够了。不过我在这里只是告诉你了一些方法的功能,只是怎么把他们用到合适的地方,就需要多多实践了。下面我会说一些关于多线程的案例,是大家更加什么地了解。
其他用法
在这部分,我会说一些和多线程知识相关的案例,可能有些很简单,大家早都知道的,不过因为这篇文章讲的是多线程嘛,所以应该尽可能的全面嘛。还有就是,我会尽可能的使用多种方法实现,让大家看看其中的区别。
线程同步
所谓线程同步就是为了防止多个线程抢夺同一个资源造成的数据安全问题,所采取的一种措施。当然也有很多实现方法,请往下看:
互斥锁 :给需要同步的代码块加一个互斥锁,就可以保证每次只有一个线程访问此代码块。
OBJECTIVE-C
@synchronized(self){
//需要执行的代码块
}
SWIFT
objc_sync_enter(self)
//需要执行的代码块
objc_sync_exit(self)
同步执行 :我们可以使用多线程的知识,把多个线程都要执行此段代码添加到同一个串行队列,这样就实现了线程同步的概念。当然这里可以使用 GCD 和 NSOperation 两种方案,我都写出来。
OBJECTIVE-C
//GCD
//需要一个全局变量queue,要让所有线程的这个操作都加到一个queue中
dispatch_sync(queue,^{
NSInteger ticket = lastTicket;
[NSThread sleepForTimeInterval:0.1];
NSLog(@"%ld - %@",ticket,[NSThread currentThread]);
ticket -= 1;
lastTicket = ticket;
});
//NSOperation& NSOperationQueue
//重点:1. 全局的 NSOperationQueue, 所有的操作添加到同一个queue中
// 2. 设置 queue 的maxConcurrentOperationCount 为 1
// 3. 如果后续操作需要Block中的结果,就需要调用每个操作的waitUntilFinished,阻塞当前线程,一直等到当前操作完成,才允许执行后面的。waitUntilFinished 要在添加到队列之后!
NSBlockOperation*operation = [NSBlockOperation blockOperationWithBlock:^{
NSInteger ticket = lastTicket;
[NSThread sleepForTimeInterval:1];
NSLog(@"%ld - %@",ticket,[NSThread currentThread]);
ticket -= 1;
lastTicket = ticket;
}];
[queueaddOperation:operation];
[operationwaitUntilFinished];
//后续要做的事
SWIFT
这里的 swift代码,我就不写了,因为每句都一样,只是语法不同而已,照着 OC 的代码就能写出 Swift 的。这篇文章已经老长老长了,我就不浪费篇幅了,又不是高中写作文。
延迟执行
所谓延迟执行就是延时一段时间再执行某段代码。下面说一些常用方法。
perform
OBJECTIVE-C
// 3秒后自动调用self的run:方法,并且传递参数:@"abc"
[selfperformSelector:@selector(run:) withObject:@"abc" afterDelay:3];
SWIFT
之前就已经说过,Swift里去掉了这个方法。
GCD
可以使用 GCD 中的 dispatch_after 方法,OC 和 Swift 都可以使用,这里只写 OC 的,Swift 的是一样的。
OBJECTIVE-C
// 创建队列
dispatch_queue_tqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 设置延时,单位秒
doubledelay = 3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,(int64_t)(delay * NSEC_PER_SEC)), queue, ^{
// 3秒后需要执行的任务
});
NSTimer
NSTimer 是iOS中的一个计时器类,除了延迟执行还有很多用法,不过这里直说延迟执行的用法。同样只写OC 版的,Swift 也是相同的。
OBJECTIVE-C
[NSTimerscheduledTimerWithTimeInterval:3.0 target:self selector:@selector(run:)userInfo:@"abc" repeats:NO];
单例模式
至于什么是单例模式,我也不多说,我只说说一般怎么实现。在 Objective-C 中,实现单例的方法已经很具体了,虽然有别的方法,但是一般都是用一个标准的方法了,下面来看看。
OBJECTIVE-C
@interfaceTool : NSObject <NSCopying>
+(instancetype)sharedTool;
@end
@implementationTool
static id_instance;
+(instancetype)sharedTool {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[Tool alloc] init];
});
return _instance;
}
@end
这里之所以将单例模式,是因为其中用到了 GCD 的 dispatch_once 方法。下面看 Swift 中的单例模式,在Swift中单例模式非常简单!想知道怎么从 OC 那么复杂的方法变成下面的写法的,请看这里
SWIFT
classTool: NSObject {
static let sharedTool = Tool()
// 私有化构造方法,阻止其他对象使用这个类的默认的'()'构造方法
private override init() {}
}
从其他线程回到主线程的方法
我们都知道在其他线程操作完成后必须到主线程更新UI。所以,介绍完所有的多线程方案后,我们来看看有哪些方法可以回到主线程。
NSThread
//Objective-C
[selfperformSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
//Swift
//swift 取消了 performSelector 方法。
GCD
//Objective-C
dispatch_async(dispatch_get_main_queue(),^{
});
//Swift
dispatch_async(dispatch_get_main_queue(),{ () -> Void in
})
NSOperationQueue
//Objective-C
[[NSOperationQueuemainQueue] addOperationWithBlock:^{
}];
//Swift
NSOperationQueue.mainQueue().addOperationWithBlock{ () -> Void in
}
总结
好的吧,总算写完了,纯手敲6k多字,感动死我了。花了两天,时间跨度有点大,所以可能有些地方上段不接下段或者有的地方不完整,如果你看着比较费力或者有什么地方有问题,都可以在评论区告诉我,我会及时修改的。当然啦,多线程的东西也不止这些,题目也就只是个题目,不要当真。想要了解更多的东西,还得自己去网上挖掘相关资料。多看看官方文档。实在是编不下去了,大家好好看~。对了,看我写的这么卖力,不打赏的话得点个喜欢也是极好的。
更新:第一次放出来的时候,有很多地方有错误,很感谢有朋友提出来了。如果你看到有错误的地方,一定记得指出来,这样对大家都有帮助。还有一点对初学者来说,遇到不懂的方法,最好的办法就是查看官方文档,那里是最准确的,就算有几个单词不认识,查一下就好了,不会影响对整体的理解。
(5) 同步,异步定义以及iOS中是如何实现同步的
同步:进程之间的关系不是相互排斥临界资源的关系,而是相互依赖的关系。进一步的说明:就是前一个进程的输出作为后一个进程的输入,当第一个进程没有输出时第二个进程必须等待。具有同步关系的一组并发进程相互发送的信息称为消息或事件。
其中并发又有伪并发和真并发,伪并发是指单核处理器的并发,真并发是指多核处理器的并发。
异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。
在IOS中我们一般情况下使用以下三种线程同步代码方式:
第一种和第二种代码同步的使用方法,一般情况下我们只需要使用NSLock和NSCondition申明2个属性。然后给此属性赋对应的值。那么即可作为安全防控的线程手段。
同时也可以保证线程的资源安全。
1:NSLock方式
[xxxlocklock] //上锁
同步代码块
[xxxlockunlock]//解锁
2:NSCondition方式
[xxxConditionlock] //上锁
同步代码块
[xxxConditionunlock]//解锁
第三种方式:在使用synchronized的时候,括号中我们一般情况下只需要传一个self即可。同步代码块 当有线程进去之后会把括号里面对象的锁旗标锁上,其他线程会在外面等着 当进去的线程出去的时候会把锁打开其余线程再进一个。这样才能保护线程放问资源的安全性。
3:@synchronized( 同一对象){
线程执行代码;
}
线程资源防控示例代码:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<spanstyle="font-size:10px;">-(void)sellTickets{
while (YES) {
NSString *name = [NSThreadcurrentThread].name;
// 同步代码块 当有线程进去之后会把括号里面对象的锁旗标锁上,其他线程会在外面等着当进去的线程出去的时候会把锁打开 其余线程再进一个
// @synchronized(self){
// [self.myLock lock];
[self.myCondition lock];
NSLog(@"%@开始卖%d号票",name,self.selledCount+1);
[NSThread sleepForTimeInterval:.2];
self.selledCount++;
NSLog(@"%@卖掉了%d号票,还剩%d张",name,self.selledCount,self.totalCount-self.selledCount);
// [self.myLock unlock];
[self.myCondition unlock];
}
// }
}</span>
(6) #import,#include,@class区别
1.
#include是C中用来引用文件的关键字,而#import是obj-c中用来代替include的关键字。#import可以确保同一个文件只能被导入一次,从而避免了使用#include容易引起的重复引用问题,即classA引用了classC,classB也引用了classC,而当classD同时引用classA,classB的时候就会报重复引用的错误。
2.
#import""与#import<>:#import""实现从当前工作目录中找要导入的文件,如果没有再到系统类库中找,而#import<>是直接从系统类库中找要导入的文件。
3.
#import与@class:
@class只是告诉编译器,后面遇到的这个名称是一个类名称,至于这个类是如何实现的暂不用考虑。引入@class主要是用来解决引用死锁--如果两个类存在循环依赖关系,即A->B,B->A,如果用#import来相互包含,就会出现编译错误:
Expectedspecifier-qualifier-list before ‘A’或者Expectedspecifier-qualifier-list before ‘B’。
一般情况下,在 .h文件中,只需要知道类的名字就可以了,所以用@class,而在 .m文件中通常需要知道类的成员变量即方法,所以要用#import来将类文件导进来。
那为什么不在 .h文件中直接用#import来将类文件导入呢,因为如果导入大量的头文件,编译器就会花大量的时间来编译。
需要在 .h文件中用#import的情况:
1/如果有继承关系的要用#import,如,A继承B,需要在A中将B import进来。
2/使用有category的类,需要在 .h文件中用#import将该类的category导进来。
(7) Ios中是否有多继承,是如何实现的
我们都知道objectiveC不能像C++一样支持多继承,但是在OC的使用经常会碰到需要使用多继承的情况。例如,ClassA中有methodA,ClassB中methodB,而现在需要使用这两个类中的方法。如何按照C++的编程思路,毫无疑问采用多继承就搞定了,在OC就需要动动脑子了。
其实我们在学习设计模式的时候知道,多继承的效率不高,而且采用组合的模式可以完全代替继承模式。那么,这种思路完全可以用在OC中实现多继承(或许OC抛弃多继承,就是强迫我们使用更高效的组合设计模式吧!)。下面用实际的代码来表示组合如何来代替多继承。
现在ClassC需要继承ClassA中methodA、ClassB中methodB,具体的代码实现为:
//定义ClassA以及其methodA
@interfaceClassA : NSObject {
}
-(void)methodA;
@end
//定义ClassB以及其methodB
@interfaceClassB : NSObject {
}
-(void)methodB;
@end
//定义ClassC以及其需要的methodA,methodB
@interfaceClassC : NSObject {
ClassA *a;
ClassB *b;
}
-(id)initWithA:(ClassA*)A b:(ClassB *)B;
-(void)methodA;
-(void)methodB;
@end
//注意在ClassC的实现
@implementation ClassC
-(id)initWithA:(ClassA*)A b:(ClassB *)B{
a=[[ClassA alloc] initWithClassA:A];//[A copy];
b=[[ClassB alloc] initWithClassB:B];//[B copy];
}
-(void)methodA{
[a methodA];
}
-(void)methodB{
[b methodB];
}
上面是采用组合的方式实现了多继承的功能,解决了OC不能多继承的语法。那么还有其他的方式来实现多继承吗?
虽然OC在语法上禁止类使用多继承,但是在协议的遵守上却允许使用多继承。所以可以用协议来实现多继承。但是协议只能提供接口,而没有提供实现方式,如果只是想多继承基类的接口,那么遵守多协议无疑是最好的方法,而既需要多继承接口,又要多继承其实现,那么协议是无能为力了。多协议遵守比较简单,具体的实现方式这里就不讲了!
(8) iOS中如何让一个对象具有拷贝功能
若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与 NSMutableCopying协议。
具体步骤:
需声明该类遵从NSCopying 协议
实现NSCopying 协议。该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到让自己的类用 copy 修饰符,我们总是想覆写copy方法,其实真正需要实现的却是 “copyWithZone” 方法。
至于如何重写带copy 关键字的 setter这个问题,
如果抛开本例来回答的话,如下:
-(void)setName:(NSString *)name {
//[_name release];
_name = [name copy];
}
(9) 单例的定义及实现
单例是全局的类实例,存放在全局内存里,不能以任何方式复制,也不会被释放。实例化的对象始终指向同一块内存。具体实现方式有两种,线程锁和GCD。代码如下,如有错误欢迎大家批评指正:
线程锁代码:
static id_instance;
+ (User*)shareInstance {
@synchronized(self) {
if (_instance == nil) {
_instance = [[User alloc] init];
}
}
return _instance;
}
+(id)allocWithZone:(struct _NSZone *)zone {
@synchronized(self) {
if (_instance == nil) {
_instance = [superallocWithZone:zone];
}
}
return _instance;
}
-(id)copyWithZone:(NSZone *)zone {
return _instance;
}
GCD代码:
+(User*)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//onceToken是GCD用来记录是否执行过,如果已经执行过就不再执行(保证执行一次)
_instance = [[User alloc] init];
});
return _instance;
}
+(id)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//onceToken是GCD用来记录是否执行过,如果已经执行过就不再执行(保证执行一次)
_instance = [super allocWithZone:zone];
});
return _instance;
}
-(id)copyWithZone:(NSZone *)zone {
return _instance;
}
(10) 定义一个标准宏MIN,实现返回输入的两个数中较小的数字
#define MIN(X,Y) ((X)>(Y)?(Y):(X))
define只会是纯替换作用,所以X,Y均需要加括号,以防止X,Y为表达式的情况
(11) 做过什么优化程序的工作(这个是面试题目,不是笔试题目。)
1. 用ARC管理内存
ARC(AutomaticReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露。它自动为你管理retain和release的过程,所以你就不必去手动干预了。忘掉代码段结尾的release简直像记得吃饭一样简单。而ARC会自动在底层为你做这些工作。除了帮你避免内存泄露,ARC还可以帮你提高性能,它能保证释放掉不再需要的对象的内存。
现在所有的iOS程序都用ARC了,这条可以忽略。
2. 在正确的地方使用 reuseIdentifier
一个开发中常见的错误就是没有给UITableViewCells, UICollectionViewCells,甚至是UITableViewHeaderFooterViews设置正确的reuseIdentifier。
为了性能最优化,tableview用tableView:cellForRowAtIndexPath:为rows分配cells的时候,它的数据应该重用自UITableViewCell。一个table view维持一个队列的数据可重用的UITableViewCell对象。
不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。
自iOS6起,除了UICollectionView的cells和补充views,你也应该在header和footerviews中使用reuseIdentifiers。
想要使用reuseIdentifiers的话,在一个table view中添加一个新的cell时在data source object中添加这个方法:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
staticNSString*CellIdentifier = @"Cell";
UITableViewCell*cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifierforIndexPath:indexPath];
这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil。
3.尽量把views设置为透明
如果你有透明的Views你应该设置它们的opaque属性为YES。
原因是这会使系统用一个最优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。
Apple的文档对于为图片设置透明属性的描述是:
(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES,渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。
在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。
你可以在模拟器中用Debug\ColorBlended Layers选项来发现哪些view没有被设置为opaque。目标就是,能设为opaque的就全设为opaque!
这里有一点需要注意,只要是有中文字符的Label,哪怕你设置成不透明,模拟器中这个Label依然会变红,这个猜测是字符绘制的时候出的问题,这个目前没找到好的解决方法。
4.避免过于庞大的XIB
iOS5中加入的Storyboards(分镜)正在快速取代XIB。然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。
如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。
需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
当家在XIB是,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage的imageNamed:方法来获取图片资源。
这个问题我深有体会,用xib写的界面加载速度比直接用代码写的要慢好多。
5.不要阻塞主线程
永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
你可以使用NSURLConnection异步地做网络操作:
+(void)sendAsynchronousRequest:(NSURLRequest *)requestqueue:(NSOperationQueue*)queue completionHandler:(void (^)(NSURLResponse*, NSData*,NSError*))handler
或者使用像AFNetworking这样的框架来异步地做这些操作。
如果你需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用Grand Central Dispatch,或者NSOperation和 NSOperationQueues.
下面代码是使用GCD的模板
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// switch to a background thread andperform your expensive operation
dispatch_async(dispatch_get_main_queue(),^{
// switch back to the main thread toupdate your UI
});
});
发现代码中有一个嵌套的dispatch_async吗?这是因为任何UIKit相关的代码需要在主线程上进行。
6. 在Image Views中调整图片大小
如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
7. 选择正确的Collection
学会选择对业务场景最合适的类或者对象是写出能效高的代码的基础。当处理collections时这句话尤其正确。
一些常见collection的总结:
· Arrays:有序的一组值。使用index来lookup很快,使用value lookup很慢,插入/删除很慢。
·Dictionaries: 存储键值对。用键来查找比较快。
· Sets: 无序的一组值。用值来查找很快,插入/删除很快。因为Set用到了哈希,所以插入删除查找速度比Array快很多
8. 打开gzip压缩
大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。
9. 重用和延迟加载(lazy load) Views
更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
这里我们用到的技巧就是模仿UITableView和UICollectionView的操作:不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:
1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
2. 当需要时才创建并展示。
每个方案都有其优缺点。用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。
第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。
10.Cache, Cache, 还是Cache!注意你的缓存
一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
+(NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest*request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy= NSURLRequestReturnCacheDataElseLoad;// this will make sure the request alwaysreturns the cached image
request.HTTPShouldHandleCookies= NO;
request.HTTPShouldUsePipelining= YES;
[requestaddValue:@"image/*"forHTTPHeaderField:@"Accept"];
returnrequest;
}
注意你可以通过NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。
NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。
11.权衡渲染方法
在iOS中可以有很多方法做出漂亮的按钮。你可以用整幅的图片,可调大小的图片,或者可以用CALayer, CoreGraphics甚至OpenGL来画它们。当然每个不同的解决方法都有不同的复杂程度和相应的性能。
简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。问题是你需要把所有你需要用到的图片放到app的bundle里面,这样就增加了体积–这就是使用可变大小的图片更好的地方了:你可以省去一些不必要的空间,也不需要再为不同的元素(比如按钮)来做不同的图。
然而,使用图片也意味着你失去了使用代码调整图片的机动性,你需要一遍又一遍不断地重做他们,这样就很浪费时间了,而且你如果要做一个动画效果,虽然每幅图只是一些细节的变化你就需要很多的图片造成bundle大小的不断增大。
总得来说,你需要权衡一下利弊,到底是要性能能还是要bundle保持合适的大小。
12.处理内存警告
一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
幸运的是,UIKit提供了几种收集低内存警告的方法:
· 在app delegate中使用applicationDidReceiveMemoryWarning:的方法
· 在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning
· 注册并接收 UIApplicationDidReceiveMemoryWarningNotification的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view,它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。
当然,现在iOS设备运行内存越来越大,这一点很难出现了。
13.重用大开销对象
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
// inyour .h or inside a class extension
@property(nonatomic, strong) NSDateFormatter *formatter;
// insidethe implementation (.m)
// Whenyou need, just use self.formatter
-(NSDateFormatter *)formatter {
if(!_formatter) {
_formatter = [[NSDateFormatter alloc]init];
_formatter.dateFormat = @"EEE MMMdd HH:mm:ss Z yyyy";// twitter date format
}
return _formatter;
}
还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。
14. 使用Sprite Sheets
Sprite sheet可以让渲染速度加快,甚至比标准的屏幕渲染方法节省内存。
15.避免反复处理数据
许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。
比如你需要数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。
这一点在处理大量数据的时候极为重要,用空间换时间的方法也许是极好的。
16.选择正确的数据格式
从app和网络服务间传输数据有很多方案,最常见的就是JSON和XML。你需要选择对你的app来说最合适的一个。
解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization就更加方便使用了。
但是XML也有XML的好处,比如使用SAX来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。
现在基本上都是JSON了。
17.正确设定背景图片
在View里放背景图片就像很多其它iOS编程一样有很多方法:
使用UIColor的 colorWithPatternImage来设置背景色;
在view中添加一个UIImageView作为一个子View。
如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
// Youcould also achieve the same result in Interface Builder
UIImageView*backgroundView = [[UIImageView alloc] initWithImage:[UIImageimageNamed:@"background"]];
[self.viewaddSubview:backgroundView];
如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
self.view.backgroundColor= [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
18. 减少使用Web特性
UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。
但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的JavaScript,避免使用过大的框架。能只用原生js就更好了。
另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。
19. 设定Shadow Path
如何在一个View或者一个layer上加一个shadow呢,QuartzCore框架是很多开发者的选择:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
UIView*view = [[UIView alloc] init];
view.layer.shadowOffset= CGSizeMake(-1.0f, 1.0f);
view.layer.shadowRadius= 5.0f;
view.layer.shadowOpacity= 0.6;
看起来很简单,对吧。可是,坏消息是使用这个方法也有它的问题… Core Animation不得不先在后台得出你的图形并加好阴影然后才渲染,这开销是很大的。
使用shadowPath的话就避免了这个问题:
view.layer.shadowPath= [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
使用shadowpath的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。但问题是自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去updateshadow path.
我更喜欢用CALayer自己画一个阴影出来,这样可以设置阴影光栅化,节省大量CPU的运算,坏处就是比较消耗内存。因为如果给view的layer设置光栅化的话整个View都会变得模糊。
20. 优化Table View
Tableview需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证tableview平滑滚动,确保你采取了以下的措施:
· 正确使用reuseIdentifier来重用cells
· 尽量使所有的view opaque,包括cell自身
· 避免渐变,图片缩放,后台选人
· 缓存行高
· 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
· 使用shadowPath来画阴影
· 减少subviews的数量
· 尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
· 使用正确的数据结构来存储数据
· 使用rowHeight, sectionFooterHeight和sectionHeaderHeight来设定固定的高,不要请求delegate
21.选择正确的数据存储选项
当存储大块数据时你会怎么做?
你有很多选择,比如:
· 使用NSUerDefaults
· 使用XML, JSON, 或者 plist
· 使用NSCoding存档
· 使用类似SQLite的本地SQL数据库
· 使用 Core Data
NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。
在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。
在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但SQLite就是一个DBMS。Apple在一般情况下建议使用CoreData,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://GitHub.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。
23. 使用Autorelease Pool
NSAutoreleasePool负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。
假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
NSArray*urls = <# An array of file URLs #>;
for(NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSStringstringWithContentsOfURL:url encoding:NSUTF8StringEncodingerror:&error];
/* Process the string, creating andautoreleasing more objects. */
}
}
这段代码在每次遍历后释放所有autorelease对象
24. 选择是否缓存图片
常见的从bundle中加载图片的方式有两种,一个是用imageNamed,二是用imageWithContentsOfFile,第一种比较常见一点。
既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?
imageNamed的优点是当加载时会缓存图片。imageNamed的文档中这么说:这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
相反的,imageWithContentsOfFile仅加载图片。
下面的代码说明了这两种方法的用法:
UIImage*img = [UIImage imageNamed:@"myImage"];// caching
// or
UIImage*img = [UIImage imageWithContentsOfFile:@"myImage"];// no caching
那么我们应该如何选择呢?
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。
然而,在图片反复重用的情况下imageNamed是一个好得多的选择。
25. 避免日期格式转换
如果你要用NSDateFormatter来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用NSDateFormatters都是一个好的实践。
然而,如果你需要更多速度,那么直接用C是一个好的方案。Sam Soffes有一个不错的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用来解析ISO-8601日期字符串的代码,简单重写一下就可以拿来用了。
嗯,直接用C来搞,看起来不错了,但是你相信吗,我们还有更好的方案!
如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:
-(NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return[NSDatedateWithTimeIntervalSince1970:timestamp];
}
这样会比用C来解析日期字符串还快!需要注意的是,许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用dateFromUnixTimestamp之前除以1000就好了。
转载至:http://blog.csdn.net/youshaoduo/article/details/53841078
1. 用ARC管理内存
ARC(AutomaticReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露。它自动为你管理retain和release的过程,所以你就不必去手动干预了。忘掉代码段结尾的release简直像记得吃饭一样简单。而ARC会自动在底层为你做这些工作。除了帮你避免内存泄露,ARC还可以帮你提高性能,它能保证释放掉不再需要的对象的内存。
现在所有的iOS程序都用ARC了,这条可以忽略。
2. 在正确的地方使用 reuseIdentifier
一个开发中常见的错误就是没有给UITableViewCells, UICollectionViewCells,甚至是UITableViewHeaderFooterViews设置正确的reuseIdentifier。
为了性能最优化,tableview用tableView:cellForRowAtIndexPath:为rows分配cells的时候,它的数据应该重用自UITableViewCell。一个table view维持一个队列的数据可重用的UITableViewCell对象。
不使用reuseIdentifier的话,每显示一行table view就不得不设置全新的cell。这对性能的影响可是相当大的,尤其会使app的滚动体验大打折扣。
自iOS6起,除了UICollectionView的cells和补充views,你也应该在header和footerviews中使用reuseIdentifiers。
想要使用reuseIdentifiers的话,在一个table view中添加一个新的cell时在data source object中添加这个方法:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
staticNSString*CellIdentifier = @"Cell";
UITableViewCell*cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifierforIndexPath:indexPath];
这个方法把那些已经存在的cell从队列中排除,或者在必要时使用先前注册的nib或者class创造新的cell。如果没有可重用的cell,你也没有注册一个class或者nib的话,这个方法返回nil。
3.尽量把views设置为透明
如果你有透明的Views你应该设置它们的opaque属性为YES。
原因是这会使系统用一个最优的方式渲染这些views。这个简单的属性在IB或者代码里都可以设定。
Apple的文档对于为图片设置透明属性的描述是:
(opaque)这个属性给渲染系统提供了一个如何处理这个view的提示。如果设为YES,渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能。如果设置为NO,渲染系统正常地和其它内容组成这个View。默认值是YES。
在相对比较静止的画面中,设置这个属性不会有太大影响。然而当这个view嵌在scroll view里边,或者是一个复杂动画的一部分,不设置这个属性的话会在很大程度上影响app的性能。
你可以在模拟器中用Debug\ColorBlended Layers选项来发现哪些view没有被设置为opaque。目标就是,能设为opaque的就全设为opaque!
这里有一点需要注意,只要是有中文字符的Label,哪怕你设置成不透明,模拟器中这个Label依然会变红,这个猜测是字符绘制的时候出的问题,这个目前没找到好的解决方法。
4.避免过于庞大的XIB
iOS5中加入的Storyboards(分镜)正在快速取代XIB。然而XIB在一些场景中仍然很有用。比如你的app需要适应iOS5之前的设备,或者你有一个自定义的可重用的view,你就不可避免地要用到他们。
如果你不得不XIB的话,使他们尽量简单。尝试为每个Controller配置一个单独的XIB,尽可能把一个View Controller的view层次结构分散到单独的XIB中去。
需要注意的是,当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller.
当家在XIB是,所有图片都被chache,如果你在做OS X开发的话,声音文件也是。Apple在相关文档中的记述是:
当你加载一个引用了图片或者声音资源的nib时,nib加载代码会把图片和声音文件写进内存。在OS X中,图片和声音资源被缓存在named cache中以便将来用到时获取。在iOS中,仅图片资源会被存进named caches。取决于你所在的平台,使用NSImage 或UIImage的imageNamed:方法来获取图片资源。
这个问题我深有体会,用xib写的界面加载速度比直接用代码写的要慢好多。
5.不要阻塞主线程
永远不要使主线程承担过多。因为UIKit在主线程上做所有工作,渲染,管理触摸反应,回应输入等都需要在它上面完成。
一直使用主线程的风险就是如果你的代码真的block了主线程,你的app会失去反应。
大部分阻碍主进程的情形是你的app在做一些牵涉到读写外部资源的I/O操作,比如存储或者网络。
你可以使用NSURLConnection异步地做网络操作:
+(void)sendAsynchronousRequest:(NSURLRequest *)requestqueue:(NSOperationQueue*)queue completionHandler:(void (^)(NSURLResponse*,NSData*, NSError*))handler
或者使用像AFNetworking这样的框架来异步地做这些操作。
如果你需要做其它类型的需要耗费巨大资源的操作(比如时间敏感的计算或者存储读写)那就用Grand Central Dispatch,或者NSOperation和 NSOperationQueues.
下面代码是使用GCD的模板
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
// switch to a background thread andperform your expensive operation
dispatch_async(dispatch_get_main_queue(),^{
// switch back to the main thread to updateyour UI
});
});
发现代码中有一个嵌套的dispatch_async吗?这是因为任何UIKit相关的代码需要在主线程上进行。
6. 在Image Views中调整图片大小
如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。
如果图片是从远端服务加载的你不能控制图片大小,比如在下载前调整到合适大小的话,你可以在下载完成后,最好是用background thread,缩放一次,然后在UIImageView中使用缩放后的图片。
7. 选择正确的Collection
学会选择对业务场景最合适的类或者对象是写出能效高的代码的基础。当处理collections时这句话尤其正确。
一些常见collection的总结:
· Arrays:有序的一组值。使用index来lookup很快,使用value lookup很慢,插入/删除很慢。
·Dictionaries: 存储键值对。用键来查找比较快。
· Sets: 无序的一组值。用值来查找很快,插入/删除很快。因为Set用到了哈希,所以插入删除查找速度比Array快很多
8. 打开gzip压缩
大量app依赖于远端资源和第三方API,你可能会开发一个需要从远端下载XML, JSON, HTML或者其它格式的app。
问题是我们的目标是移动设备,因此你就不能指望网络状况有多好。一个用户现在还在edge网络,下一分钟可能就切换到了3G。不论什么场景,你肯定不想让你的用户等太长时间。
减小文档的一个方式就是在服务端和你的app中打开gzip。这对于文字这种能有更高压缩率的数据来说会有更显著的效用。
好消息是,iOS已经在NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然。像Google App Engine这些云服务提供者也已经支持了压缩输出。
9. 重用和延迟加载(lazy load) Views
更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的app更是如此。
这里我们用到的技巧就是模仿UITableView和UICollectionView的操作:不要一次创建所有的subview,而是当需要时才创建,当它们完成了使命,把他们放进一个可重用的队列中。
这样的话你就只需要在滚动发生时创建你的views,避免了不划算的内存分配。
创建views的能效问题也适用于你app的其它方面。想象一下一个用户点击一个按钮的时候需要呈现一个view的场景。有两种实现方法:
1. 创建并隐藏这个view当这个screen加载的时候,当需要时显示它;
2. 当需要时才创建并展示。
每个方案都有其优缺点。用第一种方案的话因为你需要一开始就创建一个view并保持它直到不再使用,这就会更加消耗内存。然而这也会使你的app操作更敏感因为当用户点击按钮的时候它只需要改变一下这个view的可见性。
第二种方案则相反-消耗更少内存,但是会在点击按钮的时候比第一种稍显卡顿。
10.Cache, Cache, 还是Cache!注意你的缓存
一个极好的原则就是,缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西。
我们能缓存些什么呢?一些选项是,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高。
NSURLConnection默认会缓存资源在内存或者存储中根据它所加载的HTTP Headers。你甚至可以手动创建一个NSURLRequest然后使它只加载缓存的值。
下面是一个可用的代码段,你可以可以用它去为一个基本不会改变的图片创建一个NSURLRequest并缓存它:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
+(NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
NSMutableURLRequest*request = [NSMutableURLRequest requestWithURL:url];
request.cachePolicy= NSURLRequestReturnCacheDataElseLoad;// this will make sure the request alwaysreturns the cached image
request.HTTPShouldHandleCookies= NO;
request.HTTPShouldUsePipelining= YES;
[requestaddValue:@"image/*"forHTTPHeaderField:@"Accept"];
returnrequest;
}
注意你可以通过NSURLConnection 获取一个URL request, AFNetworking也一样的。这样你就不必为采用这条tip而改变所有的networking代码了。
如果你需要缓存其它不是HTTP Request的东西,你可以用NSCache。
NSCache和NSDictionary类似,不同的是系统回收内存的时候它会自动删掉它的内容。
11.权衡渲染方法
在iOS中可以有很多方法做出漂亮的按钮。你可以用整幅的图片,可调大小的图片,或者可以用CALayer, CoreGraphics甚至OpenGL来画它们。当然每个不同的解决方法都有不同的复杂程度和相应的性能。
简单来说,就是用事先渲染好的图片更快一些,因为如此一来iOS就免去了创建一个图片再画东西上去然后显示在屏幕上的程序。问题是你需要把所有你需要用到的图片放到app的bundle里面,这样就增加了体积–这就是使用可变大小的图片更好的地方了:你可以省去一些不必要的空间,也不需要再为不同的元素(比如按钮)来做不同的图。
然而,使用图片也意味着你失去了使用代码调整图片的机动性,你需要一遍又一遍不断地重做他们,这样就很浪费时间了,而且你如果要做一个动画效果,虽然每幅图只是一些细节的变化你就需要很多的图片造成bundle大小的不断增大。
总得来说,你需要权衡一下利弊,到底是要性能能还是要bundle保持合适的大小。
12.处理内存警告
一旦系统内存过低,iOS会通知所有运行中app。在官方文档中是这样记述:
如果你的app收到了内存警告,它就需要尽可能释放更多的内存。最佳方式是移除对缓存,图片object和其他一些可以重创建的objects的strong references.
幸运的是,UIKit提供了几种收集低内存警告的方法:
· 在app delegate中使用applicationDidReceiveMemoryWarning:的方法
· 在你的自定义UIViewController的子类(subclass)中覆盖didReceiveMemoryWarning
· 注册并接收 UIApplicationDidReceiveMemoryWarningNotification的通知
一旦收到这类通知,你就需要释放任何不必要的内存使用。
例如,UIViewController的默认行为是移除一些不可见的view,它的一些子类则可以补充这个方法,删掉一些额外的数据结构。一个有图片缓存的app可以移除不在屏幕上显示的图片。
这样对内存警报的处理是很必要的,若不重视,你的app就可能被系统杀掉。
然而,当你一定要确认你所选择的object是可以被重现创建的来释放内存。一定要在开发中用模拟器中的内存提醒模拟去测试一下。
当然,现在iOS设备运行内存越来越大,这一点很难出现了。
13.重用大开销对象
一些objects的初始化很慢,比如NSDateFormatter和NSCalendar。然而,你又不可避免地需要使用它们,比如从JSON或者XML中解析数据。
想要避免使用这个对象的瓶颈你就需要重用他们,可以通过添加属性到你的class里或者创建静态变量来实现。
注意如果你要选择第二种方法,对象会在你的app运行时一直存在于内存中,和单例(singleton)很相似。
下面的代码说明了使用一个属性来延迟加载一个date formatter. 第一次调用时它会创建一个新的实例,以后的调用则将返回已经创建的实例:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
// inyour .h or inside a class extension
@property(nonatomic, strong) NSDateFormatter *formatter;
// insidethe implementation (.m)
// Whenyou need, just use self.formatter
-(NSDateFormatter *)formatter {
if(!_formatter) {
_formatter = [[NSDateFormatter alloc]init];
_formatter.dateFormat = @"EEE MMMdd HH:mm:ss Z yyyy";// twitter date format
}
return _formatter;
}
还需要注意的是,其实设置一个NSDateFormatter的速度差不多是和创建新的一样慢的!所以如果你的app需要经常进行日期格式处理的话,你会从这个方法中得到不小的性能提升。
14. 使用Sprite Sheets
Spritesheet可以让渲染速度加快,甚至比标准的屏幕渲染方法节省内存。
15.避免反复处理数据
许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要。在内存中操作数据使它们满足你的数据结构是开销很大的。
比如你需要数据来展示一个table view,最好直接从服务器取array结构的数据以避免额外的中间数据结构改变。
类似的,如果需要从特定key中取数据,那么就使用键值对的dictionary。
这一点在处理大量数据的时候极为重要,用空间换时间的方法也许是极好的。
16.选择正确的数据格式
从app和网络服务间传输数据有很多方案,最常见的就是JSON和XML。你需要选择对你的app来说最合适的一个。
解析JSON会比XML更快一些,JSON也通常更小更便于传输。从iOS5起有了官方内建的JSON deserialization就更加方便使用了。
但是XML也有XML的好处,比如使用SAX来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。
现在基本上都是JSON了。
17.正确设定背景图片
在View里放背景图片就像很多其它iOS编程一样有很多方法:
使用UIColor的 colorWithPatternImage来设置背景色;
在view中添加一个UIImageView作为一个子View。
如果你使用全画幅的背景图,你就必须使用UIImageView因为UIColor的colorWithPatternImage是用来创建小的重复的图片作为背景的。这种情形下使用UIImageView可以节约不少的内存:
// Youcould also achieve the same result in Interface Builder
UIImageView*backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"background"]];
[self.viewaddSubview:backgroundView];
如果你用小图平铺来创建背景,你就需要用UIColor的colorWithPatternImage来做了,它会更快地渲染也不会花费很多内存:
self.view.backgroundColor= [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
18. 减少使用Web特性
UIWebView很有用,用它来展示网页内容或者创建UIKit很难做到的动画效果是很简单的一件事。
但是你可能有注意到UIWebView并不像驱动Safari的那么快。这是由于以JIT compilation为特色的Webkit的Nitro Engine的限制。
所以想要更高的性能你就要调整下你的HTML了。第一件要做的事就是尽可能移除不必要的JavaScript,避免使用过大的框架。能只用原生js就更好了。
另外,尽可能异步加载例如用户行为统计script这种不影响页面表达的javascript。
最后,永远要注意你使用的图片,保证图片的符合你使用的大小。使用Sprite sheet提高加载速度和节约内存。
19. 设定Shadow Path
如何在一个View或者一个layer上加一个shadow呢,QuartzCore框架是很多开发者的选择:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
UIView*view = [[UIView alloc] init];
view.layer.shadowOffset= CGSizeMake(-1.0f, 1.0f);
view.layer.shadowRadius= 5.0f;
view.layer.shadowOpacity= 0.6;
看起来很简单,对吧。可是,坏消息是使用这个方法也有它的问题… Core Animation不得不先在后台得出你的图形并加好阴影然后才渲染,这开销是很大的。
使用shadowPath的话就避免了这个问题:
view.layer.shadowPath= [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
使用shadowpath的话iOS就不必每次都计算如何渲染,它使用一个预先计算好的路径。但问题是自己计算path的话可能在某些View中比较困难,且每当view的frame变化的时候你都需要去updateshadow path.
我更喜欢用CALayer自己画一个阴影出来,这样可以设置阴影光栅化,节省大量CPU的运算,坏处就是比较消耗内存。因为如果给view的layer设置光栅化的话整个View都会变得模糊。
20. 优化Table View
Table view需要有很好的滚动性能,不然用户会在滚动过程中发现动画的瑕疵。
为了保证tableview平滑滚动,确保你采取了以下的措施:
· 正确使用reuseIdentifier来重用cells
· 尽量使所有的view opaque,包括cell自身
· 避免渐变,图片缩放,后台选人
· 缓存行高
· 如果cell内现实的内容来自web,使用异步加载,缓存请求结果
· 使用shadowPath来画阴影
· 减少subviews的数量
· 尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
· 使用正确的数据结构来存储数据
· 使用rowHeight, sectionFooterHeight和sectionHeaderHeight来设定固定的高,不要请求delegate
21.选择正确的数据存储选项
当存储大块数据时你会怎么做?
你有很多选择,比如:
· 使用NSUerDefaults
· 使用XML, JSON, 或者 plist
· 使用NSCoding存档
· 使用类似SQLite的本地SQL数据库
· 使用Core Data
NSUserDefaults的问题是什么?虽然它很nice也很便捷,但是它只适用于小数据,比如一些简单的布尔型的设置选项,再大点你就要考虑其它方式了
XML这种结构化档案呢?总体来说,你需要读取整个文件到内存里去解析,这样是很不经济的。使用SAX又是一个很麻烦的事情。
NSCoding?不幸的是,它也需要读写文件,所以也有以上问题。
在这种应用场景下,使用SQLite 或者 Core Data比较好。使用这些技术你用特定的查询语句就能只加载你需要的对象。
在性能层面来讲,SQLite和Core Data是很相似的。他们的不同在于具体使用方法。Core Data代表一个对象的graph model,但SQLite就是一个DBMS。Apple在一般情况下建议使用CoreData,但是如果你有理由不使用它,那么就去使用更加底层的SQLite吧。
如果你使用SQLite,你可以用FMDB(https://GitHub.com/ccgus/fmdb)这个库来简化SQLite的操作,这样你就不用花很多经历了解SQLite的C API了。
23. 使用Autorelease Pool
NSAutoreleasePool负责释放block中的autoreleased objects。一般情况下它会自动被UIKit调用。但是有些状况下你也需要手动去创建它。
假如你创建很多临时对象,你会发现内存一直在减少直到这些对象被release的时候。这是因为只有当UIKit用光了autorelease pool的时候memory才会被释放。好消息是你可以在你自己的@autoreleasepool里创建临时的对象来避免这个行为:
[objc]view plain copy 在CODE上查看代码片派生到我的代码片
NSArray*urls = <# An array of file URLs #>;
for(NSURL *url in urls) {
@autoreleasepool {
NSError *error;
NSString *fileContents = [NSStringstringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
/* Process the string, creating andautoreleasing more objects. */
}
}
这段代码在每次遍历后释放所有autorelease对象
24. 选择是否缓存图片
常见的从bundle中加载图片的方式有两种,一个是用imageNamed,二是用imageWithContentsOfFile,第一种比较常见一点。
既然有两种类似的方法来实现相同的目的,那么他们之间的差别是什么呢?
imageNamed的优点是当加载时会缓存图片。imageNamed的文档中这么说:这个方法用一个指定的名字在系统缓存中查找并返回一个图片对象如果它存在的话。如果缓存中没有找到相应的图片,这个方法从指定的文档中加载然后缓存并返回这个对象。
相反的,imageWithContentsOfFile仅加载图片。
下面的代码说明了这两种方法的用法:
UIImage*img = [UIImage imageNamed:@"myImage"];// caching
// or
UIImage*img = [UIImage imageWithContentsOfFile:@"myImage"];// no caching
那么我们应该如何选择呢?
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。
然而,在图片反复重用的情况下imageNamed是一个好得多的选择。
25. 避免日期格式转换
如果你要用NSDateFormatter来处理很多日期格式,应该小心以待。就像先前提到的,任何时候重用NSDateFormatters都是一个好的实践。
然而,如果你需要更多速度,那么直接用C是一个好的方案。Sam Soffes有一个不错的帖子(http://soff.es/how-to-drastically-improve-your-app-with-an-afternoon-and-instruments)里面有一些可以用来解析ISO-8601日期字符串的代码,简单重写一下就可以拿来用了。
嗯,直接用C来搞,看起来不错了,但是你相信吗,我们还有更好的方案!
如果你可以控制你所处理的日期格式,尽量选择Unix时间戳。你可以方便地从时间戳转换到NSDate:
-(NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return[NSDatedateWithTimeIntervalSince1970:timestamp];
}
这样会比用C来解析日期字符串还快!需要注意的是,许多web API会以微秒的形式返回时间戳,因为这种格式在javascript中更方便使用。记住用dateFromUnixTimestamp之前除以1000就好了。
(12) “https:www|.baidu.com”输出|前后两部分的字符串
根据字符串中的某个字符(A)来分割字符串
//3.分隔字符串
NSString*string =@"sdfsfsfsAdfsdf";
NSArray*array = [string componentsSeparatedByString:@"A"]; //从字符A中分隔成2个元素的数组
NSLog(@"array:%@",array); //结果是adfsfsfs和dfsdf