[iOS] InstrumentsでAutomationを実行しながらメモリ使用状況を監視したい

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Automation

AutomationとはUIに対する操作を自動化する仕組みで、UIを操作する一連の流れを継続的にテストしたい場合や、UI操作をひたすら繰り返すような耐久テストを行う際に有効です。Automationは少々とっつきづらいイメージがあるかもしれませんが、iOSのAutomationはIDEに統合されているためとても簡単に始める事ができます。

Automationを実行するための準備

Automationを実行するための準備は、実際に実行したいAutomationの実行手順の定義を用意するだけです。AutomationはInstrumentsの中のInstrumentとして機能が提供されており、Automationの実行手順の定義を指定すると、それに従ってUIの操作を実行してくれます。実行手順の定義さえ用意しておけば、あとはAutomation Instrumentが全て面倒を見てくれるため、非常に簡単に実行する事ができます。

Instrumentsに対する細かい説明は省略しますが、各種プロファイリング機能が個々の「Instrument」として用意されており、それらをまとめて管理・実行するのがInstrumentsというツールだと考えておいてもらえれば問題ないと思います。

Automationの実行手順の定義

Automationの実行手順は、JavaScriptで記述します。以下は記述例です。

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

// View上のボタンの参照を取得
var button = window.buttons()[0];
// ボタンをタップ
button.tap();

このコードでAutomationを実行すると、起動直後の画面に表示されているボタンが自動的に押下されます。

Automationではもちろんタップ操作だけでなくジェスチャもできますし、スクリーンショットを撮る事もできます。詳細については、Instruments User Guideを参照して下さい。

Automationを実行する

テストアプリの作成

では、実際にAutomationでUI操作を自動化してみましょう。まず、適当なXcodeプロジェクトを作成して、ボタン押下時にUIPickerControllerが表示されるよう実装します。静止画が撮影されたらカメラロールに保存してUIPickerControllerを閉じます。

ViewControllerの実装コードです。

ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

- (IBAction)buttonDidTouch:(id)sender;

@end

ViewController.m

#import "ViewController.h"

@interface ViewController () <UINavigationControllerDelegate, UIImagePickerControllerDelegate>

@end

@implementation ViewController

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // 撮影した静止画はカメラロールに保存
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, NULL);

    // UIImagePickerを閉じる
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (IBAction)buttonDidTouch:(id)sender
{
    // ボタンがタップされたらUIImagePickerを表示
    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
        UIImagePickerController *pickerController = [[UIImagePickerController alloc] init];
        pickerController.delegate = self;
        pickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
        [self presentViewController:pickerController animated:YES completion:nil];
    }
}

@end

Automationのスクリプトの作成

次に、Automationの実行手順のスクリプトを作成します。以下のコードを適当なファイル名をつけて保存しておきます。

automation-test.js

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

UIALogger.logDebug("Test start.");

for (var i = 0; i < 100; i++) {
    // ボタンを押下してUIPickerControllerを表示
    var launchButton = window.buttons()[0];
    launchButton.tap();
    target.delay(3.0);

    // カメラのシャッターボタンを押下
    target.tap({x: 160, y: 548});
    target.delay(5.0);
    target.logElementTree();

    // プレビュー画面のUseボタンを押下
    target.tap({x: 278, y: 523});
    target.delay(3.0);
    target.logElementTree();
}

UIALogger.logDebug("Test finished.");

このAutomation実行コードは、UIImagePickerで静止画を撮影してカメラロールに保存するという一連の操作を100回繰り返します。

なお、このスクリプトではスクリーンが4inch Retinaのデバイスを対象にしています。他のデバイスで実行する場合は適宜タップする座標を調整して下さい。

Automationの実行

準備が整いましたので、Automationを実行します。先程作成したテストアプリのXcodeプロジェクトからInstrumentsを起動します。Instrumentsの起動は、左上のRunボタンを長押しすると表示される「Profile」をクリックするか、Cmd - I を入力します。

テンプレートの選択画面が表示されますので、Automationを選択します。

ios-automation001

Instrumentsが起動すると自動的にAutomationが走り出しますが、まだスクリプトの設定をしていませんので、左上のRecordボタンを押下して一旦動作を止めます。

ios-automation002

左下のAutomation設定エリアのScriptsの中にあるAddボタンを押下してimportを選択します。ファイル選択ダイアログが表示されますので、ここで先程作成したスクリプトファイルを指定します。

ios-automation003

Scriptsの中に選択したファイル名が表示されていれば準備完了です。

ios-automation004

左上のRecordボタンを押下してAutomationを再度実行します。すると、InstrumentsにAutomationの実行ログが表示され始めます。

ios-automation005

Automationの実行時のアプリの様子

テストアプリ側は、自動的にスクリプトで定義した操作が実行されます。今回のサンプルでは、まずInitial ViewControllerで指定されている画面が表示されます。

ios-automation006

画面下のボタンが押下されて、UIImagePickerControllerが表示されます。

ios-automation007

UIImagePickerControllerのシャッターボタンが押下されて、プレビュー画面が表示されます。

ios-automation008

最後に右下のUseボタンが押下されて最初の画面に戻ります。この操作が100回繰り返されます。

Automationの実行時にAllocations・Leaks Instrumentを実行する

Allocations・Leaks Instrmentを追加する

Instrumentsはテンプレートで提供されるInstrumentの組み合わせだけでなく、手動でInstrumentを追加することによって様々なInstrumentを組み合わせて実行する事ができます。先程のAutomation InstrumentにさらにAllocations・Leaksの2つのInstrumentを追加してみましょう。Instrumentの中央上部にあるLibraryボタンを押下します。

ios-automation009

するとLibraryウィンドウが表示されて、Instrumentの一覧が表示されます。

ios-automation010

この中から、Allocationsをダブルクリックして選択します。すると、InstrumentsにAllocations Instrumentが追加されます。同じ様にLeaks Instrumentも追加しておきます。

ios-automation011

これで準備は完了です。

実行する

では、Automationと一緒にAllocationsとLeaksを動かしてみたいと思いますが、わざとメモリリークを起こすために少しソースコードに細工をします。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // 撮影した静止画はカメラロールに保存
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, NULL);

    // ブリッジキャストと同時にretain
    CFMutableDataRef data = (__bridge_retained CFMutableDataRef)image;

    // UIImagePickerを閉じる
    [self dismissViewControllerAnimated:YES completion:nil];
}

ソースコードを修正したら、再度XcodeからInstrumentsを実行します。

ios-automation012

静止画の撮影毎にUIImageがリークしていることが確認できます。さらにソースコードを修正してメモリリークが起きないように直します。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // 撮影した静止画はカメラロールに保存
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    UIImageWriteToSavedPhotosAlbum(image, nil, nil, NULL);

    // ブリッジキャストと同時にretain
    CFMutableDataRef data = (__bridge_retained CFMutableDataRef)image;
    // releaseして参照カウントを戻す
    CFRelease(data);

    // UIImagePickerを閉じる
    [self dismissViewControllerAnimated:YES completion:nil];
}

再度XcodeからInstrumentsを実行すると、メモリリークがなくなっていることが確認できます。

ios-automation013

このように、Automation実行中にもプロファイリングを行うことができます。

まとめ

Automationによる耐久テスト実施時のエラー原因を後から探りたい場合には、リソース関係の測定も同時に実行しておくと便利です。今までInstrumentsはリソース・パフォーマンス測定系の機能しか使っていませんでしたが、他の機能も積極的に活用していきたいと思いました。

参考サイト

Instruments User Guide