ログをファイルに記録する
こんにちは、サンプルアプリ開発者の山田健一です。
Monaca のデバッグログは便利な機能ですが、リリースしたアプリの動作確認には使用できません。そこで、アプリからスマートフォンのファイルにログを書き込む手法を紹介します。
筆者が作成中の GPS アプリで 動作ログを取り、仕様を検討することに活用しています。
FileReader/FileWriter 利用のサンプルとしても活用してください。
リリースしたアプリの動作確認に使用することを念頭に置いているため、次のような機能とします。
- メッセージの先頭に 2012/02/22 16:07:20.664| のようにタイムスタンプを追加する。
- アプリ起動時にログファイルの有無を確認し、ログファイルがあった時は最初の行のタイムスタンプを確認し、アプリ起動と同じ日付なら、ログを追加し、違う日付なら、ログを上書きする。
FileReader が必要になるのはこのためです。
ファイルの読み書きを行うためには、対象となるファイルを特定する必要がありますが、PhoneGap では、ファイルを特定するまでに次の手順が必要になります。
ファイルを読む手順で解説します。
- window.requestFileSystem メソッドでファイルシステムを呼び出す
- ファイルシステムからディレクトリーエントリーを得る
- ディレクトリーエントリーからファイルエントリーを得る
- ファイルエントリーからファイルオブジェクトを作成
- FileReader オブジェクトを作成し、readAsText メソッドなどの読み込みメソッドを発行する
- onloadend イベントの中で読み込んだデータを取得する
上記の手順のほとんどがコールバック関数を用いた非同期処理なので、コードは複雑になってしまいます。
非同期処理とは、例えると次のようになります。
同僚に「この書類まとめておいて、できたら連絡して」と頼んで、自分は他の仕事を遂行します。同僚からの「依頼完了」の連絡があったら、まとめてもらった書類をもとにした処理を開始します。
すなわち、仕事を依頼した直後は、その仕事をもとにした処理はできないと言うことになります。
ファイルを読む手順で言えば、window.requestFileSystem メソッドを実行した直後はまだ、ファイルシステムが取得できていません。そのため、「ファイルシステムからディレクトリーエントリーを得る」コードは window.requestFileSystem メソッドの直後に書くことができません。成功コールバック関数が呼ばれてはじめて「ファイルシステムからディレクトリーエントリーを得る」処理を実行できるので、このコードはコールバック関数に記述する必要があります。
「ファイルシステムからディレクトリーエントリーを得る」処理も非同期処理なので、さらに複雑になってきます。
これらの処理手順を図にまとめると次のようになります。
なお、図中にある「PhoneGap の使用するルートディレクトリーエントリー」ですが、fullPath プロパティーを持っていますので、これを確認することで、実際のディレクトリーを知ることができます。
筆者の Galaxy S では /mnt/sdcard を返しました。
作成されたログファイルを確認する際は、このディレクトリーを確認することになります。
ここまでのコードは次のようになります。
window.addEventListener("load", function() {
//PhoneGapロード完了になったときに onDeviceReady 関数を呼ぶようにする
document.addEventListener("deviceready", onDeviceReady, false);
});
//共通変数
var LOGFILE_NAME = "logFile.txt";
var logFile;
var seekPos;
var logWriter;
//readyState = 1(WRITING)のため書き込めなかったメッセージ
var writeMiss = [];
//PhoneGapロード完了になったときに呼ばれる処理
function onDeviceReady() {
//ログ
console.log("start");
//ファイルシステムを呼び出す
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0,
onSuccessFileSystem, onFailFileSystem);
}
function onFailFileSystem(event) {
alert(event.target.error.code);
}
//ファイルシステム呼び出し成功
function onSuccessFileSystem(fileSystem) {
//rootディレクトリーからファイルエントリー取得
var dirEntry = fileSystem.root;
dirEntry.getFile(LOGFILE_NAME,
{create: true, exclusive: false},
onSuccessGetFile, onFailGetFile);
}
function onFailGetFile(error) {
alert("Failed to retrieve file: " + error.code);
}
//getFile成功
function onSuccessGetFile(parent) {
//FileWriter用にファイルエントリーを保存
logFile = parent;
//ファイルオブジェクト作成
parent.file(onSuccessFile, onFailFile);
}
function onFailFile(error) {
alert("Failed to file method: " + error.code);
}
//file成功
function onSuccessFile(file) {
var readText;
var reader = new FileReader();
reader.onloadend = function(evt) {
console.log("read success");
//読み込んだ結果
readText = evt.target.result;
//最初の日付を判断し、上書きか、追記かを決定
var logDate = readText.substr(0,10);
if (logDate < _getYyyymmdd(new Date())) {
seekPos = 0;
} else {
seekPos = file.size;
}
logFile.createWriter(onSuccessWriter, onFailWriter);
};
reader.readAsText(file);
}
function onFailWriter(error) {
console.log("onFailWriter:" + error.code);
}
//writer成功
function onSuccessWriter(writer) {
logWriter = writer;
writer.seek(seekPos);
//アプリの初期化処理
mainInit();
}
共通に使用する関数は後ほど解説します。
//readyState = 1(WRITING)のため書き込めなかったメッセージ
var writeMiss = [];
という共通変数はコメントにあるように WRITE busy に対応する為のものです。
ここまで準備が整っていれば、ログ書き出しのための関数は次のように定義できます。
/**************************************************
* [機能] ログファイルに書き込みます。
* [引数] msg ログに書き込むメッセージ
* [戻値] なし
**************************************************/
function _logWrite(msg) {
/************************************************************
* [重要]このコード作成時点でwriteメソッドにバグがあります。
* 書き込む文字列がマルチバイトを含んでいると、
* ポインターの移動が文字数(?)のため、次のwriteで
* 前のデータの一部が上書きされます。
* そのため、(バイト数-文字数)分の半角空白を追加します。
* PhoneGapのバグがなおったらこのコードは修正の必要があります。
*************************************************************/
//(バイト数-文字数)を求める
var plusBytes = _byteCount(msg) - msg.length;
var plus = "";
for (var i = 0; i < plusBytes; i++) {
plus = plus + " ";
}
var logMsg = _getYyyymmddhhmmssfff(new Date()) + "|" + msg + "\n";
var readyState = logWriter.readyState;
console.log(readyState);
if (readyState == 1) {
//書き込み中は配列に蓄える
writeMiss.push(logMsg);
} else {
//バグ対応のため余分な空白追加
logWriter.write(logMsg + plus);
}
コメントにもあるように執筆時点の PhoneGap には、write メソッドにバグがあります。
筆者がつたない英語で issue をあげていますので、いずれバグはなおると思います。その際は、コードを修正する必要があります。
本来であれば、write 完了を待って次の write を行うべきですが、ログという性格上、いつ write したい状況になるか分かりません。そのため、write しようとしたときにステータスが WRITING だった場合には、配列に蓄えるようにしています。
配列に蓄えたデータはアプリ終了時に書きだすようにします。そのためのコードは次のようになります。
<
//終了時の処理
//書き込みできなかったメッセージを書き込む
function onUnload() {
if (writeMiss.length == 0) {
logWriter.write("---正常終了---\n");
} else {
var lastMsg = "---busyで保留されたメッセージ---\n";
for (var i = 0; i < writeMiss.length; i++) {
lastMsg = lastMsg + writeMiss[i];
}
logWriter.write(lastMsg + "------\n");
}
}
このコードを終了時に実行させるためには html に次の記述が必要です。
<body onUnload="onUnload()">
日付の整形などを行っている共通関数は次のようになります。
/**************************************************
* [機能] 日時を書式化します。
* [引数] date 日付
* [戻値] yyyy/MM/dd HH:mm:ss.fff
**************************************************/
function _getYyyymmddhhmmssfff(date) {
var res = date.getFullYear();
res = res + "/" + _comPadZero(date.getMonth() + 1, 2);
res = res + "/" + _comPadZero(date.getDate(), 2);
res = res + " " + _comPadZero(date.getHours(), 2);
res = res + ":" + _comPadZero(date.getMinutes(), 2);
res = res + ":" + _comPadZero(date.getSeconds(), 2);
res = res + "." + _comPadZero(date.getMilliseconds(), 3);
return res;
}
/**************************************************
* [機能] 日時を書式化します。
* [引数] date 日付
* [戻値] yyyy/MM/dd
**************************************************/
function _getYyyymmdd(date) {
var res = date.getFullYear();
res = res + "/" + _comPadZero(date.getMonth() + 1, 2);
res = res + "/" + _comPadZero(date.getDate(), 2);
return res;
}
/**************************************************
* [機能] ゼロパディングを行います
* [引数] value 対象の文字列
* length 長さ
* [戻値] 結果文字列
**************************************************/
function _comPadZero(value, length){
return new Array(length - ('' + value).length + 1).join('0') + value;
}
/**************************************************
* [機能] 文字列のバイト数を返します。
* [引数] 文字列
* [戻値] バイト数
**************************************************/
function _byteCount(str) {
//urlエンコー ド
var ue = encodeURI(str);
//%の数を調べる
var per = ue.match(/%/g);
if (per == null) {
return str.length;
} else {
return ue.length - per.length * 2;
}
}
サンプルがダウンロードできますので、実際に動かしてみてください。
サンプルを起動すると次のようになります。
[ログ記録]ボタンをタップすると見た目の変化はないですがログを書き込みます。MonacaIDE のデバッグログの変化に注目してください。
サンプルのダウンロードはこちらからどうぞ。