凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Objective-C で NSThread および GCD で非同期処理した結果を UI に反映する処理を書いてみた

はじめに

前回、Objective-C++ 使って iOS で Boost.Asio してみた - 凹みTips で書いた処理ですが、同期的に処理を行うため HTML を取ってくるまでの間、UI 系はブロックされてしまいます。
そこで UI 系がブロックされないようにするためにはどうすれば良いか調べてみました。

1. performSelectorInBackground / NSThread で実行

NSThread や performSelectorInBackground でセレクタを渡せば別スレッドで処理を実行することができます。

ViewController.mm
#import "ViewController.h"
#include <boost/asio.hpp>

@interface ViewController ()

@end

@implementation ViewController

- (void)setText:(NSString*)str
{
    _textView.text = str;
}

- (void)httpGet:(NSString*)url
{
    boost::asio::ip::tcp::iostream s( [url UTF8String], "http" );
    s << "GET / HTTP/1.0\r\n";
    s << "Host: www.boost.org\r\n";
    s << "\r\n";
    s << std::flush;
    std::string buf, html;
    while( std::getline(s, buf) ) {
        html += buf;
    }
    NSString* str = [[NSString alloc] initWithCString:html.c_str() encoding:NSUTF8StringEncoding];
    [self performSelectorOnMainThread:@selector(setText:) withObject:str waitUntilDone:NO];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self performSelectorInBackground:@selector(httpGet:) withObject:@"www.boost.org"];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

@end

これで、UI 系のブロックを回避することができます。
C++ で時間のかかる処理をするところは別スレッドで実行して、終わったら performSelectorOnMainThread でメインスレッドに戻って結果を反映、という処理になっています。最初はメインでない別スレッドから TextView にアクセスしようとして落ちて困ってました…。このあたりは Android と同じですね。
ちなみに、

[self performSelectorOnMainThread:@selector(setText:) withObject:str waitUntilDone:NO];

は以下のように NSThread から行なっても動きます。

[NSThread detachNewThreadSelector:@selector(httpGet:) toTarget:self withObject:@"www.boost.org"];

2. GCD の利用

Twitter@novi_ さんから GCD(Grand Central Dispatch) 使うと良いよ!と教えてもらったのでやってみました。GCD は以下のサイトを参考にすると「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものらしいです(GCDを試してみる - As Sloth As Possible)。後はブロック式使ってサクサク書けるの
-参考: iOS4でGCDとBlocksを使ってUITableViewへの非同期画像読み込みを書いてみる。 - Paamayim Nekudotayim

#import "ViewController.h"
#include <dispatch/dispatch.h>
#include <boost/asio.hpp>
#include <iostream>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_queue_t q_global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t q_main   = dispatch_get_main_queue();
    dispatch_async(q_global, ^{
        boost::asio::ip::tcp::iostream s( "www.boost.org", "http" );
        s << "GET / HTTP/1.0\r\n";
        s << "Host: www.boost.org\r\n";
        s << "\r\n";
        s << std::flush;
        std::string buf, html;
        while( std::getline(s, buf) ) {
            html += buf;
        }
        dispatch_async(q_main, ^{
            _textView.text = [[NSString alloc] initWithCString:html.c_str() encoding:NSUTF8StringEncoding];
        });
    });
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

@end

これは素晴らしい。。
dispatch_get_global_queue の1つ目の引数は優先度で、DISPATCH_QUEUE_PRIORITY_DEFAULT/HIGH/LOW を指定、2つ目の引数は for future use なので 0 を入れておけば良いようです。

なお GCD 部分は C 言語拡張なので、Boost.Asio 部分を書かないのであれば拡張子は .m で大丈夫です。

おまけ:C++ の非同期処理で最初はやろうとした...

Boost.Asio の部分を同期処理で書いてるから非同期処理で書けば良いか!と、最初は安直に思って書いていたのですが、io_service::run で結局ブロッキングが起きるため、あれ、あまり意味ない…、となってしまいました。何か良い方法があったらご教授下さい m(_ _)m

#import "ViewController.h"
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/function.hpp>

using namespace std;
namespace asio = boost::asio;
using boost::asio::ip::tcp;

class Client {
    typedef boost::function<void(const string&, const string&)> callback;
    callback callback_;
    string host_;
    string html_;
    asio::io_service& io_service_;
    tcp::socket socket_;
    tcp::resolver resolver_;
    asio::streambuf buf_;

public:
    Client(asio::io_service& io_service)
        : io_service_(io_service),
          socket_(io_service),
          resolver_(io_service)
    {}

    void get(const string& host, callback callback)
    {
        host_ = host;
        callback_ = callback;
        html_ = "";
        tcp::resolver::query query(host, "http");
        resolver_.async_resolve(
            query,
            boost::bind(
                &Client::on_resolve,
                this,
                asio::placeholders::error,
                asio::placeholders::iterator));
    }

private:
    void on_resolve(const boost::system::error_code& error,
                    tcp::resolver::iterator endpoint_iterator)
    {
        if (has_error(error)) return;
        async_connect(
            socket_,
            endpoint_iterator,
            boost::bind(&Client::on_connect, this, asio::placeholders::error));
    }

    void on_connect(const boost::system::error_code& error)
    {
        if (has_error(error)) return;
        buf_.consume(buf_.size());
        ostream s(&buf_);
        s << "GET / HTTP/1.0\r\n";
        s << "Host: " << host_ << "\r\n";
        s << "\r\n";
        async_write(
            socket_,
            buf_,
            boost::bind(&Client::on_write, this, asio::placeholders::error));
    }

    void on_write(const boost::system::error_code& error)
    {
        if (has_error(error)) return;
        buf_.consume(buf_.size());
        async_read(
            socket_,
            buf_,
            asio::transfer_at_least(1),
            boost::bind(&Client::on_receive, this, asio::placeholders::error));
    }

    void on_receive(const boost::system::error_code& error)
    {
        if (error.message().find("End of file") != std::string::npos) {
            callback_("", html_);
            return;
        }
        if (has_error(error)) {
            return;
        }
        const string data(asio::buffer_cast<const char*>(buf_.data()), buf_.size());
        buf_.consume(buf_.size());
        html_ += data;
        async_read(
            socket_,
            buf_,
            asio::transfer_at_least(1),
            boost::bind(&Client::on_receive, this, asio::placeholders::error));
    }

    bool has_error(const boost::system::error_code& error) {
        if (error) {
            callback_(error.message(), "");
            return true;
        }
        return false;
    }
};

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    asio::io_service io_service;
    
    Client client(io_service);
    const string url = "www.boost.org";
    client.get(url, [&](const string& error, const string& html) {
        if (!error.empty()) {
            cerr << error << endl;
            return;
        }
        cout << html << endl;
        _textView.text = [[NSString alloc] initWithCString:html.c_str() encoding:NSUTF8StringEncoding];
    });
    
    io_service.run();
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

@end

最近 Node.js ばっかやりすぎて、ついこういう書き方したくなってしまう…。

おわりに

非同期系処理は各言語様々で面白いですね。