tech.kayac.com

tag: AdventCalendar2011

この記事は「Android Advent Calendar 2011 」の裏エントリとして書いています。

17日の表のエントリは@out_of_kayaさんの「見せてもらおうGNのフェイスアンロックの性能とやらを! - だらだらいこうぜ」 前の日の記事は同僚の@9reによる「Support PackageとMapView _level0 Kayac Interactive Designer's Blog

最近は時間を見つけてChttp serverを書いている亀田@Gemmbu)です。

今回のネタは簡単にいうとlibcurlをiPhoneアプリからつかう方法 (ついでにopensslも)の Android 版です。

背景

Android/iOS の開発をしていると同じ機能をそれぞれ Java/Objective-C で開発することになります。 たいていの場合どちらかで作成したものを移植するのですがテストの手間を考えると頭が痛くなります。

マルチプラットフォーム開発ツールではちょっと力が足りないと感じる場面があったり、開発ツールを学ぶ時間確保するところから始めなければなりません。

それならいっそ、両環境で共通に使えるCのライブラリでやるのがいいんじゃないでしょうか?

前提

以後の作業はすべて OSX 10.6 上で行っています。 途中Android のソースをコンパイルします。そのため xcodeは3系である必要があります。

... 早くxcode4系に対応してくれないと、Lion にいけなくてiCloudが使えないよ ><

openssl のビルド

opensslは普通のライブラリの流儀にそっていなくてビルドするのはちょっと大変です。 しかしながら、 Android のソースのexternalに含まれています。これを利用することにしましょう。

android open source projectの[以下のページ](http://source.android.com/source/downloading.html)の手順にしたがってソースを取得しましょう。 結構時間がかかるので気軽に待ちましょう。

ここからは取得したソースのルートを

<ANDROID_SRC_ROOT>

として話をすすめます。

さあビルドを...の前に openssl と crypto が静的ライブラリを出力するように<ANDROID_SRC_ROOT>/external/openssl/Android.mk に下記を追記しましょう。

# for libcrypto.a
include $(CLEAR_VARS)
LOCAL_SRC_FILES:=
LOCAL_C_INCLUDES:=
LOCAL_WHOLE_STATIC_LIBRARIES += libcrypto_static
LOCAL_MODULE:= libcrypto
include $(BUILD_STATIC_LIBRARY)

# for libssl.a
include $(CLEAR_VARS)
LOCAL_SRC_FILES:=
LOCAL_C_INCLUDES:=
LOCAL_WHOLE_STATIC_LIBRARIES += libcrypto_static libssl_static
LOCAL_MODULE:= libssl
include $(BUILD_STATIC_LIBRARY)

これで準備ができました。 android open source project以下のページの手順に従いビルドを実行してください。

正しくビルドが完了すると以下のファイルが生成されているはずです。

  • <ANDROID_SRC_ROOT>/out/target/product/generic/obj/STATIC_LIBRARIES/libcrypto_intermediates/libcrypto.a
  • <ANDROID_SRC_ROOT>/out/target/product/generic/obj/STATIC_LIBRARIES/libssl_intermediates/libssl.a

Androidプロジェクトの作成

eclipse等からAndroidプロジェクトを作成します。 ここからは作成したプロジェクトのルートを

<PROJECT_ROOT>

として話をすすめます。

次に以下フォルダ/ファイルを作成し、jniを利用する準備をします。

  • <PROJECT_ROOT>/jniを作成します。
  • <PROJECT_ROOT>/jni/Application.mkを作成します。
  • <PROJECT_ROOT>/jni/Android.mkを作成します。

Application.mkの内容は以下となります

APP_MODULES := \
libcurl

# APP_OPTIM := release
APP_OPTIM := debug

Android.mkの内容は以下となります

include $(call all-subdir-makefiles)

libcurl のビルド

次に目的のlibcurlをビルドします ダウンロードページから最新のソースを取得します。

<PROJECT_ROOT>/jni/curl-*.**.*以下に展開し、以下のようなスクリプトを作成し叩きます。 NDK_ROOT以下から取得し展開したディレクトリを指定します。 ANDROID_SRC_ROOT<ANDROID_SRC_ROOT>を指定します。

#!/bin/sh

NDK_ROOT=/path/to/your/ndk/root
NDK_VER=8
ANDROID_SRC_ROOT=/path/to/your/android/src/root

PATH=$NDK_ROOT/toolchains/arm-linux-androideabi-4.4.3/prebuilt/darwin-x86/bin:$PATH

./configure \
--with-ssl \
--disable-ftp \
--disable-file \
--disable-ldap \
--disable-ldaps \
--disable-rtsp \
--disable-proxy \
--disable-dict \
--disable-telnet \
--disable-tftp \
--disable-pop3 \
--disable-imap \
--disable-smtp \
--disable-gopher \
--build=`./config.guess` \
--target=arm-eabi \
--host=arm-eabi \
LD=arm-linux-androideabi-ld \
AR=arm-linux-androideabi-ar \
AS=arm-linux-androideabi-as \
NM=arm-linux-androideabi-nm \
RANLIB=arm-linux-androideabi-ranlib \
SIZE=arm-linux-androideabi-size \
STRIP=arm-linux-androideabi-strip \
CC=arm-linux-androideabi-gcc \
CXX=arm-linux-androideabi-g++ \
OBJDUMP=arm-linux-androideabi-objdump \
CPPFLAGS="-I$NDK_ROOT/platforms/android-$NDK_VER/arch-arm/usr/include/ -I$ANDROID_SRC_ROOT/external/openssl/include/" \
CFLAGS="-nostdlib" \
LDFLAGS="-Wl,-rpath-link=$NDK_ROOT/platforms/android-$NDK_VER/arch-arm/usr/lib/ -L$NDK_ROOT/platforms/android-$NDK_VER/arch-arm/usr/lib/ -L$ANDROID_SRC_ROOT/out/target/product/generic/obj/STATIC_LIBRARIES/libssl_intermediates/ -L$ANDROID_SRC_ROOT/out/target/product/generic/obj/STATIC_LIBRARIES/libcrypto_intermediates/" \
LIBS="-lc -lz"

以下の文言が出力されているのを確認してください。

SSL support:     enabled (OpenSSL)

次は<PROJECT_ROOT>/jni/curl-*.**.*/Android.mkの修正です。

curlには既にビルドするためのAndroid.mkが用意されているのですがSSLに対応していません。 そこで以下の設定を追加します。ヘッダファイルの検索先にopensslを追加します。

LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/ \
/path/to/your/android/src/root/external/openssl/include/

また、実行ファイルは必要ないため

# Build the curl binary

以下を削除します。

ターミナルから<PROJECT_ROOT>/jni/に移動し、以下のコマンドを実行してください。

$ ndk-build

以下の文言が出力されていれば正しく作成されています。

StaticLibrary  : libcurl.a

libcurl を実際に使用する

あとは普通のJNIプログラムと同様にJavaとの連携部分を Cでごりごり書いていくだけです。

そのため<PROJECT_ROOT>/jni/external/libを作成し、libssl.aおよびlibcrypto.aをコピーしたうえで、 libcurl とリンクさせるために以下のようなAndroid.mkを書く必要があります。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := libhttpclient

LOCAL_LDLIBS := -lz
LOCAL_LDLIBS += -Lexternal/lib/ -lcrypto -lssl

LOCAL_STATIC_LIBRARIES := \
libcurl

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/../curl-*.**.*/include \
/path/to/your/android/src/root/external/openssl/include/

LOCAL_SRC_FILES :=  \
com_kayac_curltest_HttpClient.c

include $(BUILD_SHARED_LIBRARY)

まとめ

以上のような方法で、curlおよびopensslがAndroidアプリから利用できるようになります。

このようにビルドさえ出来てしまえば既存のライブラリをそのまま使用できるのはJNIのメリットと言えるでしょう。 特にこのような枯れたライブラリの再発明をJavaでやる必要はありません。どんどん車輪を使って効率の良い開発をしていきましょう。

カヤックではスマフォやるならCだよねって開発者を求めています!

最近Androidとの抗争が激化しているago@kyo_ago)です。

jQueryはCSSセレクタを多用する特徴がありますが、jQuery内では実行ブラウザやCSSセレクタの記述によって呼び出されるブラウザAPIが変わり、それによって実行速度にも影響が出ます。

この記事では「セレクタAPIとはなにか」、「CSSセレクタの記述によって呼び出されるセレクタAPIの種類」、「高速なセレクタAPIを使用するための方法」、「高速なセレクタAPIが使われるかどうか確認する方法」などを紹介したいと思います。

(※この記事はJavaScript Advent Calendar 2011 (フレームワークコース) : ATNDの1日目の記事です)

セレクタAPIとはなにか

セレクタAPIとは「#hoge .huga」のようなCSSセレクタから、DOM上に存在する要素を取得するためのAPIです。

jQueryではセレクタAPIは本体に統合されているため普段意識することは少ないかもしれませんが、CSSセレクタから要素を取得する場合はすべてこのセレクタAPIを経由して要素を取得します。

例えば以下のようなコードの場合、最大で4回セレクタAPIが呼び出されます。

$('#hoge') // 1
    .find('.huga') // 2
    .find('div') // 3
    .find('[rel="hoge"]') // 4
;

CSSセレクタの記述によって呼び出されるAPIの種類

jQueryのCSSセレクタ処理は大きく分けてブラウザネイティブAPIを使う場合と、JavaScriptで記述されたSizzleを使用する場合に分けられます。

querySelectorAllが使えるブラウザ(Chrome 1, Firefox 3.5, IE8, Safari 3.5, Opera 10、それぞれこれ以降のバージョン)では渡された引数が文字列かつセレクタとして認識できる形式の場合以下のような優先度で処理を行います。

  1. #idや.class等の簡単な記述は専用の高速なAPIを使用
  2. (1)以外でquerySelectorAllで解釈してエラーにならない場合その結果を使用
  3. (2)でエラーになる記述はSizzleを使用(非標準の記述が入っているなどの場合)

これはquerySelectorAllが使えるブラウザの例ですが、querySelectorAllが使えないブラウザでは2の処理がなくなり#idの場合とSizzleの場合にわかれます。

querySelectorAllとは

querySelectorAllとはブラウザ内蔵のCSSセレクタAPIで、jQueryのfindのように第一引数にCSSセレクタを渡すことでCSSセレクタに一致する要素を配列で返します。

// 「#hoge .huga」に一致する要素の配列
document.querySelectorAll('#hoge .huga');

これはjQuery内部でも使われており、複雑な記述のCSSセレクタは主にこのAPIで処理されます。

querySelectorAllで解釈できない記述

jQueryはCSSセレクタとして通常ブラウザが解釈できるCSSセレクタの他に、以下のようなjQueryが独自にサポートするCSSセレクタの記述を持ちます。

:animated, [name!="value"], :button,
:checkbox, :contains(), :eq(), :even,
:file, :first, :gt(), :has(), :header,
:hidden, :image, :input, :last, :lt(),
:odd, :parent, :password, :radio, :reset,
:selected, :submit, :text, :visible

これに関してはquerySelectorAllが解釈できないため、たとえquerySelectorAllが使用できるブラウザであっても必ずjQuery内部のSizzleを使用して解釈されます。

Sizzleとは

SizzleとはもともとquerySelectorAllがサポートされないブラウザ用にjQuery内部で使用されていたセレクタAPIを切り出したもので、CSSセレクタを受け取ってそれに一致する要素を返すライブラリです。

実際には高速化等の目的で他にもいろいろなコードが入っていますが、簡単に言うとgetElementsByTagName('*')で取得した要素一つ一つに対して、指定されたCSSセレクタに一致するかをJavaScriptで確認し、一致したものを配列にして返すようになっています。

このように内部がJavaScriptで記述されているため、独自の記述等をサポートすることが可能ですが、ブラウザネイティブのAPIに比べて速度的に遅くなるという問題があります。

高速なセレクタAPIを使用するための方法

ここまでセレクタAPIの内部実装に関して紹介してきましたが、ここからはそれを踏まえてjQueryがより高速なAPIを使用できる使い方を紹介します。

高速なセレクタと低速なセレクタでAPIの実行を分ける

jQueryの各APIは引数として渡されたCSSセレクタ全体がquerySelectorAllで実行可能かどうか判定してquerySelectorAllを使うかSizzleを使うかの分岐を行うので、渡されたCSSセレクタのうち一箇所でもカスタムセレクタが含まれていると全体をSizzleで評価します。

このため、CSSセレクタのうち一部分だけカスタムセレクタを使用する場合、その部分のみAPIを変えて検索することで高速化することが可能です。

// カスタムセレクタが含まれるため、全体がSizzleで評価される 
$('#hoge .huga:checked');

// ここはdocument.querySelectorAllで実行される
$('#hoge .huga')
    // ここはSizzleで評価される
    .filter(':checked');

また、querySelectorAllが使えない環境も考慮すると、以下のようにidとclassを分けることで高速化する場合もあります。
(ただ、APIを分けすぎるとAPI呼び出し自体のコストやコードの可読性にも影響するため、やりすぎると逆効果になる場合もあります)

// これはdocument.getElementByIdで評価される
$('#hoge')
    // これはブラウザによってはSizzleで評価される
    .find('.huga');

ショートカットを活用する

jQueryは基本的にquerySelectorAllを使用して要素を取得しますが、一部の記述にはより高速なAPIを使用して要素を取得します。

具体的には$('#hoge')や$('.huga')だけの場合はquerySelectorAllではなく、それぞれより高速なgetElementById、getElementsByClassNameを使用するようになっています。

このうち、getElementByIdはjQueryでサポートしている全ブラウザで使用可能なため$('#hoge')形式の場合は常にgetElementByIdが使用されますが、getElementsByClassNameは使用できるブラウザが少ないため、あくまでもquerySelectorAllが使用できる場合のみの高速な代替としてのみ使用されます。

このため、以下のような高速化が可能になります。

// document.querySelectorAllで解釈される(低速)
$('div#hoge');
// document.getElementByIdで解釈される(高速)
$('#hoge');

// document.querySelectorAllで解釈される(低速)
$('div.huga');
// document.getElementsByClassNameで解釈される(高速)
$('.huga');

jQueryセレクタAPIのベンチマーク - jsdo.it - share JavaScript, HTML5 and CSS

ただし、後者のclass名で指定する例に関して、IE6,7などのgetElementsByClassNameが使用できない環境でSizzleを使用して解釈するため以下のように高速な場合と低速な場合が逆転します。

// document.getElementsByTagName('div')後、クラスの比較を行う(高速)
$('div.huga');
// document.getElementsByTagName('*')後、クラスの比較を行う(低速)
$('.huga');

高速なセレクタAPIが使われるかどうか確認する

「あるCSSセレクタがどの程度の速度で実行されるか」は、実行されるブラウザ、実際のDOM環境等によって左右されるため一概に判断はできません。

ただ、基本的にSizzleはquerySelectorAllより遅いため、「あるCSSセレクタがSizzleを使って解釈されるか否か」は速度に関する一つの指標となります。

「あるCSSセレクタがSizzleを使って解釈されるか否か」はquerySelectorAllを使って解釈できるかどうか(エラーが出るかどうか)で判断できるため、「querySelectorAllではエラーになるけど、jQuery()に渡すと結果が返ってくる」CSSセレクタはSizzleで解釈されていると判断できます。

// jQueryのカスタムセレクタなのでエラー 
document.querySelectorAll(':input');
// jQueryでは解釈できるためSizzleが使用されている 
$(':input');

この方法でSizzleが使用される範囲を限定していくことで、querySelectorAllを活用しつつ、jQueryカスタムセレクタの柔軟な記述も行うことができます。

カヤックではAndroidと戦う技術者も募集しています!