こんにちは、iOSエンジニアのちぎらです。
昨今の端末はメモリも増え性能も良くなり、嬉しい限りです。 今回は処理速度の向上について書きます。ユーザーの中には古い端末を使い続けている方もいて、性能の低い端末での処理を注意深く見ることで新しい端末での体験も向上させることができます。一つ一つの処理にかかる時間を改善していく事はとても大事です。
この改善では、修正前後の処理時間を数値で比較できるようにしました。何がどれだけ有効なのかを明確にし、ユーザーへのメリットがある修正かどうかを判断する為です。処理時間は Date から取得したタイムスタンプの差から計算しています(参考:Date Fundamentals)。
検証は、効果の改善を顕著にする為に iPhone4s で行いました。(記載しているコードは実際のコードではなく、解説のために用意した仮のコードです。)
修正
1. CoreData の context の保存タイミングの修正
アプリの中で、一部 CoreData を使用しています。~数万の個数のデータが格納されるのですが、それらのデータをサーバーから取得して保存するための処理時間が長く、計測を行いました。以下が処理時間のグラフです。

横軸はDBに含まれる写真の枚数。縦軸は1枚あたりの保存にかかる時間[s]です。ピークがいくつか見えますが、これは他の重い処理の影響と思われます。
サーバーから1000件のデータ( items )を取得してDBへ格納する処理を、改善前は以下のようなコードで行っていました。
items.forEach { item in
// エンティティの生成・追加
let entity = NSEntityDescription.entityForName("Item", inManagedObjectContext: managedObjectContext)!
let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: managedObjectContext)
object.setValue(item.value, forKey: "value")
...
// コンテクストの保存
var error: NSError?
if !managedObjectContext.save(&error) { ... }
}
改善前はインサート毎にコンテクストの保存を行なっています。保存にはコストがかかりますから、全てインサートをした後にコンテクストの保存を行うように修正しました。
items.forEach { item in
// エンティティの生成・追加
let entity = NSEntityDescription.entityForName("Item", inManagedObjectContext: managedObjectContext)!
let object = NSManagedObject(entity: entity, insertIntoManagedObjectContext: managedObjectContext)
object.setValue(item.value, forKey: "value")
...
}
// コンテクストの保存
var error: NSError?
if !managedObjectContext.save(&error) { ... }
修正後の処理時間のグラフは以下のようになりました。

処理時間が短くなっていることが明らかに見てとれます。 この修正によって、データのインサート処理にかかる合計時間が約 9 分の 1 に短縮できました。
2. DB内の検索時にかかる時間の短縮
動作を調べていると、DB内の特定のエンティティを全て取得している箇所で処理時間が異常にかかっていました。
let fetchRequest = NSFetchRequest(entityName: "Item") var error: NSError? let fetchResults = managedObjectContext.executeFetchRequest(fetchRequest, error: &error) if let results: Array = fetchResults { results.forEach { result in ... } }
処理時間を計測すると、なんと約 28 秒もかかっていました(!!)
エンティティ Item に注目してみますと、このエンティティは一カラムの値としてバイナリデータを保持する作りになっていました。
class Media: NSManagedObject { ... @NSManaged var binary: Data? }
中身を見てみますと、キャッシュに用いている画像ファイル(4~5kb)でした。
CoreDataで大きなデータをカラムに含むデータを大量に扱うのはの良くないという噂はかねがねありましたので、これは怪しいと思い binary を削除、キャッシュの仕組みを修正しました。
| 改善前(iPhone4s) [s] | 改善前(iPhone4s) [s] |
|---|---|
| 27.86 | 0.25 |
この修正によって、データの検索処理にかかる合計時間は約 111 分の 1 に短縮できました。
3. DateFormatter の新規作成回数の削減
エンティティ Item はプロパティ date を持っていて、そこから日付の文字列を計算することで、表示時のセクション分けなどを行なっていました。
extension Item { ... func month() -> String { let dateFormatter: DateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM" self.willAccessValue(forKey: "date") let date: Date = self.primitiveValue(forKey: "date") as! Date self.didAccessValue(forKey: "date") let key: String = dateFormatter.string(from: date) return key } }
この month() は item の個数分呼ばれますので、DateFormatter の初期化も item の個数分だけ実行されます。これを、共通の DateFormatter を使うように修正しました。
extension Item { ... private static let dateFormatter: DateFormatter = DateFormatter() func month() -> String { dateFormatter.dateFormat = "yyyy-MM" self.willAccessValue(forKey: "date") let date: Date = self.primitiveValue(forKey: "date") as! Date self.didAccessValue(forKey: "date") let key: String = dateFormatter(from: date) return key } }
DateFormatter をこのようにして使う場合には、複数スレッドからのアクセスに注意する必要があります。
| 改善前(iPhone4s) [s] | 改善前(iPhone4s) [s] |
|---|---|
| 8.05 | 2.44 |
この修正によって、日付の変換処理にかかる合計時間を約 3 分の 1 に短縮できました。
まとめ
それぞれの修正で、処理時間を短縮することができました。これらの修正によって、実際にアプリを動作させた時の動作速度は約7倍になりました。キャッシュの仕組みを変更することで、スクロール時のスムーズさなど、些細なストレスを感じるポイントも改善されています。
DB関連やキャッシュ、他にも通信に関係したところでは、処理時間を短縮してユーザー体験を向上させることができるかもしれません。この修正で「おやっ」と思ってくれるユーザーがいることを密かに願っています🤗
積極採用中!!
子育て家族アプリFamm、カップル専用アプリPairyを運営するTimers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings