! This post is also available in the following languages. 英語, 韓国語

ビューモデル実装によるiOS Timelineの性能改善

はじめに

こんにちは。LINEでiOS Timelineの性能改善業務を担当しているLee Keon Hongです。最近、サービスの機能やヒストリーの増加を受け、iOS版の LINE Timeline のメンテナンス作業は困難さを増してきています。本エントリでは、改善の歴史をご紹介します。

実装の背景

まずは、実装に至った経緯からご説明します。

LINE Timelineは、友だちの様々なソーシャル活動が表示される空間であり、一本のPOST(投稿)は理論上、数十通り以上の多様な形があります。
さらに、同じPOSTでもユーザが見ている画面や置かれている状況、属性によってユーザイベントへの対応の仕方が変わってくることもしばしばあります。
iOS版のLINE Timelineでは、こうした要求に対応すべく、Apple社より提供される開発ガイドラインではなく、LINE Timeline(以下、Timeline)独自のガイドラインに基づいたPOST機能の開発・管理を行ってきました。

ところが、時間の経過とともにTimelineの機能は追加・変更され、以下のような問題点が次第にあらわになってきました。

  • ユーザの声
    • 「Timelineの表示が遅すぎる」
    • ユーザの画面スクロールの際に表示が切れてしまう問題が発生
  • 開発者の声
    • 「表示が遅い部分をどのように直せば良いのか…」
      • POST作成とデータの流れがコードから明確に読み取れず、性能低下の明確な原因究明が困難な状況。
    • 「なぜそんなに開発に時間がかかるのか」
      • Apple社のガイドラインとTimeline独自のガイドラインの両方をすり合せる過程でメンテナンスコストが増えていく。
    • 「一つ修正を行ったら、今度は別の画面の様子がおかしくなった」
      • ビューとビューの関係が重なり合っている状況。
      • ビューを修正・追加する際、その個別ビューだけでなく、Timeline全体の仕組みやそのビューが使われるところをすべて理解している必要がある。

ということで、これらの問題を解決するためシステムの見直しに取り組むこととなりました。

従来のTimeline feed listの仕組み

Timelineでは、POSTデータをユーザに届ける際、サーバーから伝えられた個々のPOSTデータとUIテーブルビューのセルを一対一にマッピングして画面表示していました。

  1. 各セルは、いくつかのビューからなっていて、一つのビューはさらに複数のサブビューからなっている。
  2. ビューにデータを送るために、データにセル内のすべてのサブビューを巡回させる。
  3. すべてのビューのデータ構成が終わったら画面上に表示される。

以上のように、Timelineに存在する多様なビューを一律に管理するために、Timelineのガイドラインに則ってデータ送信とビュー管理を行うといった仕組みでした。

性能低下と高いメンテナンスコストの原因

各セルの再利用率の低さ

一つのセルは多種多様なビューから構成されているため、データを違う形で示す過程でビューに少しでも変更が生じると、新しい形のセルを作らないといけないという欠点がありました。そのため、セルの再利用率は低く、新しいセルをどんどん作るために性能は低下していました。

高いデータ伝達コスト

セルは多種多様な下位ビューから構成されていますが、これら下位ビューをデータが巡回しているうちにUIがフリーズしてしまうことがあります。なお、どのタイミングでどのデータがビューに届いたか把握するのは困難だったため、解決策を見出すまでの時間も相当かかっていました。

度重なるUIスレッド作業

各ビューがどのようなデータを必要とするかは分からないので、必要なデータをあらかじめ加工するのは構造上不可能でした。その結果、各ビューを画面表示する上で必要なデータをUIスレッド側で加工する作業が繰り返し行われ、UI性能低下の原因となっていました。

Timeline独自の開発ガイドライン

Timelineは、Apple社から提供される開発ガイドラインではなく、Timeline独自の開発ガイドラインに基づいて開発されました。そして二つの異なるガイドラインによって生じるギャップを後から調整することは、メンテナンスコストの増加や性能低下につながると考えられました。

Timeline feed listの仕組み改善に向けた検討内容

このような過去の構造的問題点を改善すべく、私たちは以下のようなアイデアに着目しました。

セルサイズの縮小

一本のPOSTを一つのセルで表すのではなく、セルをより細かいものにして数をできるだけ増やせば、セルの再利用率の極大化と性能向上を実現することができます。大きくて複雑なセルを小さめのものに分割する過程で、構造上の無駄や間違いなどが簡単に除去・是正されるためです。さらに、サイズが小さいので、セルに必要なデータを把握するのも容易になります。

ビューモデル・プロトコルの一本化

Timelineには、様々な種類と機能を持つビューがたくさん存在するため、各ビューに必要な作業をビューコントローラで行うとなるとビューコントローラはどんどん重くなってしまいます。そこで、テーブルビューに必要なセルを生成する上で共通して必要な機能をPostTableViewModelプロトコルとして定義しました。そして以下のような方法でデータを利用し、ビューが描けるようにしました。

  1. 加工済みPOSTデータを持つプロトコルが実装された個別のビューモデルを開発する。
  2. ビューコントローラからはデータやビューに直接アクセスせず、プロトコル経由で各ビューモデルにアクセスし、ビューを生成した後、画面上に描く。

上記の方法では、ビューを描く上で必要なあらゆるロジックはビューモデルの中に実装することができます。 そうすることで、ビューコントローラではビューを把握する必要がなくなり、ビューとしても他のビューとの関係は排除しビューモデルとビューの仕組みを考慮するだけで済むため、全体としてコンパクトな構造に仕上がります。

バッググラウンドスレッドの最適化

できるだけ多くの作業について、UIスレッドではなくバッググラウンドスレッドで簡単に処理されるように変更しました。 なお、ビューが必要とするデータはバッググラウンドスレッドで加工した後、必要なときに再利用できるようにして性能向上を図りました。

低いバッググラウンドスレッド・プライオリティ

以前はユーザのUI操作の結果を速やかに表示するため、POSTデータを取得・加工するバッググラウンドスレッドのプライオリティは高めに設定していました。ただし、そのような設定を維持することは結果表示の時短化にはなりますが、ユーザの他のUI操作にも影響してしまいます。
従って、ユーザがスクロールなどの操作をしたとき、直ちにその結果を示すよりはまずバッググラウンド作業を行うことでスレッド・プライオリティを下げ、 結果表示に多少の時間がかかってもユーザの他のUI操作に性能上の影響を与えないようにする必要がありました。

新しいTimeline feed listの仕組み

上の図のように見直された仕組みでは、以下のようなプロセスを経て画面上にビューが表示されます。

  1. サーバーからPOSTデータが届けられると、当該POSTデータのアレイを生成する。
  2. 各POSTデータのアレイを加工し、ビューモデルのアレイを生成する。
  3. テーブルビューはPOSTデータではなくビューモデルのアレイにアクセスして小さいセルを作り、ビューモデルの持つデータを利用してビューを描く。

各ビューモデルには同一のプロトコルが実装されていて、テーブルビューからビューモデルにアクセスするにはデータの種類と関係なくすべてプロトコルを経由することになります。ビューモデルはセルに使われるビューを把握しているため、ビューが必要とするデータを事前に加工してビューモデルの中に格納することができます。なお、細かいセルに分けられているので、ビューモデル、セル、ビューの関係さえ押さえていればビューの修正・差し替えも簡単にできます。

ビューモデルの生成プロセス

従来の仕組みでは、サーバーからPOSTデータを取得するとDataResultコントローラがそれらのデータをアレイ型で格納し、ビューコントローラはこのアレイにアクセスしてPOSTデータを利用する、といった流れで作業が行われていました。一方で新しい仕組みでは、POSTデータを取得後そのまま使うのではなく、ファクトリを通してビューモデルに仕上げ、このビューモデルにアクセスしてデータを使用することになります。このようにファクトリを利用してPOSTデータをビューモデルにしていく一連のプロセスは、すべてバッググラウンドスレッドで行われます。

このような仕組みから、ビューモデルがビューに必要なデータをPOSTデータより取り出して加工できるように実装されていれば、必要なデータを加工する一切の作業はバッググラウンドスレッドで簡単に行われるようになります。

ビューモデルのデータ構造

前述の通りPOSTデータをビューモデルに加工することになると、新しいPOSTデータを利用してinsert、delete、updateなどの作業を行う際、各POSTとそれに割り当てられたビューモデルとの関係を管理する必要があります。

POSTとビューモデルとの関係管理は、以下の二つの方法で実装できます。

  1. 特定POSTのビューモデルがビューモデル・アレイのどこの位置を占めているかを演算し、リアルタイムでトラッキングする方法
  2. POSTとビューモデルの間に関係データ構造を置いて各POSTに該当するビューモデルを別途管理する方法

検索性能の側面からすると、リアルタイムでPOSTとビューモデルとの位置情報を把握する1.の方法が有利といえます。ただしこの方法は、演算や同期の過程で問題が発生すると問題が発生した部分だけでなく他のPOSTデータと関係しているデータにまで問題を引き起こしたり、一つのデータが修正されるたびに他のすべての関係を更新しないといけなくなるというデメリットがあります。

Timelineでは特定POSTのinsert、delete、updateよりは、accessやデータアレイのappendがより頻繁に発生します。そこで、POSTに変更が生じるたびにPOSTとビューモデルとの関係をつかさどる関係データ構造を更新し、必要なときに関係データ構造を検索して各POSTに該当するビューモデルを探し当てたほうがより安定的と判断されましたので、2.の構造を実装することになりました。

POSTからビューモデルを作るプロセスについては、DataResultコントローラの中にそれを含めることで、POSTをinsert、updateするときに自動的にビューモデル・アレイとビューモデル関係の更新が行われるように実装しました。そうするとビューモデルに直接アクセスして削除・修正する必要はなくなり、POSTの削除・修正が行われるとビューモデル・アレイと各々の関係データ構造は自動で同時に修正されます。

新しい仕組みのメリット・デメリット

メリット

セルの再利用率の向上

個々のセルのサイズ縮小と再利用率の向上によって新しいセル作りのコストは軽減され、性能向上につながります。

明示的ビュー生成とデータ伝達

すべてのビューにデータを行き渡らせて各ビューが必要とするデータを取り出して使うような方法ではないため、セルが必要とするビューはビューモデルのところで事前に把握しています。それによってデータを必要とするビューにのみ加工データを届けることで、データやり取りのプロセスはシンプルになり、可読性と性能が向上します。

バッググラウンドスレッドの最適化

バッググラウンドスレッドでのビューモデルの生成は構造的に保障されます。よって開発者はビューモデルの生成箇所にデータ処理ロジックさえ追加すれば、ビューに必要な作業をどのタイミング、どの位置で行うべきかについて悩むことなく、バッググラウンドスレッドでの作業を最大化することができます。

柔軟な仕組み

新しい構造は、POSTデータをそのまま使うのではなく、それを加工したビューモデルからビューを表示する作りになっています。そのため、仮に同じデータを違う形で表示したい場合はビューモデルの設定やデータを変更したり、他のビューモデルに切り替えたりして画面上に表示することができます。要するに、要求事項の変更や様々なスペックに合わせて構造的かつ柔軟に対応することができるわけです。

デメリット

POSTとビューモデルとの関係管理コスト

新しい仕組みはPOSTをそのまま使わずにいったん加工してビューモデルを生成し、その後ビューモデルを利用するといった二つのデータ構造からなっています。そこから、ビューモデルの追加生成コスト、POSTデータとビューモデルとの関係管理コストが発生します。 さらには、二つのデータ構造の同期という負荷も考えなければなりません。

コード数、ファイル数の増加

新しいビューが追加されると、それがたとえ簡単なものであっても、既存の構造に合わせてビューを利用するためにはビューモデルとセルを追加実装する必要がありますので、コードやファイルの量と数は増えることになります。

性能比較

構造変更を行ってからUIフリーズの発生は減り、ユーザが実感する性能も大幅に向上しました。しかし、画面上に表示されるビューの単位が変更されたため、一定の基準で性能比較を行うのは困難な状況でした。
そこでQA(quality assurance)では、ユーザが実感できる性能向上を具体的に把握しようと、様々なPOSTデータが存在する中でユーザが実際スクロールしたときにすべてのPOSTが取得され画面上に表示されるまでの合計所要時間を計測しました。以下、旧バージョンとの比較測定結果をまとめたものです。

構造変更前の旧バージョンと構造変更後の新バージョンを比較してみたところ、各POSTを画面上に表示する際に、新バージョンで19%~44%の性能改善が行われたことが測定結果から見て取れます。

旧バージョンでの所要時間 新バージョンでの所要時間 改善度
Home画面(POST 170本) 1分31.86秒 1分13.95秒 19%
Timeline画面(POST 200本) 1分38.07秒 1分13.15秒 25%
Note画面(POST 1012本) 8分40.50秒 4分49.98秒 44%

さらなる改善に向けたアイディア

  1. セルを画面上に描くためにはセル高の計算が必要になりますが、その計算はUIスレッドにて行われます。当該作業をあらかじめバッググラウンドスレッドで行い、作業の結果だけUIスレッドのほうで利用できれば、さらなる性能向上も期待できます。
  2. 現状のビューモデル・プロトコルはdequeueCellメソッドでセル生成とデータ設定のプロセスをすべて定義していますが、WillDisplayCellに使える追加のメソッドをプロトコルに定義することもできます。そのようにしてdequeueCellメソッドではセル生成のみ、WillDisplayCellメソッドではデータ設定のみ行うことにすれば、データは画面上にビューが表示されるタイミングにしか使用されないため、さらなる性能向上が見込めます。

おわりに

以上、従来型仕組みのデメリットと改善版仕組みのメリット、両方の性能比較についてご説明いたしました。これからも様々なサービス開発にご活用いただけるようアップデートに取り組んでまいります。その内容につきましても、またの機会にブログを通じてご紹介できればと思います。

引き続き、LINE Timelineをよろしくお願いします。