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でそれを保存しないと、状態が失われます。なんと恐ろしい。

Nexus 7 (2013)からの動画キャプチャ

ゲームの録画をしたくなったので動画キャプチャ環境を整えたなど。最初はChromecastの出力をHDMIスプリッターに繋いだけど、やはり帯域の問題で、Chromecast に出力した時点で画質が結構劣化してた。有線のHDMI出力アダプターの方が明らかに鮮明だった。

録画サンプルと機器の詳細をメモしておくよ

録画サンプル

(A) N7 2013 → Chromecast → HDMI スプリッタ → HDMI キャプチャ

http://juggler.jp/game-prru/video/capture-via-chromecast.mp4

(B) N7 2013 → HDMI 出力アダプタ → HDMI スプリッタ → HDMI キャプチャ

http://juggler.jp/game-prru/video/capture-via-hdmi-adapter.mp4

Windows ムービーメーカーで切り出して再エンコードしたもの
Bの方が明らかに良いね

機器の詳細

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が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

SPD Japan出品の純正品。HDMIケーブル1本+ACアダプタ付きで2599円だった。バスパワー動作は出来なかった。

HDMI キャプチャデバイス

HDMI 信号の画像と音声をPCにキャプチャする機器。まあ何でも良いと思うんだけど、今回使ったのはコレ

http://www.amazon.co.jp/gp/product/B00MIQ40JQ Elgato(エルガト)Elgato Game Capture HD60 (日本語ソフトウェア/正規輸入品)

Elgato Game Capture HD60

Elgato Game Capture HD60

メーカーのElgatoがAmazon経由で販売しているもの。18590円だった。1080p/60fps 対応、HDMI パススルー出力あり。キャプチャ時のエンコード品質は標準(14Mbps,6.4GB/時)から最高(40Mbps,18.1GB/時)まで調節可能。

irc.juggler.jp のサービス障害と irc2.juggler.jp の追加

irc.juggler.jp は2014年10月9日17時45分頃から翌日2時30分頃まで停止していました。

リレーサーバなしだと障害時にチャンネルを維持できないので、対策として irc2.juggler.jp を用意して irc.juggler.jp とリレーさせました。 使っているIRCサーバソフトは64bit環境での稼働実績がなく、またVPSでのIRCサーバ運用もこれが初めてなので公開ベータ扱いです。

irc.juggler.jpの2ch@IRC脱退について

http://irc.juggler.jp/ の告知の詳細です。

2014年4月に Jim が irc.2ch.net をDNSから消しちゃった件について、2014年9月に Jim と色々交渉した結果、彼のネットワークポリシーだと irc.juggler.jp が再度 2ch@IRC とリレーできる可能性はほとんどないという結論に至りました。よって irc.juggler.jp は 2ch@IRC から脱退して、独立したIRCサーバとなります。ユーザの皆様にはご不便をおかけします。

2ch@IRCの現状

  • irc.2ch.net : 2ch@IRCの新1鯖。Jimが9月6日に建てた。彼のネットワークポリシーにより、外部とのリレーはほぼ不可能
  • irc.2ch.sc : 2ch@IRCの旧1鯖。3月ごろまでは irc.2ch.netの名前でアクセス可能だった
  • irc.nurs.or.jp : 2ch@IRCの旧2鯖。3月ごろまでは irc2.2ch.netの名前でアクセス可能だった
  • irc.juggler.jp : 2ch@IRCの旧3鯖だった。(脱退済)

上記4つがリレーせずに存在して、ユーザが分散しているのが現在の状態です。

時系列

今回の流れを時系列順に書くとこうなります

  • 4月ごろ:irc.2ch.netおよびirc2.2ch.netがDNSから消える。ただしサーバは稼働しており、旧1鯖はIPアドレス指定で、旧2鯖は IPアドレス指定および irc.nurs.or.jp の名前でアクセス可能。
  • (時期不明) 旧1鯖に irc.2ch.sc の名前でアクセスできるようになる。ただしWebサービスは停止した
  • 6月30日: irc.nurs.or.jp が メンテナンスに入る。この時点でサーバ間リレーは機能しなくなった。
  • 9月4日:irc.juggler.jp が 2ch.net の現管理者の Jim と交渉を開始。
  • 9月5日:irc.juggler.jp は 2ch@IRC から脱退を表明。
  • 9月6日:Jimは彼のirc.2ch.netを新しく建てた
  • 9月6日:irc.nurs.or.jp がメンテナンスから復旧

2ch@IRCの今後の展開

irc.2ch.net に Jim が Web IRC を設置するのは時間の問題でしょう。そうなると「板からリンクされてゼロインストールで使えるチャット」という2ch@IRCの本来の機能は復活することになります。

ただし長期的には、西村 vs Jimの 裁判によりDNSの所有権が移動して、irc.2ch.sc が irc.2ch.net として復活する可能性がかなりあります。

irc.juggler.jp の現状

irc.juggler.jp は irc.2ch.net にも irc.2ch.sc にも肩入れしないという選択をとりました。Jimが建てた irc.2ch.net とリレーできる可能性がない状態で 2ch@IRC を名乗り続けるのも、irc.2ch.sc とのリレーが長期間存在しない状態で 2ch@IRC を名乗り続けるのも、適切ではないと感じたからです。

2ch@IRCの既存ユーザの受け入れ先について

2ch@IRCの既存のユーザの大半は現時点では irc.juggler.jp に接続していますが、ユーザがどう動くのかは私にはわかりません。

irc.juggler.jp から外部サーバへの誘導告知を積極的に行える状態にはなっていないと思います。主に受け入れ側の態勢が整っていないという認識です。お家騒動が解決して、受け入れ準備ができた状態でならirc.juggler.jp はサービスを閉じても構わないと思います。もし irc.juggler.jp 独自のユーザ層ができていればまた話は別ですが。

個人的な感想

4月からの 2ch@IRCDNSトラブルによるユーザ数の減少がどれだけ食い止められるか、というのが私の視点です。Jimが裁判に勝っても負けても、その後に 2ch@IRC のユーザがどの程度残るのかが重要でした。irc.2ch.netとそのWeb導線が生きているのなら、それを誰が運営しているかはどうでもよいと思います。
結果的に私自身と 2ch@IRC との関係がなくなるというオチになりましたが、別段それで誰が困るということもないでしょう。

irc.juggler.jp の今後については、 2ch@IRC の末端という立場ではなくなった故に可能になることもいくつかありますが、どちらかというといつまでもIRCだとモバイルとの相性が悪くてどうしようもないので、そこをなんとかしたいです。

Android アプリの間違った作り方 の続き


ずっと前に「Android アプリの間違った作り方」という記事を書いたけど、
見なおしてみると色々アレだったので、今ならこうするというのを書いておく

Activity の初期化と終了処理

Activityはバックグラウンド状態になったり画面を回転させたりすると一時的に破棄/復元される事がある。プロセスごと一時的に破棄される場合もあるので、例えばstatic変数に状態を保存しても復元の際にそれが維持されているとは限らない。初期化処理と終了処理を書く際に破棄と復元を考慮するには以下のような工夫が必要になる。

onCreate(), onNewIntent(), onSaveInstanceState(), onRestoreInstanceState()

あるActivityが作成された時に、それが最初に作られたのか、一時的に破棄された後に復元されたのかはonCreateの引数がnullかどうかで切り分けられる。この引数に渡されるBundleは onSaveInstanceState() で設定したものと同じ内容になる。

void onCreate(Bundle state){
	initUI(); // ActivityのUIを初期化する
	if( state != null ){
		// このActivityは復元されたものなので、
		// state を使って状態を復元する
		restoreState( state );
	}else{
		// このActivityは新規に作成されたものなので、
		// インテントを使って状態を初期化する
		initState( getIntent() );
	}
}

void onNewIntent(){
	// あるActivityが新しいIntentを受け取った時に呼ばれる
	// インテントを使って状態を初期化する
	initState( getIntent() );
}

void onSaveInstanceState(Bundle state){
	super.onSaveInstanceState(state)
	// TODO: state に画面の現在の状態を保存する
}
void onRestoreInstanceState(Bundle state){
	// state を使って状態を復元する
	restoreState( state );
}

void initUI(){
	// UIの初期化を行う
}
void initState(Intent intent){
	// TODO: intent の情報を使って状態を初期化する
}
void restoreState(Bundle state){
	// TODO: state の情報を使って状態を復元する
	restoreState( state );
}
isFinishing()

Activityの 終了処理をonPause,onStop,onDestoroy に書く際、一時的な破棄とfinish()による破棄を区別するには isFinishing() を使う。あるActivityが一時的に破棄される際は isFinishing()はfalseを示す。

アプリケーションレベルの状態の管理

もしあなたのアプリAで「アプリ単位の状態管理」を実現したいなら、まずはAndroidはアプリ単位の起動状態なんて管理していないことを知っておいて欲しい。特に何も指定しない場合、アプリAに含まれるActivityは別のアプリBの画面スタックの末尾に積まれることもあるし単独の画面スタックに積まれることもある。公式ドキュメントのタスクアフィニティについての記述を参照。
http://developer.android.com/guide/components/tasks-and-back-stack.html#Affinities
それでもアプリケーションレベルの状態を管理したいなら、以下の手法が使えるかもしれない。

アプリケーション単位の初期化を行うタイミング
  • 外部から起動されるActivityの種類を1個だけにする
  • そのActivityに android:launchMode="singleTask" を指定する

この状態でそのActivityのonCreate()が呼ばれて、それが復元ではない(Bundle引数がnull)場合、その時点でアプリ単位の状態を初期化していい。

アプリケーション単位の状態の保存と復元

これだけだとプロセスごと破棄された場合に状態が失われてしまうので、
アプリ単位の状態をフラッシュメモリに保存する仕掛けを入れる。
画面スタックの底にあるアクティビティとそこから呼び出される子アクティビティの全てに以下のような記述を行う。

(manifestに登録したアプリケーションクラス)
class App1 extends Application{
    static MyAppState app_state;

    void saveAppState(){
        is( app_state != null ){
            // TODO: アプリ単位の状態をフラッシュメモリに保存する
        }
    }
    
    void prepareAppState(Activity activity,Bundle activity_state){
        if( activity_state == null &&  activity is (画面スタックの底にあるアクティビティ) ) ){
            // 初回起動なので、状態を初期化する
            // もし直前までの状態が残っていればその後処理を行ってもよい
            app_state = new MyAppState();
        }else if( app_state == null ){
            // TODO: フラッシュメモリから状態を読み込む
        }
    }
}

(Activity派生クラス全て)
void onPause(){
    super.onPause();
    ((App1)getApplication()).saveAppState();
}
void onCreate(Bundle state){
    super.onCreate();
    ((App1)getApplication()).prepareAppState(activity, state);
}

この方法には問題もあって、外部から呼び出せるアクティビティが1個に限られてしまうし、
それが呼び出されるたびにアクティビティの画面スタックのルート以外の部分が全て一旦クリアされてしまう。

アプリ単位の揮発性の状態を管理する、もっとイイ手法があれば誰か教えてください。