2018年4月30日 星期一

NSThread, GCD

NSThread

有三種方式可以開Thread
  1. NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(test:) object:nil];
  2. [NSThread detachNewThreadSelector:@selector(test:) toTarget:self withObject:@"分离子线程"];
    [self performSelectorInBackground:@selector(test:) withObject:@"后台线程"];

第一種方式可以取得該線程的instance,也可以設定Thread Priority,但要手動啟動線程
第二三種方式直接開啟線程,但無法取得instance
performSelector是比較早期的API,跟NSThreadㄧ樣呼叫一次就開一個線程 
目前Object-C大多使用GCD或是NSOperationQueue,這兩個API會自行管理線程數量



GCD

分作兩種模式
  1. dispatch_async :非同步
  2. dispatch_sync:同步
同步模式下會先等待Block裡面的動作執行完,才繼續往下跑
個人感覺有點類似C#的async Task 搭配 await語法,會把工作丟給別的Thread去做,但會等工作做完才繼續動作

實際上使用dispatch_sync後,發現動作並沒有在其他Thread執行,而是使用main thread,爬文之後發現有人在討論這個問題



也就是說因為main queue是serial queue,呼叫dispatch_sync必定導致線程block住,無所謂執行block內工作的線程,所以程式會直接使用main queue執行,節省開線程的資源

如果今天是在背景呼叫dispatch_sync把工作丟給main queue,就會有線程切換的動作

簡單來說 dispatch_sync 不會開新線程,但在特定情況下,會有線程切換

另外使用dispatch_async也不一定呼叫幾次就開啟多少線程,程式會自行控制
推測應該跟C#一樣使用thread pool的概念
有文章說線程上限66個,serial 與 concurrent共用這數量


使用GCD要小心Block的問題,千萬不要在同步模式(dispatch_sync) 下丟一個Block給自己的線程,自己等自己會卡住

dispatch_queue 可以分為兩種,serial queue與concurrent queue
main_queue:serial queue
global_queue:於concurrent queue
用dispatch_queue_t可以自行宣告,自行決定類型

GCD使用方法



//sync呼叫 會在main queue執行
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for(int i=0;i<5;i++){
NSLog(@"sync: %d %@",i, [NSThread currentThread]);
}
});
//2018-04-30 16:42:57.905789+0800 NSTimer_NSRunLoop[4045:160977] sync: 0 <NSThread: 0x604000065540>{number = 1, name = main}
//2018-04-30 16:42:57.905875+0800 NSTimer_NSRunLoop[4045:160977] sync: 1 <NSThread: 0x604000065540>{number = 1, name = main}
//2018-04-30 16:42:57.905904+0800 NSTimer_NSRunLoop[4045:160977] sync: 2 <NSThread: 0x604000065540>{number = 1, name = main}
//2018-04-30 16:42:57.905929+0800 NSTimer_NSRunLoop[4045:160977] sync: 3 <NSThread: 0x604000065540>{number = 1, name = main}
//2018-04-30 16:42:57.905951+0800 NSTimer_NSRunLoop[4045:160977] sync: 4 <NSThread: 0x604000065540>{number = 1, name = main}
sleep(1);
//async global queue
//global queue都是concurrent,所以印出來的結果會是亂序
for(int i=0;i<5;i++){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"global: %d %@",i, [NSThread currentThread]);
});
}
//2018-04-30 16:42:58.907260+0800 NSTimer_NSRunLoop[4045:161039] global: 3 <NSThread: 0x604000460240>{number = 5, name = (null)}
//2018-04-30 16:42:58.907258+0800 NSTimer_NSRunLoop[4045:161045] global: 1 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:42:58.907299+0800 NSTimer_NSRunLoop[4045:161061] global: 4 <NSThread: 0x60000007d080>{number = 6, name = (null)}
//2018-04-30 16:42:58.907258+0800 NSTimer_NSRunLoop[4045:161040] global: 2 <NSThread: 0x60000007cf00>{number = 4, name = (null)}
//2018-04-30 16:42:58.907258+0800 NSTimer_NSRunLoop[4045:161038] global: 0 <NSThread: 0x604000460040>{number = 2, name = (null)}
sleep(1);
//自行建立serial queue,會按照順序執行工作
dispatch_queue_t serialQueue = \
dispatch_queue_create("thread4", DISPATCH_QUEUE_SERIAL);
for(int i=0;i<5;i++){
dispatch_async(serialQueue, ^{
NSLog(@"serial queue %d %@",i, [NSThread currentThread]);
});
}
//2018-04-30 16:42:59.908440+0800 NSTimer_NSRunLoop[4045:161045] serial queue 0 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:42:59.908514+0800 NSTimer_NSRunLoop[4045:161045] serial queue 1 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:42:59.908541+0800 NSTimer_NSRunLoop[4045:161045] serial queue 2 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:42:59.908561+0800 NSTimer_NSRunLoop[4045:161045] serial queue 3 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:42:59.908579+0800 NSTimer_NSRunLoop[4045:161045] serial queue 4 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
sleep(1);
//自行建立concurrent queue
dispatch_queue_t concurrentQueue = \
dispatch_queue_create("thread5", DISPATCH_QUEUE_CONCURRENT);
for(int i=0;i<5;i++){
dispatch_async(concurrentQueue, ^{
NSLog(@"concurrent queue %d %@",i, [NSThread currentThread]);
});
}
//2018-04-30 16:43:00.909678+0800 NSTimer_NSRunLoop[4045:161045] concurrent queue 0 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
//2018-04-30 16:43:00.909678+0800 NSTimer_NSRunLoop[4045:161040] concurrent queue 2 <NSThread: 0x60000007cf00>{number = 4, name = (null)}
//2018-04-30 16:43:00.909746+0800 NSTimer_NSRunLoop[4045:161061] concurrent queue 3 <NSThread: 0x60000007d080>{number = 6, name = (null)}
//2018-04-30 16:43:00.909678+0800 NSTimer_NSRunLoop[4045:161038] concurrent queue 1 <NSThread: 0x604000460040>{number = 2, name = (null)}
//2018-04-30 16:43:00.909757+0800 NSTimer_NSRunLoop[4045:161045] concurrent queue 4 <NSThread: 0x60000007d5c0>{number = 3, name = (null)}
view raw GCD.m hosted with ❤ by GitHub
當函式使用非同步模式,又必須回傳其運作結果時,需要使用dispatch_semaphore_t
類似於async await,之後會詳細描述

http://www.iosxxx.com/blog/2016-06-02-GCD那些事.html

2018年4月26日 星期四

Object-C Serialization

Object-C的Serialization不像C#一樣方便
需要在原本的class中寫Encode和Decode方法
寫法如下:
#import <Foundation/Foundation.h>
@interface SerializeEncoder : NSObject
+ (BOOL) Encode : (NSObject *) encodeObject fileName:(NSString *) filename;
+ (NSObject*) Decode: (NSString *) filename;
@end
#import "SerializeEncoder.h"
@implementation SerializeEncoder
+ (BOOL) Encode:(NSObject *)encodeObject fileName:(NSString *)filename{
NSLog(@"Encode %@...",filename);
//先序列化再存檔
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:encodeObject];
BOOL Success = [data writeToFile:filename atomically:YES];
//也可以合併使用 archiveRootObject 直接存檔
return Success;
}
+ (NSObject*) Decode:(NSString *)filename{
NSLog(@"Decode %@...",filename);
NSString *file_path = [NSString stringWithFormat:@"/Users/Test/ServerRecords/%@",filename];
return [NSKeyedUnarchiver unarchiveObjectWithFile:file_path];
}
@end
view raw Encoder.m hosted with ❤ by GitHub
//Class必須實作NSCoding這個Protocol,裡面有兩個方法
@protocol NSCoding
- (void)encodeWithCoder:(NSCoder *)aCoder;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder; // NS_DESIGNATED_INITIALIZER
@end
@interface Connection : NSObject<NSStreamDelegate, NSCoding>{
@public
LightImages *lightImages;
ConnectionStatus status;
NSInputStream *iStream;
NSOutputStream *oStream;
NSString * name;
CommandQueue *queue;
}
@property (copy) NSString *name;
@property (copy) NSString *connectionIP;
@property NSInteger connectionPort;
@property BOOL isReceiving;
@property BOOL isAutoConnect;
@property(weak,nonatomic) id delegate;
//方法實作
@implement Connection
- (void) encodeWithCoder:(NSCoder *)aCoder{
//Encode,必須把要序列化的變數寫在這裡
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeObject:self.connectionIP forKey:@"ip"];
[aCoder encodeInteger:self.connectionPort forKey:@"port"];
[aCoder encodeBool:self.isReceiving forKey:@"rec"];
[aCoder encodeBool:self.isAutoConnect forKey:@"auto"];
}
- (id) initWithCoder:(NSCoder *)aDecoder{
//Decode,class的建構會去decode之前encoding的檔案
if(self=[super init]){
self.name = [aDecoder decodeObjectForKey:@"name"];
self.connectionIP = [aDecoder decodeObjectForKey:@"ip"];
self.connectionPort = [aDecoder decodeIntegerForKey:@"port"];
self.isReceiving= [aDecoder decodeBoolForKey:@"rec"];
self.isAutoConnect = [aDecoder decodeBoolForKey:@"auto"];
//原本建構式要做的事情也不能忘記做
queue = [[CommandQueue alloc] init];
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(checkConnect) userInfo:nil repeats:YES];
b_send = YES;
send_thread = [[NSThread alloc] initWithTarget:self selector:@selector(send_method) object:nil];
[send_thread start];
}
return self;
}
@end

Object-C Extensions

Extension 可以把一個很大的Class拆分,有點類似Category

Extension與Category的差別

  1. Extension中可以定義變數.屬性.方法,Category只能定義方法
  2. Extension不能按照功能或層級來自定名稱(匿名),Category可以在Class後的小括弧內定義
  3. Extension定義出來的Method必須在原Class的Implement中實作 ,Category有自己的.m檔

Extension大部分使用在宣告私有的屬性跟方法,因為Method在.h檔都可以被外部呼叫,如果想宣告一個私有方法,一個方式是只寫Implement,另一個方式就是在.m檔底下使用Extension

Extension使用方法如下:



//假設有一個class EggTimer
//其中有一個內部方法doYouKnowMe
//這個方法不想讓外部看見
@interface EggTimer : NSObject{
NSTimer *timer;
NSDate *startTime;
NSTimeInterval duration;
NSTimeInterval elapsedTime;
}
@property id delegate;
- (void) startTimer;
- (void) resumeTimer;
- (void) stopTimer;
- (void) resetTimer;
@end
view raw EggTimer.h hosted with ❤ by GitHub
//第一個方法是不宣告直接在implement內實作
//另一個方法是在.m檔中使用Extension,方便管理
#import "EggTimer.h"
//Extension
@interface EggTimer ()
//外面看不見我
- (void) canYouSeeMe;
@end
//
@implementation EggTimer
- (void) canYouSeeMe{
NSLog(@"NO");
}
@end
view raw EggTimer.m hosted with ❤ by GitHub

2018年4月25日 星期三

Cocoa control - Menu

以下解說Cocoa Menu綁定Method的方法





在StoryBoard產生Menu物件之後 ,將Menu點擊動作與其ViewController綁定的流程如下:
1.先在ViewController中寫好要執行的IBAction

//原本ViewController下的按鈕點擊Method
- (IBAction)startClicked:(id)sender {
}
- (IBAction)stopClicked:(id)sender {
}
- (IBAction)resetClicked:(id)sender {
}
//IBActions - Menu
- (IBAction)startMenuClicked:(id)sender{
//呼叫ViewController原本的Method
[self startClicked:sender];
}
- (IBAction)stopMenuClicked:(id)sender{
//呼叫ViewController原本的Method
[self stopClicked:sender];
}
- (IBAction)resetMenuClicked:(id)sender{
//呼叫ViewController原本的Method
[self resetClicked:sender];
}

2.在Menu選單上按下右鍵,拖曳至上方橘色方塊( First Responder),會出現所有事件的清單

    選擇剛剛寫好的IBAction,就成功綁定了,當Menu按下後會執行Menu的IBAction,再呼叫原本的Method

    First Responder表示當前Window的第一回應對象,其對象必須是NSView或其子集,概念有點像WPF裡面keyBinding,會依當前Focus對象不同,執行不同的動作

    NSView要執行鍵盤事件,一定要先成為First Responder