ネットワーク接続状況を知る[2] SCNetworkReachabilityGetFlags はブロックする

2010年12月13日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[2011-07-08追記] ブロックの原因が判明、下記もどうぞ。
Cocoaの日々: [iOS] SCNetworkReachabilityGetFlags のブロックの件


以前、Cocoaの日々: ネットワーク接続状況を知る というブログを書いた。

その後わかったことを紹介する。

SCNetworkReachabilityGetFlags がブロックする件


ネットワーク接続状況を取得する SCNetworkReachabilityGetFlags関数 が処理中にブロックしてしまうことがわかった。次の環境でブロックする現象が確認できた。

・iPhone 3GS/iOS4.2
・3GS→× WiFi→◯ ※フライトモード+WiFi有効
・WiFiルータ→×   ※インターネット非接続(光回線断)

つまり iPhone で WiFi接続しているが、その接続先の WiFiルータから先のインターネットに接続していない。この環境で関数を呼ぶとそこで処理が停止し、60秒後にアプリケーションの動作が再開する。60秒を待たずにアプリケーションが落ちたケースもあった(クラッシュログから SCNetworkReachabilityGetFlags が原因であることが分かっている)。

タイムアウトは実測値で60秒。変更できそうな関数は見当たらない。

このことからSCNetworkReachability系の API を利用する場合は、SCNetworkReachabilityGetFlags は使わず、コールバック関数を使った非同期実装が良いと思われる。


サンプルプログラム NetworkRechable の改良


今回の情報を元に以前作成したサンプルプログラムを改良した。SCNetworkReachabilityGetFlags の使用をやめて SCNetworkReachabilitySetCallback を使った非同期方式に全面的に書き換えた。

インターフェイスはこう。
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

typedef enum {
 kNetworkReachableUninitialization = 0,
 kNetworkReachableNon,
 kNetworkReachableWiFi,
 kNetworkReachableWWAN
} NetworkReachabilityConnectionMode;

#define NetworkReachabilityChangedNotification @"NetworkReachabilityChangedNotification"

@interface NetworkReachability : NSObject {

 SCNetworkReachabilityRef reachability_;
 NetworkReachabilityConnectionMode connectionMode_;
}

+ (NetworkReachability*)networkReachabilityWithHostname:(NSString *)hostname;
- (NetworkReachabilityConnectionMode)connectionMode;
- (NSString*)connectionModeString;

@end
実装はポイントだけ。
// 初期化
- (id)initWithHostname:(NSString*)hostname
{
 if (self = [super init]) {
  reachability_=
  SCNetworkReachabilityCreateWithName(kCFAllocatorDefault,
           [hostname UTF8String]);
  connectionMode_ = kNetworkReachableUninitialization;  
  [self startNotifier_];
 }
 return self;
}

// コールバック設定
- (BOOL)startNotifier_
{
 BOOL ret = NO;
 SCNetworkReachabilityContext context = {0, self, NULL, NULL, NULL};
 if(SCNetworkReachabilitySetCallback(reachability_, ReachabilityCallback_, &context))
 {
  if(SCNetworkReachabilityScheduleWithRunLoop(
             reachability_, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
  {
   ret = YES;
  }
 }
 return ret;
} 

// コールバック先
static void ReachabilityCallback_(SCNetworkReachabilityRef target,
 SCNetworkReachabilityFlags flags, void* info)
{
 NSAutoreleasePool* myPool = [[NSAutoreleasePool alloc] init];
 
 NetworkReachability* noteObject = (NetworkReachability*)info;
 [noteObject updateConnectionModeWithFlags_:flags];
 
 [[NSNotificationCenter defaultCenter]
  postNotificationName:NetworkReachabilityChangedNotification object:noteObject];
 
 [myPool release];
}
内部的に SCNetworkReachabilitySetCallback を使い、コールバックされたタイミングで connectionMode_ を更新する。コールバックは接続状況に変化があったときに呼び出される(例えば、3G→WiFiや、WiFi→OFF)。


利用イメージ


NetworkReachability* networkReachability =
  [NetworkReachability networkReachabilityWithHostname:@"www.google.com"];
NetworkReachabilityConnectionMode mode = [networkReachability connectionMode];
何度も使う場合はインスタンスをとっておいて使い回せば良い。

接続状況が変化した時に通知を受け取りたい場合は NetworkReachabilityChangedNotification を監視すれば良い。
[[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(reachabilityChanged:)
             name:NetworkReachabilityChangedNotification
              object: nil];
- (void) reachabilityChanged: (NSNotification* )note
{
 NSLog(@"changed:%@", note);
 [self updateStatus];
}


備考


初回のタイムラグ

コールバックを使った非同期式の場合、接続状況が変化した時の通知に若干のタイムラグがある(長くはない〜1秒未満)。多くの場合問題ないがアプリケーション起動時の最初の処理でこの接続状況を使う場合は問題になることがある。実際簡単なアプリケーションでは最初の画面が表示された後に最初の変更通知が届くので、最初の画面でネットワークの接続状況を確認してからアクションを起こすという処理の場合このタイムラグが問題になる。非同期処理だから本質的な解決方法は無いので、NetworkReachabilityChangedNotification の到着を待ってから処理を行うか、初回のみ接続状況は無視するような対処が必要になる。先のサンプルではこの接続状況が決まらない状態(すなわち最初の通知が来るでの状態)として kNetworkReachableUninitialization という初期値を設定している。利用側は -[NetworkReachability connectionMode] でこの値が帰ってきた場合、このタイムラグ中であることが判断できるので、それを元になんらかの対処を行う方法が考えられる。

SCNetworkReachabilityCreateWithName()で指定するホスト

SCNetworkReachability インスタンスを作成する時に使う SCNetworkReachabilityCreateWithName 関数へ渡すホスト名は現実に存在してネットワークで到達可能なものを指定する必要がある。試しに dummy.dummy.dummy のような適当なホスト名を渡したところ、常に接続不能状態となってしまった(当たり前といえば当たり前だが...)。



ソースコード


GitHubからどうぞ。
NetworkReachable at 2010-12-13 from xcatsan/iOS-Sample-Code - GitHub



参考情報


SCNetworkReachability Reference

Responses

Leave a Response

人気の投稿(過去 30日間)