1. 程式人生 > >iOS開發中多線程基礎

iOS開發中多線程基礎

推斷 傳遞 cti self reads priority cal default value

耗時操作演練

代碼演練

  • 編寫耗時方法
- (void)longOperation {
    for (int i = 0; i < 10000; ++i) {
        NSLog(@"%@ %d", [NSThread currentThread], i);
    }
}
  • 直接調用耗時方法
// 1> 直接調用耗時方法
[self longOperation];

運行測試效果

  • 在後臺運行耗時方法
// 2> 在後臺運行耗時方法
[self performSelectorInBackground:@selector
(longOperation) withObject:nil];

運行測試效果

小結

  1. [NSThread currentThread]:當前線程對象
    • 能夠在全部的多線程技術中使用!
    • 通經常使用來在多線程開發中。Log 代碼是否在主線程運行
  2. number
    • number == 1 主線程
    • number != 1 後臺線程
    • 不要糾結 number 的具體數字

pthread演練

  • pthreadPOSIX 多線程開發框架,因為是跨平臺的 C 語言框架。在蘋果的頭文件裏並沒有具體的凝視
  • 要查閱 pthread 有關資料,能夠訪問 http://baike.baidu.com

導入頭文件

#import <pthread.h>

pthread演練

// 創建線程。並且在線程中運行 demo 函數
- (void)pthreadDemo {

    /**
     參數:
     1> 指向線程標識符的指針,C 語言中類型的結尾通常 _t/Ref,並且不須要使用 *
     2> 用來設置線程屬性
     3> 線程運行函數的起始地址
     4> 運行函數的參數

     返回值:
     - 若線程創建成功,則返回0
     - 若線程創建失敗。則返回出錯編號


     */
    pthread_t threadId = NULL
; NSString *str = @"Hello Pthread"; int result = pthread_create(&threadId, NULL, demo, (__bridge void *)(str)); if (result == 0) { NSLog(@"創建線程 OK"); } else { NSLog(@"創建線程失敗 %d", result); } } // 後臺線程調用函數 void *demo(void *params) { NSString *str = (__bridge NSString *)(params); NSLog(@"%@ - %@", [NSThread currentThread], str); return NULL; }

小結

  1. 在 C 語言中,沒有對象的概念。對象是以結構體的方式來實現的
  2. 通常,在 C 語言框架中,對象類型以 _t/Ref 結尾,並且聲明時不須要使用 *
  3. C 語言中的 void * 和 OC 中的 id 是等價的
  4. 內存管理
    • 在 OC 中,假設是 ARC 開發,編譯器會在編譯時。依據代碼結構,自己主動加入 retain/release/autorelease
    • 可是。ARC 僅僅負責管理 OC 部分的內存管理,而不負責 C 語言 代碼的內存管理
    • 因此,開發過程中。假設使用的 C 語言框架出現 retain/create/copy/new 等字樣的函數,大多都須要 release,否則會出現內存泄漏
  5. 在混合開發時,假設在 COC 之間傳遞數據,須要使用 __bridge 進行橋接,橋接的目的就是為了告訴編譯器怎樣管理內存
  6. 橋接的加入能夠借助 Xcode 的輔助功能加入
  7. MRC 中不須要使用橋接

三種創建線程的方法

準備函數

// MARK: - 後臺線程調用函數
- (void)longOperation:(id)obj {
    NSLog(@"%@ - %@", [NSThread currentThread], obj);
}

1. alloc / init - start

// MARK: - NSThread 演練
- (void)threadDemo1 {
    // 1. 實例化線程對象 => alloc(分配內存) / init(初始化)
    NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(longOperation:) object:@"alloc/init"];

    // 2. 啟動線程
    [t start];

    // 3. 當前線程?
    NSLog(@"%@", [NSThread currentThread]);
}

演練小結

  1. [t start];運行後。會在另外一個線程運行 demo 方法
  2. 在 OC 中。不論什麽一個方法的代碼都是從上向下順序運行的
  3. 同一個方法內的代碼,都是在同樣線程運行的(block除外)

2. detachNewThreadSelector

- (void)threadDemo2 {
    // detach => 分離一個子線程運行 demo: 方法
    [NSThread detachNewThreadSelector:@selector(longOperation:) toTarget:self withObject:@"Detach"];

    // 2. 當前線程?
    NSLog(@"%@", [NSThread currentThread]);
}

演練小結

  • detachNewThreadSelector 類方法不須要啟動,創建線程後自己主動啟動線程運行 @selector 方法

3. 分類方法

- (void)threadDemo3 {
    // 1. 在後臺運行 @selector 方法
    [self performSelectorInBackground:@selector(longOperation:) withObject:@"category"];

    // 2. 當前線程?
    NSLog(@"%@", [NSThread currentThread]);
}
  1. performSelectorInBackgroundNSObject 的分類方法
  2. 沒有 thread 字眼,會馬上在後臺線程運行 @selector 方法
  3. 全部 NSObject 都能夠使用此方法,在其它線程運行方法!

自己定義對象

Person 類

// MARK: - Person 類
@interface Person : NSObject
/// 姓名
@property (nonatomic, copy) NSString *name;
@end

@implementation Person

/// 使用字典實例化對象
+ (instancetype)personWithDict:(NSDictionary *)dict {
    Person *p = [[Person alloc] init];

    [p setValuesForKeysWithDictionary:dict];

    return p;
}

/// 載入數據
- (void)loadData {
    NSLog(@"載入數據 %@ %@", [NSThread currentThread], self.name);
}

@end

Person 類使用分類方法

- (void)threadDemo4 {
    Person * p = [Person personWithDict:@{@"name": @"zhangsan"}];

    [p performSelectorInBackground:@selector(loadData) withObject:nil];
}

線程狀態

演練代碼

// MARK: - 線程狀態演練
- (void)statusDemo {

    NSLog(@"睡會");
    [NSThread sleepForTimeInterval:1.0];

    for (int i = 0; i < 20; ++i) {
        if (i == 8) {
            NSLog(@"再睡會");
            [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
        }

        NSLog(@"%@ %d", [NSThread currentThread], i);

        if (i == 10) {
            NSLog(@"88");
            [NSThread exit];
        }
    }
    NSLog(@"能來嗎?");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // 註意不要在主線程上調用 exit 方法
//    [NSThread exit];

    // 實例化線程對象(新建)
    NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(statusDemo) object:nil];

    // 線程就緒(被加入到可調度線程池中)
    [t start];
}

堵塞

  • 方法運行過程。符合某一條件時,能夠利用 sleep 方法讓線程進入 堵塞 狀態

1> sleepForTimeInterval

  • 從如今起睡多少

2> sleepUntilDate

  • 從如今起睡到指定的日期

死亡

[NSThread exit];

  • 一旦強行終止線程,興許的全部代碼都不會被運行
  • 註意:在終止線程之前,應該註意釋放之前分配的對象!

就緒 -> 運行

線程從就緒運行狀態之間的切換是由 CPU 負責的。程序猿無法幹預

線程屬性

演練代碼

// MARK: - 線程屬性
- (void)threadProperty {
    NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];

    // 1. 線程名稱
    t1.name = @"Thread AAA";
    // 2. 優先級
    t1.threadPriority = 0;

    [t1 start];

    NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];

    // 1. 線程名稱
    t2.name = @"Thread BBB";
    // 2. 優先級
    t2.threadPriority = 1;

    [t2 start];
}

- (void)demo {
    for (int i = 0; i < 10; ++i) {
        // 堆棧大小
        NSLog(@"%@ 堆棧大小:%tuK", [NSThread currentThread], [NSThread currentThread].stackSize / 1024);
    }

    // 模擬崩潰
    // 推斷是否是主線程
//    if (![NSThread currentThread].isMainThread) {
//        NSMutableArray *a = [NSMutableArray array];
//
//        [a addObject:nil];
//    }
}

屬性

1. name - 線程名稱

  • 在大的商業項目中。通常須要在程序崩潰時。獲取程序準確運行所在的線程

2. threadPriority - 線程優先級

  • 優先級,是一個浮點數,取值範圍從 0~1.0
    • 1.0表示優先級最高
    • 0.0表示優先級最低
    • 默認優先級是0.5
  • 優先級高僅僅是保證 CPU 調度的可能性會高
  • 刀哥個人建議,在開發的時候。不要改動優先級
  • 多線程的目的:是將耗時的操作放在後臺,不堵塞主線程和用戶的交互。
  • 多線程開發的原則:簡單

3. stackSize - 棧區大小

  • 默認情況下,不管是主線程還是子線程。棧區大小都是 512K
  • 棧區大小能夠設置
[NSThread currentThread].stackSize = 1024 * 1024;

4. isMainThread - 是否主線程

資源共享-賣票

多線程開發的復雜度相對較高,在開發時能夠依照下面套路編寫代碼:

  1. 首先確保單個線程運行正確
  2. 加入線程

賣票邏輯

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.tickets = 20;

    [self saleTickets];
}

/// 賣票邏輯 - 每個售票邏輯(窗體)應該把全部的票賣完
- (void)saleTickets {
    while (YES) {
        if (self.tickets > 0) {
            self.tickets--;
            NSLog(@"剩余票數 %d %@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"沒票了 %@", [NSThread currentThread]);
            break;
        }
    }
}

加入線程

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    self.tickets = 20;

    NSThread *t1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    t1.name = @"售票員 A";
    [t1 start];

    NSThread *t2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    t2.name = @"售票員 B";
    [t2 start];
}

加入休眠

- (void)saleTickets {
    while (YES) {
        // 模擬休眠
        [NSThread sleepForTimeInterval:1.0];

        if (self.tickets > 0) {
            self.tickets--;
            NSLog(@"剩余票數 %d %@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"沒票了 %@", [NSThread currentThread]);
            break;
        }
    }
}

運行測試結果

相互排斥鎖

加入相互排斥鎖

- (void)saleTickets {
    while (YES) {
        // 模擬休眠
        [NSThread sleepForTimeInterval:1.0];

        @synchronized(self) {
            if (self.tickets > 0) {
                self.tickets--;
                NSLog(@"剩余票數 %d %@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"沒票了 %@", [NSThread currentThread]);
                break;
            }
        }
    }
}

相互排斥鎖小結

  1. 保證鎖內的代碼。同一時間,僅僅有一條線程能夠運行!

  2. 相互排斥鎖的鎖定範圍,應該盡量小,鎖定範圍越大,效率越差。
  3. 速記技巧 [[NSUserDefaults standardUserDefaults] synchronize];

相互排斥鎖參數

  1. 能夠加鎖的隨意 NSObject 對象
  2. 註意:鎖對象一定要保證全部的線程都能夠訪問
  3. 假設代碼中僅僅有一個地方須要加鎖,大多都使用 self。這樣能夠避免單獨再創建一個鎖對象

原子屬性

  • 原子屬性(線程安全)。是針對多線程設計的。是默認屬性
  • 多個線程在寫入原子屬性時(調用 setter 方法)。能夠保證同一時間僅僅有一個線程運行寫入操作
  • 原子屬性是一種單(線程)寫多(線程)讀的多線程技術
  • 原子屬性的效率比相互排斥鎖高,只是可能會出現臟數據
  • 在定義屬性時。必須顯示地指定 nonatomic

演練代碼

@interface ViewController ()
@property (atomic, strong) NSObject *obj1;
@property (atomic, strong) NSObject *obj2;
@end

@implementation ViewController
@synthesize obj1 = _obj1;

// 原子屬性模擬代碼
/// obj1 - getter
- (NSObject *)obj1 {
    return _obj1;
}

/// obj1 - setter
- (void)setObj1:(NSObject *)obj1 {
    @synchronized(self) {
        _obj1 = obj1;
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    long largeNumber = 1000 * 1000;

    // 相互排斥鎖測試
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNumber; ++i) {
        self.obj1 = [[NSObject alloc] init];
    }
    NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);

    // 自旋鎖測試
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNumber; ++i) {
        self.obj2 = [[NSObject alloc] init];
    }
    NSLog(@"%f", CFAbsoluteTimeGetCurrent() - start);
}

@end

原子屬性內部的鎖是自旋鎖自旋鎖的運行效率比相互排斥鎖高

自旋鎖 & 相互排斥鎖

  • 共同點

    • 都能夠保證同一時間。僅僅有一條線程運行鎖定範圍的代碼
  • 不同點

    • 相互排斥鎖:假設發現有其它線程正在運行鎖定的代碼。線程會進入休眠狀態,等待其它線程運行完畢。打開鎖之後,線程會被喚醒
    • 自旋鎖:假設發現有其它線程正在運行鎖定的代碼。線程會以死循環的方式,一直等待鎖定代碼運行完畢
  • 結論

    • 自旋鎖更適合運行很短的代碼
    • 不管什麽鎖。都是要付出代價

線程安全

  • 多個線程進行讀寫操作時,仍然能夠得到正確結果,被稱為線程安全
  • 要實現線程安全,必須要用到
  • 為了得到更佳的用戶體驗,UIKit 不是線程安全的

約定:全部更新 UI 的操作都必須主線程上運行。

  • 因此。主線程又被稱為UI 線程

iOS 開發建議

  1. 全部屬性都聲明為 nonatomic
  2. 盡量避免多線程搶奪同一塊資源
  3. 盡量將加鎖、資源搶奪的業務邏輯交給server端處理,減小移動client的壓力

線程間通訊

主線程實現

定義屬性

/// 根視圖是滾動視圖
@property (nonatomic, strong) UIScrollView *scrollView;
/// 圖像視圖
@property (nonatomic, weak) UIImageView *imageView;
/// 網絡下載的圖像
@property (nonatomic, weak) UIImage *image;

loadView

loadView 方法的作用:

  1. 載入視圖層次結構
  2. 用純代碼開發應用程序時使用
  3. 功能和 Storyboard & XIB 是等價的

假設重寫了 loadViewStoryboard & XIB 都無效

- (void)loadView {
    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.backgroundColor = [UIColor orangeColor];
    self.view = self.scrollView;

    UIImageView *iv = [[UIImageView alloc] init];
    [self.view addSubview:iv];
    self.imageView = iv;
}

viewDidLoad

  1. 視圖載入完畢後運行
  2. 能夠做一些數據初始化的工作
  3. 假設用純代碼開發,不要在此方法中設置界面 UI
- (void)viewDidLoad {
    [super viewDidLoad];

    // 下載圖像
    [self downloadImage];
}

下載網絡圖片

- (void)downloadImage {
    // 1. 網絡圖片資源路徑
    NSURL *url = [NSURL URLWithString:@"http://c.hiphotos.baidu.com/image/pic/item/4afbfbedab64034f42b14da1aec379310a551d1c.jpg"];

    // 2. 從網絡資源路徑實例化二進制數據(網絡訪問)
    NSData *data = [NSData dataWithContentsOfURL:url];

    // 3. 將二進制數據轉換成圖像
    UIImage *image = [UIImage imageWithData:data];

    // 4. 設置圖像
    self.image = image;
}

設置圖片

- (void)setImage:(UIImage *)image {
    // 1. 設置圖像視圖的圖像
    self.imageView.image = image;

    // 2. 依照圖像大小設置圖像視圖的大小
    [self.imageView sizeToFit];

    // 3. 設置滾動視圖的 contentSize
    self.scrollView.contentSize = image.size;
}

設置滾動視圖的縮放

1> 設置滾動視圖縮放屬性

// 1> 最小縮放比例
self.scrollView.minimumZoomScale = 0.5;
// 2> 最大縮放比例
self.scrollView.maximumZoomScale = 2.0;
// 3> 設置代理
self.scrollView.delegate = self;

2> 實現代理方法 - 告訴滾動視圖縮放哪一個視圖

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return self.imageView;
}

3> 跟蹤 scrollView 縮放效果

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    NSLog(@"%@", NSStringFromCGAffineTransform(self.imageView.transform));
}

線程間通訊

  • 在後臺線程下載圖像
[self performSelectorInBackground:@selector(downloadImage) withObject:nil];
  • 在主線程設置圖像
[self performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
}

iOS開發中多線程基礎