2013年8月6日火曜日

世界平和とAndroid

このエントリーをはてなブックマークに追加
エンジニアの草苅です。
スマートフォンを扱うエンジニアの皆さんは、日々Android のバグに悩まされているのではないかと思います。弊社も類に漏れず様々な Android のバグと戦っています。

特にあんさんぶるガールズ!ではアニメーションはすべて Canvas を利用していることもあり、Android の Canvas 絡みのバグに、頭を痛めています。

Android のバッドノウハウは悩んでいる人みんなで共有した方が、世のため人のためになるのではと思い立ったので、世界平和を願っていくつかまとめてみたいと思います。

1. GPUレンダリングの設定によって Canvas で不具合が発生する

Android は OS のバージョンや、WebView のレンダリングエンジンの違いによって、GPUレンダリングOFFの場合に、Canvas が正常に表示できない端末、もしくはGPUレンダリングONの場合に、Canvas が正常に表示できない端末が存在します。

このような問題は、できれば Android OS のバージョンで統一しておいて欲しいのですが、どうも機種依存のようなので、弊社では機種に応じて GPUレンダリングONなのか、OFFなのかを個別で指定しています。

いくつか実例を挙げますと、現在のドコモのツートップ Xperia A と Galaxy S4は GPU レンダリングOFFでなければ WebView 上の Canvas は正常動作しません。それとは逆に、Nexus 7 や Arrows X は GPUレンダリングON でなければ WebView 上の Canvas は正しく表示されません。また、同じ OS のバージョンであっても Galaxy Nexus はGPUレンダリングONだとクラッシュし、Nexus 7はGPUレンダリングONでなければ Canvas が正常に表示されないということが発生します。

そのため、あんさんぶるガールズ!では、ONまたはOFFでしか動作しない端末については、次のようなメソッドを使って、アプリ側で強制的にGPUレンダリング設定を変更しています。


protected void setHardwareAccelerationEnabled(boolean enable) {
  if (enable) {
    getWindow().setFlags(
        WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
        WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
  } else {
    // 下位互換性を保つため、以下のコードをリフレクションで実行
    // webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    try {
      Method setLayerTypeMethod = webView.getClass().getMethod("setLayerType", new Class[] {int.class, Paint.class});
      setLayerTypeMethod.invoke(webView, new Object[] {View.LAYER_TYPE_SOFTWARE, null});
    } catch (NoSuchMethodException e) {
      // Older OS, no HW acceleration anyway
    } catch (IllegalArgumentException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      e.printStackTrace();
    }
  }
}


2. GPUレンダリング設定以外に Canvas 周りのバグがある

Android 4.0.4 には Canvas がブラウザを巻き込んでクラッシュするというバグがあります。このバグは上記のバグと違い、WebView では再現しにくいのですが、ブラウザではかなりの頻度で発生します。

例えば Galaxy S3、Xperia SX、Xperia GX などの端末が Android 4.0.4 を搭載しています。CreateJS のコミュニティにもこの問題は上がっており、かなりの期間対処法がなかったのですが、その後、Canvas を clearRect する際、縦横に 1px だけ加えたサイズをクリアするようにすることでクラッシュしなくなるという、華麗なテクニックが編み出され、この方法が EaselJS 0.6 系に取り込まれました。

CreateJS のコミュニティの該当エントリー
http://community.createjs.com/discussions/easeljs/220-samsung-galaxy-s3-stock-android-404-browser-freezescrashes-on-stageupdate

修正のコミットログ
https://github.com/CreateJS/EaselJS/commit/7c02f0d4a7e50908b284623d23e6897f15e3bff4


また、Galaxy S4(Android 4.2.2)に関しては WebView 上で表示させたときだけ、Canvas が正しく表示されない不具合があります。
GPUレンダリングONの場合は、Canvas 全体が常に水色表示で何も出ず、OFFの場合はアニメーションが表示されたり、されなかったりがかなり不安定になってしまいます。もろもろ調べていると、日本語で次のようなブログ記事を見つけました。

Galaxy S4のWebviewで、非同期処理の中でのCanvasの描画がバグってる - 車輪を再発明 / koba04の日記
http://d.hatena.ne.jp/koba04/20130629/1372437341

この記事を見ながら「最新端末で動作しないアプリって…」と絶望していたのですが、諦めず調べていると安定してアニメーションを表示できるスピリチュアルな方法を、編み出すことができました。それはアニメーション表示直前に Canvas をクリアすることです。CreateJS であれば、Stage を作って、まずクリアしてからアニメーションを実行すると WebView 上の Canvas でも安定してアニメーションを表示することができました。

var canvas = document.getElementById('canvas');
var stage = new createjs.Stage(canvas);
stage.clear();
// clear した後アニメーションのコードを実行

3. GPUレンダリングOFFで特定のCSSが激重

さて、Canvas を正常に表示するためにGPUレンダリングを強制OFFにしたまでは良かったのですが、それに伴いスクロールが激重になってしまった端末があります。

例えば、今年の初めに発売されてツートップの発売前までドコモが推していた Xperia Z という端末は、WebView 上の Canvas を正常に表示するために GPUレンダリングを強制OFFにしたまでは良かったのですが、ほとんどのページでスクロールが激重になってしまいました。

いろいろ調べていると、shadow 系などアルファブレンディングを行うような css が GPUレンダリングOFF だと重くなってしまうようです。参考までに以下のような記事があります。

[CSS] border-radiusとbox-shadowを組み合わせると、それぞれ単体でのスタイルより5倍重たい!? - YoheiM .NET
http://yoheim.net/blog.php?q=20130713

単純に考えると、GPUレンダリングOFFの端末のみ、これらの css の重いプロパティを使用しない css で上書くというのが良さそうです。

Android には幸いなことに、Java Object を JavaScript Object として WebView に渡せるという、セキュリティ問題がとても発生しやすい、さわやかな機能があるので、これを利用します。そして、css を読み込む際、この WebView がGPUレンダリングOFFであれば、上書きする css を読み込むようにしました(以下は簡易的に書いたサンプルです)。

// this を droid という名前で JavaScript からアクセスできるようにします
webView.addJavascriptInterface(this, "droid");

// GPUレンダリングOFFのときのみ、上書き用のCSSを読み込みます。
if (!droid.webView.isHardwareAccelerated()) {
  var fileref = document.createElement("link");
  fileref.setAttribute("rel", "stylesheet");
  fileref.setAttribute("type", "text/css");
  fileref.setAttribute("href", '<%= path_to_stylesheet 'android_without_hardware_acceleration' %>');
  document.getElementsByTagName("head")[0].appendChild(fileref);
}


4. キーボード入力時に上下に揺れる端末

Android にはテキストボックスに文字を入力しようとすると、文字入力のたびになぜか画面がスクロールして、激しく上下に縦揺れするという恐ろしい端末があります。

例えばこちらに不具合が上がっています。
https://github.com/scottjehl/Device-Bugs/issues/32

あんさんぶるガールズ!では当初、コメントはすべてポップアップ画面でその場で入力できるようにしていましたが、この不具合に対応するため、Android はすべて画面遷移でテキストボックスが1つだけある画面に遷移した後、コメントを入力するように変更しました。さらに、テキスト入力中にスクロールが発生しないように、テキストボックスにフォーカスが当たった際、JavaScript でスクロールをなくす処理を入れています(以下はサンプルコードです)。

.stop-scroll {
  outline: none;
  overflow: hidden;
}

$('input[type=text]').on 'focus', (e)->
    $('body').addClass('stop-scroll')

$('input[type=text]').on 'blur', (e)->
    $('body').removeClass('stop-scroll')


おわりに

最後にうちのチームの優秀なアルバイトエンジニアの心の叫びを掲載して、世界平和を切に願いたいと思います。



Happy Elements 株式会社では世界平和を目指すエンジニア社員、およびアルバイトを募集しています。

Find Job!

Wantedly

Happy Elements株式会社 JOBS