ラベル Objective-C の投稿を表示しています。 すべての投稿を表示
ラベル Objective-C の投稿を表示しています。 すべての投稿を表示

2015年6月24日水曜日

Objective-CやiOSの隠れた仕様

X-BASIC for iOS v3.00の開発では、Objective-C、というかそのコンパイラの隠れた仕様に悩まされた部分があった。それを記録しておく。

(1)Objective-Cでの %ld の l判定はlong という宣言文字列と比較されているので実質ではない
何が言いたいのかというと、64ビットでlong != intで弾かれるのはいいとして、
32bitで実質 int == longでも「異なる」と判定されるということ。
X-BASICでは32ビット環境ではLong = longと定義していたのを、64ビット環境でLong = intと変更した。(LLong=long long;64bit)。32ビットのままでなければならない処理が山のようにあるからだ。ところが上記の仕様のため、従来%ldでいけたところをすべて%dにしなければいけなくなった。ところが%dにすると今度は32ビット環境下で警告を食らうハメになる。というわけで、結局都度(int)で型キャストするという手をとった。

(2)Objective-CのC型文字列の"~"はchar *であってsigned char *ではない。
これも警告で引っかかった。
確かにcharはコンパイルオプションでunsigned にもできるけど、でもそうでない設定の時はchar = signed charだろう?これはCの規約違反だと思うのだが。

 (3)定義済み定数__FUNCTION__ は char [n]と定義されるため、”%s”,__FUNCTION__では受けられるけど、func(char *mes)に対してfunc(__FUNCTION__)にすると警告が出る
 わけわからんかもしれないけど、やってみるとわかる。

 (4)NSUserDefaultで、setBoolしたものはintegerForKeyで読める
    [defaults setBool:NO        forKey:@"BOOL"];

    BOOL       ynb=[defaults    boolForKey:@"BOOL"];
    NSInteger yni=[defaults    integerForKey:@"BOOL”];
   どちらでも読めている。

まあ、普通はしないけど、バージョンアップで記録内容をBOOL範囲からNSInteger範囲にしなければならなかったので。

(5)array=[@"A\nB\n" componentsSeparatedByString:@"\x0a"]の結果は
[0]=@"A\n",[1]=@"B\n",[2]=@""となる。
最後に@""が入った要素ができるのが味噌。countで得られる値-1が行数である。

(6)Objective-C(というかそのライブラリ)の%sは日本語に対応できてない。
    NSString *mes=@"English日本語";
    char *cmes=[mes UTF8String];
    NSLog(@"元文字列=%@,char*=%s",mes,cmes);
    NSString *mes2=[NSString stringWithCString:cmes encoding:NSUTF8StringEncoding]; // もしくはmes2=@(cmes)だけでもOK
    NSLog(@"逆文字列=%@",mes2);

を実行すると、
元文字列=English日本語,char*=EnglishÊó•Êú¨Ë™û
逆文字列=English日本語
となる。要するに%sの表示だけがおかしい。NSLog()だけじゃなくてprintf()でも同じ。
日本語を含む文字列の表示には%sを使ってはいけないということ。
バグに近い仕様。

(7)NSStringのlength,substring*のメソッドはサロゲートペア文字を考慮していない
 サロゲートペア文字は常に2文字分の扱いをしないといけない
 サロゲートペアは見た目1文字だけど、内部では2文字の扱いになっているということ。
   length=2だし、substringのrangeもlength=2単位にしないといけない。
X-BASICではV3.10でNSStringをサロゲートペア対応にするためのカテゴリを作って対応した。


(8)NSLog(@"%@",文字列)の時、その文字列がサロゲートペアの最初の半分だった時、表示が全く出ない
これは(7)と絡むのだが、サロゲートペア文字は内部では2文字なので、その最初の1文字だけを持ってNSLog()で表示しようとすると、そのNSLog()すべてが表示されない。
NSLog(@"str=%@",[@"サロゲートペア文字" substringToIndex:1])とかすると、文字だけでなくstr=も含めて表示が出ない。
NSLog()入れてるはずなのに表示が出ないという時はこの可能性がある。

(9)これはObjective-Cではないけど、Zipアーカイブ内のファイル名は、それが作られた環境によって文字コードが異なる様子。
WindowsではSHIFT-JISで格納されている。ZipArchiveというライブラリではUTF8にしか対応していないため、Windows環境下で作られた日本語名ファイル含むZipを展開しようとすると、日本語名ファイルのみ抜けてしまう。
X-BASICではライブラリを修正して利用している。

(10)PNGファイルをiOSのリソースに入れると、ファイルが改変される。
ヘッダーの中にも情報が追加されてる。しかもその追加され方がPNGのフォーマット(規約)に合致していないので、たぶん、そのファイルを抜き出して画像ソフトに読ませても表示できない。
X-BASIC V3.10まででpngHeader()関数を内部にある画像に対して使うと正しい情報が得られないのはこのせい。

(11)iOSでPNGの透過を有効にするときは、256色にするか、インデックスカラーというものにしなければならない。普通にフルカラーで透過色を付けても透過しない。
インデックスカラーへの変換は、フリーのgimpかPhotoshopで出来る。

(12) NSTimerを実行させるにはNSRunLoopへ追加しなければならないが、これはメインスレッドで行わなければならない
NSTimer *tickTimer=[NSTimer timerWithTimeInterval:(NSTimeInterval)MML_1TICK_TIME
  target:self
selector:@selector(tickCountDown)
userInfo:nil
repeats:YES // 繰り返し
  ];

// 次がRunLoopへの追加だが、これはメインスレッドで実行しなければ有効にならない。

[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];

テストでは動いていたものが本番ではどうしても動かないので調べてみたらこれだった。
(X-BASIC'の言語処理は実はバックグラウンドで動いているのです。)
このメソッドはエラーを返さないのでわからなかった。

(13)複数の処理を並行動作させたいときはメインスレッドは使えない
メインスレッドは1つしかないから考えたらあたりまえのことなんだけど、

複数同時に走らせたい処理を
 [self performSelectorInBackground:@selector(fetch_main) withObject:nil];

[self performSelectorOnMainThread:@selector(fetch_main) withObject:nil waitUntilDone:NO];
に変更したらうまく動かなくなった(上記を複数回発行して複数本同時に走らせる)
バックグラウンドは同時に何本でも設定できる=並行動作するが、メインスレッドは1つしかないからである。メインスレッドでは、追加された順にキューに記録され、1つが終わると次のが走る。

2014年8月28日木曜日

iPadでsizeToFitを使う場合の注意

UILabelを使っている処理で、末尾の1文字が欠けるという現象が出た。
よくよく調べると、
・文字列中に半角文字が1文字だけある
・sizeToFitを使っている
・iPad(32/64bit共)のみである
であった。

更に調査した結果、興味深いことがわかった。
sizeToFitした結果のサイズを調べると、iPadのみ1ピクセル分少ないのだ。

 CGRect frame=label.frame;
 NSLog(@"元frame=%@",NSStringFromCGRect(frame));
 [label sizeToFit];
 frame=label.frame;
 NSLog(@"後frame=%@",NSStringFromCGRect(frame));

この後frameの.size.width値がiPhoneでの結果に比べiPadは1小さい値が返ってきている。

さらに、UILabelでは、文字表示必要幅に対して1ピクセル分でも足りないと、末尾1文字がまるごと欠けるしまうようである。

iOS7のバグ。iOS6以前ではどうかは不明。

なので、対策としては

if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
  // iPadのみ
  CGRect frame=label.frame;
  frame.size.width=frame.size.width+1; // +1ピクセル
  label.frame=frame;
}

とすればよい。

2014年8月8日金曜日

NSString initWithStringの罠

オブジェクトで配布されているライブラリの中で落ちるバグに遭遇した。
落ちるときのログから判断するに、initWithString:nilしている部分があるらしい。
initWithStringはnilを渡してはいけない。

普通ならソースのないライブラリ内のバグなんてどうしようもないところだが、
Objective-Cには動的にメソッドを入れ替えるという荒業があるため、
なんとかなるかもしれない、と思った。
そう、NSString initWithString:を差し替えて、引数にnilが来たら、対応処理を入れればよい。
これをSwizzlingという。

処理の入れ替えは以下のように書く。

Method method1 = class_getInstanceMethod(元のクラス,@selector(元のメソッド名));
Method method2 = class_getInstanceMethod(入れ替えるクラス,@selector(そのメソッド名));
method_exchangeImplementations(method1, method2);

例えば、NSString initWithString:を差し替えるなら、NSStringのカテゴリを作成し、
(思い出し書きなので、完全に正しい保証なし。)

NSString+debug.h

#import <objc/runtime.h> // これが重要

@interface NSString (debug)
+(void)debug;
@end

NSString+debug.m

@implement NSString (debug)

-(NSString *)initWithString2:(NSString *)str
{
 ~デバッグ用コード~
  // 元の処理を呼び出す。
  // 再帰呼び出しのように見えるが、実はこれで元の処理が呼び出される。
  // [self initWithString:str]と書くと再帰呼び出しになってしまう
  return [self initWithString2:str];}

+(void)debug
{
 Method method1 = class_getInstanceMethod([self class],@selector(initWithString:));
 Method method2 = class_getInstanceMethod([self class],@selector(initWithString2:));
 method_exchangeImplementations(method1, method2);

}

@end

で、まずどこかで[NSString debug];と呼び出しておけば、後は入れ替わる・・・はずである。

が、結果から言えば、initWithStringは入れ替わらない。
試しに、stringWithStringを差し替えてみようと、

+(NSString *)stringWithString2:(NSString *)str
{
 ~
  // 元の処理を呼び出す。
  return [[self class] stringWithString2:str];}

+(void)debug
{
 Method method1 = class_getClassMethod([self class],@selector(stringWithString:));
 Method method2 = class_getClassMethod([self class],@selector(stringWithString2:));
 method_exchangeImplementations(method1, method2);
}

とすると、うまく入れ替わる。
initWithStringはインスタンスメソッドに対し、stringWithStringはクラスメソッドなので、使っている関数が違う。

差し替える前にデバッグ出力を追うと以下のように表示されていた。

initWithString:nilの時
Foundation             0x02d99f4a -[NSPlaceholderString initWithString:] + 99

stringWithString:nil
Foundation             0x02d9af4a -[NSPlaceholderString initWithString:] + 99
Foundation             0x02d9aec6 +[NSString stringWithString:] + 67


ここから察するに、NSString initWithString:は実はNSPlaceholderString initWithString:というメソッドに置き換えられていて実体が存在しない。stringWithString:は実体があって、その中でNSPlaceholderString initWithString:を呼び出している、ようである。

実体がないメッソドは置き換えられない。故に、NSString initWithStringは置き換わってくれない。

じゃあ、そのNSPlaceholderString initWithStringを置き換えればどうかと思うかもしれないが、困ったことにこいつは非公開クラスなので直接アクセスが出来ない=Swizzling出来ない。

ってなわけで、動的デバッグは出来なかったのであった。
Swizzlingの勉強にはなったのでいいけど。
一応ライブラリの作成先に連絡を入れるつもりだけど、果たして直してくれるかどうか。

→その後の調査の結果、ライブラリのバグではなく、ライブラリに必要なファイルが足りてなかったからと判明。しかし、それはそれで「ファイルがないときはエラーを出す」処理が抜けているという問題ありコードだと思うのだが。エラー処理がいかにちゃんと実装されているかが、ライブラリの完成度の尺度。

2014年3月8日土曜日

クラスの内容をファイルに出力する方法(4)

最後にヘッダー。

//
//  saveClass
//
//  (C) 2014 by AIG-Soft
//  under Apache License

#import <Foundation/Foundation.h>

BOOL    saveClass(  id object,NSString *basePath);
BOOL    deleteClass(id object,NSString *basePath);
BOOL    loadClass(  id object,NSString *basePath);
NSArray *propertyNames(id object);
NSArray *memberNames(id object);
NSDictionary *propertiesAttributes(id object);
NSDictionary *memberAttributes(id object);






で、こんな感じで使う。
propertyNames(self);
propertiesAttributes(self);
memberNames(self);
memberAttributes(self);
saveClass(  self, makeDocumentsPath());
loadClass(  self, makeDocumentsPath());
deleteClass(self, makeDocumentsPath());
ちなみにmakeDocumentsPath()はこんな関数。
NSString *makeDocumentsPath(void)
// ディレクトリ「Documents」のフルパスを得る
// iTunesからのデータやりとりもここ
{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    // paths[0]を取り出す;そこにディレクトリ名が入っているらしい
    NSString *documentsDirectory = (([paths count] > 0)?         // 見つかったら
                                    /* これ↑は単なるポインタ参照 */
                                    paths[0]                     // 最初のもの
                                    : NSTemporaryDirectory() );  // なければ一時ディレクトリ
    return (documentsDirectory);
}
使って頂いて、デバッグや改良点などあれば教えていただければありがたいかと。

2014年3月7日金曜日

クラスの内容をファイルに出力する方法(3)

さて、先の入出力関数の中では、NSArrayの中に別のクラスが存在する場合はそのまま処理できない。
それも入出力する場合は、そのクラスの中に少し処理を追加する必要がある。

追加するのはcoderという処理。これを入れるとクラスをNSDataにシリアライズして入出力出来るようになる。

以下のように実装する。


こんなクラスの場合↓
@interface TestClass : NSObject
{
    int     testInt;
    float   testFloat;
    NSArray *testArray;
}

こういうふうに実装する↓。

- (void)encodeWithCoder:(NSCoder *)aCoder
{
//  NSLog(@"encodeWithCoder");
    // メンバーのそれぞれをencode*:forKeyで変換する
    // キー名は適当でいいけど、基本はメンバー名と同じにすればいいと思う
    [aCoder encodeObject:testArray    forKey:@"testArray"];
    [aCoder encodeInt:testInt         forKey:@"testInt"];
    [aCoder encodeDouble:testFloat    forKey:@"testFloat"];
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
//  NSLog(@"initWithCoder");
    self = [super init];
    if (self!=nil) {
        // メンバーのそれぞれをdecode*ForKeyで逆変換する
        // キー名はencodeで指定したもの
        testArray    = [aDecoder decodeObjectForKey: @"testArray"];
        testInt      = [aDecoder decodeIntegerForKey:@"testInt"];
        testFloat    = [aDecoder decodeDoubleForKey: @"testFloat"];
    }
    return self;
}

-(NSString *)description
// %@で表示するための文字列を返す処理
// これは必須ではない
{
    return [NSString stringWithFormat:
            @"< %@ : %p >"
            @"testArray=%@ ,"
            @"testInt=%u ,"
            @"testFloat=%f ,"
            ,NSStringFromClass([self class]),self
            ,testArray
            ,testInt
            ,testFloat
            ];
    // 再帰呼び出しになるので、"%@",selfは記述してはいけない
}

上記のメソッドを、出力したいクラスの@implementationの中に追加する。
なお、coderはカテゴリでは実装できない(無視される)。
また、coderを実装しただけでは[ary writeToFile]では出力されない様子である。









2014年3月6日木曜日

クラスの内容をファイルに出力する方法(2)

ということで、その本体のソース。
公開にあたって、一部書き換えたので動かなくなってたらごめんなさい。
「確認してから出せ」ッて言われそうだけど、時間がなくて。




//
//  saveClass.m : 全クラス内容を得る、保存する
//
//  (C) 2014 by AIG-Soft
//    under Apache License

/* この辺りを参照
クラスのメンバ名を文字列で指定する
http://program.station.ez-net.jp/special/handbook/objective-c/id/ivar.asp

オブジェクトが持つプロパティの型と名前のリストを取得する
http://d.hatena.ne.jp/shu223/20120226/1330231240

クラス名を取得する
http://lab.dolice.net/blog/2013/04/17/objc-ns-string-from-class/
 
シリアライズする
http://nagano.monalisa-au.org/archives/64
*/

#import "saveClass.h"
#import "objc/runtime.h"
#include <sys/stat.h>    // mkdir()
#include <sys/types.h>    // mkdir()のmode
#include <unistd.h>        // rmdir()

#define MAX_CLASS_NAME    (128) // クラス名はこれbytes以下にすること

//--------------------------------------------------------------

static BOOL saveClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバーの内容を保存する
// メモリ確保の関係でサブルーチンにする
// 未対応型クラスは実装を追加すること
{
    NSLog(@"saveClassSub:%d",cnt);
    for (int i = 0; i < cnt; i++) {
        // 属性取得
        const char *encode = ivar_getTypeEncoding(ivars[i]);
        const char *name   = ivar_getName(ivars[i]);

        // パス名=basePath/クラス名/プロパティ
        NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
        NSLog(@"パス名=%@",path);
        
        // 型別出力処理
        FILE *fp;
        // クラスの場合はwriteToFileを使うが、ファイルはとりあえずopenしておく。これにより、内容がnilだった場合も空ファイルができるようになる。
        fp=fopen(nsStringTocString(path),"w");
        if (fp==NULL) {
            // ファイルが作成できない
            NSLog(@"ファイルが作成できない");
            return(NO);
        }
        switch (encode[0]) {
            default:
                NSLog(@"不明型:%s",encode);
                break;
            
            // 小文字はsigned,大文字はunsigned
            case 'c':
            case 'C': // char系(BOOL/char)
                {
                unsigned char result;
                object_getInstanceVariable(object, name, (void**)&result);
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"char系:%d/%u",result,result);
                }
                break;
            case 's':
            case 'S': // short系
                {
                unsigned short result;
                object_getInstanceVariable(object, name, (void**)&result);
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"short系:%d/%u",result,result);
                }
                break;
            case 'i':
            case 'I': // interger系
                {
                unsigned int result;
                object_getInstanceVariable(object, name, (void**)&result);
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"interger系:%d/%u",result,result);
                }
                break;
            case 'l':
            case 'L': // long系 : iOSではintと同じはずだけど念のため
                {
                unsigned long result;
                object_getInstanceVariable(object, name, (void**)&result);
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"long系:%ld/%lu",result,result);
                }
                break;
            case 'q':
            case 'Q': // long long
                {
                unsigned long long result;
#if 0
                // 64bitでは以下の方法は正常動作しない
                // http://stackoverflow.com/questions/1219081/object-getinstancevariable-works-for-float-int-bool-but-not-for-double
                object_getInstanceVariable(object, name, (void**)&result);
#else
                // なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
                Ivar ivar = object_getInstanceVariable(object, name, NULL);
                if (ivar) {
                    result= *(long long*)((char *)object + ivar_getOffset(ivar));
                }
#endif
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"long long系:%lld/%llu",result,result);
                }
                break;
            case 'f': // float
                {
                float result;
                object_getInstanceVariable(object, name, (void**)&result);
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"float:%f",result);
                }
                break;
            case 'd': // double
                {
                double result;
#if 0
                // 64bitでは以下の方法は正常動作しない
                object_getInstanceVariable(object, name, (void**)&result);
#else
                // なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
                Ivar ivar = object_getInstanceVariable(object, name, NULL);
                if (ivar) {
                    result= *(double *)((char *)object + ivar_getOffset(ivar));
                }
#endif
                fwrite(&result,sizeof(result),1,fp);
                NSLog(@"double=%.15f",result);
                }
                break;
                
                // 汎用化を進めるときは以下に出力処理を追記すること
            case '@': // クラス名
                if (encode[1]=='"') {
                    // 以下に"クラス名"がある
                    // クラス名のみ切り出す
                    char className[MAX_CLASS_NAME+1]; // +1 for EOS
                    int j=0;
                    char c;
                    while ((c=encode[2+j])!='"') {
                        className[j]=c;
                        if (++j>=MAX_CLASS_NAME) {
                            break;
                        }
                    }
                    className[j]='\0';
                    NSLog(@"クラス=%s",className);
                    if (strcmp(className,"NSString")==0||strcmp(className,"NSMutableString")==0) {
                        // クラスはfopen()に頼らない出力をするので閉じてしまう
                        // これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
                        fclose(fp);
                        fp=NULL; // 閉じた印
                        //
                        NSString *str;
                        object_getInstanceVariable(object, name, (void**)&str);
                        NSLog(@"NSString=%@",str);
                        if (str!=nil) {
                            NSError *error=nil;
                            if (![str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error]) return(NO);
                        }
                        break;
                    }
                    else
                    if (strcmp(className,"NSData")    ==0||strcmp(className,"NSMutableData")    ==0) {
                        // クラスはfopen()に頼らない出力をするので閉じてしまう
                        // これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
                        fclose(fp);
                        fp=NULL; // 閉じた印
                        //
                        NSData *data;
                        object_getInstanceVariable(object, name, (void**)&data);
//                        NSLog(@"NSData=%@",data);
                        if (data!=nil) {
                            if (![data writeToFile:path atomically:YES]) return(NO);
                        }
                        break;
                    }
                    else
                    if (strcmp(className,"NSArray")    ==0||strcmp(className,"NSMutableArray")    ==0) {
                        // クラスはfopen()に頼らない出力をするので閉じてしまう
                        // これでとりあえずサイズ0のファイルができるので、オブジェクトの実体がnilの場合はそれが残る。
                        fclose(fp);
                        fp=NULL; // 閉じた印
                        //
                        NSArray *ary;
                        object_getInstanceVariable(object, name, (void**)&ary);
                        NSLog(@"NSArray=%@",ary);
                        if (ary!=nil) {
#if 0
                            // aryの中にwriteToFileをサポートしてないクラスがあると失敗するので、無視するためエラーは取らない
//                            if (!
                                [ary writeToFile:path atomically:YES]
                            ;
//                            ) return(NO);
#else
                            // aryの中に別のクラスが存在し、それも出力したい場合は、coderを実装した上、NSDataにシリアライズして出力する
                            // 注意
                            // 1.coderを実装しただけでは[ary writeToFile]では出力されない様子
                            // 2.coderはカテゴリでは実装できない(無視される)
                            NSData *data = [NSKeyedArchiver archivedDataWithRootObject:ary];
//                            NSLog(@"data=%@",data);
                            [data writeToFile:path atomically:YES];
#endif
                        }
                        break;
                    }
                    else
                    if (strcmp(className,"NSDate")    ==0) {
                        NSDate *date;
                        object_getInstanceVariable(object, name, (void**)&date);
                        NSLog(@"NSDate=%@",date);
                        if (date!=nil) {
                            NSTimeInterval dt=[date timeIntervalSince1970]; // 1970/1/1からの相対時間(数値)に変換する
                            fwrite(&dt,sizeof(dt),1,fp);
                            NSLog(@"dt=%f",dt);
                        }
                        break;
                    }
                    else {
                        NSLog(@"未サポートクラスは出力しない");
                        // NSSetはwriteToFileがない
                    }
                } else {
                    // クラス名文字列がないときはid型
                    // 型が特定できないので出力対象としない
                    NSLog(@"id型は出力しない");
                }
                break;
        }
        if (fp!=NULL) fclose(fp);
//        printf("型=%s/名前=%s\n",attributes,property_getName(properties[i]));
    }
    // 全出力終了
    return(YES);
}


// basePath=makeDocumentsPath();

BOOL saveClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を保存する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダが出来、その下にクラス名別ファイルが作成される)
{
    NSLog(@"saveClass:%@",object);
    
    // 全メンバ情報を得る
    unsigned int cnt;
    Ivar *ivar = class_copyIvarList([object class], &cnt);
    //
    // 先に保存ディレクトリを作成する : basePath/クラス名
    NSString *dir=[NSString stringWithFormat:@"%@/%@",basePath,NSStringFromClass([object class])];
    NSLog(@"保存ディレクトリ:%@",dir);
#if 1
    mkdir(nsStringTocString(dir),S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP|S_IROTH|S_IXOTH|S_IXOTH);
#else
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSError *error=nil;
    [fileManager createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error];
#endif
    // ここで失敗してディレクトリが出来なくても、あとのファイル書き出しでエラーが出るのでとりあえず無視する
    // 既存の場合のエラーコードがわからないので。
    //
    BOOL ret=saveClassSub(object,ivar,cnt,basePath);
    //
    free(ivar);
    NSLog(@"---------------");
    return (ret);
}

//--------------------------------------------------------------

// saveとloadは必ず同じ内容のクラスで行うこと。
// 変わっている場合、動作は保証されない。

static BOOL loadClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバーの内容を復帰する
// メモリ確保の関係でサブルーチンにする
// 未対応型クラスは実装を追加すること
{
    NSLog(@"loadClassSub:%d",cnt);
    for (int i = 0; i < cnt; i++) {
        // 属性取得
        const char *encode = ivar_getTypeEncoding(ivars[i]);
        const char *name   = ivar_getName(ivars[i]);
        
        // パス名=basePath/クラス名/メンバ名
        NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
        NSLog(@"パス名=%@",path);
        
        // 型別読み込み処理
        // 出力時に型までは保存してないので、同名の別クラスがあったりすると誤動作する
        FILE *fp;
        fp=fopen(nsStringTocString(path),"r");
        if (fp==NULL) {
            // ファイルがない
            continue; // 無視するだけ
        }
        switch (encode[0]) {
            default:
                NSLog(@"不明型:%s",encode);
                break;

            // 小文字はsigned,大文字はunsigned
            case 'c':
            case 'C': // char系(BOOL/char)
                {
                unsigned char result;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
                object_setInstanceVariable(object, name, (void*)&result);
                NSLog(@"char系:%d/%u",result,result);
                }
                break;
            case 's':
            case 'S': // short系
                {
                unsigned short result;
                fread(&result,sizeof(result),1,fp);
                object_setInstanceVariable(object, name, (void*)&result);
                NSLog(@"short系:%d/%u",result,result);
                }
                break;
            case 'i':
            case 'I': // interger系
                {
                unsigned int result;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
                object_setInstanceVariable(object, name, (void*)&result);
                NSLog(@"interger系:%d/%u",result,result);
                }
                break;
            case 'l':
            case 'L': // long系 : iOSではintと同じはずだけど念のため
                {
                unsigned long result;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
                object_setInstanceVariable(object, name, (void*)&result);
                NSLog(@"long系:%ld/%lu",result,result);
                }
                break;
            case 'q':
            case 'Q': // long long
                {
                unsigned long long result;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
#if 0
                // 64bitでは以下の方法は正常動作しない
                object_setInstanceVariable(object, name, (void*)&result);
#else
                // なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
                Ivar ivar = object_getInstanceVariable(object, name, NULL);
                if (ivar) {
                    *(long long *)((char *)object + ivar_getOffset(ivar))=result;
                }
#endif
                NSLog(@"long long系:%lld/%llu",result,result);
                }
                break;
            case 'f': // float
                {
                float result;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
                object_setInstanceVariable(object, name, (void*)&result);
                NSLog(@"float:%f",result);
                }
                break;
            case 'd': // double
                {
                double result=0;
                if (fread(&result,sizeof(result),1,fp)<1) result=0;
#if 0
                // 64bitでは以下の方法は正常動作しない
                object_setInstanceVariable(object, name, (void*)&result);
#else
                // なので、一旦メンバーの存在位置を取得して、そこから直接読み出す方法を使う
                Ivar ivar = object_getInstanceVariable(object, name, NULL);
                if (ivar) {
                    *(double *)((char *)object + ivar_getOffset(ivar))=result;
                }
#endif
                NSLog(@"double=%.15f",result);
                // doubleの表示は%f。しかし、標準ではfloatと同じ精度(7桁)までしか表示しないので、double精度(15桁)表示させるには上記のように桁数を指定する必要がある。
                // ただし、15桁は少数以下だけでなく整数位も含めた全体なので注意。要するにこの記述は必ずしも正しくはない。
                }
                break;
                
                // 汎用化を進めるときは以下に読み込み処理を追記すること
            case '@': // クラス名
                if (encode[1]=='"') {
                    // 以下に"クラス名"がある
                    // クラス名のみ切り出す
                    char className[MAX_CLASS_NAME+1]; // +1 for EOS
                    int j=0;
                    char c;
                    while ((c=encode[2+j])!='"') {
                        className[j]=c;
                        if (++j>=MAX_CLASS_NAME) {
                            break;
                        }
                    }
                    className[j]='\0';
                    NSLog(@"クラス=%s",className);
                    if (strcmp(className,"NSString")==0||strcmp(className,"NSMutableString")==0) {
                        NSError *error=nil;
                        NSString *str=[NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
                        if (error==nil) {
                            str=nil;
                        }
                        object_setInstanceVariable(object, name, (void*)str);
                        NSLog(@"NSString=%@",str);
                        break;
                    }
                    else
                    if (strcmp(className,"NSData")    ==0||strcmp(className,"NSMutableData")    ==0) {
                        NSData *data=[NSData dataWithContentsOfFile:path];
                        object_setInstanceVariable(object, name, (void*)data);
                        NSLog(@"NSData=%@",data);
                        break;
                    }
                    else
                    if (strcmp(className,"NSArray")    ==0||strcmp(className,"NSMutableArray")    ==0) {
#if 1
                        NSData *data = [NSData dataWithContentsOfFile:path];
                        NSArray *ary = [NSKeyedUnarchiver unarchiveObjectWithData:data];
#else
                        NSArray *ary=[NSArray arrayWithContentsOfFile:path];
#endif
                        object_setInstanceVariable(object, name, (void*)ary);
                        NSLog(@"NSArray=%@",ary);
                        break;
                    }
                    else
                    if (strcmp(className,"NSDate")    ==0) {
                        NSTimeInterval dt; // 1970/1/1からの相対時間(数値)で保存されている
                        if (fread(&dt,sizeof(dt),1,fp)<1) dt=0;
                        NSDate *date=[NSDate dateWithTimeIntervalSince1970:dt];
                        object_setInstanceVariable(object, name, (void*)date);
                        NSLog(@"dt=%f/date=%@",dt,date);
                        break;
                    }
                    else {
                        NSLog(@"未サポートクラスは読み込まない");
                    }
                } else {
                    // クラス名文字列がないときはid型
                    // 型が特定できないので読み込み対象としない
                    NSLog(@"id型は読み込めない");
                }
                break;
        }
        fclose(fp);
//        printf("型=%s/名前=%s\n",attributes,property_getName(properties[i]));
    }
    // 全読み込み終了
    return(YES);
}


BOOL loadClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を復帰する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
{
    NSLog(@"loadClass:%@",object);
    
    // 全メンバ情報を得る
    unsigned int cnt;
    Ivar *ivar = class_copyIvarList([object class], &cnt);
    //
    BOOL ret=loadClassSub(object,ivar,cnt,basePath);
    //
    free(ivar);
    NSLog(@"---------------");
    return (ret);
}

//--------------------------------------------------------------

static void delete1(id object,NSString *basePath,const char *name)
// 1ファイル削除
{
    NSString *path=[NSString stringWithFormat:@"%@/%@/%s",basePath,NSStringFromClass([object class]),name];
    unlink(nsStringTocString(path));
}

static BOOL deleteClassSub(id object,Ivar *ivars,unsigned int cnt,NSString *basePath)
// 全メンバー名ファイルを削除する
// メモリ確保の関係でサブルーチンにする(deleteでは関係ないけど他と合わせている)
{
    NSLog(@"deleteClassSub:%d",cnt);
    for (int i = 0; i < cnt; i++) {
        delete1(object,basePath,ivar_getName(ivars[i]));
    }
    // 全出力終了
    return(YES);
}

BOOL deleteClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容ファイルを削除する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// クラス名ディレクトリも消す
{
    NSLog(@"deleteClass:%@",object);
    
    // 全メンバ情報を得る
    unsigned int cnt;
    Ivar *ivar = class_copyIvarList([object class], &cnt);
    //
    BOOL ret=deleteClassSub(object,ivar,cnt,basePath);
    //
    // 保存ディレクトリも削除する : basePath/クラス名
    NSString *dir=[NSString stringWithFormat:@"%@/%@",basePath,NSStringFromClass([object class])];
    rmdir(nsStringTocString(dir));
//    unlink(nsStringTocString(dir));    // これではディレクトリは削除できない
    NSLog(@"削除ディレクトリ:%@",dir);
    //
    free(ivar);
    NSLog(@"---------------");
    return (ret);
}

//--------------------------------------------------------------

#if 0
static const char * getPropertyType(objc_property_t property)
// 正しく取り出せないことがあるので封印(未解析)
{
    const char *attributes = property_getAttributes(property);
    printf("型=%s\n",attributes);
    char buffer[1 + strlen(attributes)];
    strcpy(buffer, attributes);
    char *state = buffer, *attribute;
    while ((attribute = strsep(&state, ",")) != NULL) {
        if (attribute[0] == 'T' && attribute[1] != '@') {
            return (const char *)[[NSData dataWithBytes:(attribute + 1) length:strlen(attribute) - 1] bytes];
        }
        else if (attribute[0] == 'T' && attribute[1] == '@' && strlen(attribute) == 2) {
            return "id";
        }
        else if (attribute[0] == 'T' && attribute[1] == '@') {
            return (const char *)[[NSData dataWithBytes:(attribute + 3) length:strlen(attribute) - 4] bytes];
        }
    }
    return "";
}
#endif

NSArray *propertyNames(id object)
// オブジェクトの全プロパティ名を得る
// !=メンバ名なので注意
{
    unsigned int cnt;
    NSMutableArray *ary = [NSMutableArray array];
    objc_property_t *properties = class_copyPropertyList([object class], &cnt);
    for (int i = 0; i < cnt; i++) {
        objc_property_t property = properties[i];
        const char *name = property_getName(property);
        if (name) {
            [ary addObject:CstringToNSString(name)];
        }
    }
    free(properties);
    NSLog(@"プロパティ群=%@",ary);
    return ary;
}

NSArray *memberNames(id object)
// オブジェクトの全メンバ名を得る
{
    unsigned int cnt;
    NSMutableArray *ary = [NSMutableArray array];
    Ivar *ivar = class_copyIvarList([object class], &cnt);
    for (int i = 0; i < cnt; i++) {
        Ivar iv = ivar[i];
        const char *name = ivar_getName(iv);
        if (name) {
            [ary addObject:CstringToNSString(name)];
        }
    }
    free(ivar);
    NSLog(@"メンバ名群=%@",ary);
    return ary;
}

/*
 property_getAttributes()の返してくる文字列
 返ってくるのは@propertyされているもののみ
 
 T    先頭
 @"〜"    クラス名    "〜"がないときはid
 i        signed int/NSInterger
 I        unsigned int
 c        signed char/BOOL
 C        unsigned char
 q        long long
 Q        unsigned long long
 f        float
 d        double

 N    nonatomic
 C    copy
 &    retain
 R    readonly
 何もなしはassign,readwrite,atomic
 
 Vの直後からメンバ名
 */

NSDictionary *propertiesAttributes(id object)
// 全プロパティ情報を得る
{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    unsigned int cnt;
    objc_property_t *properties = class_copyPropertyList([object class], &cnt);
    for (int i = 0; i < cnt; i++) {
        const char *attributes = property_getAttributes(properties[i]);
        const char *name          = property_getName(properties[i]);
        printf("型=%s/名前=%s\n",attributes,name);
        [dic setObject:CstringToNSString(attributes) forKey:CstringToNSString(name)];
    }
    free(properties);
    NSLog(@"プロパティ=%@",dic);
    NSLog(@"-------------------------");
    return dic;
}

NSDictionary *memberAttributes(id object)
// 全メンバー情報を得る
{
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    unsigned int cnt;
    Ivar *ivar = class_copyIvarList([object class], &cnt);
    for (int i = 0; i < cnt; i++) {
        const char *encode = ivar_getTypeEncoding(ivar[i]);
        const char *name   = ivar_getName(ivar[i]);
        printf("型=%s/名前=%s\n",encode,name);
        [dic setObject:CstringToNSString(encode) forKey:CstringToNSString(name)];
    }
    free(ivar);
    NSLog(@"メンバー=%@",dic);
    NSLog(@"-------------------------");
    return dic;
}

//--------------------------------------------------------------










2014年3月5日水曜日

クラスの内容をファイルに出力する方法(1)

とあるクラスの内容をファイルに出力しようと思った。
NSArrayなどNS標準クラスならwriteToFile一発だが、自分で作ったクラスの場合はそうは行かない。
そこで、いろいろ調べて&考えてそのための関数を作った。

GitHubで公開すればいいのかもしれないが、それほど完成度は高く無いと思うので、ここで公開することにする。

まずは、どんな関数を作ったか、その一覧を公開する。

・・・

BOOL saveClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を保存する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダが出来、その下にクラス名別ファイルが作成される)
// リターン値:YES=書き込めた、NO=書き込めなかった

BOOL loadClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容を復帰する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// リターン値:YES=読み込めた、NO=読み込めなかった

BOOL deleteClass(id object,NSString *basePath)
// 指定オブジェクトの全メンバーの内容ファイルを削除する
// object : 対応オブジェクト
// basePath : 保存フォルダ(この下にクラス名フォルダがあり、その下にクラス名別ファイルがあること)
// クラス名ディレクトリも消す
// リターン値:YES=削除できた、NO=削除できなかった


NSArray *propertyNames(id object)
// オブジェクトの全プロパティ名を得る
// !=メンバ名なので注意

NSArray *memberNames(id object)
// オブジェクトの全メンバ名を得る

NSDictionary *propertiesAttributes(id object)
// 全プロパティ情報を得る

NSDictionary *memberAttributes(id object)
// 全メンバー情報を得る

・・・

なお予め断っておくと、

1. 標準NSクラスでも出力できないものがある
2. クラス名の最大長に制約がある

である。1は私が使わないと思うクラスまで処理を入れても大きくなるだけで無駄と判断したからである。2はプログラムを簡略化するための手抜き。定数1つ変更するだけで拡大できる。その辺りが「完成度が高くない」という理由であるが、その辺りは実際に使う人が改良すればいいのではないかと思う次第。

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のプログラムでは考えられない動作なので理解が難しい。
(一旦抜けた関数の、さらにその中の一部の処理だけに戻ってくるというのはありえない。)

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

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

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
「メモリ管理」も参照のこと。



2013年8月8日木曜日

Objective-Cの勉強(5);super



super



クラスを継承した子クラスでは、親クラスを同じ名前のメソッドを作り、処理を置き換えることが出来る。これをオーバーライドという。
オーバーライドした時でも、親のメソッドを呼び出す必要がある場合は「super」を使う。
(親クラスは英語で「SuperClass」と書く。子クラスは「SubClass」。)

@interface SuperClass : NSObject
// 変数がないので{}は省略
- (void)method;
@end

@interface SubClass : SuperClass
// 変数がないので{}は省略
-(void)method; // 同一名関数を作りオーバーライドする
@end

@implementation SuperClass
- (void)method
{
    printf("SuperClass.method\n");
}
@end

@implementation SubClass
-(void)method
{
    printf("SubClass.method\n");
    [super method]; // [SuperClass method]を呼び出す
}
@end

void CallMethod(id obj)
{
    [obj method];
}

int main()
{
    CallMethod([SuperClass alloc]); // [SuperClass method]が実行される
    CallMethod([  SubClass alloc]); // [SubClass method]が実行される→その中で[SuperClass method]も呼び出される
    return 0;
}

実行結果(未確認)
 SuperClass.method
 SubClass.method
 SuperClass.method

こうした時、CallMethod()は引数によって呼び出し関数を変更することになる。
Cではswitch caseか関数のアドレスを持つ配列を使った呼び出しUByte (*fnc)()[]を使ったが、
その辺りの記述が大幅に簡潔になる。
superはselfとは違いアドレスを示すものではなく予約語なので、super->という使い方は出来ない。

2013年8月1日木曜日

Objective-Cの勉強(4);メソッド実体実装


メソッド実体実装



メソッドの実体実装は、@implementationで行う。

@implementation クラス名
+(リターン値)関数名:(引数型)引数    // クラスメソッド
{
    return(リターン値); // 当然(void)なら省略
}
-(リターン型)関数名:(引数型)引数    // インスタンスメソッド
{
    return(リターン値);
}
命名規則はCと同じだが、習慣的にメソッド名は小文字から始めるらしい。
(Cとしての通常変数名とメソッド名は同じになっても良い。でも分けた方がわかりやすいのは当然。)


クラス宣言と実体記述は、ファイルを分離するのが「原則」である。
ファイル名は同じで、拡張子はヘッダーは.hでCと同じだけど、実体は.mと異なる。
実はXcodeでは.cと.mでコンパイル時の挙動がかなり変わる。
自動的にimport/includeするファイルが異なり、また.c内では、ごく一部を除きObjective-Cによる拡張部分は使えない。
<UIKit.h>のimportも許されないし、@"~"もエラーになる。と言うことは、それを使うNSLog()などの関数も使えないと言うことを意味する(代わりにprintf()を使う)。
先に書いたとおり、BOOLも.c内では使えない。
例外は、私の知る限りでは#importくらいである。これはプリプロセッサ命令だからだと思される。
逆に、.m内ではCの全てが使える。従って、基本的には.mで統一しておいた方が面倒がなくて良い。


Objective-Cではインスタンスもしくはクラスにメッセージを送ることでメソッドを実行させる。
この「メッセージを送る」式はCに対して拡張された部分なので、識別できるように[]で括る。これをメッセージ式という。
あるメソッド中で同じクラス内の別のメソッドを呼び出す場合は[self 関数]と記述する
Objective-Cにおいて、メソッドの呼び出しは基本「オブジェクトへのメッセージ送信」の形を取るからである。
自分自身クラス中である場合にはselfが必要となる(メソッド名(引数)という形で呼び出すことは出来ない)。
「基本的に」Cで記述された部分とObjective-Cで記述された部分は切り離されているからである。
その垣根を越える方法はあるが、難しいので省略。

//                                _____引数型
-(int) CheckDropPoint:(int)x arg2:(int)y
//                    ~~|~~  ~|~~
//                   引数型  第2引数ラベル
{
    ~
    return(1);
}
- (int) Cascades
{
    while ([self CheckDropPoint:x arg2:y]==0) { // ←ここ要注意
        r=[super getScrn:x+1 arg2:y];           // ←親クラスのメソッドに対してはメッセージで送る
        ~
    }
    return(0);
}

クラスインスタンスを保存しておく必要がない場合は、その確保宣言そのものを省略して、
[[Class alloc] method]
と記述することも出来る。


プログラムの実行がmain()からなのはCと同じ。
iOSアプリケーションの場合はもう1段上のクラスからの呼び出し処理が入るのだけど、
その辺りは「アプリケーション実装の勉強」で。

int main()
{
    id obj = [Class alloc];
    [obj method];
    return (0); // iOS上ではexit()は使えない
}
メソッドは、クラスを@implementationで実装すれば使えるようになる。
処理実体がメモリ上に存在するということである。
しかし、@implementationしてもメンバー変数は確保されない。
クラスインスタンスの実確保は別途行う必要があるということである。
メンバー変数にアクセスするメソッド=インスタンスメソッドはクラスの実体が確保されるまで実行できないが、クラスの構造だけに依存するメソッド=クラスメソッドはこの時点で実行できる。
この「実体の存在」の認識はObjective-Cでは非常に重要となる。



式の記述上は似ているメンバー変数とグローバル変数には、実は根本的な違いがある。
「メンバー変数はアドレスが固定でない」ということである。


グローバル変数は、コンパイラがコンパイル時にそのアドレスを確定する。
(正確にはリンカーやOSがプログラムをメモリ上に読み込み際に確定される)。
だから、プログラム内どこからアクセスしても同じアドレスにアクセスする。
しかし、クラスメンバはインスタンスによってアドレスが変わる。
だから、記述上は同じに見えても実行時のアクセスという意味では全く異なるわけである。


Cで構造体の制御関数を作るときは、必ず、引数に構造体(多くの場合そのアドレス)を渡すことが必須となる。
制御関数内でのメンバーへのアクセスは、その引数のアドレスからの相対アドレスによる。
ところがObjective-Cのクラス定義では@implementationにクラスのアドレスを渡す記述はない。ないが、内部的には、クラス内のメソッドが実行されるときには、自動的にその時使うクラスインスタンスのアドレスが渡される。
逆に言えば、クラスインスタンスのアドレス確定=領域確保は絶対必須であるということである。


クラス実装はあくまでひな形であって、クラスそのものの領域確定宣言そのものではない。このあたり、慣れるまではクラス実装を書いただけで、インスタンスがあるような錯覚を起こすことがあるので要注意である。


ここからは少し、高度なお話。
iOSは実はかなり高度にマルチタスクを行う。少なくとも、メソッド単位ではマルチタスクが基本になっているので、「このメソッドを呼び出して、返ってきてからこの処理が走って・・・」と考えてはいけない。メソッドを呼び出したら、帰りを待たずに呼び出し側は次の処理を実行してしまう、こともある。どこが待ってどこが待たないかは実のところはっきり理解してないのだが、たぶん「リターン値を持たないメソッドは並行動作する」で間違いないと思う。
少なくともUIKitは呼び出したら即並行動作するので、「表示されたはずだから」と想定してその後の処理を書いてはいけない。
表示が完了したらデリゲートが呼び出されるので、そこで後の処理を書く必要がある。
あまりに高度な並行動作なので、頭の中でなかなか追い切れなかったりする。
自分でマルチスレッドなどを書いたらさらに複雑になるわけで、μITRONのようなイベントドリブン型マルチタスクに慣れているなら、考え方を根本的に変える必要があるかもしれない。(私がそうだった)。
絶対に処理終了を待つ必要がある場合は、別途フラグなどで判定して待つ必要がある。
しかし、ループで単純に待ったら他のスレッドが走らなくなるので、いろいろと対策を入れる必要がある。

拙作X-BASIC for iOSはマルチタスクでないプログラム動作を実現するために、このあたりがかなり複雑になっている。実はBASICのコア自体がバックグラウンドで動いてたりするのだが、その辺の話は、そうさなぁ、1万本くらい売れたら書くと言うことで(^_^;)。


2013年7月23日火曜日

Objective-Cの勉強(3);クラスの宣言



クラスの宣言



クラスはObjective-Cの肝であり、Cで言う所の「構造体」を大幅に機能/思想拡張した物である。
クラスは通常何らかの親クラスからの継承で作られる。


表面上、何も継承することがない場合でも、クラスの基本的機能を内包させるため、NSObject(一部NSProxy)を親にする。
親クラスを書かない場合は、それらのクラスが持つべき全てのメソッドを自前で実装する必要があるので、よほど腕に自信がない限り行うべきではない。
メーカー提供のクラスは必ずNSObject(相当)を親にしている。



クラスの宣言には@interfaceコンパイラディレクティブを使う。
クラス名は習慣的に大文字から始めるらしい。

@interface クラス名:親クラス名
{
    変数型 メンバー変数宣言;
}
メソッド宣言;
@end
「例」
@interface AClass : NSObject
{
    id var;
}
-(void)setVar: (id)v;
@end

メンバー変数を持たないクラスの場合、{~}の部分は省略できる。
変数の型はCと同じ基本の型(int等)、もしくはid型を含む後述のクラス型となる。



クラス内で実装されるメソッドにとってメンバー変数は、プログラムの記述上はグローバル変数とほぼ同じ扱いができる。
これは楽そうに思えるが実は、「ローカル変数を使うつもりだったが実は未宣言で、同名メンバーがクラス内にある場合、コンパイルエラーにならない」ので要注意である。このあたりは命名規則を作って避けるようにしなければならない。



NSObjectから継承した場合、メンバーにはid self;というものも自動的に定義される。
self->メンバー名でメンバーに確実にアクセスできる。
メソッドの引数がメンバー名と重複する場合に有効であるが、これはCと同じで引数がグローバル変数を隠蔽してしまう「バグの元」なので、別の名前にするのが本筋であろう。慣れるまでは。

2013年7月12日金曜日

Objective-Cの勉強(2);メソッドの宣言



メソッドの宣言



メソッドは以下の書式で宣言する。
関数のプロトタイプ宣言に近い。Objective-Cでは関数名から引数のラベル名までを含め全体を「メッセージ」と呼ぶ。
 
 -(リターン型)メソッド名:(引数型)引数;                       // 1引数の場合
 -(リターン型)メソッド名:(引数型)引数1 label2:(引数型)引数2; // 2引数の場合
 -(リターン型)メソッド名:(引数型)引数,...;                   // 可変引数の場合

リターン型は必ず()で括る。なぜかというと、Objective-Cの基本型はintではなくid型というものになっているからである。要するに、この()はid型からの型キャストを示しているのである。事実id型なら()も要らないし、省略も出来る。しない方が良いけど。

id型はクラスに対し、動的に(=実行時に確定される)型や内容を変更することが出来る万能型。int等基本型の代わりにはできないけど。
idの本性は、オブジェクトのアドレスを示すポインターである。typedefで*を含めて名称定義してあるようなので、*は省略する。


引数の区切りは「:」であり「,」ではない。「,」は可変引数を示すからであるが、可変引数はあまり使われない。Apple提供のクラスでも、それほど多くは出てこない。
Objective-Cでは、同名メソッドを引数別に定義出来るため、必要性が少ないのだろう。
可変引数の取り出し型は、Cと同じくva_listとva_start()を使う。


2引数以上の場合、引数名にラベルを付けるのが「習慣」だそうな。ラベルはその直後の変数の物。すなわち、Objective-Cではメソッド名は第1引数のラベル、その後各引数にラベルを付けるということだ。ラベルなのでラベル名と:に間にスペースを入れてはいけない。
Objective-CはCからの拡張実装であるから、すでにそれが持っているラベル認識機能をうまく使った実装であると言えよう。
(実はラベルは省略可能だが「推奨されていない」。)


ラベル名を変えると、同じメソッド名でも異なるメッセージとして扱われる。
これはオーバーライドとはちょっと違い、メッセージの受信オブジェクトの変更になる。


メソッドはメンバー変数名と同じであってもかまわない。通常メンバー変数は外部から直アクセスさせることはないかららしい。
ただ、初心者は分けた方が迷わなくて良いかもしれない。



ラベル名はCの予約語名とは同じでも大丈夫だが、#define定数名と同じではいけない。
プリプロセッサの方が先にソースを通すためである。


1つのメソッドで複数の引数が同じラベル名を持っていても良いようである。
NSObjectには-(id)performSelector: withObject: withObject:というメソッドが存在したりする。
が、あんまり使うべきじゃないのでは、と思ったりする。



先頭の「-」はインスタンスへのメソッドであることを示す。
他に「+」というのもあって、これはクラスに対するメソッドであることを示す。
インスタンスメソッドは実体を持ったオブジェクトに対して実行される関数であり、クラスメソッドはクラスに実体を持たせる時に実行させる関数、もしくはクラスの特性をして持っておくべくメソッドと理解すればいい。


インスタンスメソッドはselfおよびメンバー変数にアクセスできるが、クラスメソッドはそれがアクセスできない。当然コンパイルするとエラーが出る。
クラスメソッドはクラスを実体化=インスタンス化するときなど、インスタンスがない状態でも呼び出されるからである。



init=初期化されていない/またはalloc=確保されていないクラス内のメソッドを呼び出すとどうなるか。
実は何も起こらない。エラーも何も発生せずに素通りしてしまう。Objective-Cでは初期化してしていないクラスはnilであり、nilへの呼び出しは全て無視するようになっているからである。
もし実装したはずのメソッドが実行されないときは、そのインスタンスが正しく初期化されているか確認すべきである。



2013年7月6日土曜日

Objective-Cの勉強(1);基本

しばらくの間、Objective-Cを勉強した時にまとめた文章をアップします。
全て独学で覚えたので、世間一般の認識とは違う部分があると思います。
私はアセンブラ→C言語→Objective-Cあがりで、特にCで考えることに慣れているため、それとの対比を多く行っています。


基本


まずは用語と基本を覚える。Objective-Cおよびオブジェクト指向言語特有の名称が多いが、覚えやすくするためにCの名称に置き換えることもする。その対応表の作成は後日に行うかもしれない。
要するにここはC言語は習得しているが、C++は勉強したけどあきらめた人(=用語ぐらいは少しは解る)向けで、「普通」とは異なるので注意(自分以外の誰が読んでんねん、という突っ込みは無視^_^;)。


まずは、絶対覚えなければならない基本用語。
オブジェクト メモリ上に配置されるワークエリアの一般名称
クラス オブジェクトのひな形(構造定義書)
Cの構造体に近く、その中に制御関数;メソッドも含む
メソッド クラスの専用制御関数
インスタンス クラスを実際にメモリ上に割り付けた「オブジェクト」実体
インスタンス化  クラスを定義しただけでは構造体宣言と同じく実体は存在しないので、
それを元に変数を宣言、実体をメモリ上に確保すること
継承 あるクラスの機能を受け継いでで新規のクラスを定義すること
親クラス(スーパークラス)の全ての機能を受け継いだ上で、新機能または置き換え分のみ実装すればいい
オーバーライド 既存メソッドを同名メソッドで再実装すること。
基本的には、子クラスで親クラスに存在するメソッドを同名メソッドで再実装する。
レシーバー メソッドを実行するインスタンスのこと。
Cの構造体だって関数をメンバーに出来るので(関数だって所詮はアドレスに過ぎないから)、よく言われる「構造体とクラスはメソッドの有無が違い」というのは実は間違い。


Cの構造体のメンバー関数は「呼び出す」というし、アセンブラレベルでもCallするが、Objective-Cのメソッドは「メッセージを送る」という。
実はObjective-Cの基本にして重要な思想がここに現れている。Cのメンバー関数は(基本的に)構造体に固定されているが、メソッドは受信するオブジェクトを選択することが出来る。

異なるクラスに同じメソッド(名)を実装した場合、それは「同じメッセージを受信できるクラス群」として扱われる。そして、あるメッセージを送信した場合、それを実際に受信するクラスによって動作を切り替えることが出来る。

Cでなら、同じメンバー名関数を持つ構造体群があったときに、場合によって構造体側を切り替える場合にはswitch caseか関数の実行アドレスを持つ配列に入れて切り替えるしかないが、Objective-Cでは受信するクラス名を変数に出来るため、このあたりが実に簡潔に記述できる。
この両者の「基本概念の向きの違い」を覚えておかなければならない。それが「オブジェクト指向」である(この場合「思考」の方が合ってそうだけど)。



Objective-CはCの完全な拡張であるので、一部に制約が付いたC++とは異なり、Cで使えた書式は全て使える。
変数の型も同じだが、typedefにより、見かけ上いくつか追加がある。
id オブジェクトを示す汎用変数(正確にはインスタンスのアドレス=ポインタ)
Class  クラスオブジェクト(クラスの定義内容そのものを持つオブジェクト)へのポインタ
SEL メソッドのセレクタ値
IMP メソッドの実行アドレス(ポインタ)
BOOL YES/NOのどちらかの値を持つ型


また、以下の定数もobj.hをimportすることで使える(標準定数)。
定数名用途・意味定義
nil CにおけるNULLをidに拡張したものと思えば正解。終端やエラーを示すのに多く用いられる(id)NULL
YES/true BOOLの肯定(BOOL)1
NO/false BOOLの否定(BOOL)0
NSNotFound 要素を探したが見つからないとき開発環境によって異なる
もちろんCの標準定数(NULLなど)も使える。
BOOLはCでも定義して使っている人が多いけど、私は使わない主義であった。
関数の改良時によく、それ以外のリターン値を返すようにしてたからである。でも今は「区別しといた方がいい」派になっている。「年々歳々人同じからず」である。



余談。実はCocoa Touch上には真偽値を示す型が複数ある。BOOL/Boolean/boolである。NS/UIクラスで使われているのはBOOL、しかし、plistという物で使われているのはBooleanであったりする。
Boolean/boolではtrue/falseが値として使われるが、YES=true/NO=falseである。
boolがISO Cの標準真偽値型のようなので、正確にはifで判定されるのはbool型だと言えるが、混在してもコンパイラのチェック上でも動作でも特に問題は起こさない。まあ、NS/UIクラスと同じくBOOLで統一しておくのが無難かも知れない。BOOLはObjective-Cのソース、すなわち.m内でのみ使える。BooleanはCのソース、すなわち.Cでも使える。C内ではBOOLはエラーになる。Booleanはどちらでも使えると思う。
実際の値は同じだが、相互に使える使えないがあるので、注意が必要である。




Cの関数も全くの変更なしに呼び出すことが出来る(C++はextern "C"でヘッダーの書き換えが必要)が、一部機能は強化されている。
(古い)CとObjective-Cの細かい違いとしては、
  • ヘッダーは「基本的に」#includeではなく#importで読み込む
  • プロトタイプ宣言が必須(前方参照不可)Xcode4.3では同一ファイル内では自動的に前方参照が解決されるようになった。
  • 一部の例外を除き、変数宣言をスコープの先頭で行う必要がない
と言うのがある。

#importはCのヘッダーで問題になっていた2重読み込みを自動的に排除してくれる。
ただし、#defineで定数の定義内容を書き換える(切り替える)タイプのヘッダーを#includeする場合は、#importに変更するとバグになる可能性がある。
Cのヘッダーは#includeで、Objective-Cのヘッダーは#importで読み込むのがいいかもしれない。


メソッドは、基本的に宣言と実体実装を別ファイルにするため、宣言ファイル(ヘッダー)を#importしておけばいいが、通常のC関数も、それがプロトタイプ宣言されているヘッダーを読み込んでおく必要がある。


(本当はクラスの独立性を高めるためらしいが、)ヘッダーを#importするのが面倒な場合で、クラス名であることを宣言するだけで良いなら、
  @class クラス名1,クラス名2...;
というコンパイラディレクティブを使って宣言しておける。Cにおけるexternに近い。
「コンパイラディレクティブ」とは、Objective-Cで拡張されたプリプロセッサ命令である。プリプロセッサ命令であるため、コンパイル時には実内容に展開される=静的に決定される。


古いCの場合、ローカルに新規の変数を使う場合は必ず新しいスコープの先頭(宣言文以外が出てくるまで)に宣言しなければならなかったが、Objective-Cではこれがかなり緩くなる。本当は最近のCの規格ではそういうものらしい。
ただし、一部例外がある。

Cの場合         Objective-Cの場合
{               {
int x;              int x;
    x=1;            x=1;
    {               int y; // 必要時に宣言すればよい
    int y;          y=x+1;
    y=x+1;
    }
}               }

Cの場合         Objective-Cの場合
switch (x) {      左に同じ
  case 1:         switch内だけはスコープを新規に作る必要がある
     {              (厳密にはcase文の直後に宣言するときのみ、新規スコープの宣言が必要。)
     int y;       "Switch case is in protected scope"エラーが発生することもあるが、発生しないことのほうが多い。
     y=x+1;       
     }
}
という具合である。ただし、正確には上の例は変数yのスコープが異なる。Cの場合の記述は{}の中だけで有効だが、Objective-Cの記述では宣言以降全てである。従って、厳密にスコープを規定する場合は、Objective-CでもCと同じにする。単に「使うときに宣言したい」という意味の時のみ書き換えることが出来る。


各種名称の命名規則は基本的にCと同じであるが、「習慣的」には推奨がある。
クラス 大文字から始める
メソッド 小文字から始める。_(アンダーバー)から始まる名称は禁止(アップル予約)
カテゴリ 大文字から始める
プロトコル 大文字から始める
ラベル   小文字から始める
型名 クラス名に準じる(だから見分けが付かなくてややこしい)
変数 小文字から始める
定数 Cの場合大文字だけど、Objective-Cはクラス名に準じる(だから見分けが付かなくてややこしい)

ただし、Google推奨記法ではkで始めると書いてある。アップル標準もそういうのが多い。


また、「同じ名前を付けたときにどうなんねん」については、以下の規則がある。
  • メンバー変数名とメソッドの引数が同名の場合は、メンバー変数名が隠蔽される(要注意)。この場合、self->で回避できる(メンバーアドレスへのアクセスであり、プロパティ式ではない)
  • メソッド名および引数ラベル名は予約語名と同じでもかまわない(int:とか)
  • メソッド名および引数ラベル名は#define定数名と同じではいけない。プリプロセッサの方が先にソースを通すため
  • クラス同じ名前のグローバル変数はあってはいけない(コンパイルエラー)
  • 異なるクラス間では同じメソッド名を使うことが出来る
  • 継承したクラスが親と同じメソッド名を持つ場合は、親のメソッドの代わりに実行される=オーバーライド
    親のメソッドを実行する場合は、superを使う
  • 継承したクラスに親と同じメンバー変数名を持ってはいけない(コンパイルエラー)
  • 異なるクラス間では同じメンバー変数名があってもかまわない
  • メンバー変数とメソッド名が同じでもかまわない。というか積極的にそうしてメンバー変数への直アクセスをさせないようにする=プロパティ
  • インスタンスメソッドには、クラスメソッドと同じ名前を付けてもいい(対象によって自動的に切り替わる)
  • プロトコルには、クラス、カテゴリ、その他のものと同じ名前を付けてもいい
  • 1つのクラスのカテゴリには、他のクラスのカテゴリと同じ名前を付けてもいい
この他はCと同じ。詳細はその都度。
Objective-Cでは同じ名前でも、機能さえ異なればちゃんと識別される場合が多いということだ。
同一機能であれば同じ名前にしておくとわかりやすいかもしれないが、だからといって全然異なる機能に同じ名前を付けるとバグの元なので避けるべきである。


2012年12月11日火曜日

Objective-Cでの文字列系オブジェクト変換

Objective-Cでの文字列系オブジェクト変換。
X-BASIC for iOSの開発中にいろいろ出てきたのでまとめておく。

NSString*からchar*への変換
    NSString* str = @"AAA";
    char* cp = [str UTF8String];


char*からNSString*への変換
 char* cp = "AAA";
 NSString* str = [NSString stringWithCString: cp encoding:NSUTF8StringEncoding];

NSStringからCFStringRefに変換
 CFStringRef aCFString = (CFStringRef)aNSString;

CFStringRefからNSStringに変換
 NSString *aNSString = (NSString *)aCFString;

Cでの記述部分とObjective-Cでの記述をリンクするときにはこのあたりの変換が必須となる。

追記:
stringWithFormat@"%s"ではCの文字列("〜")を受けられるが、
UTF8には対応していないので、ASCIIでない文字は化ける。
UTF8対応するにはstringWithCString:encoding:が必要となる。

2012年3月17日土曜日

methods named 'xxx' found

プログラムを組んでいると、「methods named 'xxx' found」という警告がいくつか出た。

直訳すると「xxxという名前のメソッドが複数見つかった」となろうが、
それならエラーになるはずではないか。


よくよく調べると、id型のクラスインスタンスに対してメソッドを発行しようとすると
発生する(ことがある)とわかった。

「例」
タッチイベントを処理する
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
 NSTimeInterval tm=[[touches anyObject]timestamp]; // ここでmethods named 'timestamp' foundが発生する


}

touchesに実際に格納されるのはUITouchであるが、[touches anyobject]はid型で返す。
timestampという名前のメソッドは複数のクラスが持つので、
どれか解決できずに警告が出てしまう。

実行時にはNSSetの内容は決定しているので、[touches anyobject]のidが実際は何であるか、
この場合UITouchであることがわかっているのでtimestampを呼び出す先もわかる=実行時エラーはでないのである。

警告を回避するには
 NSTimeInterval tm=[(UITouch *)[touches anyObject]timestamp];
として型を確定してやればよい。ポインター型でキャストすることに注意。

Xcodeのエラーコードはわかりにくい(その上ドキュメントがない)ので困る。