2012年12月11日火曜日

【Android】bindView内でHandlerを使うとposition:0のbindViewが大量に呼び出される

bindView内のHandlerでUI処理を行うとpositionが0のViewをバインドさせるような処理が呼ばれてしまう.
positionが0というのはListViewなら一番上のリスト,GridViewなら一番左上のリストである.
この現象はbindViewのHandler内でUI処理を行わない場合は起きない.
…が,そもそもUI処理を行わないのにHandlerなんて使わない.

それがどのくらい大量に呼び出されるかというと,コードにも依るかもしれないが
読み込んだリストの倍の数だけposition:0のbindViewが呼び出される.
1列に3個Viewが並んだGridViewだとすると通常のViewがリストの分,3つ呼び出されて,
その後にposition:0のViewが6個呼び出される.

Handler内でUI処理を行なう場合でも
ImageView.setImageResource()やTextView.setText()だけを行った場合はこの現象が起きない.
View.setVisibility()はViewが変更されれば現象が起きるが変更されなければ起きない.

謎だ.

いろいろ調べてると何箇所かでViewの高さをwrap_contentにしてると重複して呼び出されるよって書いてあるけど
fill_parentでもなる.
dp指定でも起こるうえに設定したheightが見事に無視された.

bindView内でHandlerを使うということはそれなりに重い処理(画像処理,ネット接続等)を別スレッドで行なって
処理完了後にUIに反映させるんだろうが,
こうやってムダなbindViewが呼び出されるとその分スレッドが作られてリソースを圧迫することになる.
ユーザーがファストスクロールしまくると
メモリ圧迫⇒オーバーフロー死
することになる.

fastscrollをオフにしたら問題が起こるほどではないんだろうけど
グリッドにバインドするアイテムが万単位にもなるとfastscrollオフは完璧に地獄になる.

これを抑えることができればスレッド数も1/3に抑えられてメモリリークのリスクも圧倒的に減るのだが.
最低でも強制終了はよろしくないので


対応その1(対策ではない)
設置スレッド数の上限を決めてしまう方法

このようなかんじでスレッド開始前にスレッド数確認して乱立していたらスレッドを追加しない
という方法は採ることはできるが…
スクロールを止めた画面は描画されないことに成りかねない.
それに機種によって限界は違ってくるし,バックグラウンドの状況によってもどうなるかは分からない.
強制終了よりはマシというレベル.

既にキューに並んだスレッドを間引くことができればまだ何とかできそうだが
Thread.stop()などが非推奨になっているためそれもできず.
要らないスレッド殺してえ…

対応その2
UIとの整合性が保たれていないスレッド
つまりバインドすべきViewはとっくに通り過ぎた場合なんかは
スレッド内の重い処理に到達するまでにreturnしてやる.
そしてさっさとスレッドを消化してやる.

スレッドの乱立を防止するわけではなく,立ってしまったスレッドに対しての後手策なので
ほんとうに足掻き程度だけど.

根本原因のbindView乱立が起きなければ問題は緩和されるのに.
全然わからん.

こういうbindViewでスレッドを設置して処理させるという作り方をしたのがそもそも間違いなのだろう.
bindView内の処理を大幅に見直すことにする.
が,こういう現象があるよという知見程度に.

2012年12月5日水曜日

【Android】SimpleDateFormatのタイムゾーンに注意


日時を文字列で取得する際にハマったこと.

日時を文字列で取得する際に
SimpleDateFormatクラスを使うことが多いと思う.

SimpleDateFormatクラスではフォーマットを指定して,時刻情報を与えてやると
フォーマットに合わせて整形された時刻情報が文字列で取得できる.

そこでよく使われるRFC1123形式と呼ばれる
Wed, 21 Jan 2009 10:09:52 GMT+09:00
こういったフォーマットのデータを取得したいとする.

そのためのフォーマット情報が
DateUtilsクラスにパターンが既に用意されている.

String org.apache.http.impl.cookie.DateUtils.PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy"
String org.apache.http.impl.cookie.DateUtils.PATTERN_RFC1036 = "EEEE, dd-MMM-yy HH:mm:ss zzz"
String org.apache.http.impl.cookie.DateUtils.PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz"

この3つがあるようだ.
至れり尽くせりである.

つまりこんなかんじでLong値ミリ秒UNIXタイムデータをRFC1123形式の文字列に変換できる.


ここまでは特に問題ないと思っていた.
というか,結果の文字列をUIに表示させるだけなら問題ない.
しかし,複数機器を跨いでフォーマット情報をキーに変換を行う場合問題が起こることがある.


上記のソースを使用して複数端末に時間情報を喰わせる.
すると機種依存かAndroidバージョン依存かは知らんが違うフォーマットで返ってくることがある.

欲しいのは
Wed, 21 Jan 2009 10:09:52 GMT+09:00
こういうフォーマットである.
しかし,一部機種で
Wed, 05 Dec 2012 09:48:26 JST
こんな情報が返ってきた.
タイムゾーンの表示がちがう.

UIに表示させるだけなら問題ないが,
連携する機器が【JST】を理解できなければそれはエラーである.


サーバーがGMT基準しか受け付けない場合
なおかつタイムゾーンがそんなに重要でない場合は

ハードコーディング.
こんなかんじでやっつけても大丈夫だと思う.

2012年12月3日月曜日

【Android】CursorでIllegalStateException


IllegalStateException: Couldn't init cursor window
とログを吐いてアプリが強制終了した.

原因はくだらないことだがカーソルを開いたままクローズしていなかったからだった.
メモリ不足のようだ.

このログが吐かれた場合は
・カーソルを開いて処理して…を繰り返すルーチンがある場合はちゃんとclose()してるか確認
・query()時に取得条件を増やすことで不要なデータは除去してメモリの圧迫を緩和する
ことに留意すべきである.

原因が前者であれば対応は楽だが,後者であればどれだけ対応しても場合によってはメモリ不足に陥る可能性が残るため
カーソルの扱い方の変更も視野に入れたほうがいいかもしれない.