Android 標準ブラウザの canvas 利用時のメモリリーク
- HTML5 APIs
- UI ライブラリ
- テスト支援
- ドキュメント
- コード品質ツール
- バリデータ
- キー入力支援
- テストダブル
- ハイブリッドアプリケーション
- 継続的インテグレーション
Android 標準ブラウザの canvas 利用時のメモリリーク
Android 標準ブラウザで canvas を利用したときに発生するメモリリークについて調査した結果をまとめたページです。
- 調査時期
- 2013 年 12 月
メモリリークの概要
canvas で最初に getContext('2d') したときに canvas の面積に応じたメモリが確保されるのですが、
Android 4.1.1 の標準ブラウザ では、ページをリロードしても getContext('2d') で確保されたメモリが解放されないという問題を見つけました。
※確認した機種が Galaxy Nexus だけなので機種依存の問題である可能性もあります
メモリリークが発生する条件
メモリリークが発生する条件について以下に整理しました。
メモリリークが発生する環境
いまのところメモリリークが発生する条件のうち「環境」に関しては絞り込むことができていません。
メモリリークが確認できたのは以下に挙げるように Android 4.1.1 & Galaxy Nexus の環境のみとなっております。
メモリリークが発生した環境
- Android: 4.1.1
- 端末: Galaxy Nexus
- ブラウザ: Android 標準ブラウザ(com.android.browser)
メモリリークが発生しなかった環境
- 同じ端末の Google Chrome
- Android 2.3.6(Galaxy S III) の標準ブラウザ
- iPod(iOS 6.1.2)
メモリリークが発生するコード
メモリリークが発生する環境では、以下のコードにように canvas に対して getContext('2d') を呼ぶと canvas の領域に応じてメモリが確保され、
以降はリロードや context や canvas の参照をはずしをしても確保されたメモリが解放されません。
// ここで指定した領域の大きさに応じてリークするメモリの量が変化する
canvas.width = 1000;
canvas.height = 1000;
// getContext('2d') したときにリークする
var context = canvas.getContext('2d');
// null を代入して参照をなくしてもリークする
canvas = null;
context = null;
// リロードしてもメモリが解放されない
setTimeout(function () {
location.reload();
}, 5000);
上記のような JavaScript を実行するページを標準ブラウザで開き、
adb shell 内で dumpsys meminfo com.android.browser によって標準ブラウザのメモリ使用量を確認すると、
getContext('2d') した時点で Pss の Unknown が大きく増加し、リロードしてもその値をほとんど減ることなく
getContext('2d') をするごとに増え続けていきます。
メモリリークした結果起こること
メモリリークでアプリのメモリ使用量が増加していき getContext('2d') 時に必要なメモリが確保できなくなると canvas の描画ができなくなります。
一度描画できなるなると getContext('2d') を実行してもメモリが増加しなくなるため即座にブラウザが落ちるわけではないですが、
アプリを起動したままだとメモリも解放されないので、 そのサイズの canvas の描画ができないままになります。
また、 canvas の描画ができない場合でも JavaScript 側でエラーが発生しません。
adb logcat の方にも GC 以外はなにも出力されていませんでした。
以下は実際に image を canvas に書き込むサンプルですが、
何度かリロードしてメモリが確保できなくなると sample.png の canvas への描画ができなくなります。
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no">
<title>OpenSeadragon Sample</title>
</head>
<body>
<div id="content" style="background-color:black;"></div>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script>
var image = new Image();
image.onload = function () {
var canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
var context = canvas.getContext('2d');
context.drawImage(image, 0, 0);
$('#content').append(canvas);
// リロードごとにメモリ使用量が増えていく
setTimeout(function () {
location.reload();
}, 5000);
};
// ある程度大きな画像(width, height が大きな画像)を読み込む
image.src = 'sample.png';
</script>
</body>
</html>
リークするメモリ量
以下はメモリリークが発生した Galaxy Nexus で canvas の width と height を変化させながら
一回の getContext('2d') ごとにリークするメモリ量を測定した結果になります。
- 1000x1000: 4 MB
- 2000x2000: 16 MB
- 1000x4000: 16 MB
- 4000x4000: 62 MB
上記の結果からリークするメモリ量は 1 ピクセルあたりおよそ 4 byte となっており、
1 ピクセルごとに「3色 + 透明度」の4つをそれぞれ 1byte で表現したときに必要なメモリ量とほぼ一致しています。
リークしたメモリの測定方法
adb shell の中で dumpsys meminfo com.android.browser を実行したときの Pss の Unknown が
「getContext('2d') + リロード」の後にどれぐらい増加したかを見ています。
対策
今のところ該当する環境での有効な対策は見つかっていません。