2012/11/22

【22日目】prototype vs Proxy


この記事は、@astronaughtsさんが主催の11月1日~30日まで毎日誰かがTitanium Mobileに関する記事を書くイベント「Titanium mobile “early” Advent Calendar 2012」向けにまとめたものです。



prototypeによる継承


prototypeといえば、JavaScriptで「クラス」継承のようなことをするための仕組みとして有名です。CommonJS のモジュールとしてまとめると、以下のようにして SuperClass からSubClass を作ります。


SuperClass.js

(function _SuperClassModule() {
    function _f() {
        Ti.API.debug("superclass constructed!");
        // init properties here using this keyword
        /* example
        this.no = 1;
        */
   }
   _f.prototype.classname = "superclass"; // custom defined property for debug
    // init methods here using _f.prototype

    /* example
    _f.prototype.getNo = function() { return this.no;};

    */

    // export
    module.exports = _f;
}).call(this);

SubClass.js

(function _SubClassModule() {

    var superclass = require("ui/common/SuperClass");
    function _f() {
        _f.prototype._superconstructor.apply(this, arguments);
        Ti.API.debug("subclass constructed!");
        // init properties here using this keyword
    }
    _f.prototype = Object.create(superclass.prototype);

    _f.prototype._superconstructor = _f.prototype.constructor;

    _f.prototype.constructor = _f; // for subclass of this subclass

    _f.prototype.classname = "subclass"; // custom defined property for debug
    // init methods here

    // export
    module.exports = _f;
}).call(this);

prototype により、プロパティチェーンをたどって、オブジェクトのメソッドやプロパティにアクセス可能にして、あたかも関数オブジェクトがクラスであるかのように振る舞うように見せかける仕組みです。ちょっと工夫している点は、 
`_f.prototype._superconstructor`

という独自のプロパティを prototype に定義して、コンストラクタ役の関数 _f で、スパークラスのメソッドの名前を指定しなくても良くしている点です。 また、 
_f.prototype.constructor
に対して、new _f() を使う流儀もありますが、直接 _f を設定しているのは、関数オブジェクトを増やさないための工夫です。

SubClassのインスタンスを生成するには、

var SubClass = require("SubClass"); 
var sub = new SubClass();

のように、require でモジュールを読み込んだ上で、new を用いて生成します。これで、SuperClass を継承した関数オブジェクトのインスタンスが生成されます。 通常の JavaScriptのオブジェクトであれば、prototype を使ったこの方法が一般的だと思われます。

Proxyオブジェクトの継承(擬)


Ti.UI, Ti.UI.Viewなど、Titanium の組み込みモジュールやオブジェクトは、純粋な JavaScriptのオブジェクトではなく、Ti.Proxy を継承したKroll 由来の独自のオブジェクトです。 そのため、これらを継承して新しいオブジェクトを作るには、上記の prototype を使った手法が使えません。そもそも protptypeという独特なプロパティを持たないからです。 そこで、次のようにして、継承のまねごとをしてみます。

`CustomView.js`
(function _CustomViewModule(){ 
    function testFunc(factor) { 
        Ti.API.debug("CustomView testFunc"); 
        return this.testProp * factor; 
    
    function _f(opt) { 
        var self = Ti.UI.createView(opt); // create View instance as example Ti.Proxy 
        // init properties and methods here using variable self 
        self.testProp = 1; 
        self.testFunc = testFunc; 
        Ti.API.debug("castomview constructed!");             return self; // return instance of Ti.Proxy 
    
    module.exports = _f;
}).call(this);

 `SubCustomView.js ` 


(function _SubCustomViewModule(){ 
    var CustomView = require("ui/common/CustomView"); 
    function testFunc(factor) { 
        Ti.API.debug("SubCustomView testFunc"); 
        return this.testProp * factor * factor; 
    
    function _f() { 
        var self = _f._superConstructor.apply(this, arguments); 
        // init properties and methods here using variable self 
        self.testFunc = testFunc; 
        Ti.API.debug("subcastomview constructed!"); 
        return self; 
    
    _f._superConstructor = CustomView; 
    module.exports = _f;
}).call(this);

Proxyオブジェクトは、Java や Objective-C のネイティブオブジェクトを JavaScript から使えるようにするためのオブジェクトです。純粋な JavaScript のオブジェクトではないとはいえ、JavaScript らしい性質は持っています。Proxy オブジェクトはユーザー定義のプロパティをいつでも追加できる仕組みになっています。この性質を利用して、独自のプロパティ(関数オブジェクトを設定すれば、メソッド)を自由に設定できます。

ただ一点うまくいかないのは、Proxy オブジェクトがあらかじめ持っているメソッドの変更や変数への代入ができない点です。これは、Proxy オブジェクトの定義済みメソッドがJavaScript の関数オブジェクトではないからだと考えられます。

Proxy に対して、独自に関数オブジェクトをプロパティとして追加し、呼び出すことには問題ありません(上のコードでは意味のない関数ですが View オブジェクトに testFunc を追加したり、オーバーライドしたりしています)。

prototype による継承の場合と違って、Proxy の継承の場合、コンストラクタ役の関数が Proxy オブジェクトを返さなければなりません。

return self;

を忘れないように(prototype による継承の場合、return が必要ないのは、値を返さないJavaScript 関数は、関数オブジェクト自体を返すからです)。

上記の例では、

var SubCustomView = require("SubCustomView");
var sub = SubCustomView();


のようにしてオブジェクトを生成します。new を使わなくてよい点に注意してください(new しても、表面上の動作に違いはありませんが、コンストラクタ役の関数オブジェクトが裏で生成されれる点が異なります)。

生成されたオブジェクトは Ti.UI.View オブジェクトのインスタンスにすぎないので、View や Window に add したり、通常の場合と同じ扱いができます。

View の画面上の配置、View のコントロールとしての表現、ビジネスロジックなどなど、それぞれ階層分けして組み立てるのに、この方法が使えるかもしれません。

(しかし、よく観察してみると、View に対するプロパティの組み立てを、いくつかに分けて実施するということにすぎないこともわかります。Alloy なども、煎じ詰めれば、同じことやっていることになりますね。)




Proxyオブジェクトの継承(擬) その2


前の例では、メドッドをオーバーライドする際、対応するプロパティを書き変えてしまっているので、もはや親クラスのメソッドへアクセスすることができません。 これに対する、方法もありますが、説明が長くないそうなので、GitHub にコードをあげておきました。 スーパークラスのチェーンを作って、それに基づいて、メソッドを呼び出せる仕組みになっています。興味のある方はご覧ください。

TitaiumProxyInheritance

参考文献

ECMA–236



おまけ




2012/09/06

Android : Canvas: trying to use a recycled bitmap

Android で、Bitmap をごにゅごにゅしていると、
 java.lang.RuntimeException:"Canvas: trying to use a recycled  bitmap"
ってのに、出くわすことがあります。Web でいろいろ調べてみると、怪しげな解説がかなりあるので、本当のところ何が正しい理解なのかをまとめておきたいと思います。

そもそも、Bitmap#recycle() をやらないといけないのは、主に Android のこの辺の実装がよろしくなくて、recycle() してやらないと、メモリリークになるよ、といわれていることが、大きな要因です。勢い、メモリリーク怖さになんでも recycle() しようとして、件の例外に遭遇することとなる訳です。しかし、どういったケースでそれが必要になるのか、筋の通った記事をあまり見かけません。


まず、誰が  ”trying to use a recycled  bitmap” といっているのかといえば、ログのいう通りCanvas な訳です。自分の書いたソースコードに Canvas が使われてなくても、Canvas は、Android OS の画面描画に使われる訳ですから、画面を描き変える要素があれば、必ず働いてくれています。

よく、
recycle() をしたら、”trying to use a recycled  bitmap” が発生
というように表現されることがありますが、recycle() の中で例外が発生している訳ではありません。recycle() した結果、その後で、recycle された Bitmap が使われる段になって、Canvas が、
既に recycle されてんじゃん
って、文句を言ってる訳です(*1)。

さて、自分で描画しようとしている訳でもないのに、誰が Canvas さんに描いてくださいといっているかといえば、大抵の場合、ImageView がいっている訳です。
Bitmap のインスタンスを ImageView に対して、setImageBitmap() すると、ImageView は、当然 Bitmap を描画しようとします。なので、ImageView の外側で、Bitmap のインスタンスに対して、recycle() すれば、ImageView が描こうとした段階で、件の例外発生と相成る訳です。

ということで、Bitmap#recycle() する前に、インスタンスをセットした ImageView に対して、ImageView#setImageBitmap(null) をしておくことが必要です。これで、件の例外は発生しなくなるでしょう。めでたしめでたし ....

と、そうは問屋が卸さないので Android のおもしろいところで、実は ImageView#setImageDrawable( null ) をするのが正解です。

ImageView#setImageBitmap() のソースコードを見ると

    public void setImageBitmap(Bitmap bm) {
        // if this is used frequently, may handle bitmaps explicitly
        // to reduce the intermediate drawable object
        setImageDrawable(new BitmapDrawable(mContext.getResources(), bm));
    }

となっていて、BitmapDrawable のインスタンスが必ず生成されて、それが setImageDrawable() に渡されています。


    public void setImageDrawable(Drawable drawable) {
        if (mDrawable != drawable) {
            ...(中略)...
            updateDrawable(drawable);
            ...(中略)...
        }
    }
    private void updateDrawable(Drawable d) {
        if (mDrawable != null) {
            mDrawable.setCallback(null);
            unscheduleDrawable(mDrawable);
        }
        mDrawable = d;
        ...(中略)...
    }
こうなっているので、setImageDrawable() の引数に null を指定してやると、ImageView のmDrawable に null がセットされ、参照が削除される訳です。

さて、GCがあるのだから、インスタンスへの参照さえなくなれば、問題なくきれいさっぱりメモリ上から消えてくれるはずです。実際大抵の場合そうなってくれます。

ところが、いろいろな事情でインスタンスへの参照が残ってたりすることがままあります。例えば、Activity のインスタンスを Context としていろいろなところに渡していたり、わざわざ、Context として自分自身への参照をメンバ変数に持っていたり、そういった場合、Activity のインスタンスの破棄が、思ったタイミングで起こらないこと請け合いです(*2)。
Bitmap のインスタンスへの参照をメンバ変数として持つ場合も、所有者である(例えば、Activity )のインスタンスの破棄の際にうまく破棄されないことが起こりえます。
いわゆる Finalizer の罠(*3)があるからです。

なので、onDestroy (*4)時などに、もう使わないだろう参照を含む全てのメンバ変数は null にしておくにこしたことはありません。そして、Bitmap に関しては、null にする前にrecycle() しておくのが良いと思われます。そして、もしそれがImageViewで使われているなら、recycle する前に、ImageView に対して setImageDrawable(null) を実行しておくべきです。

と、ガイシュツな結論に達した訳ですが、仕組みを理解した上で、使うことが大事ですね。きっと。

(注)

*1 興味のある人は、Canvas.javaのソースコードリーディングをしましょう。
*2 もちろん、Activityのインスタンスは一定期間破棄せず、再利用するのがAndroid流です。
*3 Finaliserの罠については、たとえば、ここを参照。
*4 onDestroyに関するリファレンスの記述がとても気になる人は、状況が許すのであれば、onPauseあるいはonStopで行うのがいいのかもしれません。




2012/02/18

Titanium for Androidでスプラッシュスクリーンを使わない

先日の「Titanium Mobile」で作るiphone/Androidアプリ勉強会で、Titanium for Androidでスプラッシュスクリーンが醜い、なんとかしたい、という話題がでました。で、いろいろ調べてみました。結論は、
1.7.5の場合、<Titanium SDK>/mobilesdk/osx/1.7.5/android/build.py の L.862 にある部分を
<resources>
<style name="Theme.Titanium" parent="android:%s">
<!--
    <item name="android:windowBackground">@drawable/background</item>
    -->
</style>
</resources>
のように書き換えれば、スプラッシュスクリーンが表示されなくなります。
めでたしめでたし。
1.8.xの場合 L.905 あたりに同じ箇所があるので、同様にすれば、いいと思われます。

しかしなんで、こんなところに theme.xml の元データが埋め込まれちゃってるのか ... ぜひとも、外持ちのファイルにしておいてほしいものです。