今天去面試的是一家規模不太大的公司,首先是筆試,是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