人生で初めてCocoaPodsのライブラリを作った話

by ysawa

前にHTTPSの記事を出して意外に好評だったみたいで、想定外でした。本当にありがとうございました。

今回は、iOSの自分でライブラリを作った話です。ライブラリを使ってぶち当たった問題、そして、解決まで。苦労や楽しみを書いてみます。HTTPSの記事同様に多くの人の参考になるように意識しました。良かったら読んでいってください。

うまく動かない…

きっかけともなる事件。今、スクショのような写真をメモとして管理するツールを作っています。

これを世の中のアプリのように快適に動作させるためには、画像を高速に処理するキャッシュ作る必要があります。当時、面倒だったので、 CocoaPods と言われる iOS のライブラリ管理ツールで検索して有名なライブラリを使ってました。

結果、うまく動かない… かの iOS のキャッシュライブラリを使ってもアプリはもっさりとしたままでした。

UITableView や UICollectionView で多数の写真をキャッシュから表示する場合、 CPU 使用率が 100% を超えたり、画面がカクつく現象が非常に気になりました。

もっさーとしたアプリ。 CPU が常に高負荷になってしまい、 iPhone はすぐ高熱になりました。キャッシュの恩恵を受けているとはとても言い難い状況でした。当時の書き込みを載せておきます。

原因調査

Xcode でプロファイリングをしていたのですが、よくわからず、 print デバッグ的なものでがんばって探しました。

犯人は NSKeyedUnarchiver でした。NSKeyedUnarchiver は、バイナリをオブジェクトに変換(デシリアライズ)します。このときに、大きなCPUパワーを必要としていたようです。

それも当然のこと。画像データは大きなもので数メガになります。これをデシリアライズするので、膨大なリソースを食っていきます。最初から画像だとわかっているので、 NSKeyedUnarchiver を使わずとももっと効率的な方法で変換することは可能です。しかし、汎用的にキャッシュを設計してしまうとこのような結果になるのでしょう。

アプリ起動中数回ならまだしも、数十回、数百回と繰り返すとあっという間にパフォーマンスが悪くなり、充電を減らす結果となるでしょう。非常にまずい状況です。

2段階キャッシュを作ることに

結局、このキャッシュの部分は、ライブラリに頼るのではなくて、自分で作ることにしました。ここで一気に肩の荷が降りて、自己責任で全部やれるフリーランス的な開放感を得ます。

今回のライブラリを作る前提ですが、画像などを表示する目的で使用して、万が一古い画像を表示してしまっても死ぬ人がいないとしておきます。

基本的にインターネット上にマスターデータがあって、最悪、ネットワーク上などに時間を掛けて取りに行けば良いようなものを想定しています。なので、排他制御などを使った厳密なものは作らないと決めました。

もうちょっと詳しく…

2層構造のキャッシュを作り、1層目(Level 1)はメモリ、2層目(Level 2)はファイルシステム。どちらにもデータが存在しなければ、キャッシュミス。インターネットなどにデータを取りにいく設計です。

iOS の場合、 NSCache を使うと、メモリに保存するタイプのキャッシュを簡単に構成することができます。また、ファイルシステムによるキャッシュも NSCachesDirectory で簡単に作ることができます。どちらも、容量いっぱいになったりすると、勝手に開放してくれるので管理をする必要がありません。

シリアライザとデシリアライザ、ダウンローダは開発者が自分で作るというのも今回の味噌です。どうしても NSKeyedUnarchiver を使いたい人は、 デシリアライザ として適応して使うこともできます。

まとめるとこうなります。

  • 1層目(Level 1) → NSCache によってメモリ上にオブジェクトをそのまま置きます
  • 2層目(Level 2) → NSCachesDirectory を使いファイルシステム上にオブジェクトをシリアライズしたデータ(ダウンロードしたデータそのもの)を置きます
  • キャッシュミス → インターネットなどに取りに行く
  • シリアライザとデシリアライザ、ダウンローダは開発者が自由に書けるようにする

(なんだか難しそうではありますが、ただ単にインターネットから画像をダウンロードして表示したい人は、サンプルコードをペッって貼り付けるだけで実装ができます。)

これで勝つる。パフォーマンスはだいぶ改善しました。

CocoaPodsにしたら良いのではないか

この設計をしていて気づきました。このままのモチベーションでいけば、ライブラリとして使用できるクオリティに仕上がるのではないか。

「よし。 CocoaPods のライブラリを作ろう!」

モチベーション最高潮のわいさわを晒しておきます。

今回作ったのは TwoLevelCache という名前のライブラリです。単純な発想ですが二層のキャッシュがあるから two-level cache ということで、 TwoLevelCache 。

はい。 CocoaPods ライブラリの作り方は簡単です。

$ pod lib create ライブラリ名

として、いくつか質問に答えると、ディレクトリが作られます。作られたディレクトリの中にある ライブラリ名.xcworkspace ファイルを開いてライブラリを完成させていきます。

メソッド名、クロージャー(ラムダ)にするかメソッドにするか。どこまで作り込むか。など書きながら消しての繰り返しが始まったのです…

Unit Testを記述

単体テスト(Unit Test)を書かないと、やっぱり、ライブラリとしては認められないです。「コアコミッター」「メンテナー」って胸を張って言えるように、単体テスト書きました。

Xcode には標準で、 XCTest というライブラリが入っているのでそれを使ってテストを書いていきます。

コードカバレッジ90%以上に挑戦

ナビゲーションバーから、プロジェクト名 → Edit Scheme → Test とたどっていきます。すると「Code Coverage」という項目があるので「Gather coverage data」にチェックを入れます。

コードカバレッジが低いというのはちょっと恥ずかしいので 90% 以上を目指して、テストコードの加筆修正を加えました。

無事 93.69% まずまずといったところではないでしょうか。

Travis CIへ同期

あともう少しです。Travis CI にソースコードをアップして、ライブラリやアプリがちゃんと動かテストしたりします。 Travis CI が先ほど定義した、単体テストを実行してお墨付きをくれるサービスです。便利だしクールなのでぜひ使ってみてください。

ここが一番大変で半日が潰れました。いや、実際は簡単にできるはずなのですが、変なところでハマりました。

プロジェクト内の README.md ファイルにある、

[![CI Status](http://img.shields.io/travis/ユーザ名/ライブラリ名.svg?style=flat)](https://travis-ci.org/ユーザ名/ライブラリ名)

の部分です。これを Github にプッシュして、クリックしてみてください。アカウント登録すると勝手に、 Github から Travis CI に同期されて必要なビルドをしてくれます。先程、定義した単体テストを含め、 CocoaPods に必要なバリデーションを全て実行してくれます。

ビルドが通らないという方は、 .travis.yml というファイルがプロジェクト内にあるのでこれを編集しないといけません。例えば、以下のように編集します。

osx_image: xcode9
language: objective-c
script:
- set -o pipefail
- xcodebuild -workspace Example/TwoLevelCache.xcworkspace -scheme TwoLevelCache-Example -sdk iphonesimulator11.0 -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 5s' ONLY_ACTIVE_ARCH=NO test | xcpretty -c
- pod lib lint

ここで、僕は間違えて osx_image: xcode9.0 としてしまい、ずっとビルドが通りませんでした。 osx_image: xcode9 です。 .0 はつけないようにしないといけません。注意してください。

やっと通りました。余計な設定をするとデフォルトの xcode7.3 が選択されてしまっていたんですね。目が節穴でした。

緊張の瞬間

ライブラリ名.podspec ファイルを変更します。

Pod::Spec.new do |s|
  s.name             = 'TwoLevelCache'
  s.version          = '0.1.0'
  s.summary          = 'Customizable two-level cache for iOS (Swift).'
  s.description      = <<-DESC
Customizable two-level cache for iOS (Swift). Level 1 is memory powered by NSCache and level 2 is filesystem which uses NSCachesDirectory.
All cache data are managed by OS level. Then you don't have to consider the number of objects and the usage of memory or storage.
                       DESC
  s.homepage         = 'https://github.com/ysawa/TwoLevelCache'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'Yoshihiro Sawa' => 'yoshihirosawa@gmail.com' }
  s.source           = { :git => 'https://github.com/ysawa/TwoLevelCache.git', :tag => s.version.to_s }
  s.social_media_url      = "https://twitter.com/yoshiiiine"
end

こんな感じになるかと思います。

$ pod lib lint

で、問題ないか一応確認します。

最後に、 Github に Git の tag を登録します。

$ git add -A && git commit
$ git tag 'バージョン番号'
$ git push --tags

そして、最後の緊張の瞬間。 CocoaPods にライブラリを送りつけます。

$ pod trunk push ライブラリ名.podspec
[!] You need to register a session first.

とします。おっと!上手く行かないですね。

$ pod trunk register メールアドレス 'Anatano Onamae' --description='デバイス名'

このコマンドで、メールアドレスにEメールが届くと思います。リンクをクリックするとメールバリデーションが完了します。

そうして、もう一度挑戦します。

$ pod trunk push ライブラリ名.podspec
Updating spec repo `master`

CocoaPods 1.4.0.beta.1 is available.
To update use: `gem install cocoapods --pre`
[!] This is a test version we'd love you to try.

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.4.0.beta.1

Validating podspec
 -> TwoLevelCache (0.1.0)

Updating spec repo `master`

CocoaPods 1.4.0.beta.1 is available.
To update use: `gem install cocoapods --pre`
[!] This is a test version we'd love you to try.

For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.4.0.beta.1


--------------------------------------------------------------------------------
 ?  Congrats

 ?  TwoLevelCache (0.1.0) successfully published
 ?  October 12th, 15:11
 ?  https://cocoapods.org/pods/TwoLevelCache
 ?  Tell your friends!
--------------------------------------------------------------------------------

通りました。すると、こんなツイートが。

めちゃくちゃ照れくさいです。そして本家にはしっかりとライブラリが反映されています。

感慨無量です。1個大人の階段を登ったような気がしました。

あと、困ったらやはり本家、Making a CocoaPodが詳しいです。こちらもぜひ読んでみてください。

まとめ

作られたライブラリは下のリンク一覧に貼っておきます。使い方もこちらに書いてあります。 iOS アプリ開発している人で画像をインターネット上から読み込んで表示するような場合、ぜひ使ってみてください。汎用的なライブラリなので重い処理にはなんでも適応できます。

このように、初めてでもそこまで大きく問題も抱えることなくライブラリを作ることができました。本当の問題はここからなのかもしれませんが、単体テストには出てこなかったバグなどが見つかりましたらご報告いただければ幸いです。

読んでいただきありがとうございました。

リンク

この記事を読んだあとに

ysawa

エヌ次元株式会社代表取締役
東京工業大学工学部計算工学専攻卒業
符号理論の応用に関する研究
在学中よりフリーランスエンジニアとして活動
「持続可能な設計」を得意領域とする
会社設立後も設計からアプリ制作や
Webサイトのコーディングまでを幅広く担当
セキュリティスペシャリスト

 このブログについて

このブログは、プログラマやエンジニアのためになる情報を垂れ流しています。
ちょっと異端的なものも含まれているかもしれません。