fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

Html.fromHtml()はカジュアルに使うものではない?

拙作のツイッタークライアントが、起動して3時間ほど放置しておくと全く動かなくなってしまうようになった。どこか触るとANRが出て落とせるので、その都度traces.txtを見ると、いつもHtml.fromHtml()を呼んでるところで止まっている様子。

内部ではパーサーオブジェクトのイニシャライズでnew char[2000]をやっているところだった。

Cross Reference: /external/tagsoup/src/org/ccil/cowan/tagsoup/Parser.java

名前やコメントから察するにこの配列はHTMLのコメント部分を読み込む時に使うバッファのようなのだが、メンバ名で検索しても宣言の箇所しかヒットしない。可視性がprivateなのでなんのためにあるのかわからなかった。

traceviewで見ると処理時間も結構かかるようだし、せいぜい1行のTextViewに使うには大げさなのかなと思ってなるべく呼ばないようにする工夫を入れた。固まってしまう原因はまだよくわからないので経過を見守ることにする。

(これはAndroid 6.0で確認した)

FragmentManager is already executing transactionsと言われた時、Fragmentの初期処理で例外が起きていないか確認する

(この現象はAndroid6.0, support-lib ver.24.2.1で確認した。)

次のような例外が出た。メッセージを読むと「トランザクションをすでに実行しているところだ」と言われているようなので、commit()するタイミングが悪かったのかななどを考えていろいろ試してみるも効果なし。ググるcommitNow()するといいよみたいなStackOverflowのやり取りが出てきたりするが、効果なし。

java.lang.IllegalStateException: FragmentManager is already executing transactions
at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:1626)
at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:679)
at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:143)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1240)
at android.support.v4.view.ViewPager.populate(ViewPager.java:1088)
at android.support.v4.view.ViewPager.onMeasure(ViewPager.java:1614)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.support.design.widget.CoordinatorLayout.onMeasureChild(CoordinatorLayout.java:700)
at android.support.design.widget.HeaderScrollingViewBehavior.onMeasureChild(HeaderScrollingViewBehavior.java:90)
at android.support.design.widget.AppBarLayout$ScrollingViewBehavior.onMeasureChild(AppBarLayout.java:1364)
at android.support.design.widget.CoordinatorLayout.onMeasure(CoordinatorLayout.java:765)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at android.support.v7.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:135)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1465)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:748)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:630)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1465)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:748)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:630)
at android.view.View.measure(View.java:18788)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5951)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
at com.android.internal.policy.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2643)
at android.view.View.measure(View.java:18788)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:2100)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1216)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1452)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1107)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6013)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:858)
at android.view.Choreographer.doCallbacks(Choreographer.java:670)
at android.view.Choreographer.doFrame(Choreographer.java:606)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:844)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

スタックトレースをみるとViewPagerの各ページを作る途中に起きているということぐらいはわかるが、私の書いたどのコードが発端になっているのかまではわからなかった。スタックトレースの一番上のメソッドを見てみると、

    public void execSingleAction(Runnable action, boolean allowStateLoss) {
        if (mExecutingActions) {
            throw new IllegalStateException("FragmentManager is already executing transactions");
        }

        if (Looper.myLooper() != mHost.getHandler().getLooper()) {
            throw new IllegalStateException("Must be called from main thread of fragment host");
        }

        if (!allowStateLoss) {
            checkStateLoss();
        }

        mExecutingActions = true;
        action.run();
        mExecutingActions = false;

        doPendingDeferredStart();
    }

となっている。mExecutingActions==trueであるために起きているということなのだが、これを書き換えているのはそのすぐ下と、FragmentManagerImpl#execPengingActions()の中。どっちもRunnable#run()の前後にフラグのon/offをやっている。Runnable#run()のなかで例外が起きてしまうとmExecutingActionがtrueのままになるのか、と考えて追加したコードのバグを取ったら期待したとおりに動いた。

FragmentManagerImpl#execSingleAction()は比較的最近追加されたAPIであるようす。action.run()の例外がどこで握りつぶされているのかなどの解析はまだしていない。このエラーメッセージでぐぐる日本人の時間を少しでも救えればいいかなと思って書いた。

Twitter cardとOpen graphのデータを取ってくる

Twitterカードのデータは個々のwebページのmetaタグなので、ページをフェッチしてhtmlを解析すればよい。AndroidではXmlPullParserを使えばお手軽にhtmlを解析できる。

dev.twitter.com

developer.android.com

ちなみに、上のサイトには

  • KXmlParser via XmlPullParserFactory.newPullParser().
  • ExpatPullParser, via Xml.newPullParser().

などと書いてあるが少なくともM(API Level 23)ではXml.newPullParser()でも返ってくるのはKXmlPullParserである様子。

ここでの肝はXmlPullParser#setFeature(Xml.FEATURE_RELAXED, true)をすること。これがないと閉じてないmetaタグやlinkタグのせいでheadの閉じタグを読み込むところで例外がでる。文法的にゆるふわなところをいい感じに無視してくれるフラグということだと思う。しかし、これをもってしてもhtml 4.0のドキュメントタイプ宣言部分をパースできないようなので、XmlPullParserに食わせる前に削っておくなどの配慮が必要である(追記:冷静に考えたらそんなわけないじゃんとなった。doctype宣言が適切でないのが原因のようす)。また、上のサイトではXmlPullParser#require()でpullparserの状態をチェックしているが、このメソッドは大文字小文字を厳格にチェックするようなのでオススメしない。

あとはhttpクライアントのリダイレクトを有効にしておくことぐらいだろうか。OkHttp3はデフォルトではリダイレクトが有効になっていた。

twitter cardに対応していないサイトでも、open graph protocolに従ったデータを書いているサイトもある。これも合わせて読み込むようにしておくと、カードを作れるサイトの幅が広がるので良い。instagramとか。

ogp.me

カードの準備をすることによってgoo.glとかで圧縮されたurlの展開を先んじて行えるので、モバイルでのリダイレクト遅すぎ問題を解決できる。androidならintentでアプリを直接開くこともできる。