Xamarin4から${AppDirectory}マクロが無くなった

Xamarin4にアップグレードしたところビルドしたiOSアプリがまったく動作しなくなってしまった。
調べたらビルド時イベントに使用していたマクロ${AppDirectory}の値がブランクでコマンドラインに渡されるようになっていた。これが原因で必要なファイルが正常な位置に配置されていなかった。

マクロが無くなったのかどうかは不明だが時間がなかったので以下のマクロに置き換えた。

'${TargetDir}/${ProjectName}.app'

とりあえずこれで回避。

Xamarin4はこれ以外にも

  • Unified APIではないプロジェクトがビルドできない
  • StartWWAN()でVPNがオンデマンドしない
  • アプリ起動中に2回目のVPNオンデマンド接続が発生した場合、その後の最初のHttpWebRequestが100%タイムアウトする

とかなりバグが多い。

iOS9でATS関連の詳細ログをXamarin.iOSなプロジェクトで出力する

iOS9で導入されたATS関連の詳細ログをXamarin.iOSなプロジェクトで出力するには
プロジェクトの設定で、Run->GeneralのEnvironment Variablesの所にCFNETWORK_DIAGNOSTICS=3という値を追加すればいい。
f:id:spiratesta:20151106144336p:plain

この状態で実行するとApplication Outputに

2015-11-06 14:37:54.738 TestApp[56042:676213] CFNetwork diagnostics log file created at: ログファイルのフルパス

という出力がでる。
このログファイルに詳細が出力されている。

デバイスの空き容量を取得する

デバイスの空き容量を知りたく検索するとObjective-CのものはたくさんひっかかるがXamarin.iOSのものは見つけられず。
P/Invokeなコードを書かなければならないのか(汗)と思っていたらちゃんとクラスがありました。

エラー処理等を考慮しないでCSなコードにコンバートするとこんな感じに。

using Foundation;

string[] aa = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User, true);
NSFileSystemAttributes ab = NSFileManager.DefaultManager.GetFileSystemAttributes(aa[aa.Length - 1]);
System.Console.WriteLine(string.Format("{0}", ab.FreeSize));  // 空き容量。設定.app->一般->情報->使用可能 の値とほぼいっしょ
System.Console.WriteLine(string.Format("{0}", ab.Size));  // 総容量。設定.app->一般->情報->容量 の値とほぼいっしょ

でもNSSearchPath.GetDirectoriesの部分はEnvironment.GetFolderPathで置き換えられるので

using Foundation;

NSFileSystemAttributes ab = NSFileManager.DefaultManager.GetFileSystemAttributes(Environment.GetFolderPath(Environment.SpecialFolder.Personal));
System.Console.WriteLine(string.Format("{0}", ab.FreeSize));  // 空き容量。
System.Console.WriteLine(string.Format("{0}", ab.Size));  // 総容量。

でもOK。こっちのほうがシンプル。

XmlWriter.WriteRawメソッドでサロゲートペアを含んだ文字列を書き込むと文字化けする

XmlWriter.WriteRawメソッドを使ってXMLを生成するコードがあるのだが、XMLに書き込む文字列にサロゲートペアを含んでいる場合生成されたXMLが文字化けする問題がおきた。
文字化けには法則があり4バイトで構成されたサロゲートペアの文字コード(UTF8)が必ずed a0 bd ef bf bdの6バイトに変わってしまう。
しかしながら書き込む文字列長やサロゲートペアの文字位置によって問題が起きたり起きなかったりで詳しい原因は追いきれなかった。
同じコードを.NETの環境で動かすと文字化けしないのでMonoFrameworkの不具合なんだろう。

回避方法

サロゲートペアな文字を16進数のエンティティ表記(文字列)に置換してから書き込むことで問題が回避できた。
最終的には以下の様なコードになった。

XmlWriter writer = ...;
string srcValue = (サロゲートペアを含んだ文字列);

(略)

char[] chars = srcValue.ToCharArray();
for (int i = 0; i < chars.Length; i++) {
    char ch1 = chars[i];
    if (Char.IsHighSurrogate(ch1))
    {
        if (i == chars.Length - 1)
            throw new InvalidDataException("Invalid surrogate pare. LowSurrogate not found.");
        char ch2 = chars[++i];
        int high = Convert.ToInt32(ch1);
        int low = Convert.ToInt32(ch2);
        if (!Char.IsLowSurrogate(ch2))
            throw new InvalidDataException(string.Format("Invalid surrogate pare. LowSurrogate=[{0:x}]", low));
        long surrogateEntity = (high - 0xd800) * 0x400 + (low - 0xdc00) + 0x10000;
        string surrogateEntityString = string.Format("&#x{0:x};", surrogateEntity);
        writer.WriteRaw(surrogateEntityString);
    }
    else
    {
        writer.WriteString(ch1.ToString());
    }
}

別のバグ

MonoFrameworkにはエンティティ表記に置換してくれるXmlWriter.WriteSurrogateCharEntityというメソッドがあるのだが、Unicode6.0のコード領域に対応していないのか文字によっては上位サロゲートの引数が範囲外だという例外が発生してまともに使えなかった(もちろん.NET環境では問題ない)。
なのでMSDNにあった計算式を元にコーディングした。

Mac版のXamarin.Studioでディレクトリのシンボリックリンクを経由したファイルをリンク形式でプロジェクトに参加させると使い物にならない

Xamarin.Studioには共有プロジェクトといったコード共有機能が備わっているが、ソースコード管理の都合でどうしてもシンボリックリンクを経由した上でソースコードをプロジェクトに追加させなければならなくなってしまった。
追加自体は問題なくビルドもデプロイも問題ない。
しかしソースコード解析機能がまったく機能しないことが判った。
シンタックスハイライト、FindReferences、Go to Declaration、インテリセンスといった編集時に当たり前のように使えている機能が全て使えないので非常にストレスになる。
調べるとv4の頃からの不具合のようでv5.6.3(入手時AlphaChannel)でも改善していなかった。
Bugzillaには同様の内容でいくつか登録されているがほったらかしの模様。おそらく早急な改善はしてくれないんだろう。

シンボリックリンクではなくハードリンクも検討したがMacではかなり裏技的扱いのようで躊躇。
色々試行錯誤した結果osxfuse+bindfsの組み合わせでディレクトリをマウントさせるという方式に行き着いた。
これはこれで再マウント等の問題があるが簡単なスクリプトを書いて対応した。

ニュートラルカルチャ名から任意の特定のカルチャ名を得る

ニュートラルカルチャ名から任意の特定のカルチャ名を得るにはCultureInfo.TextInfo.CultureNameを見ればよい。
例えば英語(en)ならアメリカ英語(en-US)、日本(ja)なら日本語(ja-JP)を取得するというもの。

The CultureName property always reflects a specific culture rather than a neutral culture. If CultureInfo.Name has a neutral culture as its value, then the corresponding CultureName has as its value an arbitrary specific culture that uses the same language.

http://msdn.microsoft.com/en-us/library/system.globalization.textinfo.culturename%28v=vs.100%29.aspx

この機能がMono Frameworkではまだ実装されていなかった。

System.Diagnostics.Debug.WriteLine((new System.Globalization.CultureInfo("en")).TextInfo.CultureName);
// -> "en-US"

System.Console.WriteLine((new System.Globalization.CultureInfo("en")).TextInfo.CultureName);
// -> System.ArgumentException: Region ID 9 (0x0009) is not a supported region.

以前、Mono FrameworkでニュートラルカルチャなCultureInfoクラスはインスタンス化できなかったが最近になってできるようになった。しかしまだ対応できていない所があるようだ。

Xamarin.iOSでInstrumentsを使わずにアプリの使用メモリ量を取得する

アプリの使っているメモリ量はInstrumentsを使うと詳しく調査できるのだが、
パフォーマンスが悪かったりコードの特定の地点での値を得るのが難しい。
こういう場合本家ではtask_info()を使用してtask_basic_info構造体を取得すればよいのだが、Xamarin.iOSではまだバイディングされていないようなので自前でバイディングしてみた。

using System;
using System.Runtime.InteropServices;

static class Diag
{
  public const int TASK_BASIC_INFO = 4;

  public struct TimeValue
  {
    public int Seconds;
    public int Microseconds;
  }

  public struct TaskBasicInfo
  {
    public int SuspendCount;
    public uint VirtualSize;
    public uint ResidentSize;
    public TimeValue UserTime;
    public TimeValue SystemTime;
    public int Policy;
  }

  [DllImport("/usr/lib/system/libsystem_kernel.dylib", CallingConvention = CallingConvention.Cdecl)]
  public static extern uint mach_task_self();

  [DllImport("/usr/lib/system/libsystem_kernel.dylib", CallingConvention = CallingConvention.Cdecl)]
  public static extern int task_info(uint targetTaskID, int flavor, ref TaskBasicInfo taskInfo, ref int size);

  public static TaskBasicInfo GetTaskInfo()
  {
    TaskBasicInfo taskInfo = new TaskBasicInfo();
    uint taskid = mach_task_self();
    int size = Marshal.SizeOf(typeof(TaskBasicInfo));
    task_info(taskid, TASK_BASIC_INFO, ref taskInfo, ref size);
    return taskInfo;
  }
}

※このコードではTaskBasicInfo.UserTimeとTaskBasicInfo.SystemTimeが取得できない。おそらくどこか間違っているんだろう。