note6月タイマー

iOSアプリのしくみ・メモリ管理とタイマー入門

アプリの様々な機能を裏で支えるしくみについて知りプログラミングの幅を広げてください。今回は「タイマー」と「メモリ管理のさわり」を解説します。
プログラマを苦しめてきたメモリ管理はSwiftではかなり楽になっていますが注意も必要です。

毎月札幌でiOSアプリ作りをアシストするセミナーをやっています。1時間にわたるセミナーの全内容を、物理的に参加できない方のためにnote上で公開します。ぜひアプリ作りにチャレンジしてください。

iOSアプリ作りをアシストするセミナーは今後も月一回のペースで続ける予定です。
詳細は connpass.com の 札幌Swiftでご確認ください。そして機会があればぜひ参加してください。
アプリ作りプログラミング教育に関連する話題は 札幌Swift のfacebookページで発信しています。


タイマーとは

時計アプリの「タイマー機能」を思い浮かべるかもしれませんが、ここではもっともっと基本的な機能です。画面表示機能はなく、指定時間後に処理を行うためのしくみです。

タイマーは Foundation フレームワークのクラスです。

// Timerクラスの宣言
class Timer : NSObject

タイマーのドキュメントのOverview(概要)冒頭には次のように書かれています。

タイマーは実行ループと連携して動作します
実行ループ(run loop)はそれらのタイマーへの強い参照(strong references)を保持しているので、実行ループに追加した後にタイマーへのあなた自身の強い参照を保持する必要はありません。

強い参照はメモリ管理の用語です。まず先にメモリ管理について確認しましょう。


メモリ管理とは

メモリ(memory、メモリーとのばす場合もある)はプログラミングの世界では記憶装置をさします。
メモリは情報とプログラムそのものを記憶する重要部品です。

情報はバイト単位でメモリに自由に読み書きできます。使い道は自由(文字でも数値でも画像でも音声でも保存できるとの意味で)です。
複雑な情報を記憶するにはたくさんのメモリが必要です。

メモリは高価だったため、デバイスに搭載される量は限られています。「限られたメモリをいかに使い回すか」がプログラミングの大命題です。
特にMacはマルチウインドウで多くのデータを使いますが、メモリの搭載量は(ほかのPCよりも多かったものの)限られていました。
初代Macから続くプログラマの苦労それは「メモリ管理」です。

プログラムは空いているメモリを自分の用途に確保する処理の繰り返しです。
処理を続けると不要になる情報があります。不要になった確保済み情報を「空き」状態に戻し、別の情報の確保に回すことがメモリ管理の基本です。

メモリはコンピュータの基本です。この時間だけでは説明しきれなのでいろいろ端折っていますがぜひ基本も調べてみてください。
ここでのメモリ管理はアプリの内部に限った内容です。(現代のコンピュータにはメモリ保護がありほかのアプリやシステムのメモリへのアクセスは制限されます)
このため『メモリ管理が正しくないアプリがあったとしてもそのアプリを終了させてしまえば』システムやデバイスに悪影響を与える心配はありません。

アプリが使っていたすべてのメモリは終了と同時に解放され、他のアプリが利用可能になります。

メモリ管理にはいくつかの方法が使われてきました。Swift では「リファレンスカウンタ」を使った方法が採用されています。

リファレンスカウンタによる管理

Swift言語では型は値型と参照型があります。メモリ管理が必要なのはクラスなどの参照型のインスタンスです。
参照型は一つの実態(管理が必要なメモリ上の情報の塊)を変数が「参照」します。

必要な情報はそのままに、不要な情報は削除し別の情報の確保に利用することで有限のメモリを効率良く使うのがメモリ管理の目的です。

解放するための判断材料はリファレンスカウンタによる参照数です。
参照数がゼロになったら不要と判断し解放するシンプルなしくみです。

リファレンスカウンタの動き

リファレンスカウンタはインスタンスを「参照している変数などの数の合計」です。

しくみはとてもシンプルです。
・使う変数ひとつごと リファレンスカウンタを +1 する
・使い終わったら リファレンスカウンタを -1 する
これだけで参照する変数がなくなれば、リファレンスカウンタの値はゼロになります。

iOSで動作するアプリは複雑です。処理が進むとメモリ上の必要だった情報は不要になります。不要な情報はすみやかに解放されます。

かつてはリファレンスカウンタの参照数処理(変数ごとに専用メソッドを呼び出さなければならなかった)はプログラマの仕事でした。人間がやるとミスが発生しトラブルの元になる場合がありました。

主なミスのひとつはまだ必要なのに誤ってリファレンスカウンタをマイナス1してしまうものです。インスタンスは解放され、アクセスした時点でアプリは強制終了しました。
このミスは致命的なので、プログラマはリファレンスカウンタをマイナス1する処理に慎重になります。すると解放されるべきインスタンスが残ったままになり、あらたにメモリを確保できない「メモリ不足」が発生してしまいます。

『メモリ管理用のコードをプログラマが忘れずに書かなければならない問題』の救世主としてARC(Automatic Reference Counting、アークと発音される場合あり)が開発されました。
Swift言語では最初からARCによるメモリ管理がなされています。
上記のようなリファレンスカウンタを勝手に変更することはできなくなっています。

強い参照

ARCによるメモリ管理でリファレンスカウンタをプラスマイナス1する変数や定数などを強い参照(Strong Reference)と呼びます。
強い参照はデフォルトの属性です。

強い参照の変数がコード内でどのように使われるかにより、ARCはコンパイル時に適切にリファレンスカウンタをプラスあるいはマイナス1する場所を見つけ、自動でバイナリコード内に埋め込みます。

強い参照の変数や定数(またはその変数などを持つオブジェクト)を「所有者owner)」と表現する場合もあります。

たとえばビューの所有者はそのビューのスーパービューです。スーパービューをたどるとビューコントローラーのビューが所有者です。
画面遷移でビューコントローラーが処分されると所有しているビューも全て解放される仕組みです。

リファレンスカウンタのしくみでは所有者が複数あるインスタンスは不要とは判断されません。
実際のコードではアウトレットなどのように所有者以外から参照したい場合はたくさんあります。そのままでは正常なメモリ解放を妨げます。

関数(メソッド)内で一時的に参照する場合は、関数の処理が終わり次第リファレンスカウンタはマイナス1されます。
問題になるのはビューコントローラーなど長時間存在するインスタンスのプロパティです。

この場合のシンプルな解決策として所有者をひとつだけにする方法があります。本当に必要な所有者と『見てるだけ』をARCに区別させる手段が必要です。この区別は自動ではできません。

所有者以外が参照できて、かつ不要になった場合の解放を邪魔しないために、ARCにはインスタンスを所有せず参照する「weak」キーワードと「unowned」キーワードがあります

ビューなどのアウトレットが weak キーワードを明示しているのは、スーパービューのみを唯一の所有者とするためなのです。
weak と指定するとビューコントローラーを解放する時にビューの解放を妨げません。

救世主ARCの弱点

ARCはメモリ管理の救世主ですが、自動で管理できない場合があります。
実際には必要なくなっても参照され続けていると判断される場合です。

上記のアウトレットの例以外にも「循環参照」と呼ばれる二つのインスタンスがお互いに所有関係にある場合(三つ以上でインスタンスの所有関係を辿るともとのインスタンスに戻る場合を含む)です。

循環参照を避けるときにも weak または unowned キーワードを使います。

「weak」キーワード

weak キーワードはその変数などをARCによるリファレンスカウンタ管理から除外します。
また参照しているインスタンスが解放された場合、自動的に値なしを設定します。
このため weakを指定する場合は変数で型はオプショナルでなければなりません。
weakキーワードはこの「インスタンスの解放に対応する処理」のため若干のオーバーヘッドがあるとされています。

weak キーワードの注意

次のコードは weak キーワードの正しくない使い方の例です。

// 正しくないweakの例(ワーニングが出ます)
weak var button = UIButton(type: .system)

UIButton のイニシャライザは新しいインスタンスを作りますが、それを直接weak の変数に代入すると所有者がいないために直ちに解放され値なしになってしまいます。
この場合は weak キーワードはつけてはいけません。( 何も付けなければデフォルトのstrong属性になります)

weak キーワードを付けた変数はオプションなので『値なしか』の確認が必要です。オプショナルバインディングや

「unowned」キーワード

unowned キーワードもその変数などがリファレンスカウンタに影響しないことをARCに知らせます。
weak キーワードと違ってインスタンスが解放されても unowned の変数(または定数)は影響を受けません。
このため参照先が解放されてから unowned 変数にアクセスすると実行時のエラーを発生します。
このため unowned キーワードの利用には注意が必要です。(weak よりも指定できる場面は限られます)

直接イニシャライザでインスタンスを代入しても直ちに解放されるのは weak キーワードと同じです。

// 正しくないunownedの例
unowned var badButton = UIButton(type: .system)

メモリ管理の概要はここまでにしてタイマーに話を戻します。

メモリ管理は重要

WWDC 2019でiPadアプリはMacでも使えるようになると発表されました。
Macは電源を切らずに使われ続けることが多く、アプリも終了されずに開いたままの状態にされがちです。
メモリリークなど必要以上にメモリを消費するアプリにならないよう、これまで以上にメモリ管理が重要になります。


Timer クラス

Swift 言語の Timer クラスは Objective-C では NSTimer クラスです。

Timer は定期的にコードを実行するために用意されたしくみです。もちろん一度限りでも可能です。
時間間隔を指定して実行させるしくみですが、厳密な時間精度はありません。(コードの実行が指定間隔よりも遅れる可能性があります)
厳密な時間精度がないのはイベント処理なども担当している RunLoop で時間監視しているためです。実用上十分なのでこのようになっています。
指定時間間隔で写真を撮影するような用途に最適です。
Timer のしくみはアニメーションには使われません。

Timer の用語

fire
名詞では炎とか火事の意味ですが、ここでは動詞として使われています。
指定時間が経過してコードを実行することの意味で使われます。
辞書では「発射する・矢を放つ・ボールなどを投げつける、クビにする」とありますが、Timerのドキュメントでは「起動」とか「実行」の意味で使われています。「発火」と訳す人もいます。

Timer の使い方

タイマーのドキュメントのOverview(概要)冒頭の説明文をもう一度確認しましょう。

タイマーは実行ループと連携して動作します。
実行ループ(run loop)はそれらのタイマーへの強い参照(strong references)を保持しているので、実行ループに追加した後にタイマーへのあなた自身の強い参照を保持する必要はありません。

Timer は RunLoop によって実際に時間の監視がおこなわれ実行されます。
RunLoop については iOSアプリを作ろう・アプリの仕組みとUIKit の「操作に対応」も参照してください。

RunLoop に登録する方法はいくつかありますが、もっとも手軽なのは Timer クラスの type メソッドを使う方法です。

scheduledTimer(timeInterval:target:selector:userInfo:repeats:) メソッド

class キーワード付きなのでクラス名で記述する type メソッドです。
イニシャライザのように Timer のインスタンスを作りますが、そのインスタンスを RunLoop に登録しただちに時間の監視を開始する点が特殊です。(インスタンスは関数値で返します)

ここから先は

4,839字 / 1ファイル
この記事のみ ¥ 500

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。