Mastodonを設置したときのメモ
Mastodonは公式ドキュメントが比較的充実しているので設置そのものは簡単な部類です。
mastodon.juggler.jpは4/12に稼働開始しました。特に人を増やす予定もなかったのですが、当時の日本サーバ群は色々と不安定だったので避難所的な使われ方をして人数が増えてしまいました。その頃に発生した問題についてメモしておきます。
作成時に行っておくと良い作業
masterブランチを使わない
Mastodonのmasterブランチは開発ブランチです。使ってはいけません。
最新のタグをチェックアウトしましょう。
git clone https://github.com/tootsuite/mastodon.git cd mastodon git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
Docker構成の公式ドキュメント
https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Docker-Guide.md
ボリューム永続化のすすめ
git update-index --skip-worktree docker-compose.yml してから docker-compose.yml を編集します。
ファイル中に指定されているボリュームは次の5か所です。
- ./postgres:/var/lib/postgresql/data
- ./redis:/data
- ./public/assets:/mastodon/public/assets
- ./public/system:/mastodon/public/system
- ./public/system:/mastodon/public/system
ただし最初の2つはコメントアウトしないとホスト側フォルダに保存されません。コンテナ自体にデータが保存されるため、コンテナが削除された時点でpostgresやredisのデータが消えてしまいます。
「### Uncomment to enable DB persistance」の下の2行 x 2か所のコメントを外します。こうしておくとコンテナをdown やrm してもホスト上にフォルダがそのまま残っているので、コンテナをいつ作り直しても問題ありません。まとめてtarできるので管理も楽になります。
もし postgresやredis のボリュームを指定せずに始めてしまった場合は、コンテナが消える前に(stopしてから) docker cp でコンテナが保持しているデータを抽出して、docker-compose.yml を編集しなおすのが良いと思います。
Minio
私は行っていないけど、参考までに。
nullkalさんのトゥート https://mstdn.jp/@nullkal/125745 によると「あ,一つ忠告なんですけど,どんなに小さい公開インスタンス作るとしてもMinioとか駆使して画像アセットだけはドメイン分けといたほうが後で人が多くなったときに楽です (二回目のリセットの理由はここです)」
Mastodonはユーザが投稿した画像などのデータをS3などのストレージサービスに保存する設定がある。ただし素直にS3を使ってしまうと費用的にバカ高いのでMinioなどのS3互換のストレージサーバを自分で建てることを推奨しているようだ。
最初にこれを行うことを勧めているのは、後から移行するのが困難だからだろう。
発生した問題
300人超で自宅鯖だと帯域の限界に
上り帯域が一日15GBを超えて、般家庭用のプロバイダ契約だと帯域規制の対象になりそうでした。一週間くらいでプロバイダから警告のメールやお手紙が届くので、そうなる前にVPSに移行することにしました。
- 永続化が必要なボリュームには最初からホスト側のパスを指定していたのでそれ全部とmastodonのビルドツリーをtar cpjf で固めておく
- 移動先のVPSで 同じフォルダ構成で展開する
- docker-compose build && docker-compose up -d
で移行終了。
ここで下手にgit pull とか docker-compose run --rm web rails assets:precompile とかしちゃうとassets がうまくなくてWebアプリのJavaScriptがうまく動かなくなる。assetsこわい
Webアプリ用にプリコンパイルされるassetsを削除する方法は rails --tasks に2つ説明されています。
rails assets:clean[keep] # Remove old compiled assets rails assets:clobber # Remove compiled assets
assets:clobberの方は「Errno::EBUSY: Resource busy @ dir_s_rmdir - /mastodon/public/assets」と怒られますが、削除するファイルはassets:cleanより多いです。
docker-compose run --rm web rails assets:clean docker-compose run --rm web rails assets:clobber
のように両方を呼び出すと、public/assets/ には何もファイルがない状態になります。
800人超 で 帯域4Mbpsの限界に
だいたい4Mbpsあたりでクリップして帯域不足が発生しました。
VPSのお試し期間を切り上げて本登録したら 8Mbps前後でも安定するようになりました。
1100人超
今度はCPU負荷やディスク負荷が頭打ちになりました。
どのプロセスが重いのかdstat -tdng --file --lock --top-io --top-bio で調べてみたら sidekiq でした。
管理者メニューでsidekiqを見てみると待機中のpushキューが8kありました。
対策は https://medium.com/@Gargron/scaling-mastodon-1becde463090 に書いてあったものです。
- 「ネイティブスレッドじゃないRubyだから、1プロセス複数スレッド => 複数プロセス複数スレッド に分けた方がより速い」
- 「4種類のキューを並列にうごかすと良い。defaultキューが捌けるのが早いとユーザレスポンスが向上する」
sidekiqのpushは先方のサーバの処理速度に影響されるものだし、やや重いのは仕方ないですね
作業としては docker-compose.yml を編集してsidekiqのコンテナを増やしました。
記述例 https://gist.github.com/tateisu/646969c6281c9ef99c24a765cbd701ef
sidekiq -c に指定する並列数ですが、この時点ではメールキューとpullは5、defaultとpush は20程度で不足ありませんでした。
そしてこの並列数の最大値にあわせて、.env.production に DB_POOL=20 を指定します。
DB_POOL環境変数は mastodon/config/database.yml で参照されています。
railsプロセス内のDBコネクションプールの数を指定するものです。
プロセスごとに最大でこの数までのDB接続を保持します。
設定ファイルを書き換えたら docker-compose down --remove-orphan とか docker compose up -d を行うと反映されます。
dockerコンテナのイメージを作成するのに10分前後かかりました。
溜まったキューはこれで消化されて、CPU使用率もディスク使用率も大幅に下がりました。
キューが溢れる原因は?
Mastodonの場合はどんなインスタンスと連動するか事前には分かりません。中には「とりあえず動かしてみたけどすぐ止めた」ような感じで稼働が安定しておらず、キューのジョブが接続タイムアウトで終わるようなインスタンスもあります。
基本的には処理プロセス数やスレッド数を増やして、いくつかブロッキングしても残りで十分に回せるようにするしかなさそうです。
手動で何かする余裕があるなら、管理者メニューのsidekiqで再試行や死亡を見てダメそうなのは削除するのも良いと思います。
アクティブユーザ500人超 ?
docker 公式の nginx イメージは worker_processes 1 と worker_connections 1024 が指定されています。
ストリーミングAPIを多数抱えるとこれでは足りなくなるので、適当に増やしましょう。
worker_processes 3 、worker_connections 20000 くらいにしました。
もし nginx環境でulimit -n して 低い数字が出るなら、worker_rlimit_nofile も指定する必要があるかもしれません。
DHCPを使わずに無線LANに接続する
普通はDHCPクライアントが自動的にIPアドレスを取得してくれるが、今回はDHCPサーバにしたかったので dhcpcd5 と isc-dhcp-client をpurge してから以下の方法でネットワーク情報を設定した。
resolvconf のインストール
「dpkg -l |grep resolvconf」で確認して、もし入っていないなら「sudo apt-get install resolvconf 」でインストールする。
Raspbian Lite 等ではデフォルトではインストールされていない。
これで /etc/network/interfaces 中に dns-nameservers を記述できるようになる
/etc/network/interfaces の編集
source-directory /etc/network/interfaces.d auto lo iface lo inet loopback auto eth0 iface eth0 inet manual auto wlan0 allow-hotplug wlan0 iface wlan0 inet manual wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf wireless-power off allow-hotplug wlan1 iface wlan1 inet manual wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf wireless-power off iface default inet dhcp iface home inet static address xxx.xxx.xxx.xxx network xxx.xxx.xxx.xxx netmask xxx.xxx.xxx.xxx broadcast xxx.xxx.xxx.xxx gateway xxx.xxx.xxx.xxx dns-nameservers xxx.xxx.xxx.xxx
ポイントはwpa-conf ではなく wpa-roamを使うことと、 ネットワーク情報を記述するインタフェースを別途追加しておくこと。
ついでに wireless-power off で無線LANの省電力機能を無効化している。実際に無効化されたかどうかは「iwconfig」コマンドで確認できる。
アクセスポイントのパスワードの暗号化
「wpa_passphrase (your_ssid_name)」を実行して標準入力にパスワードを打ち込むと次のような出力が得られる
network={ ssid="(your_ssid_name)" #psk="**********" psk=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX }
"**********" の部分には生のパスワードが、 XXX... の部分には暗号化されたパスワードが入る。ダブルクオートの有無に注意。
暗号化されたパスワードだけコピペして、すぐにCTRL+Lで画面を消してしまおう。
/etc/wpa_supplicant/wpa_supplicant.conf の編集
country=JP ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 network={ ssid="(your_ssid_name)" psk=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX priority=0 id_str="home" }
ssidとpskの部分は先ほど得られた内容に置き換える。ダブルクオートの有無に注意。暗号化されたパスワードの前後にダブルクオートをつけてはいけない。
id_strには、 /etc/network/interfaces 中の、ネットワーク情報を記述したインタフェースの名前を指定する
java.net.PlainSocketImpl.finalize() timed out after 10 seconds
java.util.concurrent.TimeoutException: java.net.PlainSocketImpl.finalize() timed out after 10 seconds at libcore.io.Posix.close(Native Method) at libcore.io.BlockGuardOs.close( BlockGuardOs.java:101) at libcore.io.IoBridge.closeAndSignalBlockedThreads( IoBridge.java:207) at java.net.PlainSocketImpl.close( PlainSocketImpl.java:148) at java.net.PlainSocketImpl.finalize( PlainSocketImpl.java:206) at java.lang.Daemons$FinalizerDaemon.doFinalize( Daemons.java:190) at java.lang.Daemons$FinalizerDaemon.run(Daemons.java:173) at java.lang.Thread.run(Thread.java:818)
この例外、コールスタックからだとFinalizerDaemon.run() を動かしているスレッドから投げられたように見える。しかし https://android.googlesource.com/platform/libcore/+/master/libart/src/main/java/java/lang/Daemons.java を確認すると、実際にはこの例外はFinalizerDaemonからではなく java.lang.Daemons$FinalizerWatchdogDaemon#finalizerTimedOut()で生成されてThread.getDefaultUncaughtExceptionHandler().uncaughtException(...) に渡される。一見だとそう見えないのは、この例外のコールスタック情報はFinalizerDaemonのthread.getStackTrace()のもので上書きされているからだ。
FinalizerWatchdogDaemonはこの例外を uncaughtException()に渡した後は終了してしまう。
FinalizerDaemonはおそらくPosix.close(Native Method) でブロックしているが、これが一定時間内に復帰する保証はない。
アプリ側であらかじめソケットをクローズしておけばいいのかもしれないが、そのソケットというのはHttpUrlConnectionが内部に持つコネクションプールのものだ。このコネクションプールを明示的に制御する方法はよくわからない。
そもそも、10秒タイムアウトでアプリを落とすというのは、日本のモバイルネットワークだとやや短すぎるのではないかと思う。かといってFinalizerWatchdogDaemonのタイムアウト定数をアプリ側から変更できる仕組みは用意されていない。
対策としては、Thread.setDefaultUncaughtExceptionHandler() で設定したハンドラでこの種のエラーを検出したら何もせずにreturnすれば良いかもしれない。FinalizerDaemonがブロックし続けた場合はまた別のエラーがどこかで発生するだろう。
back stack を保持したまま現在のfragmentを入れ替える
素直に入れ替えると「back stackに記録された最後のフラグメントとcontainerに現在保持されているフラグメントがマッチしない」という問題が発生します。
back stack の内容を書き換えるのは難易度が高いので、別の対応を考えてみましょう。
- back stack をpopする時に、現在のフラグメントがback stackに記録されていないならremoveする
コードは後述。これだけでもとりあえず動きますね。ライフサイクルイベントがこんな順序で発生します。
#---------------------------------------- # アプリを起動してフラグメントを1つ追加 (ActMain onApplyThemeResource)ActMain onApplyThemeResource (ActMain onCreate(1arg) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onContentChanged) )ActMain onCreate(1arg) (ActMain onStart (FragmentTest#1 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#1 onCreate state=null,is_view_destroyed=false) (FragmentTest#1 onCreateView state=null,is_view_destroyed=false) (FragmentTest#1 onCreateAnimation) (FragmentTest#1 onViewCreated state=null,is_view_destroyed=false) (FragmentTest#1 onActivityCreated state=null) (FragmentTest#1 onViewStateRestored state=null) (FragmentTest#1 onStart) )ActMain onStart (ActMain onPostCreate(1arg) (ActMain onTitleChanged) )ActMain onPostCreate(1arg) (ActMain onResume) (ActMain onPostResume (ActMain onResumeFragments (FragmentTest#1 onResume) )ActMain onResumeFragments )ActMain onPostResume (ActMain onAttachedToWindow) (ActMain onWindowFocusChanged) (ActMain onEnterAnimationComplete) #---------------------------------------- # 状態の確認 (ActMain onUserInteraction) backstack: count=0 getFragments: FragmentTest#1 findFragmentById: FragmentTest#1 #---------------------------------------- # 現在のフラグメントを差し替えて、バックスタックに追加する (ActMain onUserInteraction) (FragmentTest#1 onPause) (FragmentTest#1 onStop) (FragmentTest#1 onDestroyView) (FragmentTest#1 onCreateAnimation) (FragmentTest#2 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#2 onCreate state=null,is_view_destroyed=false) (FragmentTest#2 onCreateView state=null,is_view_destroyed=false) (FragmentTest#2 onCreateAnimation) (FragmentTest#2 onViewCreated state=null,is_view_destroyed=false) (FragmentTest#2 onActivityCreated state=null) (FragmentTest#2 onViewStateRestored state=null) (FragmentTest#2 onStart) (FragmentTest#2 onResume) #---------------------------------------- # 状態の確認 (ActMain onUserInteraction) backstack: count=1 backstack[0]: FragmentTest#2 getFragments: FragmentTest#1 getFragments: FragmentTest#2 findFragmentById: FragmentTest#2 #------------------------------------ # 現在のフラグメントを差し替えるが、バックスタックは変更しない (ActMain onUserInteraction) replace FragmentTest#2 => FragmentTest#3 (FragmentTest#2 onPause) (FragmentTest#2 onStop) (FragmentTest#2 onDestroyView) (FragmentTest#2 onCreateAnimation) (FragmentTest#3 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#3 onCreate state=null,is_view_destroyed=false) (FragmentTest#3 onCreateView state=null,is_view_destroyed=false) (FragmentTest#3 onCreateAnimation) (FragmentTest#3 onViewCreated state=null,is_view_destroyed=false) (FragmentTest#3 onActivityCreated state=null) (FragmentTest#3 onViewStateRestored state=null) (FragmentTest#3 onStart) (FragmentTest#3 onResume) #---------------------------------------- # 状態の確認 (ActMain onUserInteraction) backstack: count=1 backstack[0]: FragmentTest#2 getFragments: FragmentTest#1 getFragments: FragmentTest#2 getFragments: FragmentTest#3 findFragmentById: FragmentTest#3 #------------------------------------------- # ホームキーを押した(1回目) (ActMain onUserInteraction) (ActMain onUserLeaveHint) (ActMain onPause (FragmentTest#3 onPause) )ActMain onPause (ActMain onWindowFocusChanged) (ActMain onCreateDescription) (ActMain onSaveInstanceState (FragmentTest#1 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=true,is_resumed=false) (FragmentTest#2 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=true,is_resumed=false) (FragmentTest#3 onSaveInstanceState is_created=true,is_view_created=true,is_view_destroyed=false,is_resumed=false) )ActMain onSaveInstanceState (ActMain onStop (FragmentTest#3 onStop) )ActMain onStop (ActMain onTrimMemory) (ActMain onDestroy (FragmentTest#1 onDestroy is_view_destroyed=true) (FragmentTest#1 onDetach) (FragmentTest#2 onDestroy is_view_destroyed=true) (FragmentTest#2 onDetach) (FragmentTest#3 onDestroyView) (FragmentTest#3 onDestroy is_view_destroyed=true) (FragmentTest#3 onDetach) )ActMain onDestroy (ActMain onDetachedFromWindow) #---------------------------------------- # 履歴からアプリを復元 (ActMain onApplyThemeResource) (ActMain onCreate(1arg) (FragmentTest#1 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#1 onCreate state=(Bundle),is_view_destroyed=false) (FragmentTest#2 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#2 onCreate state=(Bundle),is_view_destroyed=false) (FragmentTest#3 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#3 onCreate state=(Bundle),is_view_destroyed=false) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onContentChanged) )ActMain onCreate(1arg) (ActMain onStart (FragmentTest#3 onCreateView state=(Bundle),is_view_destroyed=false) (FragmentTest#3 onCreateAnimation) (FragmentTest#3 onViewCreated state=(Bundle),is_view_destroyed=false) (FragmentTest#3 onActivityCreated state=(Bundle)) (FragmentTest#3 onViewStateRestored state=(Bundle)) (FragmentTest#3 onStart) )ActMain onStart (ActMain onRestoreInstanceState(1arg)) (ActMain onPostCreate(1arg) (ActMain onTitleChanged) )ActMain onPostCreate(1arg) (ActMain onResume) (ActMain onPostResume (ActMain onResumeFragments (FragmentTest#3 onResume) )ActMain onResumeFragments )ActMain onPostResume (ActMain onAttachedToWindow) (ActMain onWindowFocusChanged) (ActMain onEnterAnimationComplete) #---------------------------------------- # 状態の確認 (ActMain onUserInteraction) backstack: count=1 backstack[0]: FragmentTest#2 getFragments: FragmentTest#1 getFragments: FragmentTest#2 getFragments: FragmentTest#3 findFragmentById: FragmentTest#3 #---------------------------------------- # ホームキーを押した(2回目) (ActMain onUserInteraction) (ActMain onUserLeaveHint) (ActMain onPause (FragmentTest#3 onPause) )ActMain onPause (ActMain onWindowFocusChanged) (ActMain onCreateDescription) (ActMain onSaveInstanceState (FragmentTest#1 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=false,is_resumed=false) (FragmentTest#2 onSaveInstanceState is_created=true,is_view_created=false,is_view_destroyed=false,is_resumed=false) (FragmentTest#3 onSaveInstanceState is_created=true,is_view_created=true,is_view_destroyed=false,is_resumed=false) )ActMain onSaveInstanceState (ActMain onStop (FragmentTest#3 onStop) )ActMain onStop (ActMain onTrimMemory) (ActMain onDestroy (FragmentTest#1 onDestroy is_view_destroyed=false) (FragmentTest#1 onDetach) (FragmentTest#2 onDestroy is_view_destroyed=false) (FragmentTest#2 onDetach) (FragmentTest#3 onDestroyView) (FragmentTest#3 onDestroy is_view_destroyed=true) (FragmentTest#3 onDetach) )ActMain onDestroy (ActMain onDetachedFromWindow) #---------------------------------------- # 履歴からアプリを復元 (ActMain onApplyThemeResource) (ActMain onCreate(1arg) (FragmentTest#1 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#1 onCreate state=(Bundle),is_view_destroyed=false) (FragmentTest#2 onAttach) (ActMain onAttachFragment(support.Fragment)) (FragmentTest#2 onCreate state=(Bundle),is_view_destroyed=false) (FragmentTest#3 onAttach)FragmentTest#3 onAttach (ActMain onAttachFragment(support.Fragment)) (FragmentTest#3 onCreate state=(Bundle),is_view_destroyed=false) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onWindowAttributesChanged) (ActMain onContentChanged) )ActMain onCreate(1arg) (ActMain onStart (FragmentTest#3 onCreateView state=(Bundle),is_view_destroyed=false ) (FragmentTest#3 onCreateAnimation) (FragmentTest#3 onViewCreated state=(Bundle),is_view_destroyed=false ) (FragmentTest#3 onActivityCreated state=(Bundle) ) (FragmentTest#3 onViewStateRestored state=(Bundle) ) (FragmentTest#3 onStart) )ActMain onStart (ActMain onRestoreInstanceState(1arg)) (ActMain onPostCreate(1arg) (ActMain onTitleChanged) )ActMain onPostCreate(1arg) (ActMain onResume) (ActMain onPostResume (ActMain onResumeFragments (FragmentTest#3 onResume) )ActMain onResumeFragments )ActMain onPostResume (ActMain onAttachedToWindow)ActMain onAttachedToWindow (ActMain onWindowFocusChanged)ActMain onWindowFocusChanged (ActMain onEnterAnimationComplete)ActMain onEnterAnimationComplete #------------------------------------------ # 状態の確認 (ActMain onUserInteraction) backstack: count=1 backstack[0]: FragmentTest#2 getFragments: FragmentTest#1 getFragments: FragmentTest#2 getFragments: FragmentTest#3 findFragmentById: FragmentTest#3 #------------------------------------------ # 戻るキーを押した (ActMain onUserInteraction ) (ActMain onKeyDown ) (ActMain onUserInteraction) (ActMain onKeyUp (ActMain onBackPressed page_pop: back stack name and current fragment are not match. remove current fragment. )ActMain onBackPressed )ActMain onKeyUp (FragmentTest#3 onPause ) (FragmentTest#3 onStop) (FragmentTest#3 onDestroyView ) (FragmentTest#3 onCreateAnimation ) (FragmentTest#2 onDestroy is_view_destroyed=false ) (FragmentTest#2 onDetach ) (FragmentTest#1 onCreateView state=(Bundle),is_view_destroyed=false ) (FragmentTest#1 onCreateAnimation ) (FragmentTest#1 onViewCreated state=(Bundle),is_view_destroyed=false ) (FragmentTest#1 onActivityCreated state=(Bundle) ) (FragmentTest#1 onViewStateRestored state=(Bundle) ) (FragmentTest#1 onStart) (FragmentTest#1 onResume) (FragmentTest#3 onDestroy is_view_destroyed=true) (FragmentTest#3 onDetach) #------------------------------------------ # 状態の確認 (ActMain onUserInteraction) backstack: count=0 getFragments: FragmentTest#1 getFragments: null getFragments: null findFragmentById: FragmentTest#1 #------------------------------------------ # 戻るキーを押した (ActMain onUserInteraction) (ActMain onKeyDown) (ActMain onUserInteraction) (ActMain onKeyUp (ActMain onBackPressed) )ActMain onKeyUp (ActMain onWindowFocusChanged) (ActMain onPause (FragmentTest#1 onPause ) )ActMain onPause (ActMain onStop (FragmentTest#1 onStop ) )ActMain onStop (ActMain onDestroy (FragmentTest#1 onDestroyView ) (FragmentTest#1 onDestroy is_view_destroyed=true ) (FragmentTest#1 onDetach ) )ActMain onDestroy (ActMain onDetachedFromWindow )
このアプローチは入れ替えの時点でback stackを操作しようとするよりもラクですが、問題もあります。
問題1: 入れ替えによって表示されなくなったフラグメントが back stack に保持され続ける
onDestroyView などのタイミングでビューへの参照を外すなどしてFragmentの消費メモリを軽減すると良いです。ただし onDestroyView が呼ばれた時点ではビューはまだ画面に表示されています。Bitmapの後処理を行うタイミングはFragment#onCreateAnimation で作成するアニメーションにセットするリスナーの onAnimationEnd イベントなどが適しているのかもしれません。
問題2: ホーム画面が再生成される際に、FragmentTransaction#setCustomAnimation() に指定した情報の一部が失われる
他アプリに移動せずに操作していると正常に動くのですが、他アプリに移動してまたテストアプリに戻ってきてから操作すると、popBackStack() した際のアニメーションが一部行われない場合がありました。
onCreateAnimation に渡される引数を追いかけてみるとこんな感じです。
バックスタックの頭のF#6と、バックスタックを更新しない入れ替えを行った F#3,F#5でnextAnimの情報が失われています。
# 初期配置。アニメーションは指定していない (FragmentTest#1 onCreateAnimation transit=0,enter=true,nextAnim=0 # f#1 がexit, f#2がenter (通常の入れ替え) (FragmentTest#1 onCreateAnimation transit=0,enter=false,nextAnim=2131034118 (FragmentTest#2 onCreateAnimation transit=0,enter=true,nextAnim=2131034121 # f#2 がexit, f#3がenter (back stackを更新しない入れ替え) (FragmentTest#2 onCreateAnimation transit=0,enter=false,nextAnim=2131034118 (FragmentTest#3 onCreateAnimation transit=0,enter=true,nextAnim=2131034121 # f#3 がexit, f#4がenter (通常の入れ替え) (FragmentTest#3 onCreateAnimation transit=0,enter=false,nextAnim=2131034118 (FragmentTest#4 onCreateAnimation transit=0,enter=true,nextAnim=2131034121 # f#4 がexit, f#5がenter (back stackを更新しない入れ替え) (FragmentTest#4 onCreateAnimation transit=0,enter=false,nextAnim=2131034118 (FragmentTest#5 onCreateAnimation transit=0,enter=true,nextAnim=2131034121 # f#5 がexit, f#6がenter (通常の入れ替え) (FragmentTest#5 onCreateAnimation transit=0,enter=false,nextAnim=2131034118 (FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=2131034121 # 状態の確認 backstack: count=3 backstack[0]: FragmentTest#2 backstack[1]: FragmentTest#4 backstack[2]: FragmentTest#6 getFragments: FragmentTest#1 getFragments: FragmentTest#2 getFragments: FragmentTest#3 getFragments: FragmentTest#4 getFragments: FragmentTest#5 getFragments: FragmentTest#6 findFragmentById: FragmentTest#6 # 他アプリから戻ってきた (FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=0 # 他アプリから戻ってきた (FragmentTest#6 onCreateAnimation transit=0,enter=true,nextAnim=0 # f#6 がexit, f#5がenter (FragmentTest#6 onCreateAnimation transit=0,enter=false,nextAnim=0 (FragmentTest#5 onCreateAnimation transit=0,enter=true,nextAnim=0 # f#5 がexit, f#3がenter (FragmentTest#5 onCreateAnimation transit=0,enter=false,nextAnim=2131034119 (FragmentTest#3 onCreateAnimation transit=0,enter=true,nextAnim=0 # f#3 がexit, f#1がenter (FragmentTest#3 onCreateAnimation transit=0,enter=false,nextAnim=2131034119 (FragmentTest#1 onCreateAnimation transit=0,enter=true,nextAnim=0
対策ですが、fragment を pop した直後だけ onCreateAnimation でnextAnimを適切に補完できるようにします。今回は以下のようなことを行いました。
- Fragment#toString() をオーバライドして、フラグメントのインスタンスを特定できるような名前を返す
- addToBackStack(name) に指定する名前を、除去したフラグメントと追加したフラグメントの名前を含めたパターンにする
- popBackStackする直前にBackStackEntry#getName()でそのパターンを取り出し、補完するアニメーションのリソースIDをフラグメントに渡しておく
- フラグメントのonCreateAnimation では、もしリソースIDが渡されていたらそれを使ってアニメーションを生成する。渡されたリソースIDはリセットして、別のタイミングで呼びだされた時に使わないようにする
この問題はこれで解決したようです。
コード例
// Fragment 側 static final String ARG_NAME = "name"; static final FragmentTest create(int idx){ Bundle b = new Bundle(); b.putString( ARG_NAME, String.format( "%s", idx ) ); FragmentTest fragment = new FragmentTest(); fragment.setArguments( b ); return fragment; } @Override public String toString(){ return this.getClass().getSimpleName() + "#" + getArguments().getString( ARG_NAME ); } int nextAnim = 0; public void setNextAnimation( int nextAnim ){ this.nextAnim =nextAnim; } // Called when a fragment loads an animation. public Animation onCreateAnimation( int transit, boolean enter, int nextAnim ){ if( DEBUG_LIFECYCLE ) log.d( "(%s onCreateAnimation transit=%s,enter=%s,nextAnim=%s", toString() ,transit,enter,nextAnim); Animation rv; if( nextAnim == 0 && this.nextAnim != 0){ log.d("supply next Anim: %s",this.nextAnim); rv = AnimationUtils.loadAnimation( getActivity(), this.nextAnim ); this.nextAnim = 0; }else{ rv = super.onCreateAnimation( transit, enter, nextAnim ); } return rv; } // Activity側 int index_seed = 0; static final String STATE_INDEX_SEED="index_seed"; @Override protected void onCreate( Bundle state ){ is_created = true; super.onCreate( state ); setContentView( R.layout.act_main ); if( state == null ){ getSupportFragmentManager().beginTransaction() .add( R.id.container, FragmentTest.create(++index_seed) ) .commit(); }else{ index_seed = state.getInt(STATE_INDEX_SEED); } } @Override protected void onSaveInstanceState( Bundle state ){ super.onSaveInstanceState( state ); state.putInt(STATE_INDEX_SEED,index_seed); } @Override public void onBackPressed(){ // XXX: may you want to dispatch this event to fragment ? if( getSupportFragmentManager().getBackStackEntryCount() > 0){ page_pop(); }else{ super.onBackPressed(); } } void page_push(){ FragmentManager fm = getSupportFragmentManager(); FragmentTest f_new = FragmentTest.create(++index_seed); Fragment f_old = fm.findFragmentById( R.id.container ); String stack_name = (f_old==null?"null": f_old.toString() +"=>" + f_new.toString() ); getSupportFragmentManager().beginTransaction() .setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる R.anim.right_to_center, R.anim.center_to_left, R.anim.left_to_center, R.anim.center_to_right ) .replace( R.id.container, f_new ) .addToBackStack( stack_name ) .commit(); } void page_replace(boolean back){ FragmentManager fm = getSupportFragmentManager(); Fragment f_old = fm.findFragmentById( R.id.container ); Fragment f_new = FragmentTest.create( ++ index_seed ); log.d( "replace %s => %s", f_old, f_new ); FragmentTransaction ft = fm.beginTransaction(); if( back ){ ft.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる R.anim.left_to_center, R.anim.center_to_right, R.anim.right_to_center, R.anim.center_to_left ); }else{ ft.setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる R.anim.right_to_center, R.anim.center_to_left, R.anim.left_to_center, R.anim.center_to_right ); } ft.replace( R.id.container, f_new ) .commit(); // 結果として、最後のbackstackで追加されたfragmentと現在のフラグメントが食い違うことになる。 // これをこの時点で解決することはできないので、pop_pageで処理する } public boolean page_pop(){ FragmentManager fm = getSupportFragmentManager(); // FragmentManager.BackStackEntry entry = fm.getBackStackEntryCount() == 0 ? null : fm.getBackStackEntryAt( fm.getBackStackEntryCount() - 1 ); if( entry == null ){ log.d( "page_pop: back stack is empty." ); return false; } // BackStackに記録されているFragment Fragment f_backstack_old =null; Fragment f_backstack_current =null; Matcher m = Pattern.compile( "(.+?)=>(.+?)" ).matcher( entry.getName() ); if( m.find() ){ f_backstack_old = getFragmentByName( m.group( 1 ) ); f_backstack_current = getFragmentByName( m.group( 2 ) ); } //Activityの破棄と復元によりアニメーションが失われた場合に補う if( f_backstack_old != null ) ((FragmentTest)f_backstack_old).setNextAnimation( R.anim.left_to_center ); if( f_backstack_current != null ) ((FragmentTest)f_backstack_current).setNextAnimation( R.anim.center_to_right ); // 現在container に格納されているFragment Fragment f_current = fm.findFragmentById( R.id.container ); // 現在container に格納されているFragmentがバックスタックに記録されている追加したFragmentと異なる場合は、アニメーションつきで除去する if( f_current != null && f_current != f_backstack_current ){ log.d( "page_pop: current fragment mismatch between back stack and holded in container. remove fragment in container." ); fm.beginTransaction() .setCustomAnimations( // enter,exit,popEnter,popExit 後ろ2つは popBackStack の際に使われる R.anim.left_to_center, R.anim.center_to_right, R.anim.right_to_center, R.anim.center_to_left ) .remove( f_current ) .commit(); } // バックスタックをpopする fm.popBackStack(); return true; } private FragmentTest getFragmentByName( String name ){ for( Fragment f : getSupportFragmentManager().getFragments()){ if(f ==null ) continue; if( f instanceof FragmentTest ){ if( f.toString().equals( name ) ) return (FragmentTest) f; } } return null; }
その他
今回の話題とは直接関係ありませんが、上のログを記録した時は開発者オプションで「アクティビティを保持しない」を指定していました。ホームボタンを押すとアクティビティが破棄されて、履歴から戻った際に作成と復元が行われます。再度ホームボタンを押すとまたアクティビティが破棄されるのですが、この間、back stack にだけ存在する fragment に対して呼び出されるライフサイクルイベントは onAttach, onCreate, onSaveInstanceState, onDestroy, onDetach だけです。つまりonCreateで全ての状態をsavedInstanceStateから取得してonSaveInstanceStateでそれを保存しないと、状態が失われます。なんと恐ろしい。
Androidの通知アイコン
2.3から5.0までの端末で表示確認を行ってみました。
画像が多いので別ページで http://juggler.jp/tateisu/android/notification_looks/
Nexus 7 (2013)からの動画キャプチャ
ゲームの録画をしたくなったので動画キャプチャ環境を整えたなど。最初はChromecastの出力をHDMIスプリッターに繋いだけど、やはり帯域の問題で、Chromecast に出力した時点で画質が結構劣化してた。有線のHDMI出力アダプターの方が明らかに鮮明だった。
録画サンプルと機器の詳細をメモしておくよ
録画サンプル
(A) N7 2013 → Chromecast → HDMI スプリッタ → HDMI キャプチャ
http://juggler.jp/game-prru/video/capture-via-chromecast.mp4
機器の詳細
SlimPort 用 HDMI 出力アダプタ
microUSBから映像を出力する規格はMHLとSlimPortがあり、GalaxyやXperia系はMHL、Nexus系はSlimPort らしい。それぞれ異なる出力アダプタが必要になる。
http://www.amazon.co.jp/gp/product/B00I1C6RZ6 Nexus対応 SlimPort to HDMI 変換アダプタ 【Nexus7(2013動作確認済】 〔添付品 : HDMI ケーブル・充電用ケーブル〕
Vodaview Slimport to HDMI 変換アダプタ 添付品:HDMIケーブル・充電用ケーブル
- 出版社/メーカー: Vodaview
- 発売日: 2017/09/25
- メディア: Personal Computers
- この商品を含むブログ (1件) を見る
メーカーのVodaviewがAmazon経由で販売しているもの。充電用USBケーブルとHDMIケーブル付きで2789円だった。画面の縦横は自動的に処理される。Full HD を縦画面でそのまま…というモードはないみたい
HDMI スプリッター
HDCP 周りの問題を解決するためにHDMIスプリッターを挟む。
http://www.amazon.co.jp/gp/product/B003U0M3BQ LEICKE KanaaN 1入力2出力 対応HDMI 分配器 スプリッター Y-アダプタ 1080p FullHD1.3b 2-port/ 1 to 2
KanaaN HDMIスプリッター 1入力2出力 4k対応 Y-アダプタ 2160p Full UHD/ HD 1.4b 2-fach / 2-port
- 出版社/メーカー: LEICKE ライケ
- メディア: エレクトロニクス
- この商品を含むブログを見る
SPD Japan出品の純正品。HDMIケーブル1本+ACアダプタ付きで2599円だった。バスパワー動作は出来なかった。
HDMI キャプチャデバイス
HDMI 信号の画像と音声をPCにキャプチャする機器。まあ何でも良いと思うんだけど、今回使ったのはコレ
http://www.amazon.co.jp/gp/product/B00MIQ40JQ Elgato(エルガト)Elgato Game Capture HD60 (日本語ソフトウェア/正規輸入品)
- 出版社/メーカー: Elgato
- 発売日: 2014/08/23
- メディア: Personal Computers
- この商品を含むブログ (2件) を見る
メーカーのElgatoがAmazon経由で販売しているもの。18590円だった。1080p/60fps 対応、HDMI パススルー出力あり。キャプチャ時のエンコード品質は標準(14Mbps,6.4GB/時)から最高(40Mbps,18.1GB/時)まで調節可能。