MonoTouch製Appが生成したクラッシュログのシンボルを解決する

Appがクラッシュした時に生成されるクラッシュログ。この中身は関数アドレスが書いてあるだけでとても読み取りにくい。本家Appならばオーガナイザでアドレスのシンボル解決を自動的にやってくれるのだが、MonoTouch製Appの場合は手動で行う必要がある。

下準備

シンボル解決コマンドsymbolicatorに対する環境変数が必要なのでホームディレクトリの.bash_profileに以下を追加する

export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer/

そしてコマンドへのパスがとても長い。
Xcode v4.5.2の時点でパスは/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resourcesになる。いちいちこれを入力するのは面倒なので.bash_profileに以下を追加した。

export PATH=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources:$PATH

Xcode v5では/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrashになる。

dSYM情報の準備

symbolicatorはdSYM情報を必要とする。本家Appの場合は~/Library/Developer/Xcode... なディレクトリに自動生成されている。
MonoTouchの場合はビルド出力先に{プロジェクト名}.app.dSYMという名前のフォルダが生成されている。しかし実はこのままでは必要なファイルが足りずシンボル解決できない。これは{プロジェクト名}.appフォルダ以下のファイルをコピーするだけで解決する。

cd {ビルド先}
cp -nR {プロジェクト名}.app/* {プロジェクト名}.app.dSYM

※ビルドするとdSYMフォルダは削除されるのでその度この作業が必要になる。

クラッシュログの取得

オーガナイザでエクスポートしたり、iTunesで同期をしてクラッシュログファイルをMacの方にコピーする。

symbolicatorの実行

symbolicatorを実行する

symbolicator クラッシュログファイル名 {プロジェクト名}.app.dSYM

これでようやくシンボルが解決される。

追記 2014/03/11 14:10

Xcode5でのパスを追記

キーボードのReturnキー(の部分)が押された時の処理

キーボードのReturnキーの部分が押された時の処理はShouldReturnデリゲートを使う。

  • 次のフィールドにカーソルを移動する場合
UITextField text1;
UITextField text2;

text1.ShouldReturn = delegate {
  // BecomeFirstResponderメソッドで移動させる
  text2.BecomeFirstResponder ();
  return true;
};
  • キーボードを閉じたい場合
UITextField text1;
text1.ShouldReturn = delegate {
  // ResignFirstResponderメソッドでレスポンダを解除する
  text1.ResignFirstResponder ();
  return true;
};

NSUserDefaultsへのアクセス

Settings.Bundleで定義された設定を操作してみた。

取得

NSUserDefaults prefs = NSUserDefaults.StandardUserDefaults;
string testValue = prefs.StringForKey("testKey");

設定

NSUserDefaults prefs = NSUserDefaults.StandardUserDefaults;
prefs.SetString("1", "testKey");
prefs.Synchronize(); // この行がないと保存されない

完全初期化(KeyChainの内容も消えてしまう?)

NSUserDefaults prefs = NSUserDefaults.StandardUserDefaults;
prefs.RemovePersistentDomain(NSBundle.MainBundle.BundleIdentifier);

KeyChainを使ってみた

入力されたパスワードを安全に保存するためにKeyChainを使ってみた。
最初保存したレコードを読み出せず苦労したがAccessible属性を設定することで解決した。この属性は必須なのだろうか。

using MonoTouch.Security;

private string LoadPassword (string userCode)
{
  if (string.IsNullOrWhiteSpace (userCode))
    return "";
  using (SecRecord query = CreateSecRecord(userCode)) {
    SecStatusCode res;
    using (SecRecord rec = SecKeyChain.QueryAsRecord (query, out res)) {
      if (res == SecStatusCode.Success && rec.Generic != null) {
        return NSString.FromData (rec.Generic, NSStringEncoding.ASCIIStringEncoding);
      } else {
        return "";
      }
    }
  }
}

private void SavePassword (string userCode, string password)
{
  if (string.IsNullOrWhiteSpace (userCode))
    return;
  RemovePassword (userCode);
  using (SecRecord newRecord  = CreateSecRecord(userCode)) {
    using (NSData pass = NSData.FromString(password)) {
      newRecord.Generic = pass;
      // ↓この行が無いと保存したレコードを読み取ることが出来なかった。
      newRecord.Accessible = SecAccessible.Always;
      SecKeyChain.Add (newRecord);
    }
  }
}

private void RemovePassword (string userCode)
{
  if (string.IsNullOrWhiteSpace (userCode))
    return;
  using (SecRecord query = CreateSecRecord(userCode)) {
    SecKeyChain.Remove (query);
  }
}

private SecRecord CreateSecRecord (string userCode)
{
  // SecKind.GenericPasswordの時、AccountとServiceの2つでもってレコードがユニークになる
  SecRecord rec = new SecRecord (SecKind.GenericPassword);
  rec.Account = userCode.ToLower ();
  rec.Service = "MyService";
  return rec;
}

コンパイル時のワーニングを無視させる

こちらの都合で使わないけど引数を宣言する場合がある。
この時MonoDevelop(というかコンパイラ)が律儀にワーニングを出してくれるができれば無視して欲しい。
で、こんな時はpragmaで制御できるのでその備忘録

// variable declared but not used.
#pragma warning disable 0168
int a;
#pragma warning restore 0168

// variable assigned but not used.
#pragma warning disable 0219
int a = 0;
#pragma warning restore 0219

// private field assigned but not used.
#pragma warning disable 0414
#pragma warning restore 0414

CATextLayerのRetina対応

RetinaなiPadでCATextLayerの文字がぼやけているのに気づいた。
UILabel等はくっきり表示されている。
調べるとCALayerのContentsScaleを適切に変更しなければならなかった。

CATextLayer textLayer = new CATextLayer();
textLayer.ContentsScale = UIScreen.MainScreen.Scale;

これで綺麗に表示されるようになった。

画像をタイル上に並べて描画してみた

画像をタイル上に並べて描画する必要があったのでMonoTouchでコードを書いてみた。
ここでの描画先はCALayer。
アニメーションは必要無かったのでOFFにしてある。

using MonoTouch.CoreAnimation;
using MonoTouch.CoreGraphics;

private CALayer layer;
private CGImage image
private float[] components = new float[]{1.0f};
private CGAffineTransform matrix = new CGAffineTransform (1, 0, 0, 1, 0, 0);

// CGImageを作ったらこのメソッドを呼ぶ
private void Draw()
{
  CATransaction.Begin ();
  CATransaction.AnimationDuration = 0;

  float imageWidth = image.Width;
  float imageHeight = image.Height;
  RectangleF rect = new RectangleF (0, 0, imageWidth, imageHeight);
  using (CGPattern pattern = new CGPattern (rect, matrix, imageWidth, imageHeight, CGPatternTiling.ConstantSpacing, true, DrawTiledImage)) {
    using (CGColorSpace space = CGColorSpace.CreatePattern (null)) {
      using (CGColor color = new CGColor (space, pattern, components)) {
        layer.BackgroundColor = color;
      }
    }
  }

  CATransaction.Commit ();
}

private void DrawTiledImage (CGContext context)
{
  float imageWidth = image.Width;
  float imageHeight = image.Height;
  context.DrawImage (new RectangleF (0, 0, imageWidth, imageHeight), image);
}