https://hamait.tistory.com/702


먼저 안드로이드는 개발을 2주동안 했다. 기능은 인증/푸쉬/전광판/사용히스토리/지도/음성인식/설정 정도의 복잡하지 않은 것들로 전광판에 보여줄 IoT 정보는 15분 스케쥴링으로 서버에 요청하여 JSON으로 가지고 오는 방식이었다. 서버는  Akka TCP 서버라 Java 기본 소켓통신 (JAVA IO) 을 이용하여 한번 콜하고 바로 커넥션을 끊는 방식을 택했다. 마찬가지로 푸쉬도 구글에서 제공하는 푸쉬를 사용하지 않고 서비스에서 always 커넥션되어 직접 받도록 하였다. 

이제 iOS 를 개발해야하는데 자료는 Object-C 가 많았지만 개인적으로 Swift 가 맘에 들었기 때문에 이것으로 구현 할 생각이다. UI를 개발하기전에 일단 가장 중요하다고 생각되는 네트워킹 및 쓰레드에 관련된 기능을 확인하기 위해 찾아보았고 그것에 해당하는 안드로이드의 AsyncTask 와 그와 비슷하다고 생각되는 iOS 의  GCD 에 대해 말할 예정이다. 이미 눈치 채셨듯이 두개(iOS 와 안드로이드) 에 대해서 잘 알지는 못하며 나 스스로를 위한 정리쯤으로 남겨놓는 글임을 밝힌다. 틀린 정보가 있을 수 있다는 야그..즉 추후에 업데이트되거나 삭제될수도 있을거 같다..


이만 잡담을 마무리하고 본론으로 들어가보자.


1. 안드로이드  AsyncTask

처음에는  AsycnTask를 사용하진 않고 그냥 자바쓰레드를 만들어서 쓰레드안에서 서버와 커뮤니케이션을 한 후에 받은 데이터는 Handler 를 통해서 UI 업데이트를 했다. 그걸 다시 AsyncTask로 바꾸진 않았지만 앞으로 개발할땐 AsyncTask 를 사용할것 같다. 왜? 조금이라도 더 편하니깐. 

무엇이 편하냐?  일단 안드로이드에서는 Activity 라는 하나의 View/controller 가 있고 (iOS에서는 UIViewController ) 그것을 다루기위한 단 하나의 쓰레드가 있다. 쓰레드가 단 하나이기 때문에 시간이 좀 걸리는 기능 (서버와의 커넥션을 해서 데이터를 가져오는) 을 그 쓰레드에서 그냥 해버리면 UI가 버벅댈 수 있으니 따로 쓰레드를 만들어서 사용해야한다. 그때 그냥 일반적인 자바 쓰레드를 하나 띄워서 거기서 데이터를 받은 후에 그 데이터를 UI 에 업데이트해야하는데 그냥 해버리면 에러가 나며 (두개의 쓰레드가 동시에 하나의 구조에 접근하게 되면 문제가 생길거라는건  유츄가능하죠.) 그때 연결고리 역할을 할 Handler 라는것을 활용한다.

* 참고로 멀티쓰레드 패턴에 대해 공부하고 싶은 분은 이 글을 한번 읽어보시길 -> 생산자 - 소비자 패턴 

서브쓰레드에서 데이터를 메인쓰레드에게 자연스럽게 넘겨주는 방식인데 다음과 같이 코딩되는데

일반쓰레드 와 Handler 예)


public
class SearchActivity extends AppCompatActivity { UpdateHandler updatehandler; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); updatehandler = new UpdateHandler(); } public void OnClick(View view) { switch (view.getId()) { case R.id.button: try { ConnectThread cthread = new ConnectThread("192.168.1.x"); cthread.start(); } catch (MalformedURLException e) { e.printStackTrace(); } break; } } class ConnectThread extends Thread { String hostname; public ConnectThread(String addr) { this.hostname = addr; } public void run() { try { String result = SimpleSocketUtil.getGWLocation(....); Message msg = updatehandler.obtainMessage(); Bundle data = new Bundle(); data.putString(PKConst.connection, PKConst.connection_success); data.putString(PKConst.useStatus, result); msg.setData(data); updatehandler.sendMessage(msg); } catch(Exception ex) { ex.printStackTrace(); } } } public class UpdateHandler extends Handler { public void handleMessage(Message msg) { String is_connected = msg.getData().getString(PKConst.connection); String result = msg.getData().getString(PKConst.useStatus); if (is_connected.equals(PKConst.connection_success)){ onUpdateSuccess(result); } else { onConnectionFailed(); } } }

 SearchActivity 라는 메인 쓰레드와 cThread 서브쓰레드가 있으며 cThread 에서 서버측에서 데이터를 얻은후에 그 값을 SearchActivity 로 넘겨줄때 Handler 를 이용해서 넘겨주고 있다.

방식은

1. Message 객체를 만든다.
2. Bundle 객체를 만든다.
3. Bundle 객체에 넘겨줄 데이터 key, value 형식으로 담는다. 
4. Message 객체에 Bundle 를 담은 후에 
5. Handler 의 sendMessage 메소드를 통해 보내준다.
6. Handler 는 그 데이터를 받아서 UI 를 업데이트 해준다. (위 코드에서는 onUpdateSuccess 함수 내에서 업데이트) 

이다. 

근데 이 방식 말고도 다양하게 있는 듯 하고 . 더 자세히 알려면 다른 블로그를 참고하시라.

어쨋든 저렇게 하려면 쓰레드도 따로 만들어야하고 핸들러도 따로 만들어야하고 데이터를 메세지와 번들을 이용하여 따로 만들어서 보내줘야하는등 좀 할것들이 있어 보입니다. 이걸 더 간편하게 만들기 위한것이 AsyncTask  인데 

예를 봅시다.

AsyncTask 예 )


public class SearchActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

 
    public void OnClick(View view) {
        switch (view.getId()) {
            case R.id.button:
                try {
                    new ConnectServerTask().execute("192.168.1.x");
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
        }
    }

    private class ConnectServerTask extends AsyncTask<String,String,String> {
        
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
        }

        @Override
        protected String doInBackground(String... ip) {
            String result = SimpleSocketUtil.getGWLocation(ip[0]);
            return result;  // onPostExecute 인자로 넘어감
        }

        @Override
        protected void onProgressUpdate(String... progress) {
           // doInBackgournd 에서 publishProgress 호출하면 이리로옴
        }

        @Override
        protected void onPostExecute(String result) {   // UI 업데이트용 
          super.onPostExecute()
            String is_connected  = result.getData().getString(PKConst.connection);
            String status  = result.getData().getString(PKConst.useStatus);
            if( is_connected.equals(PKConst.connection_success)){
                onUpdateSuccess(status);
            }
            else {
                onConnectionFailed();
            }
        }
    }
} 

소스 설명   

1. AsyncTask 를 상속받은 클래스를 만든다.
2. AsyncTask 객체를 만들어서 execute 로 실행시킨다. (필요한 인자전달) 
3. doInBackgournd 에서 서버에 접속하여 데이터를 가져온 후에 리턴하면 onPostExecute 의 인자로 넘겨집니다.
4. onPostExecute 메소드에서 메인쓰레드의 UI 를 업데이트 시켜준다. 


위 코드에서 중요한것은 
onPreExecute  :   실행되기전에 처리해야할것들
doInBackground  : 서브쓰레드에서 실행되는 부분 (이외의 메소드는 모두 메인쓰레드에서 사용됨) 
onProgressUpdate :  서브쓰레드에서 실행되는 진행상태에 대해 처리하는 함수
onPostExecute  :  결과에 대해서 메인쓰레드에서 실행될 부분
이 밖에 onCancelled 등 이 있으나 생략함.


또한 AsyncTask의 <> 에 들어갈 3가지 파라미터 타입은 

첫번째인자 =>  doInBackground()의 인자타입이며 execute() 에서 넘겨줄 타입이다.
두번째인자 =>  onProgressUpdate() 의 인자타입
세번째인자=>  onPostExcute() 의 인자타입. 즉 doInBackgournd 의 리턴타입이자 onPostExecute 의 인자타입이다.



2. iOS GCD (Grand Central Dispatch) 

참고 : https://www.appcoda.com/ios-concurrency/ 


GCD 는 가장 일반적으로 사용되는 쓰레딩(NSThread)을 직접구현하지 않기 위한 비동기 매니져이며 C 로 구성되었고 iOS4 부터 지원한다고 한다. GCD 는 2가지 방식으로 비동기 업무에 대한 관리를 해주며 , 내부의 쓰레드풀에서 일감을 큐에서 가져와서 처리하는데 구체적으로 살펴보자.


- serial 큐 

 이름처럼 하나의 시간에 하나의 업무만 실행되는 큐이다. 여러개의 시리얼 큐를 만들면, 동시에 일을 처리하게 만들 수도 있다. 공유리소스에 차례대로 접근하기때문에 race condition 문제가 발생하지 않는다. 또한 a 다음에 b 를 처리 하기때문에 순서가 분명한 일에 적합하다.  GCD 에서 dispatch_get_main_queue 가 순차적큐이며 UI 작업은 메인큐에서만 해야한다. 

- concurrent 큐

하나의 큐에 넣은 업무들이 동시적으로 실행된다. 따라서 순서를 보장 받을 수 없다. a 와 b 를 시작시켰는데 
무엇이 먼저 끝날지 모른다. 


serial  및 concurrent 큐에 대해 설명했는데  이제는 어떻게 사용할 수 있는지 알아보자. 기본적으로 시스템은 각 응용 프로그램에 단일 직렬 대기열과 네 개의 동시 대기열을 제공한다주 디스패치 큐는 응용 프로그램의 주 스레드에서 작업을 실행하는 전역적으로 사용 가능한 serial 큐입니다. 앱 UI를 업데이트하고 UIView 업데이트와 관련된 모든 작업을 수행하는 데 사용된다. 한 번에 하나의 작업 만 실행되므로 메인 큐에서 무거운 작업을 실행할 때 UI의 움직임이 멈출 수 있다. 

메인 큐 외에 시스템은 4 개의 concurrent 큐를 제공하는데 그것들을 Global Dispatch 큐 라고 한다. 이 큐는 응용 프로그램에 대해 전역이며 우선 순위 수준에 의해서만 구분됩니다. 전역 concurrent 큐 중 하나를 사용하려면 첫 번째 매개 변수 인 다음 값 중 하나를 취하는 dispatch_get_global_queue 함수를 사용하여 원하는 대기열에 대한 참조를 가져와야합니다.

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

이러한 유형은 실행 우선 순위를 나타낸다. HIGH로 설정된  큐가  가장 높은 우선 순위를 가지며 BACKGROUND가 가장 우선 순위가 낮다.. 따라서 작업의 우선 순위에 따라 사용하는 큐를 결정할 수 있게된다. 마지막으로, 임의의 수의 직렬 또는 concurrent 큐를  작성할 수 있다. concurrent 큐의 경우 네 개의 글로벌 대기열 중 하나를 사용하는 것이 좋지만 직접 만들 수도 있다..

즉 대략 코드의 모형은 이렇다. 

let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { // 서버로 부터 데이터를 가져오는 코드 여기다 넣음 dispatch_async(dispatch_get_main_queue()) { // UI 를 업데이트함. } }


좀 더 구체적인 예를 가지고 말해보자.

Concurrent 큐 사용하기

이제 Xcode 프로젝트의 ViewController.swift 파일의  didClick 메서드에 이미지 다운로드를 처리한다고 하자. 

@IBAction func didClickOnStart(sender: AnyObject) {
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    self.imageView1.image = img1
    
    let img2 = Downloader.downloadImageWithURL(imageURLs[1])
    self.imageView2.image = img2
    
    let img3 = Downloader.downloadImageWithURL(imageURLs[2])
    self.imageView3.image = img3
    
    let img4 = Downloader.downloadImageWithURL(imageURLs[3])
    self.imageView4.image = img4
    
}

각 다운로더는 하나의 작업으로 간주되며 모든 작업은 이제 메인 큐에서 수행된다. 메인큐는 알다시피 시리얼 큐이기때문에 망했다고 볼 수 있다. ;; 

이제 기본 우선 순위 큐인 글로벌concurrent 큐중 하나에 대한 참조를 얻자.

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        dispatch_async(queue) { () -> Void in
            
            let img1 = Downloader.downloadImageWithURL(imageURLs[0])
            dispatch_async(dispatch_get_main_queue(), {
                
                self.imageView1.image = img1
            })
            
        }

먼저 dispatch_get_global_queue를 사용하여 기본 concurrent 큐에 대한 참조를 가져온 다음 블록 내부에 첫 번째 이미지를 다운로드하는 작업을 제출한다. 이미지 다운로드가 완료되면 메인큐에 작업을 제출하여 다운로드 한 이미지로 업데이트 한다. 즉, 이미지 다운로드 작업을 백그라운드 스레드에 하고  메인 대기열에서 UI 관련 작업을 실행 한다.

이미지의 나머지 부분도 똑같이 하면 요렇게 되겠지.

@IBAction func didClickOnStart(sender: AnyObject) {
    
    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) { () -> Void in
        
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(queue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}

네 개의 이미지 다운로드를  기본 제공되는 concurrent  에 제출한다.  이렇게 하면 이제 이미지를 다운로드하는 동안 버벅거림 없이 슬라이더를 드래그 할 수 있게 된다.

Serial 큐 사용하기 

지연 문제를 해결하기위한 또 다른 방법은 serial 큐를 사용하는 것 인데 이제 ViewController.swift 파일에서 동일한 didClickOnStart () 메서드로 돌아가서 이미지를 다운로드하기 위한 직렬 큐를 사용해보자. 직렬 큐ㄹ 사용할 때는 참조하는 직렬 큐에 세심한 주의를 기울여야 한다. 각 응용 프로그램에는 실제로 UI의 기본 대기열인 하나의 기본 직렬 큐가 있기때문에 직렬 큐를사용할 때 새로운 큐를  만들어야하며, 그렇지 않으면 앱이 UI 업데이트 작업을 실행하는 동안 작업을 같이 실행하게 된다. 이로 인해 다양한 오류 및 지연이 발생할 수 있다.
dispatch_queue_create 함수를 사용하여 새 큐를 만든 다음 이전과 같은 방식으로 모든 작업을 제출할 수 있다. 변경 후 코드를 살펴보자
@IBAction func didClickOnStart(sender: AnyObject) {
    
    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)
    
    
    dispatch_async(serialQueue) { () -> Void in
        
        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView1.image = img1
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView2.image = img2
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView3.image = img3
        })
        
    }
    dispatch_async(serialQueue) { () -> Void in
        
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        
        dispatch_async(dispatch_get_main_queue(), {
            
            self.imageView4.image = img4
        })
    }
    
}
concurrent  큐와 다른 유일한 점은 직렬 큐 생성이다. 앱을 다시 빌드하고 실행하면 UI에 계속해서 상호 작용할 수 있도록 이미지가 백그라운드에서 다시 다운로드됨을 알 수 있다.

그러나 두 가지 사실을 알게 될 것인데 concurrent 큐의 경우와 비교하여 이미지를 다운로드하는 데 약간 시간이 걸립니다. 그 이유는 한 번에 하나의 이미지 만 로드하기 때문. 각 작업은 이전 작업이 완료되기 전에 대기한다.

이미지는 image1, image2, image3 및 image4 순서로 로드되며  이는 대기열이 한 번에 하나의 작업을 실행하는 시리얼로 작업되는 큐이기 때문이다.


스위프트 초보코너 

코드중에 ()-> Void in 으로 시작하는건 말이지. 스위프트에서 클로저가 저 모냥으로 생겼다. (여기서 클로저는 함수형프로그래밍에서 말하는 그 클로저를 말하는것은 아니다. 람다나 익명함수를 스위프트에서는 클로저로 지칭한다) 

즉  

func something() -> String {
   ... 뭔가 함..

}

이런 함수에서 함수명하고 앞에 func 를 뺀것이다. 그리고 in 을 붙히고 외곽에 {} 로 감싸면 클로저!

{ ()-> String in 

   .... 뭔가 함 ..

}


그리고 어떤 언어에서나 개발자가 사용 하기 편하게 만들어 놓은 희안하게 생긴것들이 있는데 그걸 Syntataic sugar 라고 한다. 위에서는 아래와 같은 코드가 그러한 것이다. 그냥 외워야한다. ;;;

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) {


3. Swift 3.0 에서의 GCD 

swift 2 에서 dispatch_get_global_queue 이렇게 길게 써야 했던게 
swift 3 에서는 DispatchQueue.global 식으로 간단히 바뀌었다.

예를 들면 )

swift 2.0 

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) { () -> Void in
        
    let img1 = Downloader.downloadImageWithURL(imageURLs[0])
    dispatch_async(dispatch_get_main_queue(), {
            
        self.imageView1.image = img1
    })
        
}
swift 3.0

 let queue = DispatchQueue.global()
       
 queue.async { () -> Void in
     let img1 = Downloader.downloadImageWithURL(imageURLs[0])
     DispatchQueue.main.async {
          self.imageView1.image = img1
     }
 }


4. GCD 를 이용한 서비스  

iOS 에는 안드로이드의 서비스 개념이 없는듯 하다. 먼가 계속 돌아가는것에 대한 부정적 입장 때문인듯 싶은데 ,이때 GCD내부에서 while 을 돌면서 먼가를 계속 수행하면 될 듯 싶다.다만 app 이 백그라운드로 가면 멈추더라~ (iOS 는 정해져있는듯)

 let queue = DispatchQueue.global()
        
        queue.async { () -> Void in  
            while true {
                sleep(1)
                // 먼가를 한다. 
                // 결과  a 가 생성 
                DispatchQueue.main.async {
                   // a 를 가지고 먼가를 한다. 
                    
                }
                
            }
            
        }
서버로부터 받은 a에 담겨진 값을 localnotification 으로 날리면 그게 바로 실시간 푸쉬서비스~ 아닐까?


5. SwiftTask  https://github.com/ReactKit/SwiftTask

이것에 대한 설명은 일단 사용해보고 난 후에 하기로 한다. 예만 저 링크 페이지에서 가져왔다. 
뭔가 안드로이드의 AsyncTask 같다. -.-a


let task = Task<Float, String, NSError> { progress, fulfill, reject, configure in player.doSomethingWithProgress({ (progressValue: Float) in progress(progressValue) }, completion: { (value: NSData?, error: NSError?) in if error == nil { fulfill("OK") } else { reject(error) } }) // pause/resume/cancel configuration (optional) configure.pause = { [weak player] in player?.pause() } configure.resume = { [weak player] in player?.resume() } configure.cancel = { [weak player] in player?.cancel() } } task.success { (value: String) -> Void in // do something with fulfilled value }.failure { (error: NSError?, isCancelled: Bool) -> Void in // do something with rejected error } task.pause() task.resume() task.cancel()





출처: https://hamait.tistory.com/706?category=187020 [HAMA 블로그]