Android ネイティブアプリ開発について調査し、このサイトを作成したそもそもの理由は、情報不足でした。
Visual C++ で自動生成されたコードにある関数の実行順、送られるメッセージの順序、途中経過の出力など、 ログが出力できないと、かなりの苦労をしなくてはなりません。
main.cpp には、LOGCAT と言われている Android のログを記録するための定義があります。
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "AndroidProject1.NativeActivity", __VA_ARGS__)) #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "AndroidProject1.NativeActivity", __VA_ARGS__))
しかし、いつの間にか統合環境から
Android 開発トップ にも書いていましたが、どうしてもログファイルを作成したく、 いろいろ試しましたので、このページに記録しています。
【 2021 年 3 月 1 日】
この記事の作成時には Visual C++ 2015 を使用していましたが、
現在は Visual C++ 2019 を使用しています。
Windows PC への環境のセットアップについては
「VS Community 2019 を共存インストール(デスクトップ)」
で無料で利用可能なコミュニティ版をインストールし、
「VS2019 の Android 開発環境を整える」
で Android 開発環境をセットアップしています。
久しぶりに新しい端末で開発を再開したところ、AndroidManifest.xml に記述する読み書きのパーミッションは、 Android 6 以降では、意味を持たなくなっていました。 ですので、下記のコードのみではログファイルは作成されません。
実行時にアプリの権限を確認し、権限がない場合はユーザーの問い合わせを出す必要があります。 が、C/C++ コードでそれを実現しようとしたところ、すぐにはうまくいきませんでした。
とりあえずの回避策としては、配布用アプリには使えませんが、端末にインストールしたアプリのアイコンを長押しし、 「アプリ情報」をタップ(出ない場合は「設定」アプリからアプリ情報)、 「権限」をタップして、「ストレージ」をオンにすることで、少なくとも開発時には利用可能となります。
追記 June 15, 2020
VC++ NativeActivity で、実行時にストレージの権限を確認し、必要なら問い合わせる Java コードと、
それを呼び出す JNI クラスをまとめました。
「動的にパーミッションを取得する」をご覧ください。
このページ、および開発関連ページは、PC 向けデザインとなっております。 画面サイズの小さいスマホでは、快適な表示が得られませんので、ご了承ください。
ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
また、本サイトが初めての方は、まずこのページの注意事項をご覧ください。
このセクションは、February 16, 2018 付けで Android 開発トップに記載していたセクションと同内容です。 コードに関する部分のみでしたら必要ありませんので、 次のセクションへスキップしていだいても構いません。
Android ネイティブアプリも、Windows と同じように、アプリにシステムからメッセージが送られて、それを処理する形で進むようです。 画面の作成に関しては、デバイスの画面サイズがいろいろあることや、1画面に1アプリであることなど(新しい Android OS ではそうとは限りません)、 画面に関する部分がかなり違いますが(OpenGLES を使って画面を描画するとか)、 まずはメッセージの流れを掴みたいと思い、標準的な出力コードで、ログを出力しました。
Android のログは logcat と呼ばれる部分に残る(見られる)ということなのですが、
システムのログも、プログラムのログもまとまってしまうため、自分のプログラムのものだけを確認しにくいです。
しかも Visual C++ 2015 の
Windows/MFC では、 GetLocalTime 関数で現在の時刻を取得して、 CString オブジェクトに書き込みたい文字列を作成し、 CStdioFile クラスでログファイルを開いて、 SeekToEnd 関数でファイル末尾まで移動して、 WriteString 関数でテキスト書き込みすれば、自由に簡単にファイルを作成、参照できます。
しかし Android アプリは、自由に書き込める場所が制限されています。 これをプライベートな領域と呼ぶならば、 「アプリ固有のファイルへのアクセス」に書いているように、 自分の(インストール先)フォルダ直下の files フォルダ内で自由に読み書きできます。
ファイル名をフルパスで用意して、 FILE* fp = fopen(ファイル名,"rb") でファイルを開き fread で読む、同様の方法で書き込む、というわけです。 しかし、この files サブフォルダの場合、 ES ファイルエクスプローラーのようなアプリでも、 USB 接続した PC からも、ファイルは見えません。 自由に見えなければ、ログの意味がありません。
他のアプリでは専用フォルダを作るものもあるようですが、とりあえずはログだけ見えればいいので、 Download のような、パブリックなフォルダにファイルを作成して、とりあえず見えるようにしよう、と考えました。
JNI によるパブリックなパスの取得
JNI で Java クラスを使い、Download ディレクトリのパスを取得する方法について、書いています。
同じ手順で、他のパスも取得できます。
ネームスペースを定義し、ログファイルを出力する
【 Android 6 より前のバージョンの場合】
なお、パブリックな領域にアクセスするには、Android Manifest に以下の記述が必要となるようでした。
Download フォルダなど、アプリケーション専用のフォルダ以外にアクセスしたい場合、 AndroidManifest.xml の </manifest> の前に、以下の行を書き込みます(READ_ のほうは、読み込みもしたいときに必要)。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
【 Android 6 またはそれより新しいバージョンの場合】
AndroidManifest に上記の記述を追加しても、ストレージにはアクセスできません。 プログラムはストレージにアクセスするときに、ユーザーに許可を求める必要があります。 その方法は、「動的にパーミッションを取得する」に書いています。
投稿 September 18, 2018、追記 March 1, 2021
すぐ上に書いているように、Download フォルダなど、アプリケーション専用のフォルダ以外にアクセスしたい場合、 Android 6 より前のバージョンの場合、 AndroidManifest.xml の </manifest> の前に、以下の行を書き込みます(READ_ のほうは、読み込みもしたいときに必要)。
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Android 6 またはそれより新しいバージョンの場合、 AndroidManifest に上記の記述を追加しても、ストレージにはアクセスできませんので、 ストレージにアクセスするときに、ユーザーに許可を求めます。 その方法は、「動的にパーミッションを取得する」に書いています。
そして、ログ出力用のクラスを用意します。
私は
私の場合は、ヘッダーファイルを jvmLog.h、実装ファイルを jvmLog.cpp として、
クラス名を CjvmLog としました。
クラス名の先頭の C は Windows らしく(マイクロソフトらしく?) Class を意味し、続く jvm は、
グローバルなオブジェクトを定義するファイルとして、globals.h として次のように記述しました。 ログを出力したいファイルでは、このヘッダーファイルをインクルードします。
#pragma once #include "jvmLog.h" // ログ記録クラス extern CjvmLog g_log; // ログ記録クラス
参考までに、テキストファイルにしたものをリンクしておきます。
タブは半角 4 文字としていますので、環境によっては読みにくく表示されるかも知れません。
globals.h
実装ファイルとしては、globals.cpp を用意し、次のように記述しました。
#include "pch.h" #include "globals.h" CjvmLog g_log; // ログ記録クラス
こちらも参考までに、テキストファイルにしたものをリンクしておきます。
タブは半角 4 文字としていますので、環境によっては読みにくく表示されるかも知れません。
globals.cpp
ここでは、CjvmLog クラスは namespace saAX に定義されているとしています。
投稿 September 18, 2018
実装イメージとしては、init 関数で初期化し (現在は初期化する内容がなく、実装していませんので、ここでは省略しています)、 setFilename 関数でログファイル名を指定、以降はずっとここで指定したファイルに書き込まれるようにします。 add 関数に文字列を渡すと、 書き込み時刻とともにログファイルに1行追加される、という形です。
上記のように globals.cpp でオブジェクトを定義してプロジェクトに含めると、 アプリ起動時にコンストラクタが呼び出されます。 コンストラクタでは、CjvmLog クラスに protected で定義された、 ファイル名を保持する変数 char m_filename[1024] をクリアしています。
CjvmLog::CjvmLog() { m_filename[0] = (char)0; }
デストラクタでは、何もしていません。
投稿 September 18, 2018、追記 March 1, 2021
setFilename 関数で、メンバー変数 m_filename に、ファイル名を設定しています。
void CjvmLog::setFilename(struct android_app* app, const char* filename, const char* filenamePrev)
引数 filename でファイル名を受け付けます。例えば、"myLogFile.txt" です。
基本の使い方として、アプリ起動時にログを削除し、起動中はずっと追記すると想定しました。 この場合、どこかでクラッシュするなどの場合、欲しいログが消されてしまう可能性があるため、 ひとつ前のログを別名で保存して、参照できるようにしようとしました。
引数 filenamePrev で、コピー先ファイル名を受け付けます。例えば、"myLogFilePrev.txt" です。 このファイルは、上書きされていくことになります。
開発中は、すぐにログを見たいので、Download サブフォルダにファイルを作成することとしました。 Download などのパブリックなパスを取得するためのコード例は、 「JNI によるパブリックなパスの取得」に記載していますので、 ここでは扱いません。 下の getDownloadFolder 関数は、Download フォルダへのパスを char* で返す関数です。
strcpy(m_filename, getDownloadFolder(app)); strcat(m_filename, "/"); strcat(m_filename, filename);
リリース時には、インストール先パスの files サブフォルダに作成されるようにします。 アプリで、ログファイルを見えるところにコピーする機能を用意するとして、通常は見えなくなります。
sprintf(m_filename, "%s/%s", getPrivateFolder(app), filename);
上の getPrivateFolder 関数は、アプリのパスの files サブフォルダへのパスを返す関数です。 実体は、以下のようになっています(実際にはネームスペースに追加しています)。
const char* getPrivateFolder(struct android_app* app) { // アセットマネージャーを取得します。 AAssetManager* mgr = app->activity->assetManager; if (!mgr) { return NULL; } // internalDataPath には、アプリ用内部パスが格納されています。 // "data/data/<AppName>/files" の形式のパスになります。 // 実際には、"Android/data/data/<AppName>/files" を指しているようです。 return app->activity->internalDataPath;
開発中、リリース時の切り分けは、Windows 開発と同じようにするため、 プロジェクトのプロパティ(<プロジェクト名>.NativeActivity のプロパティ)で、次のように、 プリプロセッサの定義で _DEBUG シンボルを定義して、 #ifndef _DEBUG のようにして、切り分けています。
Visual C++ 2019 の場合は、すでに別の定義があるので、それに追加します。
Visual C++ 2015 でも、同じような画面です。
投稿 September 18, 2018
g_log.add(app, "APP_CMD_START を受信"); のように使えるような関数を用意しました。
void CjvmLog::add(struct android_app* app, const char* text) { FILE* fp; // ファイル指定が未実行の場合は、書き込めません。 if (!(m_filename[0])) { return; } // 追記モードで開きます。 fp = fopen(m_filename, "a"); // ファイルを開けなければ、書き込めません。 if (!fp) { return; } char str[1024]; timespec ts; clock_gettime(CLOCK_REALTIME, &ts); time_t t = ts.tv_sec; tm tmv; localtime_r(&t, &tmv); int msec = (int)(ts.tv_nsec / 1000000); sprintf(str, "%02d:%02d:%02d:%03d %s\n", tmv.tm_hour, tmv.tm_min, tmv.tm_sec, msec, text); size_t size = fwrite((const void*)str, sizeof(char), strlen(str), fp); fclose(fp); }
コードにある、時刻の追加に関する部分の詳細は、「C/C++による現在日時の取得」に詳細があります。
なお、別途インクルードファイルが必要な関数が含まれているかも知れませんので、適宜追加してください。 今はどれがどのインクルードを必要とするか、調べきれていません。
投稿 September 18, 2018
ログファイルにずっと追記していると、どんどんサイズが大きくなります。 アプリ起動時に(前回の)ログファイルを削除するための関数を用意しておきます。
void CjvmLog::clear(void) { if (m_filename[0]) { remove(m_filename); } }
このほか、
AAssetManager* mgr = app->activity->assetManager;
のように「アセットマネージャー」を取得すると、ファイルが存在するかを確認したり、
フォルダに存在するファイルを列挙したりできますが、ここでは触れません。
また、アプリの起動時間が長くなるようなら、ログファイルが肥大化する可能性もありますので、 ファイルサイズを返す関数を用意するか、書き込み時に自動でサイズを調整する機能があると良いかもしれません。
投稿 March 5, 2021
ここまででコードの説明は完了です。
ここから、Visual C++ 2019 で作成した、新規の Android Native-Activity プロジェクトに、 ログを出力できるようになるまでのコードを追加する手順をまとめました。
VC++ 2019/2015 で新規プロジェクトを作成する
Win10(64 ビット)と Visual Studio Community 2019 の C++ で、 Android ネイティブアプリを作成する手順を記録しています。
アプリ名の設定や、ターゲット API の指定までを含んでいます。
まずは、画面上部のターゲット CPU で、ARM64 を選択します。
特に意味ないように見えますが、おそらく ARM を選択すると 32 ビットコードが、
ARM64 なら 64 ビットコードが出力されます。
そして今、Play ストアにアプリを公開するには、
ソリューション エクスプローラーから、 <プロジェクト名>.Packaging に src フォルダーを追加します。 フォルダー名には決まりはないと思います。
作成した src フォルダーに、ファイル書き込みパーミッションを動的に得るための ObtainPermission.java を追加します。 このファイルは、別トピックで作成した Java コードです。 下にリンクの先で作成したものです。
他のフォルダに置いてある同ファイルを、フォルダー名を右クリックし、
「追加」、「既存の項目」と進み、
追加したいファイルを選べば、このフォルダーに
ObtainPermission.java の1行目は、プロジェクトのパッケージ名に変更する必要があります。
動的にパーミッションを取得する
Android 6 以降では、AndroidManifest.xml で外部ストレージの権限を記録しただけでは、実際にはアクセスできません。
ネイティブアプリで動的にパーミッションを得るためのコードを検討しています。
プログラムが Java のコードを持っていることを知らせるために、 AndroidManifest.xml を開き、 application タグの android:hasCode="false" 設定を true に書き換えます。
プロジェクトの設定は以上ですので、ログを出力できるまで、コードを追加します。
エクスプローラーで、<プロジェクト名>.NativeActibity フォルダに、 sa と app サブフォルダを作成します。
※ 以降、明記されていなくても、フォルダ名やファイル名には決まりはありません。 命名ルールやファイル分け、ネームスペース分けはすべて我流ですので、ご注意ください。
sa サブフォルダには、アプリごとには書き換えない、「いつも使うコード」を入れることにしています。
ログを出力するクラスの実装 jvmLog の .h と .cpp、
jvmLog が利用する
sa フォルダに saX サブフォルダを作成しています。 saX サブフォルダには、 ファイルバックアップに利用する mxFileCopy の .h と .cpp をコピーします。 ここにはファイルのコピーを作成する copy 関数が定義されていますが、 今はそれしか入れていませんので、このような構成(ファイル名やサブフォルダ)にする必要はありません。 このフォルダには、Android など OS に依存しないコードを置こうと考えています。
また、Java コードを呼び出しやすくするクラスの実装 axJavaCaller の .h と .cpp も、 sa サブフォルダにコピーします。 getStaticMethod 関数などが定義されています。 ファイル名は、Android 用のソースファイルは ax で始める、のように決めています。
いずれも、<プロジェクト名>.NativeActivity に追加登録します。
app サブフォルダには、アプリごとに書き換えるコードを入れることにしています。 グローバルオブジェクトを定義している globals の .h と .cpp をコピーし、 プロジェクトに追加登録します。
また、結局、Android 6 以降で動的にパーミッションを得る方法でも、 AndroidManifest.xml に uses-permission の行は必要のようです。 詳しくは調べていませんが、追加しておいて問題はないようですから、追加しておきます。
クラス定義等の追加は以上ですので、アプリ起動時に「開始されました」ログを書き込むまで、 main.cpp にコードを追加します。
まずはパスを参照できるよう、プロジェクトのプロパティで「追加のインクルードディレクトリ」を指定します。 sa サブフォルダや、app サブフォルダのコードが簡単になります。
<プロジェクト名>.NativeActivity のプロパティで、 追加のインクルードディレクトリ指定を行えます。
項目右端にある下向きの矢印ボタンをクリックし、「編集」を選択するとダイアログが表示されますので、2つのディレクトリを追加します。 サブフォルダ内のファイルから pch.h を参照するために、.\ も、先頭に追加しています。
また、開発時の切り分けのために、プリプロセッサで _DEBUG を定義します。 これにより、#ifdef で、ログの出力先を、開発中は簡単に参照可能な Download フォルダに、リリース時にはアプリの files サブフォルダに変えられます。
main.cpp の LOGI や LOGW マクロの定義のすぐあとで、 globals.h と axJavaCaller.h をインクルードします。 将来別クラスで初期化を行うことになったら、axJavaCaller.h は、そこへ移動させますが、 今はここでしか使いません。
アプリ起動時にログを出力できるよう、android_main 関数の、 engine.animating = 1 の行のあとに、以下の行を追加します。
// JavaVM をカレントスレッドにアタッチします。 JNIEnv* env = state->activity->env; JavaVM* vm = state->activity->vm; vm->AttachCurrentThread(&env, NULL);
これは Java VM を使用する前に必要な手続きですので、将来は関数化します。
saAX::CaxJavaCaller jc; jc.prepare(state); if (jc.obtainJavaClass(state, "com/straightapps/SimpleClock/ObtainPermission") == true) { jmethodID obtainPermissionMethod = jc.env->GetStaticMethodID(jc.classRequested, "obtainPermission_storage", "(Landroid/app/NativeActivity;)Z"); jboolean b = jc.env->CallStaticBooleanMethod(jc.classRequested, obtainPermissionMethod, state->activity->clazz); if (b == true) { // この場合のみ、g_log でログを出力するようにして、処理効率をよくします。 } else { // この場合はログを出力できません(エラーにもなりません)。 } } jc.finish(state);
jc.prepare 関数は、AttachCurrentThread 関数の実行のみ行っています。 このコードだと2回実行されてしまい無駄ですが、問題はないようです。
jc.obtainJavaClass 関数は、 第2引数で指定した Java クラスを取得し、jclass 型の CaxJavaCaller::classRequested に設定するまでを行っています。 簡単には書けないので、専用トピックを用意します。 指定しているクラスは、ObtainPermission.java の1行目に記載したパッケージ名と、そのファイルで定義したクラス名です。
GetStaticMethodID 関数は、
指定のクラスで定義されている
CallStaticBooleanMethod 関数は、スタティックで bool 値を返すメソッドを実行するものです。 Java と C/C++ をつなぐために仕方ありませんが、大変面倒に感じます。 第1引数にあるクラスの、第2引数にあるメソッドを、第3引数以降の引数を渡して実行します。
Java 関数は、書き込み許可がすでに得られているか、問い合わせをして得られた場合は true を返します。 この場合は、ログを出力できます。 false が戻された場合は、拒否されたか、ずっと拒否の設定になっています。 今は気にせずログを出力していますが(拒否された場合は書き込まれません)、 ちゃんと切り分けるべきでしょう。
jc.finish 関数は、DetachCurrentThread 関数を呼び出し、JVM をクリーンアップしています。
このあとファイル名を指定すれば、ログを出力できます。
g_log.setFilename(state, "saSimpleClock.txt", "saSimpleClockPrev.txt"); g_log.add(state, "開始されました。");
このようなコードを、メインループ開始前に置けば、アプリ起動時に記録されることになります。
これでビルドすると、たくさんの警告が出ました。
※ もちろん全ファイルが揃わないと通りませんので、このページのコードだけではビルドできません。今後、整理します。
ObtainPermission.java:10: 警告: この文字は、エンコーディングUTF-8にマップできません
java ソースコードに全角文字がある場合、例えそれがコメントであっても、1文字ごとに1警告でているようです。 日本語を使わないか、もしかしたら java コード自体を UTF-8 で作成すれば回避できるかも知れません。 無視しても問題なさそうです。
これで実機で実行させましたが、問い合わせは発生しませんでした。
理由は、ObtainPermission.java コードの 「今後表示しない」にチェックを入れて「許可しない」されたかどうかを調べます。 とコメントがある部分、 shouldShowRequestPermissionRationale 関数の呼び出しに問題があるようです。
この関数呼び出しにより、インストール直後の許可も拒否もしていない状態でも条件が成立し、 問い合わせを行う requestPermissions 関数が実行されていないようでした。 逆に、いったん許可されたか、「今後表示しない」で拒否されている場合、 requestPermissions 関数を実行しても問い合わせは行われないようですので、 この if 文ごと不要のようです。
今の shouldShowRequestPermissionRationale 関数ありの状態の場合、 設定アプリからいったん権限を与えたり、それを解除したりすると、 問い合わせが行われるようになりました。
開発中のパーミッション設定は、いったんアンインストールすると削除され、 再インストールで(同名プロジェクトでも)初期状態になっているようです。
以上で、アプリ起動時に書き込みについて問い合わせが表示され、 許可するとログが記録されるようになりました。
VS2019 による再スタートのシリーズ初回ページ 「VC++ 2019/2015 で新規プロジェクトを作成する」 からの流れとしては、次は、とりあえず画面表示を制御したいと思いますので、 「OpenGL ES での色指定」がお勧めです。
基本的なプロジェクト構成を確認したい場合は、古いですが 「android_main 関数(ループ前)」 でコードを検証しています。
Android 開発に関する記事をまとめた Android 開発トップ もご覧ください。