複数のRecyclerViewで巨大なリスト扱ってめちゃくちゃ死にまくったまとめ

前提 : RecyclerView

いわゆるVirtualScroll実装。
Viewの初期化時の計算や転送コストを下げつつ、画面に表示されるリストを「見えてる分だけ」に制限してリストのガワを最大限再利用することで、レイアウトのレンダリングコストを下げる実装。
似たような機構はiOSやwebでの無限スクロール系ライブラリにもある。
VirtualDOM的なアレとは非なる。

アンチパターンと解決

画像の再利用をしない

  • 問題
    リストの行を表現するxml上で直接drawableフォルダ内のimageリソースを指定したりすると、レイアウトの初期化時にどんどんメモリが確保されていってやがてOOMを迎える。
    また、新しいリストの生成時にも画像の確保コストでUIスレッドがブロックし、なんかカクカクするようになる。

  • 解決
    面倒でもPicassoやGlideなどのライブラリを使いながら、コード側で随時画像キャッシュを使ってリスト内の該当箇所にimageを当てていく。
    と言うかAndroidにおいて直接imageviewにbitmapをアタッチすることは根本的にアンチパターンだと思っておく。確保時にOOMで死ぬ。

Picassoなどは内部にWeakMap?的な実装を持てるため、割り当てたメモリ上限を超えると勝手にアクセス頻度の少ないものは捨ててくれる。

// インスタンス実装
ActivityManager am = (ActivityManager) MainApplication.getApplication().getSystemService(Context.ACTIVITY_SERVICE);
boolean largeHeap = (MainApplication.getApplication().getApplicationInfo().flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0;
int memoryClass = am.getMemoryClass();
if (largeHeap && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    memoryClass = am.getLargeMemoryClass();
}
int cacheSize = 1024 * 1024 * memoryClass / 8;
Picasso picasso = new Picasso.Builder(MainApplication.getApplication()).memoryCache(new LruCache(cacheSize)).build();
Picasso.setSingletonInstance(picasso);

// 画像当てこみ

Picasso.with(view.getContext())
    .load(hashMap.get("url"))
    .resize(size ,size)
    .into((ImageView) imageView);

リストの追加通知にとりあえずnotifyDataSetChanged()を叩く

  • 問題
    VirtualScrollとVirtualDOM的なアレと同じような感じでとりあえず adapter:::notifyDataSetChanged()を雑に叩くと計算コストが半端ない。
    予期せぬリストまでviewが再生成されたりする。
    今までのアプリでは実は表示するものの数が大したことないので割と問題なかったが、複数タブにそれぞれ複数のrecyclerView実装を抱えて、なおかつ巨大なリスト群を扱うとスレッドがブロックする。

  • 解決
    多少実装が複雑になっても myAdapter.notifyItemRangeInserted(lastCount , newCount); など適切に追加/変更を伝える必要がある。
    ただし最近はAdapterに食わせるviewModelにidを与えておくことで、雑に notifyDataSetChanged() を呼んでも問題ないような作りになってきているらしい(試してない)

スクロールリスナー動きすぎ問題

  • 問題
    無限スクロール実装のために、スクロール時に全体のリスト数と表示されているアイテムのindexを監視している。
    数十ms間隔でイベントが来る時とかもあって、そんなに毎回計算が要らない。

  • 解決
    現在時間を使う。 new Date().getTime() あたりを使って、適当に前の時間との差分を取りながらスロットルする。
    リスナー内であまり複雑なことをしないなら必要ないかも。

setHasFixedSize(true)を設定しない

  • 解決
    特に必要がなければデフォルトで設定しておく。

行内のインタラクションのためにnotify~系を叩く

  • 問題
    別の場所で起こったインタラクションなどをリスト内の行viewに反映させる時、前述の notifyDataSetChanged()だともちろん重い。
    アダプターの機構でrangeをとって更新でもいいけど正直管理が複雑なのと融通が利かないのがある。

  • 解決
    リストで表示された各行Viewの onAttachedToWindow() , onDetachedToWindow() ライフサイクルイベントで、適当なObserverパターン実装を購読し、各行viewが自分で差分を計算したほうが個人的には融通がきくと思われる。
    なんか負けた気がするけど。
    画面上に表示されているものだけになるので、observer側でたくさんイベントが走ろうが走査数は実は大したことないはず。

VideoView , 横スクロールページャーなどをリストの一部に実装する

  • 問題
    スライダーやVideoViewがリストの一部になると、画面から出た際に随時detacheされ、状態が初期化してしまう時。

  • 解決

 RecyclerView:::setItemViewCacheSize(i : int);

のサイズを必要な分大きくしておくか、リスト再生成時に、適切に状態を復元する機構を用意しないといけない。
特にVideoViewはPlayer部分とレンダリングされるViewを分離しなくてはいけないので、ちゃちゃっとビデオ表示したい用途だとだいぶ面倒くさそうだけど。
多分にステートフルなコンポーネントだとまあしょうがないという気はする。

無限スクロールでどんどんメモリに行情報溜まっていくよ問題

  • 問題
    別にAndroidに限ったことではないが、無限スクロール実装でだんだん表示すべきdataのリストが貯まっていく。

  • 解決
    リスト内で表示しないものは、行情報の取得時に別スレッド側であらかじめプロパティをどんどん捨てておく。サーバー側で抜けるならサーバー側で。
    description , リリースノートなど巨大な文字列を有するものには割と効く。
    あとは保持しているリストの上の方の過去のデータを適切に消したり、SharedPreference的なところに一時的に書き出しておく、などの実装が考えられる。結構複雑になりそうのでやってないけど。

Read More

RxJavaで並列処理と非同期チェーン

今のとこAndroid開発でRxJava使ってる。
待ち受けストリーム的な実装は基本的にしていなくて、jsでいうPromise的な扱いとLinq代わりのコレクション操作をRxJavaに任せてる、という感じ。
適当にやってるとその辺幾つか忘れる項目があるのでメモ。

並列処理

Observable.zipを使う。

Observable<Integer> obs1 = Observable.just(256);
Observable<String> obs2 = Observable.just("String");
Observable<Boolean> obs3 = Observable.just(true);

Observable.zip(obs1, obs2, obs3, (Integer i, String s, Boolean b) -> i + " " + s + " " + b)
 .subscribe(str -> Log.d("debug" , str));

非同期チェーン

複数のServiceとして分けたObservable実装をつないでいくとき、
愚直に書くとどんどんネストが深くなっていって、なんかコールバック地獄みたいになる。
ここではflatMapを使っていく。
flatMapは次の処理にObservableを渡せる。

//observable1 , observable2 , observable3がそれぞれAPIを叩くような処理のとき。

observable1
    .flatMap((result1)->{  
        if (result1の結果){
            return observavle2();
        } else {
            return observable3();
        }
    })
    .subscribeOn(Schedulers.newThread())
    .onBackpressureBuffer()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe((result)->{
     // onNext
    },(err)->{
      // onError 

    },()->{
      //onComplete
    });

ただ、他の言語でPromise返すような実装もそうだが、もし処理によって返す型が違うObservableを繋がなきゃいけないときの個人的プラクティスはあんまり固まってない。
この辺迷うくらいならAPIを叩くような個々のServiceはObservable単位で切らない方がいい。

Promise代替的なチェーンよりはなんだかんだasync/await的な構文の方が分岐周りとかスッキリするような気になっていて、
それもあって次はネイティブ部分が必要になる処理書くときはkotlin使ってみたい。

それ普通にhandler立てたら??????とか言わない。

Read More

Google先生が考えているモバイル体験2017(Web/ネイティブ)

Googleは「モバイルWebの読み込み負荷の問題」や「ネイティブアプリのユーザー体験の冗長さ」を解消するために、昨今いろんな施策を出している。
今年に入ってからもいろいろな実装をリリース/プレビューとして入れ込んできていて、今現在の状況が追いかけづらいので何が何なのか一旦整理しようと思う。

  • AMP (Accelerated Mobile Pages)
  • PWA (Progressive Web App)
  • WebAPK
  • Instant Apps

今回取り上げるのは以上の4つである。


AMP (Accelerated Mobile Pages)

AMPは、WebサイトをGoogleのキャッシュサーバーから配信することで高速な表示を目指そう、という技術の総称である。すでに日本でも実装済み。

たまにモバイル検索結果で出てくるこの⚡マークが目印。

 
 

 
 
基本的には記事メディアでの用途になる。現行ですでに実装されており、対応サイトも多い。

Pros

高速

これに尽きる。Googleがキャッシュしているのでムッチャ速い。
データ転送量が従来の1/10にもなるという。

あくまでGoogle検索上での実装なので、もちろんiOS/Android問わずその恩恵に預かることができる。

Cons

基本的には開発コスト/広告周りがデメリットとしてよく挙げられる。
AMPに対応したHTMLファイルを別途別ページとして用意する手間はがっつりかかってしまい、それなりの構造化作業を必要とする。

タグ制限/独自タグへの置き換え

script , frame,input , labelなど禁止されているタグもいっぱいある。
このへんはChromeでデバッグが可能。
MediaElements系は<amp-image />, <amp-video />などの独自タグに置き換えなければいけないなど、制限が多い。
ソーシャル連携のための<amp-twitter /> などもあって、対応には独自の知識が何かと必要になりそう。

AMP JS以外のスクリプトの制限

jsによる実行ブロックを防ぎ、ページの高速化を図るために amp HTMLでは規定のjavascriptファイル以外の実行を許されていない。
当ブログもAMPのプラグイン入れたけど、コードブロックの表示にhighlight.jsを使っているため、特にその辺はまともに動いていないので、この辺はがっつりCSSで描画をなおす必要がありそう。

広告

js/iframeなどの実行が許されていないので、当然既存の広告プラグインもそのままでは動作しない。
このへんはGoogleが <amp-ad /> などの専用タグを使った施策を紹介している。

 
 
 


PWA (Progressive Web App)


Webアプリ(サイト)をネイティブアプリみたいに使おうぜ、というGoogle先生の施策の一つである。
プログラムやデータをバックグラウンドプロセス上でキャッシュすることで、ページの高速化やオフラインでの動作が可能になる。
また付随技術としてWebアプリでもPush通知が使えるようになる。

ここにサンプルになりそうなサイトが溜まっている。
 
PWA Rocks
 
特にAndroidだと、対応サイトは「ホーム画面に追加」してネイティブアプリの感覚で扱うことができる。

Pros

ServiceWorker/ ホーム画面アプリモードによるスタンドアローン

W3Cで策定されている、ServiceWorker API というWebページとは別に実行されるスクリプトを使う。バックグラウンドプロセスみたいなものかな?
(WebWorkerは別スレッドみたいなもんだから間違えないように。)

Android Chromeの場合、サイトのルートに規定のjsonファイルを置けば、GoogleがPWA対応アプリとして認識し、ホーム画面への登録を促すToastを出してくれる。
タップすると、すぐにホームにアイコンが追加されるはず。

Push通知

Push API(WebPush)も基本このServiceWorkerのプロセス上に乗っているはずなので、PUSH通知ができる。
対応サイトでは、通知の認可を求めてくるようになる。
使える場面は結構多いはず。

Cons

iOSとかいうやつ

iOS Safariは ServiceWorker/WebPushなどのAPIが2017.1 現在非対応である。
要するにオフラインでの プログラム/APIからのデータのキャッシュや PUSH通知機能を使うことができない。

iOSは現状でもホーム画面アプリモードとしてWebアプリを登録自体は可能である。(Safariが立ち上がらず、ヘッダーとか隠せるようになるやつ。)
できないこともないが、もちろんオフラインで操作することはできない。 (あるいはAppCacheなどで頑張る)
 
URL遷移をやろうと思ったら、普通に遷移するとネイティブのSafariが立ち上がってしまうので、シングルページアプリケーション的なルーティングが必要になってくる。  
また、Webアプリの実行状態から抜けるたびにレンダリングやルーティングの状態がリセットされるので、状態を再度復元しておこうと思ったら、
localStorageなどを駆使して、がっつりアプリケーションの状態を保存するアーキテクチャを用意しないといけない。
ネイティブアプリ/SPA的な知識がかなり要求されてくる。
 
 
個人的には面白そうな分野なのでiOS SafariにはぜひServiceWorker,WebPUSH等の技術に対応してもらいたい。
だけど、Appleが考える(Appleが胴元の)ユーザーエクスペリエンスのあり方と微妙に違う気もするので、ホーム画面アプリとしてのPWAには及び腰になりそうな感じもする。

バックグラウンド動作でのバッテリー消費とかメモリ食いとか

PCのChromeアプリが裏で連れてくる、元気に跳ね回るプロセスのみなさんを彷彿とさせて若干不安になる。
Facebookアプリのバッテリー消費プロセスと同様のことがWebサイト開発者側の実装次第で起きるとしたら、携帯端末の状態が結構カオスになりそうな気もする。
このへんは技術要件をがっつり調べたわけではないので、確認しておきたい。
 
 
 


WebAPK(Android)


ここから先はAndroid独自の話となる。

WebAPK というのは先日実装が開始された機能である。   
Google、Android用ChromeにWEBサイトをアプリとして利用可能にする「WebAPK」を実装開始 -ガジェット通信-
 
 
これによると、先のPWAをパッケージングして、ネイティブアプリと同じようなchromium上で動作させる仕組みらしい。要するにWebView上のアプリである。
WebサイトをAndroidアプリのパッケージ形式であるAPKに変換して端末にインストールさせる。  
具体的にPWAのホーム画面登録時とユーザー体験としてどのように違うのかは、資料が足りないのでよくわかっていない。  

(規定のWebView上で動作させるようになるとすれば、Webアプリケーション側から専用のAPIとか叩けるようになったりするのかな?)
ただ、この方式のアプリの配布はどうやらGooglePlay側からできるようになるそうで、そこは面白いかな、と思う。 
(WebAPKの配布は既にGooglePlay側で機能しているそうである。QRコードからもインストールできるようになるとか。)
 
 
 


Instant Apps (Android)

Chrome Nightly Buildから実験的に実装が始まった機能。
これに関してはHTML5のWebアプリを動かそう!という文脈ではなく、コンパイル済みのAndroidのネイティブアプリをChrome上で一時的に呼び出して動作させようという施策のようである。

公式の先のVideoや、下のgifを見ると使用感がわかりやすい。
 
 
公式
事前のインストールなしでアプリを実行できる「Android Instant Apps」の実地試験が開始される -Gigazine
Android Instant Apps starts initial live testing
 
 

 
 

既存のAndroidネイティブのコードがそのまま利用可能で、Android4.2から対応できるようになるそうだ。(やめろ)
呼び出しの重さや対応できるAPIが気になるところ。SDKが提供されたら触ってみたい。
 
 
 


まとめ

AMPのようなGoogle独自の高速化施策を打つ一方、Webアプリとネイティブの垣根を取り払っていくようなアプローチが続々出てきて、大変だと思いつつものによっては面白いこともできそう。
全部の実装が果たして軌道に乗るのか、どれに乗っかるべきなのかは注視していきたい。

Read More

(Android & Java) Lombokでざっくりシャローコピー

伏見です。

Lombokとは、JavaでGetter,Setter,コンストラクタなどを自動生成していい感じに隠蔽してくれるツールです。
様々なアノテーションを使って表現します。
Lombok 使い方メモ

現在所属しているチームではモバイルアプリ開発を行っています。
Swift,Web,AndroidのAPIのIOやビジネスロジックの共通の大元をまずTypeScriptで書いており、そこからSwiftやJavaのデータ定義用コードを自動生成して各自で使っています。

Typescriptでデータ用モデルを定義するとき、ゲッターセッターだのまず書いていません。
Androidでの開発においても、最近はREST APIから渡ってくる情報や、アプリケーションのビジネスロジックを表現するようなシンプルなデータ用クラスを、js側で扱うpureなJSONとなるべく見た目的に等価にしておきたい気分でいます。

で、ゲッターセッター、コンストラクタの定義にLombokを使っているわけです。

で、本題ですが、このLombok、オブジェクトの浅いコピーにも使えます。
データ用クラスがCloneableを継承すれば当然Lombokなしでもシャローコピー(?)は実装できますが、前述の理由で余計なinterfaceやメソッドを定義してクラス内を汚したくないです。

(参考にしたリンク)

定義側


@AllArgsConstructor
@NoArgsConstructor
@Builder(toBuilder = true) //<-これ
public class PlayerStore {

    public int nowPlayOrder = 0;

}

使用側


            PlayerStore oldPlayerStore = allStore.playerStore;
            PlayerStore oldPlayerStore2 = allStore.playerStore;

            oldPlayerStore.nowPlayOrder = 20;
    
            Log.d("" , oldPlayerStore2.nowPlayOrder+""); //20
            Log.d("" , allStore.playerStore.nowPlayOrder + ""); //20

            allStore.playerStore = oldPlayerStore.toBuilder().build();
            oldPlayerStore.nowPlayOrder = 120;
   
            Log.d("" , oldPlayerStore2.nowPlayOrder+""); //120
            Log.d("" , allStore.playerStore.nowPlayOrder + ""); //20



結果は以上のような感じになります。

便利。
Android用オレオレRedux実装のReducerにあたる部分で使う予定ですが、他に使う場面あるかな…。

Read More

Android TextViewのmarqueeの挙動

三年目のfushimiです。

今回はAndroidの開発で地味にハマったとこをメモ。

 

世の中にはmarqueeエフェクトなるものがあります。HTMLでいうこれなんですが。↙︎

vr

( http://www.mbs1179.com/ さんから)

要するに文字が1行に収まりきらない時に文字を流すエフェクトです。

Androidのアプリ開発でこれを使う機会がありまして、妙な挙動があったのでメモります。

 

デフォルトのTextViewにはmarquee設定用の属性が一応あります。

(こんなの)


<TextView
 android:ellipsize="marquee"
 android:focusable="true"
 android:focusableInTouchMode="true"
 android:singleLine="true"
 android:marqueeRepeatLimit="marquee_forever"
 android:textColor="@color/playerText"
 android:id="@+id/song_title"
 android:text="@{nowSong.title}"
 android:textStyle="bold"
 android:textSize="20dp"
 android:layout_centerHorizontal="true"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" />

       

しかしこのTextView、focusが当たってる時にしか動作しません。それだけならまだいいのですが、
他のTextViewにsetTextを叩くと、そちらにfocusが移り、marqueeのアニメーションがリセットされてしまいます。カクカク。
もちろん複数のTetViewにmarqueeを設定することもできません。

いろんな方法を試した方がおられるようですが、こちらにシンプルな解決法がありました。

どうやら同じViewGroup内に他のTextがなければOKなようです。そこで、TextViewを一段階他のレイアウトでラップします。


 <LinearLayout 
android:layout_width="wrap_content" 
android:layout_height="wrap_content">
 <TextView
 android:ellipsize="marquee"
 android:focusable="true"
 android:focusableInTouchMode="true"
 android:singleLine="true"
 android:marqueeRepeatLimit="marquee_forever"
 android:scrollHorizontally="true"
 android:duplicateParentState="true"
 android:layout_margin="16dp"
 android:id="@+id/artist_name"
 android:textColor="@color/playerText"
 android:layout_below="@+id/song_title"
 android:text="@{nowSong.artist_name}"
 android:layout_centerHorizontal="true"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" />;
 
</LinearLayout>;

これで他のTextViewにsetTextしてfocusが変わっても、ひとまず正常動作するようになりました。

 

謎い。

Read More