第2章 巧用多線程提高項目性能


  上篇博客被吐槽無干活,好吧,我的博客確實不是教大家很酷炫的動畫,很實用的框架,我的博客講到都是一些基礎知識,是幫助大家在做完一個項目之后,反芻項目設計,項目內容,然后加以修改的東西。如果各位有什么意見,或者我寫的概念中,有什么不對,或者不完善的地方,希望大家可以提出來,告訴我,我好及時修改,避免有學習的人帶了錯誤的概念,謝謝大家,我希望技術是用來分享的,幫助更多的人,更快的提供專業技術。

  本章主要介紹在iOS中多線程的使用方法,這對於合理利用cpu使用效率尤為重要。多線程實現了對CPU的並發執行,避免阻塞所造成的CPU計算時間浪費。在單核CPU的時代,實現多線程技術還局限於軟件層面,這樣給線程間切換帶來一定的成本開銷,但由於多線程的優勢更大所以開發者還是會選擇使用子線程處理邏輯問題,而到了多核CPU時代,由於硬件本身支持了多線程技術,就可以真正的讓多線程同時地運行。

而OS X和iOS作為多線程操作系統,它們繼承UNIX系統使用的POSIX線程模型。OS X和iOS都提供了一套底層為C語言的POSIX線程API來創建和管理線程。但實際上的應用開發中大可不必使用那些晦澀難懂的c語言函數,因為蘋果公司已經為開發者提供了更為簡單的oc的解決方案。接下來會逐一分析。

2.1 不得不提一下的NSThread

   使用多線程技術,對於整個計算機科學的發展而言可謂至關重要,與其在手機內存並不是十分充裕的情況下,靈活地使用多線程,可以充分利用現有資源,解決很多復雜地邏輯問題,而在iOS系統中,蘋果公司為廣大開發者提供了非常豐富地多線程框架,供開發者們使用,接下來,本書將由淺入深,從最為輕量地NSThread開始,分段講解iOS系統中的多線程技術。

2.1.1 iOS中的多線程

在iOS系統中,多線程技術也分成不同的層級,層級越高其封裝度越高,使用的方便性,以及功能性也就更加完整,那么根據不同的層級,多線程技術由低到高可以分成:

1)Cocoa threads;

2)Operation objects;

3)Grand Central Dispatch (GCD)。

Cocoa threads是Cocoa框架中的范式,通常使用NSThread類,以及一些NSObject中的方法實現開辟多線程方法。Operation objects為操作隊列對象主要用於構建線程池,通常使用NSOperation及其子類實現。而GCD是純C的API,也是iOS4之后蘋果主推的多線程處理方法,它的執行效率,以及管理方法都比較人性化,這些在后面將會進行詳細的講解。

2.1.2 NSThread的基本使用方法

NSThread是iOS中比較基礎的多線程處理的類,該類較為輕量,非常容易使用,如下代碼。

   /**

      *創建一個子線程

      *:param: childThreadTarget 使用子線程執行改方法

      */

 NSThread *thread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget:)

                                                object:nil];

   /**

     *  線程名稱

     */

    thread.name = @"iOS性能優化";

    /**

     *  執行該線程

     */

    [thread start];

    /**

      *  線程執行方法

      */

-(void)childThreadTarget:(NSThread *)thread

{

     NSLog(@"線程名:%@",[[NSThread currentThread] name]);

}

打印結果為:2015-06-08 15:16:56.602 iOS性能優化[54761:847490] 線程名:iOS性能優化

這里使用多線程技術並不能讓人很直觀的看到其效果,因為處理方法比較簡單並不會造成特別嚴重線程的阻塞,接下來的事例將會說明多線程的實用性。

  /**

     *  實用for循環模擬數據阻塞

     *  由於處理次數過多造成后面代碼無法正常執行

     */

    for (int i = 0; i<MAXFLOAT; i++)

    {

        NSLog(@"執行次數:%d",i);

    }

    /**

     *  初始化一個圖層

     *  並將圖層放入窗口中

     */

    UIView *view = [[UIView alloc]initWithFrame:self.window.frame];

    /**

     *  設置圖層的背景顏色為紅色

     */

    view.backgroundColor = [UIColor redColor];

    /**

     *  將視圖添加到窗口

     */

    [self.window addSubview:view];

運行結果如圖2.1所示。

 

圖2.1  由於線程阻塞導致圖層無法加載到window上

從演示圖中可以清晰的看到,由於主線程中處理較為復雜的方法,導致UIView視圖無法成功加載到窗口容器中,這種情況常發生在同步網絡請求中,這里並不做過多介紹,但需要知道的是,在程序開發中要將數據處理的邏輯代碼盡可能放到子線程中,這樣可以有效提高界面的流暢度,就好比A城市要向B城市運輸一些建築材料,或者其他資源,但目前只一條高速公路,如果運輸車輛較少的情況下,這條公路還是可以滿足需求的,但是一旦車輛增多公路就會變得在十分擁擠從而造成堵車,這時目的城市所需要的材料無法被順利的運輸過去,這時需要做的就是為這條主路修建一些輔路,這樣一些不需要立即送達的資源可以放到輔路中行駛,這將會大大減輕主路的行駛壓力,使得資源可以順利運往目的地,而這里多線程技術就是拓展這些輔路的方法,接下來在通過事例演示,看一下運行結果的變化。如下代碼。

    /**

      *創建一個子線程

      *:param: childThreadTarget 使用子線程執行改方法

      */

    NSThread *thread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget)

                                                object:nil];

    /**

     *  線程名稱

     */

    thread.name = @"iOS性能優化";

    /**

     *  執行該線程

     */

    [thread start];

 

    /**

     *  初始化一個圖層

     *  並將圖層放入窗口中

     */

    UIView *view = [[UIView alloc]initWithFrame:self.window.frame];

    /**

     *  設置圖層的背景顏色為紅色

     */

    view.backgroundColor = [UIColor redColor];

    /**

     *  將視圖添加到窗口

     */

    [self.window addSubview:view];

/**

 *  線程執行方法

 */

-(void)childThreadTarget

{

    /**

     *  實用for循環模擬數據阻塞

     *  由於處理次數過多造成后面代碼無法正常執行

     */

    for (int i = 0; i<MAXFLOAT; i++)

    {

        NSLog(@"執行次數:%d",i);

    }

}

控制台打印結果:2015-06-08 15:48:42.082 Suibian[56673:870857] 執行次數:0

2015-06-08 15:48:42.084 iOS性能優化[56673:870857] 執行次數:1

2015-06-08 15:48:42.084 iOS性能優化[56673:870857] 執行次數:2

2015-06-08 15:48:42.084 iOS性能優化[56673:870857] 執行次數:3

2015-06-08 15:48:42.084 iOS性能優化[56673:870857] 執行次數:4

2015-06-08 15:48:42.084 iOS性能優化[56673:870857] 執行次數:5

2015-06-08 15:48:42.085 iOS性能優化[56673:870857] 執行次數:6

2015-06-08 15:48:42.085 iOS性能優化[56673:870857] 執行次數:7

2015-06-08 15:48:42.085 iOS性能優化[56673:870857] 執行次數:8

2015-06-08 15:48:42.085 iOS性能優化[56673:870857] 執行次數:9

2015-06-08 15:48:42.085 iOS性能優化[56673:870857] 執行次數:10

從控制台打印結果可以看出,程序運行並沒有影響但for的計算,那UI是否與剛剛的結果一樣,並無變化呢?如圖2.2所示。

 

圖2.2  頁面加載成功

正如圖2.2中運行結果一樣,利用子線程處理for循環的龐大的信息處理,並不會影響主線程加載UI視圖,但這里有一個建議,就是在處理UI視圖的時候,不要放在子線程中執行,不然會發生很嚴重的bug。

雖然使用NSThread較為輕量簡單,但在處理線程池,線程同步是NSThread卻顯得十分臃腫麻煩,這里舉一個銀行取款的例子,來解釋線程同步的概念,如下代碼。

 

@interface AppDelegate : UIResponder <UIApplicationDelegate>

{

    /**

     *  創建實例變量總錢數

     */

    int _allMoney;
        /**

     *  創建實例變量已取數量

     */

    int _num;

}

@property (strong, nonatomic) UIWindow *window;

 

@end

@implementation AppDelegate

-(BOOL)application:(UIApplication*)application          didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

    _allMoney = 100;

    _num = 0;

    /**

      *創建一個子線程

      *:param: childThreadTarget 使用子線程執行改方法

      */

    NSThread *thread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget)

                                                object:nil];

    /**

     *  線程名稱

     */

    thread.name = @"A人員";

    /**

     *  執行該線程

     */

    [thread start];

    /**

     *創建一個同步子線程

     *:param: childThreadTarget 使用子線程執行改方法

     */

    NSThread *sameThread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget)

                                                object:nil];

    sameThread.name = @"B人員";

    [sameThread start];

/**

 *  線程執行方法

 */

-(void)childThreadTarget

{

    while (TRUE) {

        if(_allMoney >= 0){

            _num = 100 - _allMoney;

            NSLog(@"當前存款數量:%d,已取款:%d,線程名:%@",_allMoney,_num,[[NSThread currentThread] name]);

            _allMoney--;

        }else{

            break;

        }

    }

}

運行結果為:2015-06-08 17:53:22.413  iOS性能優化[64395:958812] 當前票數是:100,售出:0,線程名:B人員

2015-06-08 17:53:22.415 iOS性能優化[64395:958812] 當前票數是:99,售出:1,線程名:B人員

2015-06-08 17:53:22.413 iOS性能優化[64395:958811] 當前票數是:100,售出:0,線程名:A人員

2015-06-08 17:53:22.415 iOS性能優化[64395:958812] 當前票數是:98,售出:2,線程名:B人員

2015-06-08 17:53:22.415 iOS性能優化[64395:958812] 當前票數是:97,售出:3,線程名:B人員

2015-06-08 17:53:22.416 iOS性能優化[64395:958812] 當前票數是:95,售出:5,線程名:B人員

2015-06-08 17:53:22.416 iOS性能優化[64395:958812] 當前票數是:94,售出:6,線程名:B人員

2015-06-08 17:53:22.417 iOS性能優化[64395:958812] 當前票數是:93,售出:7,線程名:B人員

2015-06-08 17:53:22.418 iOS性能優化[64395:958812] 當前票數是:92,售出:8,線程名:B人員

從運行結果可以看出,由於兩個線程同時訪問一個方法,一塊資源,帶來的數據混亂,如果發生在現實生活中將是十分恐怖的一件事情。就如果當用戶A與用戶B持有相同的信用卡,只不過一個是主卡一個是副卡,當兩人同時取款的時候B的率先被處理了兩筆交易,而此時A的取款請求才被處理,但顯示給A的信息卻還是100。當然沒人希望被機器欺騙,這就需要開發者可以謹慎思考這個問題了,在iOS系統中,提供了一個類用了處理線程同步,稱之為線程鎖。這里要介紹iOS系統框架提供NSLock類,這是一個比較容易掌握的方法,那么使用這個類后之前的代碼應該修改為如下代碼。

@interface AppDelegate : UIResponder <UIApplicationDelegate>

{

    /**

     *  創建線程鎖實力對象

     */

    NSLock *_theLock;

    /**

     *  創建實例變量總錢數

     */

    int _allMoney;

    /**

     *  創建實例變量已取數量

     */

    int _num;

}

@property (strong, nonatomic) UIWindow *window;

@end

@implementation AppDelegate

 

-(BOOL)application:(UIApplication*)application       didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    // Override point for customization after application launch.

    self.window.backgroundColor = [UIColor whiteColor];

    _allMoney = 100;

    _num = 0;

    /**

     * 初始化線程鎖實例變量

     */

    _theLock = [[NSLock alloc]init];

   

    /**

      *創建一個子線程

      *:param: childThreadTarget 使用子線程執行改方法

      */

    NSThread *thread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget)

                                                object:nil];

    /**

     *  線程名稱

     */

    thread.name = @"A人員";

    /**

     *  執行該線程

     */

    [thread start];

    /**

     *創建一個同步子線程

     *:param: childThreadTarget 使用子線程執行改方法

     */

    NSThread *sameThread = [[NSThread alloc]initWithTarget:self

                                              selector:@selector(childThreadTarget)

                                                object:nil];

    sameThread.name = @"B人員";

    [sameThread start];

    [self.window makeKeyAndVisible];

    return YES;

}

/**

 *  線程執行方法

 */

-(void)childThreadTarget

{

    while (TRUE) {

        // 上鎖

        //        [ticketsCondition lock];

        [_theLock lock];

        if(_allMoney >= 0){

            _num = 100 - _allMoney;

            NSLog(@"當前存款數量:%d,已取款:%d,線程名:%@",_allMoney,_num,[[NSThread currentThread] name]);

            _allMoney--;

        }else{

            break;

        }

        [_theLock unlock];

    }

}

運行結果為:2015-06-09 11:25:45.542 iOS性能優化[7190:70542] 當前存款數量:10,已取款:90,線程名:A人員

2015-06-09 11:25:45.542 iOS性能優化[7190:70543] 當前存款數量:9,已取款:91,線程名:B人員

2015-06-09 11:25:45.543 iOS性能優化[7190:70542] 當前存款數量:8,已取款:92,線程名:A人員

2015-06-09 11:25:45.543 iOS性能優化[7190:70543] 當前存款數量:7,已取款:93,線程名:B人員

2015-06-09 11:25:45.543 iOS性能優化[7190:70542] 當前存款數量:6,已取款:94,線程名:A人員

2015-06-09 11:25:45.543 iOS性能優化[7190:70543] 當前存款數量:5,已取款:95,線程名:B人員

2015-06-09 11:25:45.544 iOS性能優化[7190:70542] 當前存款數量:4,已取款:96,線程名:A人員

2015-06-09 11:25:45.544 iOS性能優化[7190:70543] 當前存款數量:3,已取款:97,線程名:B人員

2015-06-09 11:25:45.544 iOS性能優化[7190:70542] 當前存款數量:2,已取款:98,線程名:A人員

2015-06-09 11:25:45.545 iOS性能優化[7190:70543] 當前存款數量:1,已取款:99,線程名:B人員

2015-06-09 11:25:45.546 iOS性能優化[7190:70542] 當前存款數量:0,已取款:100,線程名:A人員

這里可以看到,使用線程鎖后,A線程與B線程就像兩個排隊取款的人員一樣,十分有序的取出自己的資源,這就是線程同步中線程鎖的作用,從而合理的避免了多線程同步訪問造成的數據錯誤,除此之外iOS系統還提供了很多線程鎖的方法,這里並不一一介紹,在后面會進行詳細的探討。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM