AWS CLIでAWS Athenaのクエリがデータベースが見つからないためになんども失敗する場合は結果出力先のS3のバケットのregionを確認する
俺の屍を越えていけ、的なメモ。
require 'aws-sdk-athena' client = Aws::Athena::Client.new begin start_response = client.start_query_execution({ query_string: "SELECT * FROM samples limit 10", query_execution_context: { database: "mydatabase", }, result_configuration: { output_location: "s3://sample-woshidan-test-tokyo/athena_query_result" }, }) sleep(5) result_response = client.get_query_results({ query_execution_id: start_response.query_execution_id }) rescue Aws::Athena::Errors::InvalidRequestException => ex puts ex.inspect ensure puts "Ensure" end
上記のコードで aws-sdk-athena
でAthena のクエリを飛ばそうとしたら
Query did not finish successfully. Final query state: FAILED (Aws::Athena::Errors::InvalidRequestException)
というエラーが出て失敗する。なんで失敗したのかさっぱりわからないので、CLIで実行し直すと、
$ aws athena start-query-execution \ > --query-string "SELECT * FROM mydatabase.samples limit 10;" \ > --result-configuration OutputLocation=s3://sample-woshidan-test/athena_query_result { "QueryExecutionId": "f8e45456-238a-44ba-955a-40f048e5c3b2" } $ aws athena get-query-execution --query-execution-id f8e45456-238a-44ba-955a-40f048e5c3b2 { "QueryExecution": { "Status": { "SubmissionDateTime": 1506675885.243, "State": "FAILED", "CompletionDateTime": 1506675885.387, "StateChangeReason": "com.facebook.presto.hive.DataCatalogException: Namespace mydatabase not found. Please check your query." }, "Query": "SELECT * FROM mydatabase.logs limit 10", "Statistics": { "DataScannedInBytes": 0, "EngineExecutionTimeInMillis": 53 }, "ResultConfiguration": { "OutputLocation": "s3://sample-woshidan-test/athena_query_result/f8e45456-238a-44ba-955a-40f048e5c3b2.csv" }, "QueryExecutionId": "f8e45456-238a-44ba-955a-40f048e5c3b2" } }
というメッセージが出て、どうも database
が見つからないらしい。
もしかして、 AWS のアカウントの region
を間違ったかな、とAWS CLIの設定で使う region を Athenaの管理画面で表示されている region に設定しなおしてみると
- region = ap-northeast-1 + region = us-east-2
The S3 location provided to save your query results is invalid. Please check your S3 location is correct and is in the same region and try again. If you continue to see the issue, contact customer support for further assistance.
戻すと
+ region = ap-northeast-1 - region = us-east-2
Query did not finish successfully. Final query state: FAILED (Aws::Athena::Errors::InvalidRequestException)
どっちに設定しても何かしらダメっぽい...
最終的に
自分がAthenaのクライアントとして選んだ aws-athena-client
はその実装でAWS CLIを利用しています。
そして、AWS CLIで設定値として使うregion は ~/.aws/config
で指定しているためか
- クエリを投げるAthenaのデータベースが
~/.aws/config
で指定したregionに存在すること - クエリ結果の出力先のS3のバケットが
~/.aws/config
で指定したregionに存在すること
の2点がAWS CLIでAthenaにクエリを投げるためには必要みたいです。そして最終的に結果出力先のS3のバケットのregionをAthenaのデータベースのregionに揃えてことなきを得ました。
AthenaのクエリでスキャンするS3のバケットのregionは、AWS CLIで使うIAMアカウントがアクセス権を持ってさえいればAthenaのデータベースのregionと違ってもクエリが実行できるのもあっていまいち原因がわからず焦りました。
もしかしたらAthena - S3 間だけでなく、複数のAWSのサービスが協調して動く場合、CLIが一個しかregionの値を持たない、みたいなことが原因で似たようなトラブルはあるかもなと思いつつ、現場からは以上です。
iOSにてzlibのdeflate()コマンドでファイルをgzip形式で圧縮する方法について
zlibについてコピペで使うのはいやだなと思ったのでちょっと調べてメモ。
比較的小さいファイルの圧縮を行うなど、圧縮前後のデータを全てメモリ上に展開できる場合は、 compress()
を使って一気に圧縮できますが、今回はストリームから少しずつデータを出し入れして圧縮していく deflate()
コマンドを利用する場合についてメモします。
zlibとは
まず、zlibが何かをこちらのページから引用します。
zlibとは、圧縮アルゴリズムの一種である Deflate のライブラリであり,C#, Haskell, Java, Perl, Python, Ruby など,主要なプログラミング言語では,軒並み使えるように整備されています.圧縮・伸長が高速なこともあり,ディスク領域の有効利用や通信量の削減を目的として,zlib は気軽に利用できます.
XCodeでのiOSの開発環境への導入について
XCodeでのiOSの開発環境下では、iOS SDKに一部APIが利用できる状態でzlib.hが含まれているため umcompress()
はリンクエラーで使えないようでしたが、compress()
については zlib.h
をimportすれば利用可能でした。
導入しようとしたプロジェクトによっては zlib.h
のimportだけでは
Undefined symbols for architecture x86_64: "_deflate", referenced from: +[GzipSample compress:] in Sunaba(GzipSample.o) "_deflateInit2_", referenced from: +[GzipSample compress:] in Sunaba(GzipSample.o) "_deflateEnd", referenced from: +[GzipSample compress:] in Sunaba(GzipSample.o) ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
のようなエラーメッセージでリンクに失敗することがあります。
その場合は、
libzを利用するアプリのプロジェクトのターゲットの設定で、 Build Phases > Link Binary With Libraries
から libz
と入力して出てきたものの中から利用するバージョンの libz
を追加してビルドしなおしてください *1 。
今回は圧縮の方法についてだけひとまずメモしようと思っているのでこれで進みますが、iOS添付でない zlib.h
の導入には以下が参考になりそうです。
https://gist.github.com/dulaccc/75f1f49f53e544cef549
zlibでファイルを圧縮する
z_stream構造体のオブジェクトを生成
zlibの基本的な使い方ですが、 z_stream
構造体のオブジェクトを生成して、 zalloc, zfree, opaque
のメンバを設定します。
z_stream stream; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL;
この時、設定する値はデフォルト値である Z_NULL
で基本的に問題ありません。gzip形式で圧縮する場合は、 delateInit2()
で stream の初期化処理を行います。
入力データの設定
z_stream
構造体の avail_in
, next_in
の設定をします。
stream.next_in = (Bytef *)[inputData bytes]; // 入力バッファへのポインタ stream.avail_in = (uint)[inputData length]; // 入力データの量
出力データの初期設定
自分の環境のzlibではデフォルト値でもよかったんですが、ライブラリなどにする場合、どのzlibとリンクされるか固定ではないので、 z_stream
構造体の avail_out
, total_out
の初期値の設定をしておきます。
stream.total_out = 0; // これまでの出力されたデータの合計長 stream.avail_out = 0; // 出力データのバッファ上の残量
ストリームの初期化
gzip圧縮の場合、ストリームの初期化は下記の deflateInit2()
コマンドで行います。
// int deflateInit2(z_stream *strm, // int level, .... 圧縮レベル(数値が小さいほど圧縮時間が小さく、大きいほど圧縮後のサイズが小さい) // デフォルト Z_DEFAULT_COMPRESSION(=6) // 圧縮速度最高 Z_BEST_SPEED(=1) // 圧縮率最高 Z_BEST_COMPRESSION(=9) // 圧縮無効 Z_NO_COMPRESSION(=0) // int method, .... 圧縮方法 zlib-1.2.6 で指定できるのは Z_DEFLATED のみ // int windowBits, ウィンドウ・サイズ gzip 形式では 31 を指定する必要あり // int memLevel, .. メモリの消費量を指定 大きくても特に嬉しいことはない. デフォルト8 // int strategy); . 圧縮の方式を指定。デフォルトは Z_DEFAULT_STRATEGY deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY)
これによって、出力バッファの合計長 stream.total_out
や、出力データのバッファ上の残量 stream.avail_out
などの出力関連のパラメータが設定されます。
圧縮したデータをstreamに出力する
int result = deflate(&stream, Z_FINISH); // 戻り値の意味 // Z_OK ... まだ出力できる範囲が残っているのでもう一回deflate()が呼び出せる // Z_STREAM_END ... もう出力できる範囲がない // Z_BUF_ERROR ... 入力データ、出力バッファの一時的な不足 // Z_STREAM_ERROR ... next_in, next_outがNULLの時や内部状態が破壊された
deflate
を実行するたびに stream.next_out
で指定されたポインタのバッファへ圧縮結果が書き込まれます。
この出力先のポインタは書き込むたびに書き込んだ分ずらす必要があります。
stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out; // ... 出力先のバッファのポインタの位置をこれまで書き込んだ分 stream.total_out だけずらして設定 // [NSMutableData mutableBytes] はNSMutableDataで管理している生のバイト列へのポインタを返してくれる
また、出力を書き込むたびにバッファ残量の値が減少するので、不足するようであれば書き込み先のバイト列の長さを伸ばしたり、次に書き込む前に出力バッファ残量が不足しないよう設定し直してやる必要があります。
if (stream.total_out >= [data length])
{
data.length += ChunkSize;
}
stream.avail_out = (uInt)([data length] - stream.total_out);
圧縮処理に使ったメモリの解放
最後に後始末のため deflateEnd(&stream)
を呼び出して、使ったメモリを開放します。
deflateEnd(&stream);
最終的に書いたみたコード
以上を踏まえつつ、
のコードを参考にして書くとこういうコードになりました。
#import <Foundation/Foundation.h> #import <zlib.h> static const NSUInteger CHUNK_SIZE = 1000; // バイト列を伸ばす処理を確認するために小さめにしている static const int GZIP_WINDOW_BITS = 31; static const int DEFAULT_MEMORY_USAGE = 8; + (NSData *)compress:(NSData *)sourceData { if ([sourceData length]) { z_stream stream; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; stream.avail_in = (uint)[sourceData length]; stream.next_in = (Bytef *)[sourceData bytes]; stream.total_out = 0; stream.avail_out = 0; if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, GZIP_WINDOW_BITS, DEFAULT_MEMORY_USAGE, Z_DEFAULT_STRATEGY) == Z_OK) { NSMutableData *data = [NSMutableData dataWithLength:CHUNK_SIZE]; int result = 0; while (result == Z_OK) { if (stream.total_out >= [data length]) { data.length += CHUNK_SIZE; } stream.next_out = (uint8_t *)[data mutableBytes] + stream.total_out; stream.avail_out = (uInt)([data length] - stream.total_out); result = deflate(&stream, Z_FINISH); } deflateEnd(&stream); if (result != Z_STREAM_END) { return nil; } data.length = stream.total_out; return data; } } return nil; } @end
参考
- http://s-yata.jp/docs/zlib/
- https://discussions.apple.com/thread/1494368?start=0&tstart=0
- https://gist.github.com/dulaccc/75f1f49f53e544cef549
- https://oku.edu.mie-u.ac.jp/~okumura/compression/zlib.html
- https://developer.apple.com/documentation/foundation/nsmutabledata?language=objc
- http://d.hatena.ne.jp/MoonMtLab/20141105/1415136219
- https://github.com/nicklockwood/GZIP
- https://stackoverflow.com/questions/18053546/undefined-symbols-for-architecture-i386-deflate-referenced-from-platcompres
どー考えてもライブラリのリンク関連についてまた勉強する必要がありますが、それはおかわり案件にします。現場からは以上です。
Heartbeatとブロックデバイスについて
短いですが、一応何かしら書くぞということで、本を読んでいたら「Heartbeatがスタンバイしたサーバを有効にする」「ブロックデバイスをレプリケーション」と出てきたのが意味がわからなかったのでメモ。
Heartbeat関連
- クラスタに関することでハートビートといえばHAクラスタを実現する上で利用される仕組み
- クラスタの種類
- ハートビート色々
- http://www.atmarkit.co.jp/ait/articles/0711/13/news139_2.html の記事では
- Heartbeat …
オープンソースソフトウェア「Heartbeat」全体
- heartbeat …
上記のHeartbeatで提供されるプログラム、もしくはそれを起動したときのプロセス
- ハートビート …
HAクラスタを構成するPCが相互に状態監視をする際のネットワーク構成、およびその通信を指すものとします
- 数ページしか調べてない時点で上記とちょっと違う定義が出て来ていて、一番上の行にあるのがそれ。要注意単語。
- Heartbeat …
- http://www.atmarkit.co.jp/ait/articles/0711/13/news139_2.html の記事では
- ハートビートの通信をどう行うか
Linux系OSのブロックデバイスとキャラクタデバイス
- Linux系OSでは、デバイスはコンピュータに接続された周辺機器のことをデバイスと呼び、
/dev
以下に作成されるデバイスファイルが周辺機器のハードウェアへのインタフェースとなっている - ブロックデバイスとは、ある一定の量(通常512~2048バイト)の読み書きをランダムアクセスで行えるデバイスで、ディスク装置全般が該当する
- キャラクタデバイスはデータの入出力をバイト(1文字)単位で扱うデバイスで、データの読み出しや書き込みがシーケンシャルアクセスとなる
- 一度読み書きしたデータを後戻りして再び読み書きすることはできない
- キーボードやマウスなどが該当する
参考
- Heartbeat関連
- http://www.atmarkit.co.jp/ait/articles/0711/13/news139.html
- http://itpro.nikkeibp.co.jp/article/COLUMN/20090224/325384
- https://h50146.www5.hpe.com/products/software/oe/hpux/developer/column/sg_text_02/
- https://qiita.com/growsic/items/fead30272a5fa374ac7b
- https://reboot.makeall.net/2010/11/25/about-heatbeat/
- ブロックデバイスとキャラクタデバイス関連
ウェブオペレーションまとめ用に読み直してるんですが、真面目に読むと11章や12章がとても重くて少しいやですね。わかんないものはわかんないで仕方ないですが。
*1:ここのwebアプリケーションサーバをEC2で調達したりする話を聞いたことが多いので、なんとなくクラスタ=クラウド感がありました
Amazon Athenaについて少し調べたのでメモ
まとまってないですが、忘れる前にメモ。
Amazon Athena とは
Amazon S3に入ったCSVやJSONをAthena上で定義したスキーマに沿ってSQLで検索できるようにしてくれるクエリサービスです。
Athenaはサーバーレスのサービスで、利用するまでに最低限必要なステップは
- ファイルの入っているS3バケットの指定を含むスキーマの定義
- クエリを投げる
だけです。利用開始までにデータの抽出や変換、ロード(ETL)は必要なくなっています。ログ分析基盤などでやるようなアドホック分析に活用することができます。
スキーマというかテーブルの定義について
データベースはAthenaの管理画面の[Catalog Manager]などから作成可能です。 データベースごとにテーブルが100個まで作成できます*1。
チュートリアルなどではテーブルをクエリエディタで定義していますが、この場合Hive互換のDDLを使って定義できます。
たとえばこういう形式のJSONが詰まったS3のバケットの中のファイルを検索させるためのテーブルを定義したい場合
{ "user_id": 12345, "data": { "imp_ids": [1,2,3,4,5] } }
{ "user_id": 23456, "data": { } }
このようにテーブルを定義することが可能です。
CREATE EXTERNAL TABLE IF NOT EXISTS array_tests ( user_id String, data struct<imp_ids:array<INT>> ) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' LOCATION 's3://exmaple-woshidan-test/sample';
一度スキーマを定義した後にS3にあるファイルに変更を加えることなく後からスキーマを変更することも可能です。
ただし、Athenaで利用するS3のバケットの中のファイルはすべてスキーマの定義に沿った形式である必要があったり、同じバケットの中で違う形式のファイルを検索できるようにすることはできないので、ログ収集時に決めた部分を後から違うフォーマットで検索可能にすることは難しいです。
同じバケットの中でテーブルの定義にそぐわないフォーマットのファイルが見つかった場合は、そぐわない部分の列だけ空白の行が返ってきます。 アドホック分析なので途中から検索方法が違っても大丈夫、ということなのかもしれないです。
また、テーブルのパーティションについてはS3のバケットのパスに依存するので、こちらも後から変更することは難しい場合があります。
テーブルの変更はテーブルのドロップ -> 再作成という手順で行います。
RDBMSのテーブルと異なり、Athenaのスキーマ定義はS3の中のファイルを走査する際に利用されるメタデータのようなものなだけで、保存されているファイルのフォーマットがAthenaのスキーマ定義に影響を受けることはありません。
データの検索
データの検索もSQLのように行うことができます。 Athenaの検索の実行はPrestoを使って行われ、検索クエリを書く際はPrestoで利用できるキーワードの一部が利用可能となっています。
先ほどの例を用いていくらかクエリと実行結果を書いてみます。
SELECT * FROM mydatabase."array_tests" limit 10;
user_id | data | |
---|---|---|
1 | 12345 | {imp_ids=[1, 2, 3, 4, 5]} |
2 | 23456 | {imp_ids=null} |
SELECT * FROM mydatabase."array_tests" limit 10;
SELECT data.imp_ids FROM mydatabase."array_tests" WHERE user_id = '12345';
imp_ids | |
---|---|
1 | [1, 2, 3, 4, 5] |
配列の検索は UNNEST
と CROSS JOIN
を使って、配列の行を展開した行を生成することで可能なようです。パフォーマンスとかはあんまりわかってません。。
SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id);
user_id | data | imp_id |
---|---|---|
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 1 |
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 2 |
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 3 |
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 4 |
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 5 |
SELECT * FROM mydatabase."array_tests" cross join UNNEST (data.imp_ids) AS t (imp_id) WHERE imp_id = 3;
user_id | data | imp_id |
---|---|---|
12345 | {imp_ids=[1, 2, 3, 4, 5]} | 3 |
その他
Athenaでクエリを実行する先のS3に上げるJSONは1行
Athenaのクエリ実行先として指定するS3の中にあげるJSONは1行にminifyしておく必要があります。
Hive_CURSOR_ERROR: Row is not a valid JSON Object ...
というエラーが出てよく見たら1行目の{あたりから怒られているような場合はこれが原因です。
参考
- http://dev.classmethod.jp/cloud/aws/athena-json/
- http://dev.classmethod.jp/cloud/aws/amazon-athena-search-nested-json-seslog/
- https://aws.amazon.com/jp/blogs/news/create-tables-in-amazon-athena-from-nested-json-and-mappings-using-jsonserde/
- http://qiita.com/iron-breaker/items/f35c1d54887c434a321a
現場からは以上です。
*1:このあたりの制限は上限緩和申請が可能
「ライブ配信のなかみ[あれくま]」を読んだ
春の技術書典で手に取った時から、勝手にこの本は自分のための本だ、と思っていたのですが、仕事でバタバタしていまして昨日ようやく読みました。
読み終えてやっぱり自分にとってめちゃくちゃ面白かったので自己満足に感想をおいておきます。
この本を読む以前に関わっていたサービスでChromeでFlashを使うと警告が出るようになるのを避けるため、動画再生画面で使うファイルの形式をflvからmp4に切り替えるという対応をしたことがあります。
その時、flvのファイルとFlashのプレイヤーの組み合わせだとストリーミング再生ができるのに、mp4とHTML5のvideo要素の組み合わせに切り替えると全てよみこむまで再生できないのをどうにかしたいのだけれど、どこをどう調べたらいいかわからなくてずっともやもやしていました*1。
その辺りがこの本で触れられていて、めちゃくちゃ無知に対する恥ずかしさと嬉しさで悶えて死ぬかな、という感じです。
やっぱり感想うまくまとまらなかったので残りの感想は箇条書きにしておきます。。
- ストリーミング配信のシークバーを実装する際に必要になるデータ形式の雰囲気から具体的なコーデックまで順を追って説明してもらえるのでめちゃくちゃわかりやすかった
- 配信の遅延の原因でバッファにデータを貯める時間の話が面白かった
- コーデックとコンテナの違いはこの本を読むまで知らず、動画のファイル形式、JavaScriptで二つ取れるなーみたいな感じで圧倒的無知で死にたさすらある
- MicrosoftやAdobeが独自規格をあきらめていくのにAppleお前ときたら…!
- HTTPのプログレッシブダウンロードでは落としきっていないファイルを途中まででも無理やり再生プレイヤーが必要という話を聞いて、Flashのプレイヤー強い… 感がありました
- 動画の配信をするメディアサーバの話をあまり聞いたことがないので新鮮でした
具体的な内容についてはぜひ下記リンクから買って確かめてくださいませ(ダイレクトマーケティング)。
動画を扱うサービスのRails書くWebエンジニアやアプリ作るエンジニアでちょっと動画に興味があって調べたことある人ならきっとめちゃくちゃ面白いと思います。
現場からは以上です。
インテントフラグとActivityStackのふるまいについていくらか
Android
の Activity
のスタックの振る舞いを設定するものとして、
AndroidManifest.xml
に記述するactivity
要素のlaunchMode
属性Activity
を立ち上げる時に使うIntent
に付与するインテントフラグ
があります。先日、launchModeの話をしたので、今日はインテントフラグの実験を少ししようかと思います。
今回インテントフラグがどんなものか確認する際に取り上げる具体例としては、
あたりにします。今回の実験に使ったコードの全体像はこちらで適宜、/* some flags */
周辺のコードを編集した分を実行結果と一緒に紹介します。
TL;DR
長くなったので。
- FLAG_ACTIVITY_SINGLE_TOP
launchMode
のsingleTop
と同じ- このフラグをつけて起動したActivityと同じActivityがスタックの一番上にあるなら、既存のインスタンスの
onNewIntent()
を呼びだす
- FLAG_ACTIVITY_CLEAR_TOP
- FLAG_ACTIVITY_NO_HISTORY
- このフラグをつけて起動した
Activity
はActivity
のスタックに積まれない - =
onActivityResult
が決して呼び出されない
- このフラグをつけて起動した
FLAG_ACTIVITY_SINGLE_TOP
まず、MainActivity
を起動するときだけ、FLAG_ACTIVITY_SINGLE_TOP
を利用してどのような挙動をするか見てみましょう。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); }
MainActivityが一番上にあるときはその上にActivityが積まれず
NextActivityが一番上にあるときは新しいMainActivityが積まれます。
launchMode
の singleTop
と同じ振る舞いですね。
FLAG_ACTIVITY_CLEAR_TOP
MainActivityを表示しようとしたとき、FLAG_ACTIVITY_CLEAR_TOP
のインテントフラグを付与することにします。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("MainActivity", "MainActivity " + MainActivity.this.toString()); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); }
MainActivityが一番上にあるとき、すでにあるActivityが破棄されてから別のActivityが作り直されて一番上に積まれ直します。
Log.d("MainActivity", "MainActivity " + MainActivity.this.toString());
でインスタンスが変わっているというログがこちらです。
D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@3a2375cd D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@21a00ad7 D/MainActivity: MainActivity com.example.woshidan.intentflagtest.MainActivity@ac67751
何枚か NextActivity
をスタックに積んでから MainActivity
を起動し、その後バックキーを押すとアプリが終了します。
この場合のLog.d("MainActivity", "MainActivity " + MainActivity.this.toString());
でインスタンスが変わっているというログがこちらです。
D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@2007c35 D/MainActivity: MainActivity com.example.woshidan.contexttest.MainActivity@25e3619b
もう少し別の条件で検証するとわかりやすいですが、起動しようとした MainActivity
より上のスタックにある NextActivity
と古い MainActivity
を捨て、MainActivity
を作り直して、呼び出していることがわかります。
ドキュメントのフラグ一覧のところの FLAG_ACTIVITY_CLEAR_TOP
の欄には、
If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.
とあり、これは上記の挙動と異なるように見えます。
しかし、これは仕様どおりで、さらに FLAG_ACTIVITY_CLEAR_TOP
の単体の項目のドキュメントを読むと、FLAG_ACTIVITY_SINGLE_TOP
との併用か、 Activity
の launchMode
が singleTop
に設定されていなければ、既存のインスタンスへちょっとIntentが送られないことがあることがわかります。
それでは、 FLAG_ACTIVITY_CLEAR_TOP
とFLAG_ACTIVITY_SINGLE_TOP
との併用した場合の挙動を
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("MainActivity", "MainActivity " + MainActivity.this.toString()); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); startActivity(intent); } }); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Log.d("MainActivity", "onNewIntent"); Toast.makeText(this, intent.getStringExtra("KEY_MESSAGE"), Toast.LENGTH_SHORT).show(); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); startActivity(intent); } }); } }
で試してみましょう。今度は、一覧の部分にあるように既存のMainActivityのインスタンスにIntentが送られるようになったみたいです。
FLAG_ACTIVITY_NO_HISTORY
どのActivityを起動するときも FLAG_ACTIVITY_NO_HISTORY
のインテントフラグを付与してがんがんActivityを起動してみます。
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, NextActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); }
public class NextActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_next); findViewById(R.id.button_launch_first_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, MainActivity.class); intent.putExtra("KEY_MESSAGE", "sent new intent"); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); findViewById(R.id.button_launch_second_activity).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(NextActivity.this, NextActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); startActivity(intent); } }); } }
たくさんスタックを積んだはずが、2回バックキーを押すとアプリが終了してしまいました。
最初の一回のバックキーで一番最初に起動した MainActivity
へ戻ってしまったようです。
FLAG_ACTIVITY_NO_HISTORY
のインテントフラグをつけて起動されたActivityは、Activityのスタックに積まれないようです。
インテントフラグはActivityを起動する側が指定するので、使う側の都合によってActivityのスタック上での扱いを決める方法、みたいな感じなんですかね。
現場からは以上です。
参考
AndroidのCamera2 APIを触ってみた
ちょっと面白そうだったので調べました。
概要
https://developer.android.com/reference/android/hardware/camera2/package-summary.html からざっくり読み取った結果、Camera2 APIでカメラ経由で画像を取得したりする流れをかなり大雑把にいうと
CameraManager
を利用し、CameraDevice.StateCallback
経由でCameraDevice
のインスタンスを取得- Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して
CameraDevice
のインスタンスからcreateCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドによりCameraCaptureSession
を生成 - 1フレーム分の入力を取得するための入力リクエスト(
CaptureRequest
のインスタンス)を作る CaptureSession
にリクエストをセットする- リクエストが処理されて、
TotalCaptureResult
(カメラデバイスの現在の状態など)オブジェクトの生成や、出力先への1フレーム分への画像データの送信が行われる
となるようです。それぞれ見ていきましょう。
なお、ただでさえややこしいので、今回のサンプルコードは意図的にtargetSDKVersionを21にして、パーミッション周りやエラー対応の処理はビルドが通るギリギリくらいまで省いています。。
CameraManager
を利用し、CameraDevice.StateCallback
経由で CameraDevice
のインスタンスを取得
CanmeraManager
を利用してそのデバイスで利用できるカメラのIdを取得- カメラのIdと
CameraDevice.StateCallback
のインスタンスを利用して、CameraManager.openCamera()
メソッドでCameraDevice
をオープン CameraDevice.StateCallback
のonOpened
コールバックでCameraDevice
のインスタンスを取得
利用するカメラの種類を制限したかったりする場合はCameraDevice
をオープンする前に行います。
また、サンプルによるとプレビュー領域のサイズや向きなどを設定したい場合はプレビューを表示する TextureView
に対して行うようです。
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); String selectedCameraId = ""; try { selectedCameraId = manager.getCameraIdList()[0]; // https://github.com/googlesamples/android-Camera2Basic/blob/5dad16c103715b5e7e3c001cc5f6067f8d23f29e/Application/src/main/java/com/example/android/camera2basic/Camera2BasicFragment.java#L499 // あたりにあるのですが、顔用カメラを使いたくないなどがあれば、CameraCharacteristicsを経由して確認可能 // CameraCharacteristics characteristics // = manager.getCameraCharacteristics(selectedCameraId); } catch (CameraAccessException e) { e.printStackTrace(); } try { manager.openCamera(selectedCameraId, mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); }
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice cameraDevice) { mCameraDevice = cameraDevice; } @Override public void onDisconnected(CameraDevice cameraDevice) { cameraDevice.close(); mCameraDevice = null; } @Override public void onError(CameraDevice cameraDevice, int error) { cameraDevice.close(); mCameraDevice = null; } };
CameraDevice
のインスタンスから CameraCaptureSession
を生成
Surface系のViewやMediaCodecなどの出力先とカメラデバイスを利用して CameraDevice
のインスタンスから createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)
メソッドにより CameraCaptureSession
を生成します。
- 出力先として
TextureView
を用意する TextureView
からSurfaceTexture
を取得してSurface
を生成する- 2の
Surface
をListに入れて、CameraCaptureSession.createCaptureSession()
メソッドを呼び出す
2の Surface
は、 ImageReader
や MediaCodec
など他の出力先クラスからも生成/取得が可能です*1。
// Activity.onCreate内 mTextureView = (TextureView) findViewById(R.id.texture); mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { // 先ほどのカメラを開く部分をメソッド化した openCamera(); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } });
private CameraCaptureSession mCaptureSession = null; // ... private void createCameraPreviewSession() { SurfaceTexture texture = mTextureView.getSurfaceTexture(); texture.setDefaultBufferSize(320, 240); // 自分の手元のデバイスで決めうちしてます Surface surface = new Surface(texture); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } } mCaptureSession = session; @Override public void onConfigureFailed(CameraCaptureSession session) { } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } }
1フレーム分の入力を取得するための入力リクエスト( CaptureRequest
のインスタンス)を作る
CaptureRequest
はカメラが何ができるかなどの情報が必要なため、 CameraDevice
クラスが CaptureRequest.Builder
のファクトリメソッドを持っていますので利用します。
リクエストには色々ありますが、今回のリクエストは Target
に与えた Surface
によくあるプレビューの表示を作るための画像データを送ってくれ、というものです(たぶん)。
private CaptureRequest.Builder mPreviewRequestBuilder; private CaptureRequest mPreviewRequest; ... // createCameraPreviewSession メソッド内で try { mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mPreviewRequestBuilder.addTarget(surface); mPreviewRequest = mPreviewRequestBuilder.build(); } catch (CameraAccessException e) { e.printStackTrace(); }
CaptureSession
にリクエストをセットする
先ほど生成したリクエストに加えて、 CameraCaptureSession.CaptureCallback
が必要です。
プレビュー表示のいろいろなエラーハンドリングをする場合は結構書く必要があるみたいですが、今回はその辺は考えないのでnullを与えています。
この時点で TextureView を置いた領域にカメラで撮影した範囲が表示されているのが確認できます。
mCameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { // カメラがcloseされている場合 if (null == mCameraDevice) { return; } mCaptureSession = session; try { session.setRepeatingRequest(mPreviewRequest, null, null); // `CaptureSession` にリクエストをセットする } catch (CameraAccessException e) { e.printStackTrace(); } }
TotalCaptureResult
へ送られてきた1フレーム分への画像データの処理について
前節で TotalCaptureResult
へデータが送られてくるところまでは確認しましたが、 プレビューだけ延々と表示しても仕方ないので、ここへ飛んできたデータを利用する方法の一例を書いておしまいにします。
下記のコードでは、ボタンを押すとプレビューの表示を止めて、プレビューに表示している画像をファイルへ保存しています。
Button capture = (Button) findViewById(R.id.button_capture); capture.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { mCaptureSession.stopRepeating(); // プレビューの更新を止める if(mTextureView.isAvailable()) { File file = new File(getFilesDir(), "surface_text.jpg"); FileOutputStream fos = new FileOutputStream(file); Bitmap bitmap = mTextureView.getBitmap(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos); fos.close(); } } catch (CameraAccessException e) { e.printStackTrace(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } });
真剣にやろうと思ったらActivityやFragmentのライフサイクルに合わせてリソースの解放とか色々あるんですが今日は流れを把握するためにこんな感じで。
プレビューの表示止めてファイルに書き込む部分まで含めて全体のコードは以下となります。一番初めに書きましたが、今回はCamera2 APIの流れだけを把握しようと例外処理を省くため、targetSDKVersion=21
にしているので少々注意してお試しください。
現場からは以上です。