Chattoの作成の歴史

私たちのチャットは時代遅れです:数年にわたる進化の過程で、誰にも分からない奇妙な修正が施された面倒なView Controllerになりました。 新しいタイプのメッセージを追加することは困難になりましたが、新しいバグは簡単に現れました。 そのため、Swiftのチャットをゼロから書き直して、 オープンソースに配置することにしました。

プロジェクトの作業を開始し、2つの目標を設定しました。



この記事では、目標をどのように達成したか、どの方法が使用され、最終的に何が行われたかについて詳しく説明します。 GitHubのページには、アプリケーションアーキテクチャのかなり詳細な説明があります。







どちらが良いですか:UICollectionViewまたはUITableView?



以前のチャットではUITableViewを使用していました。 これは非常に優れていますが、UICollectionViewは、設定( アニメーションUIDynamicsなど)および最適化(UICollectionViewLayoutおよびUICollectionViewLayoutInvalidationContext)のオプションを備えたより豊富なAPIを提供します。

さらに、複数の既存のチャットアプリケーションを調査した結果、すべてが正確にUICollectionViewを使用していることがわかりました。 したがって、UICollectionViewの選択を支持する決定は当たり前のことでした。



テキストメッセージ



テキストクラウドなしではチャットはできません。 実際、パフォーマンスの観点から、このタイプのメッセージは、テキストのレンダリングとスケーリングが遅いため、実装が最も困難です。 iMessageのように、チャットがリンクを自動的に検出し、定期的なアクティビティを実行することを望んでいました。

UITextViewは当初これらすべての要件をサポートしているため、リンクを処理するために1行のコードを記述する必要はありません。 そのため、このクラスを選択しましたが、このソリューションは問題の原因になりました。 次に、理由を説明します。



自動レイアウトとセルのサイズ変更



レイアウトとサイズの計算は常に困難を引き起こします。複製コードを書くのは非常に簡単で、維持するのがより難しく、バグの出現につながるので、これを避けようとしました。 当初からiOS 8のサポートを提供していたため、自動レイアウトとセルのサイズ変更を試してみることにしました。 このアプローチの実装の一般的な説明を含むブランチを次に示します。 試したところ、2つの大きな問題が発生しました。





手動レイアウト



そのため、自動レイアウトではなくレイアウトのために、従来のアプローチを使用することにしました。 空白セルを使用してサイズを計算し、レイアウトとサイズをカウントするために、できるだけ多くの一般的なコードを使用する古典的な方法に決めました。 このアプローチははるかに高速に動作しましたが、iPhone 4sにはまだ十分ではありませんでした。 プロファイリングにより、layoutSubviewsメソッド内の作業が多すぎることが判明しました。

実際、同じジョブを2回実行しました。最初は空白のサイズをカウントしてから、layoutSubviews内の実際のセルで再度実行しました。 この問題を解決するために、UITextViewのsizeThatFits(_ :)値をキャッシュできますが、これは計算が非常に高価ですが、さらに進んで、すべてのサブビューのセルサイズとフレームが計算されてキャッシュされるレイアウトモデルを作成しました。 その結果、スクロール速度を大幅に向上させるだけでなく、sizeThatFits(_ :)とlayoutSubviewsの呼び出し間でコードを最大限の効率で再利用することができました。

さらに、updateViewsメソッドが注目を集めました。 小さいサイズでは、これは、指定されたスタイルと表示されるデータのタイプに従ってセルを更新する主要なメソッドの1つであることが判明しました。 UIを更新するための1つの主な方法の存在により、将来のコードのロジックとメンテナンスが簡素化されましたが、同時に、セルのプロパティを変更するほとんどすべてのアクションに対して呼び出されました。 この問題に対処するために、最適化する2つの方法を考え出しました。





さらに高速



すでに十分なスクロール速度を達成していますが、より多くのメッセージ(50単位のパッケージ)を読み込むと、メインスレッドが長時間ブロックされ、その結果、スクロールが一瞬停止しました。 もちろん、UITextView.sizeThatFits(_ :)関数もボトルネックでした。 リンクを検出して空白セルのテキストを選択する機能を無効にし、非連続レイアウトを有効にすることで、処理を大幅に高速化することができました。



textView.layoutManager.allowsNonContiguousLayout = true textView.dataDetectorTypes = .None textView.selectable = false
      
      





その後、50個の新しいメッセージを同時に表示することは問題ではなくなりました。ただし、それ以前にはあまりメッセージがなかったことが前提です。 しかし、さらに先へ進むことができると判断しました。

サイズと位置を計算するタスクを実行するためにレイアウトモデルをキャッシュして再利用することによって達成された抽象化のレベルを考慮すると、バックグラウンドスレッドで計算を実行するために必要なものがすべて揃いました。 しかし... ... UIKitはカウントしません。

ご存知のように、UIKitはスレッドセーフではなく、元の戦略(単にこの事実を無視することでした)により、予想される多くのUITextViewクラッシュが発生しました。 NSString.boundingRectWithSize(_:options:attributes:context)メソッドをバックグラウンドで使用できることはわかっていましたが、返されるサイズはUITextView.sizeThatFits(_ :)から取得したサイズと一致しませんでした。 私たちは多くの時間を費やしましたが、それでも解決策を見つけることができました。



  textView.textContainerInset = UIEdgeInsetsZero textView.textContainer.lineFragmentPadding = 0
      
      





NSString.boundingRectWithSize(_:オプション:属性:コンテキスト)から取得したサイズの丸めを使用して、ピクセルをスクリーンします



 extension CGSize { func bma_round() -> CGSize { return CGSize(width: ceil(self.width * scale) * (1.0 / scale), height: ceil(self.height * scale) * (1.0 / scale) ) } }
      
      







したがって、キャッシュをバックグラウンドスレッドで準備し、レイアウトが5000メッセージを処理する必要がない限り、メインスレッドのすべてのサイズを非常に迅速に取得できます。

この場合、UICollectionViewLayout.prepareLayout()メソッドが呼び出されると、iPhone 4sの速度が低下し始めました。 主なボトルネックは、UICollectionViewLayoutAttributesの作成とNSCacheからの5,000メッセージのサイズ設定でした。 この問題をどのように解決しましたか? セルと同じことを行いました。UICollectionViewLayoutAttributesの作成に関与するUICollectionViewLayoutのモデルを作成し、同様にその作成をバックグラウンドストリームに転送しました。 ここで、メインスレッドで、古いモデルを新しいモデルに単純に置き換えました。 そして、すべてが驚くほど速く動作し始めましたが、...



回転と分割ビュー



デバイスの回転または分割ビューのサイズ変更中に、メッセージを表示するために使用可能な幅が変更されたため、メッセージのすべてのサイズと位置を再度読み取る必要がありました。 このアプリケーションは回転をサポートしていないため、これは特に問題になりませんでしたが、すでにChattoをオープンソースでリリースし、回転と分割ビューの適切なサポートがこれらの目的に大きなプラスになると判断しました。 そのときまでに、バックグラウンドスレッドでサイズの計算を既に実装しており、スムーズなスクロールと新しいメッセージの読み込みを行っていましたが、アプリケーションが10,000メッセージを処理しなければならない場合はあまり役に立ちませんでした。 バックグラウンドでのこのような多数のメッセージのサイズを計算するために、iPhone 4sは10〜20秒かかりました。もちろん、ユーザーをそれほど待たせることはできませんでした。 問題を解決する2つの方法を見ました。



最初のオプションは、ソリューション自体よりもハックです-スプリットビューモードでは実際には役に立たず、スケールしません。 したがって、2番目の方法を選択しました。



スライディングデータソース



iPhone 4sでのいくつかのテストの後、高速ローテーションのサポートは500メッセージ以下の処理を意味するという結論に達しました。そこで、カスタマイズ可能な数のメッセージを同時に表示する移動データソースを実装しました。 したがって、チャットを開くと、最初に50のメッセージをロードする必要があり、その後、ユーザーがチャットをスクロールして以前のレコードを表示すると、50のメッセージの次の部分がロードされます。 ユーザーが十分な数のメッセージをスクロールバックすると、最初のメッセージがメモリから削除されました。 したがって、ページネーションは両方の方法で機能しました。 このメソッドの実装は非常に簡単なタスクでしたが、別のケースで問題が発生しました。データソースが既にいっぱいで、新しいメッセージが到着したときです。

すでに500件のメッセージを受信して​​新しいメッセージを受信した場合、一番上のメッセージを削除し、他のすべてのメッセージを1つ上に移動して、到着したチャットに挿入する必要がありました。 これを解決するのに困難はありませんでしたが、UICollectionView.performBatchUpdates(_:completion :)メソッドはこのアプローチを好みませんでした。 主に2つの問題がありました( ここで再現できます )。





これらの問題に対処するために、許可されるメッセージの最大数の制限を緩和することにしました。 これで、アプリケーションが新しいメッセージを挿入できるようになり、設定された制限に違反し、UICollectionViewのスムーズな更新が保証されました。 挿入を完了した後、更新キューに未処理の変更がない場合、データソースに到着するメッセージが多すぎるという警告を送信しました。 その後、performBatchUpdatesではなくreloadDataを使用して必要な調整を行いました。 これが起こる瞬間を実際に制御することはできず、ユーザーがチャットを任意の位置にスクロールできることを考慮して、現在表示されているメッセージを削除しないように、ユーザーがチャットをスクロールした場所をデータソースに伝える必要がありました:



 public protocol ChatDataSourceProtocol: class { ... func adjustNumberOfMessages(preferredMaxCount preferredMaxCount: Int?, focusPosition: Double, completion:(didAdjust: Bool) -> Void) }
      
      





カーキUITextView



そのため、これまで、自動レイアウトのパフォーマンスとサイズ計算の問題だけでなく、NSString.boundingRectWithSize(_:options:attributes:context)を使用してバックグラウンドスレッドでサイズを計算する問題を解決するための障害のみを考慮してきました。

リンク検出機能と他の利用可能なアクションを利用するには、UITextView.selectableプロパティをアクティブにする必要がありました。 これにより、雲に望ましくない副作用が発生しました(たとえば、テキストを選択する機会や虫眼鏡の外観)。 さらに、これらの機能をサポートするために、UITextViewはジェスチャ認識システムを使用して、テキストクラウドの選択やその内部での長いクリックの処理などのアクションを妨害します。 ハックの助けを借りてこれらの問題をどのように回避したかについては詳しく説明しませんが、 ChatMessageTextViewBaseMessagePresenterのリンクをたどることで詳細を知ることができます。



インタラクティブキーボード



前述の問題に加えて、UITextViewの操作もキーボードに影響しました。 理論的には、最近では対話型キーボードの非表示の実装は非常に簡単な作業になります。 ここに示すように、使用するコントローラーのinputAccessoryViewとcanBecomeFirstResponderをオーバーライドするだけで十分です 。 ただし、ユーザーが任意のリンクをクリックしているときにUITextViewからUIActionSheetを表示すると、このメソッドは非効率的に機能しました。

問題の本質は、メニューがキーボードの下に表示され、まったく表示されないことでした。 この問題を自分で操作できる別のスレッドを次に示しますrdar:// 23753306 )。

入力フィールドをView Controller階層の一部にし、キーボードからの通知を追跡し、UICollectionViewのcontentInsetsを手動で変更しようとしました。 ただし、ユーザーがキーボードを操作したとき、通知は受信されず、入力フィールドは画面の中央に表示され、ユーザーがキーボードを下に引いたときにユーザーとキーボードの間に隙間が残りました。 この問題は、ダミーのinputAccessoryView(入力フィールドの下にある)を使用し、KVOを使用してそれを観察するという特別なハックを使用して解決されます。 詳細については、 こちらをご覧ください



まとめ





Badoo iOS開発チーム



All Articles