2014年1月29日水曜日

iOSシミュレーターのUITextView/UItextFiledの入力バグ

シミュレーター上でのUITextView/UITextFiledではMacのキーボードから日本語を含め入力できるが、たまに出来なくなることがある。

シミュレーターで
 ハードウエア〜デバイス
で機器を切り替えると治ったが、ようやくもっと根本的に原因がわかった。

Commandキーが入力状態でロックされている。
なので、入力できなくなった状態でAを押すとCommand-Aとみなされ、全選択になる。
治すには、Commandキーを押せばよい。

入力中にCommand+<-/->で画面回転させると、発生する。
それ以外にも、MissionControlでのキー操作でも発生するので、
MacOS側でCommandを使うキー操作をすると軒並みダメになるみたい。

私はシステム環境設定のキーボードではControlとCommandを入れ替えているが、
それが影響している可能性はある。
いずれにせよシミュレーターのバグである。

追記:
Commandキーを押しても治らないこともある様子。その場合はデバイス切り替えをするか、シミュレーターを再起動するしかないかもしれない。

2014年1月22日水曜日

Block構文の罠

先日、ブロック構文で引っかかったので覚え書き。

たとえば、 イカのようなブロック構文を引数に保つメソッドを実装したとする。

- (void)blockTest:(id)arg
                       onSuccess:(void(^)(void))successBlock
                       onFailure:(void(^)(NSInteger errCode, NSError* error))failureBlock

この処理の中で通信とか遅い処理を行う。成功すればonSuccessが実行され、失敗すればonFailureが実行される、と言う作りである。

この時、

-(void)method
{
    [〜 blockTest:@"arg"
        onSuccess:^(void) {
            // 成功時処理
        }
        onFailure:^(NSInteger errCode, NSError *error) {
            // エラー処理
        }
    ];
    // 素通り
}
   
という処理はどう走るか。

実は、まず素通りし、methodからも抜けてしまう。
しかるのち、blockTestが実際に終了した時に、その結果に応じてonSuccessまたはonFailureのいずれか「だけ」が走る。
その時には素通りの部分は実行されない。

すなわち、(見かけ上)一旦抜けたメソッド内に、後から、非同期にまた戻ってくるということになる。
これは通常のCのプログラムでは考えられない動作なので理解が難しい。
(一旦抜けた関数の、さらにその中の一部の処理だけに戻ってくるというのはありえない。)

このブロック文はデリゲートの代わりだからこういうことになる。
なまじブロックでメソッド内に記述するからややこしくなるわけである。

ブロックは便利な記述方法ではあるが、プログラムの動きで言えば、記述の流れと処理の流れが一致せずわかりにくくなるので、
注意が必要である。

2014年1月15日水曜日

iOS7のUITextViewのバグ(その2)

iOS7のUITextViewには前にも書いたとおり、非常にたくさんのバグが存在するが、
またバグを発見してしまった。これは表に出てこないのでちょっとわかりにくいバグ。

 (1)入力状態にある間中、メモリ利用量が増加し続ける
64バイトずつメモリが確保され続けている。
だいたいではあるが13KB/分くらいの増加量。
調べると、libdispatch.dylibが_dispatch_continueation_alloc_from_heapを発行し続けているらしいが、
詳細は不明。

終了すると一括開放されるのでリークとはならない。



(2)メモリリークもある模様
しかし、別のところでメモリリークがある。内部で呼び出されていると思われる、NSUndoManagerというものが、メモリを開放しないで終了している。
1回あたりは少量だが、メモリリークが検出されること自体余り良い気分ではない。

なにはともあれ、iOS7のUITextViewはバグが多すぎて困る。一から作り直したりするからこういうことになるのだ。 従来版も残しながら新版をリリースし、以降を推奨しながら、バグが枯れた頃に旧版を廃止するのが普通ではないかと思うのだが、アップルには世間の常識は通用しないからなぁ。


おまけ
(3)UIDatePickerを回し続けると急激にメモリ利用量が増加する
ただし、止めて一定時間立つと開放される様子。
メモリ残り容量が少ない時にUIDatePickerを 動かすとメモリ不足で落ちたりするかもしれない。

2013年12月16日月曜日

MacOSでのファイルを開くアプリケーションの設定

MacOSでのファイルを開くアプリケーションの設定の覚書。
Windowsでは拡張子別にしか開くアプリケーションの設定は付けられない(と思う)が、Macではファイル単位でもできるし、拡張子単位でも設定できる。

まずファイル単位。ファイルを選択状態にしてマウス右クリック。
「このアプリケーションで開く」で「その他」を選ぶと開くアプリケーションを選択できるので、「常にこのアプリケーションで開く」にチェックを入れると、以降、常にこれで開くようになる。



拡張子別の場合。ファイルを選択し、右クリックから「情報を見る」を選択。
「このアプリケーションで開く」で設定し、「すべてを変更」を押すと、同じ拡張子のものはすべてこれで開くようになる。


2013年12月13日金曜日

Mac用LibreOfficeとApache OpenOfficeについて

ことMac版で、Calcについてのみ言えば、LibreOffice(v4.1.3.2)よりApache OpenOffice(v4.0.1)の方が出来が良い。

LibreOfficeのダメな点は以下の3つ。
・入力中の文字がずれるというか、変な空白が入ってしまう。空白が入るだけでなく、カーソル位置がずれるので文字列編集がものすごくしにくい。
上がLibreOfficeで下がOpenOffice。

・保存のたびに図形の位置がずれたりサイズが変わったりする(変形する)
こちらのほうが致命的。毎回直さなくてはいけない。というか、直しても無駄という話もある。

・デフォルトの図形のアンカーがセルでない
 だから、行の挿入・削除・幅変更をすると図形がずれる。

 ExcelとOpenOfficeはセルアンカー。絶対そのほうが良い。


でもOpenOfficeにもバグがある。
ツールバーのカスタマイズに追加しようとすると、表示が真っ黒になってしまう。
追加からドラッグでツールバーの内容のところに入れようとするとこうなる。
一応入れることは可能で、スクロールすると表示は戻るのだが、ものすごく面倒。

あと、枠線が勝手に変更されてしまう(太くなる)というバグもいる模様。隣接するセル(例えば左右)で両方から枠線設定をするとそうなるような。片方で罫線を解除しても消えないという、ほとんどバグとも言える仕様もある。これは改善してほしい。

・・・

LibreOfficeで知りたいことをまとめておく。上記のバグが直り、これらの設定法がわかればだいぶ使いやすくなるのに。

Calcのデフォルト設定の変更
 図形のアンカーの対象をセルにする
 外部ツールでの編集のツールを変更する

あと、ウインドウの分割が「ウインドウ〜固定〜組み合わせ解除」というわけのわからん方法じゃなくて、素直に固定だけか分割ってしてほしい。

ということで、私はApache OpenOfficeをおすすめ、ということで。

2013年12月9日月曜日

Xcode5;謎のSecurityエラー

Xcode5で(実機をつないで)アプリを実行しようとしたら、こんなエラーが出て実行できなかった。

一度アプリを削除しても、何度やってもダメなのだが、ホーム画面上にはアイコンが出来るのでとりあえずそれを実行してみたら、なんかメッセージが出た後に(はっきり読まなかったので失念)、実行できた。

調べてみると、
http://stackoverflow.com/questions/19112794/xcode-5-could-not-launch-only-reports-security-as-error
の現象が同じではないかと思われた。上記画像もそれから拝借したもの。

どうやら、何らかの原因でこれが発生した時は、開発者が信頼出来ないので起動できないと言っているようなので、ホームに出来たアイコンを手動で起動しなければならないらしい。

実は今2台の開発機で開発していて、いつも使っているのとは違う機械で実行しようとしたらこうなった。2台は証明書などは同じなのだが、何かまだ違う部分があるのかもしれない。

また今度発生したらもう少し調べてみたい・・・とも思うが、いろいろと「あれ」なのでできないかもしれない。

2013年11月29日金曜日

Xcode5上でiOS5のシミュレーターがインストールできたりできなかったりする理由

Xcode5がいくつかのMacに入れてあるのだけど、そのDownloadsにiOS5シミュレーターが出てきたり出てこなかったりして「なぜ?」となっていた。

X-BASIC for iOSもまだiOS5対応をしている(つもり)なので、入れたい。

で、いろいろ調べて分かった、その理由。

「OS X 10.9上のXcode5ではiOS5シミュレーターは新ストールできない」ということだった。
10.8上でならインストールできる。
また、Xcode4.6.xでもインストールできる。

ということで、iOS5でも開発が必要なら、10.8環境を残すか、Xcode4.6.xを残すしかない。iOS7までの開発を一本化したいなら、10.8を残す以外の選択肢はない。

Xcodeのキーバインド設定ファイルの位置

Xcodeの環境を別マシンに移植する際、キーバインド設定ファイルのコピーに手間取ったので覚書。

その設定ファイルは
/Users/ユーザー名/Library/Developer/Xcode/UserData/KeyBindings
の下にある、*.idekeybindingsというファイルに入っている。
*は自分の使っているバインド設定名。

Xcodeを閉じた状態でこれを別のマシンの同ディレクトリにコピーし、Xcode上で設定すれば移植できる。

LibreCalcのデフォルトフォントの変更方法

LibreCalcでデフォルトのフォントの変更に手間取ったので覚書。
2ボタンマウスの利用を前提として書いているので、1ボタンの人は適当に読み替えて。

(1)メニューバーから書式を選択

(2)スタイルと書式設定を選択
 (3)これが出てくるので標準で右クリック
 (4)変更を選択
 (5)フォントを選択し、変更する。


以後、これに従って作成される。

iOSシミュレーターでSafariが起動しなくなったりした時の解決法・・・じゃなくて原因

Xcode上でプログラムを作っていて、iOSシミュレーターで動作させようとするのに、
Safariが起動しなくなった。
具体的には、このようなコードを実行してもSafariが起動しない。

NSURL *url=[NSURL URLWithString:@"http://www.blogger.com"];
// httpがあるので自動的にブラウザが立ち上がる
if ([[UIApplication sharedApplication]canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url];
}

最初はプログラムやURLを疑ったのだけどどうにもならなくて、結局iOSシミュレーターで「コンテンツと設定のリセット」をすることで治った。

でもその原因は何だったのか。

調べた結果これだった。

(1)iOSシミュレーター(正確にはXcode)は以下の場所にプログラムの実行コードを作成する
/Users/ユーザー名/Library/Application Support/iPhone Simulator/バージョン/Applications/
バージョンはiOS7.0.3用なら7.0.3になる。
この下にアルファベットと数字とハイフンからなるディレクトリがいくつか作られている。
なお、このディレクトリは、Buildだけでは作成されず、Run後、実際にシミュレーター上にプログラムが起動する直前に初めて作成される。

(2)このディレクトリの内、少なくとも1つはユーザーの作ったアプリケーションであるが、他に6つほど、見に覚えのないディレクトリが存在する。その内容は、以下のとおり。
    Web.app
    StoreKitUIService.app
    WebViewService.app
    MobileSafari.app
    WebContentAnalysisUI.app
    DDActionsService.app

(3)見知らぬアプリのフォルダなので、「消してしまえ」とすると、以後動作がおかしくなる。

という次第である。今回の場合、MobileSafari.appを消してしまったので上記現象が発生したと。

アプリ内で作る中間ファイルなどを確認するために、アプリのフォルダの中を見ることはよくあるのだけど、他のが必要なんて今まで気づかなかった。

・・・と言うか、このフォルダ構成、iOS7.0以降でこうなった気がする。
6.xや5.xの下にはこういうのは出来ていない。

ということで、「フォルダはむやみに消しちゃダメ」というお話。

2013年11月6日水曜日

Mavericksのバグ

Mavericks、どうやらぱっと見ではないけど、深いところにバグが多数いる模様。
とりあえず情報集約開始。

(1)WDの外付けHDDを付けていると、その内容を消してしまう可能性があるらしい。
WDからも直接警告のメイルが来た。うちもまさにそれに相当するので、まだアップデートしてないMacはアップデートを当面しない、すでにMavericksにした機体は当面起動しないことに決定。

(2)ネットワークで共有したWindows7管理下のHDDに大量(GB越え)のファイルを書き込んでいると完全にハングアップしてしまう(電源の強制OFFしかなくなる)。MoutainLionでは同じ作業しても問題なかった。

(3)どうやら動作中にかなり頻繁にネットワークにアクセスする模様。会社ではProxyサーバー経由でネットワークにつながれているのだけど、そのパスワード要求が頻繁に出てくるので気がついた。MoutainLionでは「全く」出てこない。

(4)キーチェーンのパスワードを要求してくるのもバグかもしれない
   (3)とあわせて、もうどうしようもなくなったので、MountainLionに戻した。

(5)USB-ディスプレイでディスプレイを接続すると、その上でのマウスカーソル移動が異常に遅くなる。もちろんこれはドライバ側の問題の可能性もある。また、USBディスプレイをつないでいる状態ではMissionControlによる複数画面切り替えが正常動作しない。正確には、画面は切り替えられるが、ウインドウの画面間の移動を行えない。ウインドウが表示されないから。

2013年10月24日木曜日

Macのシリアル番号の調べ方

iWorkの件でアップルに電話すると、インストールしたMac本体の情報を求められる。

そこで、教えてもらった、本体シリアル番号の調べ方。

(1)メニューバー(タスクバーだっけ?)からリンゴマーク~このMacについてを選ぶ

(2)「バージョン」と書かれたあたりをクリック すると、ビルド番号が表示される



(3)「ビルド」と書かれたあたりをクリック すると、シリアル番号が表示される


以下、この繰り返しとなる。

2013年10月23日水曜日

OS X Mavericks上でのiWorkの'09からのアップデートについて

OS X Mavericksにアップデートすると、iWorkの既存ユーザーは無料で最新版にアップデートできる、という告知がされている。

しかし、実際MacAppSoreでKeynotes/Pages/Numbers見るとそれぞれ¥2000と表示されていて、無料でアップデートできるように見えない。

アップルとのやりとりの結果、どうもMac mini2010プリインストールのは対象外らしい。
 「それならそうと書いておけ!!」大損失。
一転、「やっぱり無償だった」という連絡が来た。
返金手続きを行った。


実際に「Appを購入」を押すと、「(古いバージョン)がインストール済みであるため、Mac App Storeからの購入は行われませんでした」と表示され、ここで「購入する」を押すと、課金されずにインストール可能である。

現在はやらない方が良い。課金されてしまう。
問題が解決されてから行うこと!!
プリインストールモデルで無ければ、コメントにもいただきましたが、英語環境に変更してから行うとすんなり通るらしいです。

2013/11/05追記
「無料」と表示されるようになっていることを確認。
2013/10/24追記
こんなメッセージがくるのに、iTunes Storeからは料金が引き落とされたとのメイルがきていた。いったいどっちが本当なのか現在確認中。

2013/10/24確認
この課金に関しては、iTunes Store側の問題であるということらしく、「取り消される」という見解をいただいた。なので、アップルのサポートに電話して、この件に関して伝え、向こうの言うとおりに作業すれば課金取り消しされる「らしい」(まだ本当に処理されたかまでは確認できていない)。
この際、インストールしたMac本体の情報(シリアル番号など)と、受領メイルにある注文番号、およびインターネットにつながる環境(アップルのサポートHPにつなぐため)が必要となる。

2013/10/26追記
どうやらこの件で相当の問い合わせが来ているらしく、時間をくれとの返答。

2013/10/30追記
上記の通りで、課金対象になることもあると判明。

2013/10/31追記
やっぱり無償という連絡が来た。と言うことで訳がわからん状態。
なので、
(1)英語環境でやってみて、すんなり行ければそれでよし。
(2)プリインストール版の場合は、やる前にアップルに問い合わせた方が良いかもしれない
という感じ。


iLife(iMovie/iPhoto/GarageBand)に関してはこんなことは無い。

なお、インストールしてもiWork'09や旧バージョンのiMovie/iPhoto/GarageBandもフォルダを作って残されるので、互換性に問題があるファイルの場合はそのまま使い続けるのもありだろう。

・・・

そういえば、Mavericks。この上で正常に動かないアプリは、今のところ手持ちの中では
  DIM2.0.2:アイコンの位置を保存・復帰するフリーウエア
      アイコン位置は復帰するが保存できない(パネルが開かない)
 DoubleBoth:マウスカーソルの位置をわかりやすく表示するフリーウエア
     表示され方がおかしい(が一応使える)
だけで、Xcode4.6.3も動作している(他は最新版で確認)。

Xcode5.0.1は、MoutainLion上でアップデートしていても、Mavericks上でも再度アップデートを求められた。ただしインストール時間は大幅に短かったので、何か少しだけ違うのかもしれない。

ただ、マルチモニタにしていると、OS(設定)上は認識されているのに表示が出ないという問題が発生した。再起動したら治ったけど、そのあたりにはまだ何かいるのかもしれない。

それでも、MoutainLionが最初問題ありまくりだったことに比べると、格段に安定しているように思う。なんか応答速度も早くなっているし、良さげじゃないんだろうか。

2013年10月2日水曜日

iOS7やXcode5のバグとかiOS6との違いとか(随時追加)

X-BASIC for iOSのiOS7対応を始めて、iOS6との違いとかバグとかが見えてきたので、
ここに覚書をしていく。
発見した時点のものを書くので、最新バージョンのiOS7(もしくはSDK7)でどうかは、特に調べたもの以外は未確認。

・・・

(1)NSMutableArrayにUILabelを入れ、
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
    return [lblArray objectAtIndex:row];
}
とすると、選択項目(中央)の表示が抜けてしまう。

{
    UILabel *lbl=[lblArray objectAtIndex:row];
    if (getSystemVersion()>=7.0) {
        // iOS7上では一旦別のUILabelにコピーしないと選択中項目の表示が抜けてしまう
        // iOS7上でSDK6までアプリを動かす場合も同様になる
        UILabel *label = [[[UILabel alloc] initWithFrame:lbl.frame]autorelease];
        label.backgroundColor = [UIColor clearColor];
        label.font = lbl.font;
        label.text = lbl.text;
        return label;
    }
    // iOS6以前ではこれでOK
    return (lbl);
}
とするとうまくいく。.font/.text..back〜Colorの設定が肝ではなく、UILabelを再確保するのが肝(やればわかる)。
ただし、項目別に高さが変わるように設定している場合、幅が極端に変わるとき、表示上欠けてしまうことがある(Zapfinoフォントなど)。

(2)UITextView.contentSizeの返り値が異なる。
iOS6では設定されている内容の高さ(スクロールを伴なう場合は、表示領域だけじゃなく全体の表示高さ)を返すが、iOS7では意味不明の値を返してきている(UITextView.frame.sizeとも微妙に違う)。
このため、contentSizeで実表示行高さを得ている処理はすべからく動かなくなる。
 以下のコードでとりあえず回避可能だが、iOS6の値とは同じではない。
-(CGSize)getContentSize:(UITextView *)myTextView
{
// getSystemVersion()はiOSのバージョンをfloatで返す関数とする
    if (getSystemVersion()>=7.0) {
        // FLT_MAX : float最大値
        return [myTextView sizeThatFits:CGSizeMake(myTextView.frame.size.width, FLT_MAX)];
    }
    // iOS6以前
    return myTextView.contentSize;
}

更に困った事に、これで得られた各行の高さや位置を.contentOffsetに設定しても、正確にはその位置にスクロールされない(微妙にずれる)。補正係数が必要である。

これも、UITextViewが従来のWebKit派生から、完全独立したせいだと思うが、互換性は保ってほしい。


(3)Search BarのBar Style(iOS6ではStyle)のBlack Translucentの表示が違う
iOS7ではdeprecated=廃止予定になってるので、defaultにする。

(4)Xcode5上のiOS6シミュレーターの挙動がXcode4上それと異なるので、
iOS6での挙動を正確に調べるにはXcode5は使えない。
両方の環境は共存できるが、シミュレーターは同時に両方起動できないので、
それぞれでの実行時に一度終了させる必要がある。

 XcodeのバージョンとiOSのバージョンの組み合わせによる動作結果はこんな感じ。

Xcode4で作ったiOS6用オブジェクトをiOS6で動かす
 →普通
Xcode4で作ったiOS6用オブジェクトをiOS7で動かす
 →iOS6互換モード
 iOS6上とほぼ同じ動作&表示になるけど違う部分も多少ある(上記1など)
 実機でのみ可能(シミュレーター上でも無理やりやれば出来るのだけど、面倒)
  X-BASIC for iOS v2.7はこれで正常動作することを確認した。

Xcode5で作ったiOS6用オブジェクトをiOS6で動かす
 →動く。問題ないように見える。
  実機でのみ可能
Xcode5で作ったiOS6用オブジェクトをiOS7で動かす
 →iOS6互換モードとは異なる結果になる事がある

Xcode5で作った32bit iOS7用オブジェクトをiOS7で動かす
 →iOS7 32bitモード
 表示や動作がiOS6と大幅に異なるのでプログラムの修正が極めて面倒。
 でもそれとなく動作はする。

Xcode5で作った64bit iOS7用オブジェクトをiOS7 64bitで動かす
 →iOS7 64bitモード
 動作が大幅に異なるので動かない(X-BASICの場合)。


(4)シミュレーターで「Appをインストールできませんでした」となることがある
 一旦アプリを削除すると直るが、設定なども初期化されるのでとても面倒。
 発生原因は特定できず。iOSのバージョンを切り替えた時に発生しやすいが、発生しないこともある。
 Xcode5のバグと思われる。5.0.1/2では発生頻度は下がったが完治はしていない。


(5)「SpringBoardがAppを起動できません」となることがある
 シミュレーターを再起動すると直る。
 発生原因は特定できず。
 Xcode5のバグと思われる。5.0.1では発生していない。5.0.2ではまた発生するようになった。

(6)バグじゃないけど、Xcode5で編集したxibはXcode4で編集できなくなってしまう
確認した限りxibがそうなる。
なので、Xcode4/5を共存させる場合でも同じソースを両方で編集してはいけない。
現状、Xcode5上のiOS6用環境が信用出来ないので、この点は要注意。
(戻せなくなってえらく苦労した。)

(7)UITextViewでキーボードを表示→消去したあと、下になった部分の表示が欠ける(戻らない)ことがある。
キーボード表示前後でUITextViewの表示サイズを変更するとこうなるみたい。
(キーボードに重ならないようにサイズを変更するなど。)
OS内部での再描画エリアの計算を間違っていると思われる。
setNeedDisplayをかけても再表示されない。
さんざん手を尽くして見つけた回避方法は、 強制的にスクロールをさせること。
「例」
        CGPoint ofst=textview.contentOffset;
        ofst.y++;
        textview.contentOffset=ofst;
        ofst.y--;
        textview.contentOffset=ofst;
iOS7のバグ。7.0.3でも治っていない。iOS6まででは発生しない。
また、UIWebViewでは発生しない(iOS6までで発生しないのはこのため)。

(8)UIScrollView.scrollsToTopのデフォルト値がNOになってる
iOS6はYESなので、ステータスバーのところをタップすると先頭へスクロールしたが、
iOS7はステータスバーを見かけ上一体にしたせいかNOになっており、
強制的にYESにしないとスクロールしない。

(9)xib内にレイアウトしているActivity indicatorの表示座標が画面外になってしまう
Xcode4→Xcode5での変換に失敗していると思われる。
多分レイアウトしているviewの高さの座標が入れられている。
IB上で座標を再設定するか、表示座標は プログラム的に設定するようにする。

それ以前にActivitor indicatorがIB上で表示されないような・・・。

(10)iPod touch/iPhoneでpushViewControllerで画面を表示したとき、
Viewが上がって=NavigatioBarに重なって表示されてしまう
viewDidloadにて
    self.edgeForExtendedLayout=UIRectEdgeNone;
を発行すれば良い。
NaviのないiPadのpickerViewの中では不要。

(11)ナビゲーションバーとステータスバーが重なってしまう
iOS 6/7 Deltasで補正するとか、そもそもnavigation barの置き方が悪いとかちまたにいろいろと情報があるけど、うまくいかない。
強制的に表示位置を変えて重ならないようには出来るが、それをすると下に来るUIViewのサイズが「なぜか」ステータスバー分狭くなる。
StoryBoardを使わない画面遷移は考慮されてないんじゃないか?と思っている。

→いろいろやった結果、 統括しているUIViewのbackgroundColorをclearColorにし、Naviバーの表示位置を下げ、その下に来るUIViewのサイズを調整して同等の画面を作り出せた。

UIView----------------------------Status Bar=default,Background=clearcolor
  Navigation Bar----------------viewDidLoadでこのframe.origin.y+=20する
  UIView---------------------------同上
     いろいろな表示コントロール-------ここは基本的に調整不要


(12)UIBarButtonItemに動的に画像を入れた場合、正しく表示されない
btnBreak.image=[UIImage imageWithContentsOfFile:〜]
解決手段は見つからず。X-BASICでは結局画像をやめて文字にしてしまった。
(identiferが簡単に変更できれば良かったのだが。)
.enable=YES/NOとか、UINavigationItemあたりは表示結果がiOS6とは大幅に異なるのでかなりの変更が必要そう。

(13)Xcode5のエディター上でUndoした時、表示上は元に戻っているのに内部的に戻ってないことがある。
謎のコンパイルエラーが出るので調べたらこれだった。
ソースをちょっといじれば正しくなる。
Xcode5.0のバグ。5.0.1でも頻発。5.0.2でも発生を確認。

(14)UIWebViewに対してstringByEvaluatingJavaScriptFromString:でJavascriptを実行するとき、その対象となるHTMLが読み込み終わってないと、呼び出し後に表示が消えてしまう。
たとえば、
    [web reload]; // [web loadHTMLstring:~]でも同じ
    [web stringByEvaluatingJavaScriptFromString:@"~"];
とすると、表示が消える。
だからといって、
 while (web.loading) {
    NSDate *dt=[NSDate dateWithTimeIntervalSinceNow:1.0]
    [[NSRunloop currentRunLoop]runUntilDate:dt]
  }
とかして読み込み終了を待っても同じだった。.loading自体が正しい状態を返してないのでは?とも思っている。

(15)同一フォントでもサイズが異なる
iOS6と7では、同一名フォントでも大きさが微妙に異なることがある。
このため、フォントサイズに厳密に依存しているアプリは表示位置の調整が必要になる。
X-BASICではファンクションキーの表示位置が違ってくる。

(16)UITextView:styleStringが使えなくなった
iOS6までは非公開APIのstyleStringで行の表示状態を強制変更できたが、iOS7ではこれが呼び出されなくなったので効かない。
X-BASICでは、これを使えばUITextViewの高さが得られない問題を回避できるかと思ったが、どうやってもうまくいかないので調べたらこのことに気がついた。
ただし、そもそも styleStringを使っていると拒絶されるという話もあるので使わないで良かったのかも。
→UITextViewはiOS6まではWebKitから派生されていたがiOS7では完全に分離されたらしいので、この辺りは使えなくなったようである。attributedTextと使えということだろう。

(17)UIWebView.backgroundColorの設定が無視される
IB上でも設定可能なのに、表示すると色が出ない。常に透明になっている気がする。
設定があるということは反映されるべきで、間違いなくiOS7のバグ。

(18)UIWebViewとUITextViewを表示面で同調させてた処理は、ことごとくだめになる
何度も書くとおり、iOS6まではこの2つはWebKitを使っていたが、iOS7では後者は分離されたため、表示結果もことごとく異なる。フォントサイズの指定結果とか。UIWebViewをテキスト表示に使ってたような処理は、UITextViewにattributedTextで渡すように作り直す必要があるかもしれない
(X-BASIC for iOSはそうした)。
iOS7対応のためにiOS6以前を切り捨てるアプリがあるのは、こういう違いが大きく、双方をサポートするのが面倒なためだと思う。

(19)UITextViewでズームしなくなる
これは、スクロール対象ビューを返すviewForZoomingInScrollViewデリゲートで返すべき値が変わるから。iOS6ではUIWebDocumentViewだったが、iOS7では_UITextContainerViewに変わっている。以下のようにして回避。

-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scview
// UIScrollViewのデリゲート:ズーム対象のUIViewを返す
{
  NSString *zoomSubviewDescription;
  if (getSystemVersion()>=7.0) {
    zoomSubviewDescription=@"<<_UITextContainerView"; // <_なので注意
  } else {
    zoomSubviewDescription=@"<UIWebDocumentView";
  }
  if (scview==対象view) {
    // 返すView:editorの中のスクロール対象UIView
    for (id subview in scview.subviews) { // subviewsはUIViewのプロパティ
        //NSLog(@"subView=%@",[subview description]);
        if ([[subview description]hasPrefix:zoomSubviewDescription]) {
        //NSLog(@"リターンsubview=%@",subview);
        return subview;
        }
    }
  }
  return nil;
}

(20)UITextViewでテキストが途中で切れてしまい、それ以降表示されないし、スクロールもできなくなる
X-BASIC for iOSで最後まで悩まされたバグ。
1ヶ月以上かかって、http://stackoverflow.com/questions/18859637/setting-uitextview-frame-to-content-size-no-longer-works-in-xcode-5 を参考に(これだけじゃだめ)、ようやく以下で回避可能になった。

-(void)setAttributedText:(UITextView *)textView text:(NSAttributedString *)atext
{
  // 一旦スクロールをOFFにする
  [textView setScrollEnabled:NO];
  // テキスト設定
  textView.attributedText=atext;
  // サイズを再設定
  [textView sizeThatFits:textView.frame.size];
  // スクロールをONに戻す
  [textView setScrollEnabled:YES];
  // 強制スクロールをさせて再描画をかける;これも必要
  CGPoint pt=textView.contentOffset;
  pt.y++;
  textView.contentOffset=pt;
  pt.y--;
  textView.contentOffset=pt;

}

(21)XcodeのIBでUser Defined Runtime Attributesを設定すると・・・
(21−1)Key Pathに入力しても消えてしまうことがある
 入力→フォーカスを他に移動→再度入力でようやく入る
(21-2)入力を続けていると、高確率でXcodeがハングアップする
 複数の値を設定するときは、1つ入れてはビルド、を行うと回避できる
拡張したクラスへの値を設定するにはとても便利な機能なのだけど、このバグのせいで使い勝手が悪くなっているという残念さ。

(22)Xcodeのエディタ上で、半角カナ+半・濁点文字を入力すると、2つは1文字として扱われてしまう
メモ帳でも発生する。Xcode4以前ではどうだったかは不明だが多分同じだろう。
TextWranglerなどのテキストエディタ(でSHIFT-JISで処理している場合)では問題ない。
めったに出てこないとは思うけど、半角カナを扱うプログラムを書く場合は要注意。


(23)Mavericks上では、iOS5シミュレーターがインストール出来ない
これはバグと言うより仕様。10.8上のXcode5でならインストールできる。
iOS5向け開発するならMavericksは入れない方が吉。

(24)Xcode5の64bitシミュレーター上で、UITextFieldなどで物理キーボードからの入力ができない
32ビットシミュレーター上では物理キーボードから(漢字も含め)入力ができるので、効率が非常に良いが、64ビット上ではこれが「なぜか」できない。
最初、プログラムのバグではないかと疑ったが、シミュレーターのバグだった。
5.0.2で確認。
→32ビットシミュレーターでも発生することがあると判明。どうやったらそうなるのか全くわからない。
設定も見つからないし、あったとしてもいじった記憶は全くないのだが。


(25)シミュレーター上で実行中、HOMEダブルタップでタスク選択に入って、実行中アプリを削除(これでXcode上でも実行が止まる)、もう一度XcodeからRUNすると、画面が表示されない。
内部的には動いているつもりになっているようだが、画面が一切表示されない。
(動いているつもりなので、起動できないエラーは発生しない。)
こうなったら、一度シミュレーターを終了して、再度Xcode上からRUNするしかない。


・・・おまけ:iOS6のバグ
・UITextViewに入れるテキスト中に連続するスペースがあった場合、まとめられてしまう
 たとえば、スペース4つが2つになる。いったいいくつがいくつにまとめられるのか、規則性はわかっていない。iOS7では同様の現象は起こらない。
おそらく、iOS6までのUITextViewはWebKitを使っていることに起因していると思われる。

・・・

とにもかくにもiOS7は変わりすぎてて困る。UITextViewは同じメソッドやプロパティーで結果が異なるので特に注意が必要。これだけ異なるなら別ものにすれば良かったのに。


フラットデザインは、どこがボタンがわからなくなって操作もしにくいし、なんか妙にアニメーションしてて酔いそうになるし(7.0.3である程度抑えられるようになったようだけど)、良いことないと思うのだが。
(UISwitchのON/OFFだけは設定~一般~アクセシビリティ~オン/オフラベルで表示させることが出来るので、是非ともオン(1)にすることを勧める。ボタン系も枠を表示するよう設定できれば良いのに。)





2013年9月27日金曜日

Mac miniのHDD交換方法

Mac mini(2011)のHDDに交代保留セクターが増え続け、さらに全体にアクセス速度が遅くなってきているので、完全にだめになる前に交換することにした。


基本的には下記サイト↓
http://henjinkutsu.com/text/pc/macmini2011_upgrade/
を参考にしたが、いくつか間違いがあるので補足しようと思う。


まず、交換前にTimeMachineで内容を外部HDDに全バックアップしておくこと。
BOOTCAMP領域がある場合はさらに、WincloneもしくはWindows上のバックアップツールを使ってバックアップしておく。


交換に必要なものはトルクスドライバT5/T5.5/T6/T8。T5.5がないときはペンチ。それに非常に細い(薄い)マイナスドライバ。多分にほこりも溜まっていると思うので、掃除機とかほこり取りは適当に。


外すねじは、まずは以下の通り。ねじはすべてトルクスねじ。赤線はT8、白はT6だったと思う、青はT5.5。ねじを外す前にメモリは外しておく(外さなくてもHDD交換は出来ると思うけど、念のため)。金属部分に触れて、静電気防止を忘れずに。
青の部分のねじはT5.5と思われ、私の持っているトルクスビットには存在しなかった。しかし、ここは緩いねじなので、ドライバがなければペンチでつまんで外すことが出来る(というか、そうした)。


白線T5の内左2本は2重のねじで、上側=ファンを留めているのがT5(上の写真)、ファンを外した下にあるのがT8で、それを外すと左側の黒のカバーが外れる。

これで冷却ファンが外れるようになるが、電源線のコネクタも抜かなければならない。
このコネクタは基本的には上からはめるだけだが、抜くときは隙間に非常に細いマイナスドライバを入れてゆっくりすき間を広げていく。

その後上の金属網プレートを横によける。基板とは極小コネクタで接続されているので、外さないようにする(外しても良いが、はめるのはちょっと難しいので)。
次に左側の黒カバーを外す。ちょっと引っかかりがあるので揺らしながら右方向へ抜く。



HDDは黒カバーの下、この位置でコネクタで基盤につながっているので、上に引き抜く。
HDDは手前上方向に引き出すようにする。


外したHDDがこれ。左上に見えているのが、外したコネクタ。

HDDは、横側がT8ねじで留められているので外す。

また、HDD全体に黒いシートがシールでくっついているので、丁寧にはがす。このときにも薄いマイナスドライバがあると便利。


後は、元通りに戻していくだけ。
コネクタ類をちゃんとはめることを忘れずに。
私はメモリが半分しか認識されてなくて再度開く羽目になったので。

上の金属プレートのT8ねじは、このようにちょっと浮いたような感じで閉まるので注意。

くだんのHPではHDD(SSD)の増設を目指しているようで、交換だけには不要な部分のねじなども外すように指示している。それをしようとするとT5.5ドライバが絶対必須。


新HDDへの内容リカバリは2段階で行う。
(1)インターネットリカバリーでOSを戻す。
電源ON時に「command+option+R」を押しっぱなしにして起動する。
ただし、USB-Apple純正キーボードで行うこと。
(Windowsキーでは配置が違いうまく行かない。)
このとき、下記以外のHDDや機器は一切繋がないこと。
 (USB)キーボード
 (Bluetooth)マウス
 有線LAN
 モニタ

(2)TimeMachineで復元
Command+Rで起動してTimeMachineから復元する。
新HDDの容量が旧と異なっていても問題ない(もちろん、容量的に入りきる必要はある)。
Macはこれで完全復旧する。すごい!

BOOTCAMPがある場合は、Wincloneなどで戻す。

HDDの内容を戻すのは簡単だけど、HDDの交換自体は結構手間がかかるのがMac mini2011の欠点かも。



 そうそう、外したHDDを予備に使うとか、そこから内容をコピーするとかいう場合にはSATA 2.5'のHDDケースがあると便利、と書いておこう。うちでもWindows側の設定戻しに重宝したので。
Amazonなら800円くらい。



使ったHDD


使ったドライバーセット(ただし、T5.5はない)




2013年9月18日水曜日

Objective-Cの勉強(10):Class型



Class型



クラス変数を「インスタンス化した」時、そのオブジェクトの中には「自分自身のクラス定義内容」も入っている。これを「クラスオブジェクト」という。実はこれこそがNSObjectクラスの主な機能である。
これはClass型で定義され、変数にも代入できる。代入するメソッドはclassである。


万が一クラスオブジェクトが存在しない時は「Nil」という値が返ってくる。ここではNULLは使わんようだ。



定義内容そのものを動的に取得可能というわけだ。しかも、その定義内容を元にインスタンスを作成することも出来る。型(定義)そのものを変数に代入し、その変数に代入された型で変数を宣言できるということだ。
うぉっ、ややこしい。でもなんかうまく使えば強力そう。

@interface Test : NSObject
+ (void)Write;  // クラスメソッド
- (id)init;     // インスタンスメソッド
@end
@inprementation Test
    省略
@end
int main()
{
    Class testClass = [Test class]; // Class型変数testClassを宣言し、Testというクラスのクラス情報を代入する
       // testClassはクラス宣言に相当する
    [testClass Write];         // testClass->Write();相当 Writeがクラスメソッド(+)だから呼び出せる
    [[testClass new] ObjFree]; // testClassの保持するクラス型のオブジェクトを作成→即解放している
    [testClass ObjFree];       // クラスオブジェクトを解放
    return(0);
} 
 

2013年9月11日水曜日

Objective-C勉強(9);メンバーの公開範囲



メンバーの公開範囲



通常、クラスメンバー変数はそのメソッドによってのみアクセスされるべきである。
が、メソッドの呼び出しはCの関数呼び出しに比べ(出来ることが多い分)遅いので、
高速性を求められるプログラムではメンバー変数に直アクセスアクセスする必要があるかもしれない。


しかし、全メンバーを公開してしまうと、いじられたくない部分までいじられる可能性がある(特に派生クラスを作る時)ので、公開範囲を限定できる。


@public 全公開(外部、このクラス内、子クラス内)
@private このクラス内のみ公開
@protected このクラスおよび子クラス内のみ公開;デフォルト


これを記述した後のメンバー変数は全てその公開範囲となる。クラス定義内で何回、どの順序で書いても良い。
クラスの属性値は@publicに、内部変数は@privateまたは@protectedにするといいのだろう。
C言語で言うなら、グローバル変数、ファイル内static変数、auto変数と考えるとわかりやすい。

Apple純正のObjective-C解説

によると
@publicの利用は避けるべき

と書かれている。
デバッグ時に内容読むのには使えると思うけどね(デバッグ中だけ@publicにしておく)。


ついでに書くと、クラスをidで受けてしまうとメンバー変数がコンパイル時には不定でエラーになるので、「クラス名 *」で定義する必要がある。
@interface A : NSObject
{
@public
    int a;
@protected
    int b;
@private
    int c;
}
- (id)initWithA:(int)a arg2:(int)b arg3:(int)c;
- (void)WriteA;
@end

@interface B : A
// 変数がないので{}は省略
- (void)WriteB;
@end

@implementation A
- (id)initWithA:(int)a arg2:(int)b arg3:(int)c
{
    self->a = a;  // @publicメンバーへの代入はポインタを経由して行う
    self->b = b;
    self->c = c;
    return(self);
}
- (void)WriteA
{
    printf("[A Write a=%d, b=%d, c=%d]\n", a , b , c);
}
@end

@implementation B
- (void)WriteB
{
    printf("[B Write a=%d, b=%d]\n", a , b); // cにはアクセスできない
}
@end

int main()
{
    B * objb = [[B new] initWithA:1000 arg2:100 arg3:10];
    printf("[main() a=%d]\n" , objb->a); // @publicなのでメンバー変数に直接アクセスできる
    objb->a=200; // 当然代入も出来る
    [objb WriteB];
    [objb WriteA];
    [objb ObjFree];
    return(0);
}
このあたりはCで書いてた構造体を持つライブラリを移植していくと理解が深まるのであろう。

Objective-C V2.0で導入されたプロパティを使うと自動的にアクセスメソッドを生成させることが出来るので、この公開範囲をいじるというのは「高速化を除き」今後余り行わない方が良いのかもしれない。


世間に公開するようなライブラリを作るならいざ知らず、個人の開発においてはほぼ無用であろう。
私は一度も使ったことがない。

2013年9月5日木曜日

Objective-Cの勉強(8):id型よりクラス名 *の方が安全



id型よりクラス名 *の方が安全



クラスの型としてはid型が汎用的で使いやすいが、逆に言えば型(クラス)が特定出来ないので、
持っていないはずのメソッドを呼び出してしまったりする可能性がある。

それを動的に調べる方法もあるのだが、コンパイラレベルでもある程度チェックできる。
素直に元のクラス名のアドレス型で宣言すればよいだけである。
これはコンパイルレベルのチェックを強化させるだけなので、実行時には全く影響を与えない。

@interface A : NSObject
- (void) Write;
@end
@implementation A
- (void) Write
{
    printf("Write\n");
}
@end
int main()
{
    id obj1 = [A new];      // クラスAにはWriteというメソッドが存在するが、
    id obj2 = [NSObject new]; // 親クラスであるNSObjectにはない(場合)

    [obj1 Write];
    [obj2 Write];   // 実行時にエラーとなる
    [obj1 ObjFree]
    [obj2 ObjFree]
    return(0);
}
↓これを明示的なクラス名で記述する。
int main()
{
    A * obja = [A new]; // idは元々ポインタを示す型なので、クラス名で明示する時は"*"が必須
    [obja Write];       // もしクラスAがWriteメソッドを持たない場合は、コンパイル時にエラーになる
    [obja ObjFree];
    return(0);
}
この場合は呼び出す元となるインスタンスが固定されているので、メソッドの存在があらかじめ解るが、Objective-Cでは逆に、メソッド名自体=メッセージを固定して呼び出し対象とするインスタンスを変更していく;送るメッセージは同じだが、それを解釈するオブジェクトが違う、という記述をよく行う。その場合はこの方法は使えない。

その「動的なメソッドの存在チェック方法」については後述。

2013年8月26日月曜日

Objective-Cの勉強(7):オブジェクトの所有権



オブジェクトの所有権


Objective-Cでは、前述の通りクラスはローカルやグローバルな領域に実体を確保できないため、必ずオブジェクトの生成と破棄を必要とする。
アクセスしたメソッドが、それを解放して良いか、解放するならそれはいつかは、わかりにくいこともある。
特にメソッドのリターン値になる場合などはそうである。
誰が解放するかということについて、一応の規約が存在する。


オブジェクトを作成する alloc / allocWithZone:
オブジェクトをコピーする  copy / copyWithZone: / mutableCopy / mutableCopyWithZone: 
オブジェクトを保持する retain
以上を自身で行った場合はreleaseでオブジェクトを解放する。
行ってない場合は「そのオブジェクトの所有権はない」として解放処理は行わない。


別のまとめ方をすると、以下の通りとなる。
ローカルでallocしたそのスコープ内でrelease
クラスメソッド(+で始まるメソッド)で確保された基本的には何もしない(OSが解放する)
または明示的にAutorelease Poolを確保~解放する
プロパティでcopyまたはretaindeallocメソッド内でrelease
allocとcopyは実メモリの確保を伴い、retainはアドレスが示す内容の解放を遅らせるだけ、と意味が異なる。にもかかわらず、同じreleaseで解放指示を行うところが注意である。



allocとcopyは特にわかりにくいことはないと思うが、retainは少しわかりにくい概念かもしれない。
Objective-Cのクラスはアドレスでやりとりされる。
従って、代入したもしくはメッセージに引数として渡したと思って元のオブジェクトを解放してしまうと、代入先が参照できなくなってしまう。これを渡された側で「解放に待ったをかける」方法が「retain」である。
Objective-Cではマルチタスクを前提としているので、呼び出されたメソッドと呼び出したメソッドは並行動作する可能性が高い。そのためこのような指示が必要なのである。
呼び出し側でその後の処理でreleaseをかけても、そこでは解放せず、呼び出された側もreleaseをした時に初めて実解放がかかるようにする。



しかしこれもあくまで解放の遅延だけであって、内容の書き換え禁止ではないことには注意すべきである。元の内容を書き換えると、代入先の内容まで書き換わってしまう。これが顕著になるのが配列オブジェクトを使う場合である。



Objective-CのクラスはCの構造体と見た目似ているが決定的に違う部分があって、それは「実体を代入することが出来ない」である。このことは配列クラスであるNS(Mutable)Arrayの要素にて特に問題になる。
NSArrayの要素はid型であるが、これはすなわち要素として実際に格納されるのはアドレスであることを示している。addObject:メッセージで要素を追加しても、その要素のオブジェクトのアドレスが格納されるだけであって、要素実体のコピーが格納されるわけではい。Cで構造体の配列を作った場合は、その実体がコピーされる。これが最大の違いである。
 
// 構造体の場合
typedef struct {
    int x;
    int y;
} XY;
XY xy[10]; // 構造体XYの実内容を持つワークが10個分用意される。
~
    xy[0]=xy[1]; // xy[1]の内容全てがxy[0]の領域にコピーされる


// クラスの場合
@interface XY : NSObject
{
    int x;
    int y;
}
~
{
    XY *xy0=[[XY alloc]init];
    NSMutableArray *ary=[[NSMurableArray alloc]init];
    //
    [ary addObject:xy0]; // ここではxyのアドレスだけが格納され、xyの実体は格納されない
    XY *xy1=[ary objectAtIndex:0];

(構造体とクラスのプログラムは同じ意味ではない。)
ここで、構造体の場合はxy[1]の内容をこの後いじってもxy[0]の内容は変化しない。
実内容がコピーされているからである。
ところが、クラスの場合、xy0の内容をいじるとxy1から読み出す内容も同じになる。
配列に格納されているのはあくまで実体アドレスであって、内容ではないからだ。
このことを理解しておかないと、「配列に要素を保存したはずなのに内容が変わってる!」という現象に悩まされる。クラスではこれはバグではなく仕様なのである。
仮にretainを指定しても、それはあくまでアドレスの示す領域の解放を遅らせるだけであって、内容の書き換えを禁止するわけではない(その方法はない)ので要注意である。


MacのCocoaではメモリの有効活用のためにワークを移動させてまとめたり不要になった領域をメモリ上から消すガベージコレクション機能を持つらしい。古くはBASICで文字列を扱うと発生していたのが「ガベージコレクション」だし、X68のSX-Window上でもワークの移動はOSの大事な機能だったが、それとほぼ同じである(こんな事書いても解る人の方が少ないだろうが)。
確かにそれがあれば解放を気にしなくて済むようになるからプログラムは楽に書けるようになるかもしれない。
が、本来メモリ管理は明確かつ厳密に行われるべきであって、ガベージコレクションなどに頼るべきではないと思う。
アセンブラやCの時代からプログラムを組んでいると、ガベージコレクションどころか、クラスインスタンス以外のAutorelease Poolだって甘やかしだと思うのだが。

2013年8月17日土曜日

Objective-Cの勉強(6):インスタンスの確保/初期化/解放とAutorelease Pool



インスタンスの確保/初期化/解放とAutorelease Pool

クラスのメンバー変数やインスタンスメソッドは、クラスを宣言するだけでは使えない。
実体が存在しないからである。利用時には必ず実体の確保が必要となる。
これは、基本的にはCの構造体と変わらない。


構造体の場合、その確保はグローバルまたはローカルの変数として宣言することで行う。
typedef struct {
    int a;
    int b;
} SINT2; // 構造体の宣言

SINT2 x; // 実体の確保
Objective-Cでも宣言に関しては(記述方法はともかく))基本的に同じであるが、実体の確保はかなり異なる。
クラスを示す変数はアドレスで宣言し、実体確保もメソッドで行うである。
クラスの実体はローカルでもグローバルでも直接確保は出来ない。
なので、

// CINT2がクラスの場合
CINT2 x; // エラー
 
はエラーとなる。宣言時はクラス位置を示すアドレスを宣言、実体確保は別途allocメソッドで行わなければならない。

CINT2 *x;
x=[INT2 alloc];
~
[x release];

C風に書くなら、
x=malloc(sizeof(CINT2));
~
free(x);
となるだろう。

なぜこんな面倒なことをするのかであるが、Objective-Cのクラスは、実行アドレスの確定を初め動的な部分が多いので、ある特定のメモリ領域に一括して格納し、管理するためではないかと思われる。Cのヒープ領域よりもっと高度に管理されたメモリ領域を使うということだ。
そのため、位置不定のローカルや、逆に位置が完全に固定されるグローバルな確保をさせないのだと思う。


クラスの利用を宣言するとき、多くの場合2つの確保し方/され方がある。
alloc/copyなどで明確に領域の確保を指定する場合と、クラスインスタンスを使って確保する場合である。

たとえば、NSStringでは、
(1)NSString *str1=[[NSString alloc]initWithString:@"文字列"];
(2)NSString *str2=[ NSString     stringWithString:@"文字列"];
の2つは、どちらも領域を確保して、その実体として@"文字列"をコピーする。
もっと正確に言えば、str1/str2には確保された領域の先頭アドレスが入り、そのポインタが指し示す領域に"文字列"が格納される(ここではどのように文字列が格納されているかは言及しない)。


ではこの2つは同じなのかというと、「メソッド内で使うだけなら同じ」であるが「メソッドを抜けた後の動作が全く違う」。

(1)がmalloc相当による確保、(2)はAutorelease Poolという自動開放領域への確保となる。
(1)はメソッドを抜けてもその領域は確保されたまま=有効であるが、
(2)はメソッドを抜けると、あるタイミングで自動解放されてしまい内容が不定となる。
メソッドだけでなく、関数で使う場合も同じ。


(1)は必ず対応するreleaseが必要となる。
それは、同一関数内である必要はない、と言うか、無理な場合も多い。その場合は呼び出し側でreleaseすることを忘れてはいけない。ただし同一関数内でreleaseしないと、Analyzeを掛けたとき警告が出る。基本的にallocで確保したものをそのまま関数のリターン値に使うことは避けた方が良い、という考えなのであろう。


ちなみに、自分で翻訳しておいてなんだが、アップルのメモリ管理の規約に従えば、メソッドや関数内でallocしたままリターンし、呼び出し側でreleaseするのは御法度とされている。クラスメソッドを使って返すか、allocする場合はautoreleaseしておけと書いてある。

今度からそうしよう(^_^;)


なおreleaseは厳密にはfree()とは異なる。詳しくは次のオブジェクトの所有権に譲るが、即時解放ではなく解放要求である。

というのも、Objective-Cではオブジェクトは複数から保持要求されることがあり、その場合、全てのrelease要求が揃った時点で初めてメモリが解放されるからである。


Cocoa Touch上では、NSArray等のように要素を自動的にretainするようになっているクラスあり、
その場合、プログラムの記述上では要素に代入したら即時relaseするような、
Cで考えたら「おかしいんじゃねぇ?」と思えるような書き方も当然のように必要だったりするが、
それもreleaseが「即時解放」ではなく「解放要求」だからこそ成立する仕組みである。


(2)のようなクラスメソッドによる確保は、いちいちallocする必要もないし、終了時にreleaseも必要ない(してはいけない)。
なので、ローカルで使うには便利であるが、逆にローカルから外へは持ち出す=リターン値として返す場合は、受け側でcopyするか、retainして明示的に解放を遅らせる必要がある。


それをしなかったらどうなるか。それは、str2の記憶しているポインタアドレスそのものは変化しないのに、その指し示す領域の内容が変化するということになり、デバッグ時に非常にやっかいな現象を引き起こす。Cでローカルワークに格納したものをリターン値にして、呼び出し側で受けようとしたら内容が化けてた、というのと同じである。


(1)(2)の違いを正しく把握することは、Cocoa Touch上でプログラムを組む上で、きわめて重要である。にもかかわらず、アップルのドキュメントにおいてもそれに関する記述が見つけられなかったし、インターネットで調べても書いてあるサイトが1つも見つけられなかったという始末である。
ひょっとして、誰も正しく理解してなかった?それとも理解している人は隠していたか。



Autorelease Pool(自動解放プール)とは、登録されたオブジェクトを、自動解放プールを解放したときに一括して解放する仕組みである。自動開放プールの宣言からその開放までの間に登録されたオブジェクトに対し、自動的に release メッセージを送ってくれる。


NSAutoreleasePool

メソッド名動作
-(void)drain全強制解放する(ガベージコレクション付き)
-(void)release解放する(ガベージコレクションなし)

+(void)addObject:(id)object

-(void)addObject:(id)object
オブジェクトを追加する
-(id)autorelease解放要求+1
-(id)retain保存要求+1

// プール作成
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// たくさんのオブジェクトを登録
id *anObject = [[NSObject alloc] init];
[pool addObject:anObject];

    ~ 
 
//プール解放時に、登録したオブジェクトにもreleaseが送信される
 [pool drain]; // プールの解放 / iOSではreleaseでも同じ
 
このように、自動解放プールに登録したオブジェクトは、自動解放プールを解放したときと同時に解放される。
iOSではガベージコレクションを行わないので、drainとreleaseは同じである(Xcodeが自動的に作るmain.mでもreleaseを使っている)。



私は、iOSアプリを作り始めてしばらくは「クラスメソッドによる確保はローカルスタックに作られる」と理解していた。実際動作だけから見ると、その理解は「ほぼ」間違いではない。
が、もっと詳細に動作を追跡する必要が出て、それをやっている内に「Autorelease Pool領域内に確保されている」と理解した次第である。

なぜか。

Instrumentという解析ツールの中にメモリの確保状況を見る機能がある。
これで追跡していると、クラスを抜けた後にもかかわらず、クラスメソッドで確保したワークが解放されていないことが分かった。
ローカルスタックなら、リターン時に絶対に解放されるはずである(内容が破壊されるかどうかは別にして)。
試しに呼び出し前後でAutorelease poolを確保~解放したら,そのワークも解放された。
ここで「Autorelease pool内に確保される」と理解した次第である。


Autorelease poolは、iOSアプリ起動時にmain()内で自動的に確保される。
だから、ユーザープログラムの中でそれを宣言しないでもクラスインスタンスによる確保が使えるわけだが、これはメインスレッド用なので、ユーザーが別スレッドを動かす場合は、その中で明示的にAutorelease poolを確保~解放する必要がある。
これをしないと、メイン用のAutorelease PoolがあふれてiOSが異常動作を起こすこともある。
経験から言えば、アプリが落ちるのではなく、画面表示がされなくなることが多い。
非常にわかりにくいバグになるので要注意。


スレッド内でクラスインスタンスによる確保をしていないと思っても、一応AutoreleasePoolは確保しておいた方が良い。
なぜなら、アップル純正や、ちまたに流れているクラスライブラリの用意しているメソッドや関数の中には、autoreleaseオブジェクトを返してくるものがあるからである。
確認している範囲では、
  • becomeFirstResponder
  • UIGraphicsGetImageFromCurrentImageContext()
  • UTF8String
等がある。特にbecomeFirstResponderやUTF8Stringは見逃しがちである。
逆に、クラスメソッドでもシステム内のアドレスを返すだけのものは例外的に自動開放プールを使わない。
まあ、なんにせよ転ばぬ先の杖。



また、for等ループの中も同様で、ループを抜けないとAutorelease Poolが解放されないため、
ループ内でクラスインスタンスによる確保を行うとあふれてしまうことがある。
ここでも、ループ内で別途Autorelease Poolの宣言が必要となる。
 
NSMutableArray *array = [NSMutableArray array];
 
for ( int i = 0; i < 10000; i++ ) {
    NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
    [array addObject:[NSNumber numberWithInt:i]];
    [p drain];
}
for ( id n in array ) NSLog( @"%@", n );
 
for 内の[NSNumber numberWithInt:i] は、クラスメソッドによる確保であり、自動開放プールなしでループを回すと、NSNumberインスタンスが一時的に 10000個作成されることになる。
このようなところでは局所的に自動開放プールを作成・解放しておく。


allocした結果をpropertyで宣言したインスタンスに代入する場合、たとえば
 
@interface MyClass : NSObject
{
    NSString *string;
}
proterty (nonatomic,retain) NSString *string;
@end

の場合は、確保は同じだが、解放は
 
self.string=nil; // [string release]; string=nil;相当

でもいいらしい。プロパティーとして作用させる必要があるので、「self.」は外してはいけない。
でも、allocしたのにreleaseがないというのはやはり美しくない。それはCでmallocしたのにfreeがないのと同様で、メモリの確保と解放は、見た目上も対で存在すべきなのだ。

世の中にはreleaseを書かないことを美しいように言っているホームページもあるようだが、メモリの確保と解放を意識しないことがメモリ漏れバグの温床であるから、むしろ「releaseは意識的に書く」方が美しく、理にかなっていると思う。
まあ、個人が「ちゃんと」理解して使っている分にはどちらでもいいのだが。


Cocoa Touchで提供されているクラスは、allocに対しては単純にreleaseを発行すれば解放される。
しかし、自分でクラスを設計するとき、その中で別の領域を確保するかもしれない。
このような場合は、単純にreleaseすると中の領域が開放されないまま残ってしまう。
このようなクラスの場合、中にdeallocメソッドを作って、独自の解放処理を記述しておく。
deallocはクラスをreleaseするときに内部的に呼び出されるメソッドである。
C++でいうところの「デストラクタ」に相当する、のだと思う。
 
@interface MyClass : NSObject
{
    NSString *string;
}
@end

@implementation MyClass

-(NSString *)initString
{
    string=[[NSString alloc]initWithString:@"初期化"];
}

-(void)dealloc
{
    [string release];
}
@end

~

MyClass *my;
    my=[[MyClass alloc]init];
    ~
    [my release]; // ここでdeallocが呼び出される
 
なお、deallocをユーザーが呼び出してはいけない。実行エラーとなる。
それはシステムがオブジェクトを解放するときに自動的に呼び出すものだからである。
ユーザーが呼び出すのはあくまでreleaseのみである。


Google Objective-Cスタイルガイド日本語訳 では「確保する変数はautoreleaseしてから代入せよ」と書いてあるけど、これは「すべての確保系をautoreleaseで行うときのみ」にのみ使える方法であって、上記のようにallocを使っている場合にはしてはいけない。確実に落ちる。
私個人的な感想としては、Googleのスタイルガイドには従いづらい部分が多いので、基本的には無視している。クラスメンバー名の付け方とか、TABとか。


オブジェクトは多くの場合、初期化を必要とするC++ではコンストラクタと呼んでオブジェクト生成時に自動的に呼び出しがかかるが、Objective-Cでは初期化をイニシャライザまたはコンビニエンス コンストラクタと言い、明示的に呼び出す必要がある。


イニシャライザ(initializer)/コンビニエンス コンストラクタ(convenience constructor)はどちらも初期化を行うメソッドであり、よく似た機能を持つが、前者がインスタンスメソッド用、後者はクラスメソッド用で、動作も微妙に違う。


実はクラスの「内容=メンバー変数」はインスタンス化されるときに0で初期化されることが決まっている。Cはauto変数は内容不定だったのとは違う。これは、実はObjective-Cのクラスはスタック上に作られるauto変数ではなく、メモリ上に直に確保されるグローバル変数だからである。Cもグルーバル変数は0で初期化された。Objective-Cで特別に変更された仕様ではない。
(ただし、通常はイニシャライザで必要な初期値を与えておくべきである)。


しかし、インスタンスのアドレス自体はnil(=0)ではない(そのアドレスがselfである)。
プログラム上、時にインスタンスのアドレスをnilで初期化して呼び出すことを要求しているクラスがあるので注意が必要である。



initはNSObject(NSObject)が用意しているインスタンスの初期化用の標準的にイニシャライザー(メソッド)である。多くのクラスはこれをオーバーライドして独自の初期化処理を追加している。
initは多くの場合、初期化された自身のオブジェクトのアドレス=selfを返すが、オーバーライドの実装によっては他の値を返したり、nilでエラーを返す場合もある。
従って、そのリターン値を確認することは重要である。


@interface Point : NSObject
{
    int x;
    int y;
}
- (id)init;     // 引数なしだから変数名のように見えるけど、関数
- (id)ObjFree;  // 〃
- (int)getX;    // 〃
- (int)getY;    // 〃
@end

@implementation Point
- (id)init  // initは引数を持たせることが出来ない
{
    [super init]; // 親クラスのinitを呼び出しておく;子クラスを作る時は必須
    self = [super init];
    if (self != nil) {
        x = y = 0;
        printf("init method\n");
    } // self==nilの時は何らかの原因で初期化に失敗している
    return(self);
}
- (id)ObjFree
{
    printf("ObjFree method\n");
    return [super ObjFree]; // 親クラスのObjFreeを呼び出しておく;子クラスを作る時は必須
}
- (int)getX
{
    return(x);
}
- (int)getY
{
    return(y);
}
@end

int main()
{
    id pt = [[Point alloc]init]; // 確保して初期化
    //      [Point new];    と書き換えることも出来る。new=alloc+init
    printf("x=%d,y=%d\n", [pt getX] , [pt getY]); // なんか変数を並べているようで違和感あるなぁ
    [pt ObjFree];
    return(0);
}
initはオーバーライド時にも引数を持たせることが出来ない。無理に持たせようと定義してもコンパイラがエラーを出す。これはnewというクラスメソッドが=alloc+initと定義されているので、その互換性を維持するためだろう。


そういうこともあって、Point.init()は引数を取れないので=0という固定値で初期化しているが、
引数付きとして任意の値で初期化する場合は別途そういうイニシャライザを定義する。
それを「指定イニシャライザ」といい、慣習的に名称はinitWith~とする。


initと同じく、子クラスのイニシャライザでは、superで親クラスの初期化も行う必要がある。
 
@interface Point : NSObject
{
    ~
}
- (id)initWithPoint:(int)x arg2:(int)y;
~

@implementation Point
- (id)init // initは引数を持たせることが出来ない
{
    [super init];   // 親クラスのinitを呼び出しておく;子クラスを作る時は必須
    return [self initWithPoint:0 arg2:0];
}
- (id)initWithPoint:(int)x arg2:(int)y
{
    self->x = x;
    self->y = y;
    return(self);
}
int main()
{
    id pt1 = [Point new]; // alloc+init=new
    id pt2 = [[Point alloc] initWithPoint:400 arg2:300]; // こちらはnewにはできない
    //
    printf("pt1.x=%d,pt1.y=%d\n" , [pt1 getX] , [pt1 getY]);
    printf("pt2.x=%d,pt2.y=%d\n" , [pt2 getX] , [pt2 getY]);
    [pt1 ObjFree]
    [pt2 ObjFree]
    return(0);

実行結果(未確認)
pt1.x=0,pt1.y=0
pt2.x=400,pt2.y=300
ObjFree method
ObjFree method
「メモリ管理」も参照のこと。