Click here to load reader
Upload
irgaly
View
1.383
Download
14
Embed Size (px)
DESCRIPTION
2014/11/22 第2回 Japan Xamarin User Group Conference 東日本編 で発表したスライドです。 https://atnd.org/events/57246
Citation preview
iOS の動画アプリ開発に Xamarin を使ってみた
@irgaly
Japan Xamarin User GroupJXUG Conference (East) #2
2014/11/22@日本マイクロソフト 品川本社 セミナールームA
目次• 自己紹介 • 実績 • アプリ説明/設計 • Xamarin.iOSのメリット/デメリット • メモリ管理の罠
• 原因と対策 • メディアの扱い • 画面回転 • Background Fetch • Jenkinsビルド • まとめ
自己紹介• 北川大智 / @irgaly • フェンリル株式会社
• 共同開発部 • Android / iOS エンジニア • Android Lollipop, Material Design に興味あり • いずれ Dart の時代が来ると思っている • vim / arch linux
とりあえず新しいものに食い付いておくタイプ
実績
過去の実績• NHK 紅白歌合戦アプリ(2013)
ViewModel 実装の担当 • Xamarin/C# エンジニアを名乗れるようになった
• Objective-C からの解脱
NHK紅白アプリ
業務に使ってみて
今回のアプリ
• マルチメディア系 iPhone アプリ • iOS 7.0 以上
• Android は開発対象外 • Android 版は先行開発済み • Android 版の移植開発
• 機能 • ソーシャルログイン
• Twitter, Facebook • 動画撮影
• ショートムービー、連続撮影、Landscape, Portrait • 動画ダウンロード・再生
• 動画の連続再生
設計
• MVVMCross で MVVM • いつでもクロスプラットフォーム展開できるように • Xamarin を選ぶ理由
• Model, API などは Service で制御。それ以外は ViewModel + View • Service または MVVMCross Plugin
• Facobook iOS SDK (Xamarin Components) • Xamarin.Social (Twitter ログイン) (Xamarin Components) • 効果測定ネイティブ SDK バイディング • 録画機能もプラグイン化 (クロスプラットフォーム対応に向けて)
• その他 • Mac/Xamarin Studio • Xib/AutoLayout/AssetCatalog...
Xamarin.iOS を使って・メリット• モバイルアプリ開発に MVVM を導入できる • C# の恩恵
• LINQ の威力 • Objective-C の闇にはまらない • Attribute, Reflection...
• 必要な機能はひととおり NuGet/Xamarin Component で調達 できる
• OS のすべての機能にアクセスでき、ネイティブ Binding も作れる
• 業務利用にも耐えられる • C# ノウハウでの知識集約
• クロスプラットフォーム展開 • OS やプロジェクトを越えてのプラグイン利用
• MVVMCross の強力な武器を使える • Service (DI), Messenger, Plugin...
※ iOS/Objective-C 開発と比較した場合
Xamarin.iOS を使って・デメリット• MVVM のオーバーヘッド
• クラス数/インスタンス数が多く、画面生成のパフォーマンスに難儀
• iOSはもともとMVVM設計ではない → 内部的にリフレクションを多用することのオーバーヘッド
• iOS でのメモリリーク問題 • どう頑張ってもメモリリークを駆逐できない • GC の問題
• 基本的な対策はとりつつ、今後のXamarin社の対応に期待
• エンジニアの問題 • チームメンバーが多いと、どうしてもメモリリーク対応漏れが発生する
※ iOS/Objective-C 開発と比較した場合
現実• 開発スピードが上がったり、コストが削減されるようなものではない。
• MVVMの採用で品質は向上する • もちろんクロスプラットフォーム対象に開発すれば総合で見た場合のスピード・コスト・品質に変化あり
• エンジニアとしてはモダンな開発環境で揃えられるので心安らぐ
• 構成切り替え、Jenkins ビルド、Reveal、Crashlytics などに対応しており、不便になることはない
• Xamarin Profilerなど、今後もツールは充実する方向にある
• C#を書けるスマホアプリエンジニアの確保が難しい
※ iOS/Objective-C 開発と比較した場合
メモリ管理の罠
メモリリークに気付くある日
(絶望)※当時はXcode Instrumentsでした
調べる• c# - Xamarin iOS memory leaks everywhere - Stack Overflow
• http://stackoverflow.com/questions/25532870/xamarin-ios-memory-leaks-everywhere • Best way to recursively dispose UIViews in Xamarin iOS
• http://education-eco.blogspot.jp/2014/08/best-way-to-recursively-dispose-uiviews.html
• Memory and Performance Best Practices | Xamarin • http://developer.xamarin.com/guides/cross-platform/
deployment,_testing,_and_metrics/memory_perf_best_practices/ • Advanced Memory Management on iOS and Android - Mark Probst
and Rodri… • http://www.slideshare.net/Xamarin/advanced-memory-management-on-ios-and-
android-mark-probst-and-rodrigo-kumpera • Memory Management Pitfalls in Xamarin iOS - Introduction
• http://bluetubeinc.com/blog/2013/6/memory-management-pitfalls-in-xamarin-ios-introduction
奥が深そう
原因は?
• 二つのGCが同時に機能している • Xamarin.iOS の GC と iOS の GC の相性が悪い
• 参照カウンタ方式の闇 • UIView でだいたいリークする
原因と発生パターン• ネイティブオブジェクト間での循環参照 • サブクラス化と EventHandler • アンマネージリソースを Dispose していない
UIView の循環参照
• NSObject を継承したクラスを作ると発生
対策
1 public class MyView : UIView { 2 WeakReference<UIViewController> _parent; 3 public MyView (UIViewController parent) { 4 _parent = new WeakReference<UIViewController>(parent); 5 } 6 }
• WeakReference を使う • Objective-C ではおなじみの習慣
• C#で書いていると忘れがちになるが、同じように対応する
サブクラス化とEventHandler
1 public class CustomButton : UIButton {} 2 3 public class MyViewController : UIViewController { 4 public override void ViewDidLoad () { 5 var button = new CustomButton(); 6 View.Add(button); 7 button.TouchUpInside += (s, e) => { 8 ButtonClick(); 9 };10 }11 }
• EventHandler で循環参照発生
対策 1 public override void ViewWillAppear(bool animated) 2 3 { 4 UIButton.TouchUpInside += ButtonClick; 5 } 6 7 public override void ViewWillDisappear(bool animated) 8 { 9 UIButton.TouchUpInside -= ButtonClick;10 }
• EventHandler の削除
アンマネージリソースの Dispose 漏れ
1 public override void ViewDidLoad () { 2 var imageView = ...; 3 var image = UIImage.FromBundle ("hoge.png"); 4 imageView.Image = image; 5 6 _otherButton.TouchUpInside += (s, e) => { 7 imageView.RemoveFromSuperview();// UIImageを解放できていない 8 } 9 }
対策 1 public override void ViewDidLoad () { 2 var imageView = ...; 3 var image = UIImage.FromBundle ("hoge.png"); 4 imageView.Image = image; 5 6 _otherButton.TouchUpInside += (s, e) => { 7 imageView.RemoveFromSuperview(); 8 imageView.Dispose(); 9 image.Dispose(); // すぐに解放する10 }11 }
対策まとめ• 親の参照には WeakReference を使う • 不要になったら EventHandler の削除 • 怪しかったら Dispose する
• → むしろ Touch プロジェクト(Xamarin.iOS) 側のコードビハインドをできる限り減らす
• Core プロジェクト(PCL) プロジェクトのコードはこれらの危険が少ない
メディアの扱い
メディアの扱い• 動画撮影
• プラグインを独自実装 • PlatformService
• プラットフォーム固有の機能について • 機能紹介
DIを使うことで、Core プロジェクトから プラットフォームの機能を扱えるようにする。
独自のMVVMCross Plugin
• PCLとプラットフォームとでプロジェクトを分ける
IMvxVideoCapture 1 public interface IMvxVideoCapture 2 { 3 Task<bool> SetupCaptureSessionAsync(); 4 bool StartRecording(string filePath); 5 Task<bool> StopRecording(); 6 bool SwitchCamera(); 7 bool UpdateOrientation(VideoOrientation orientation); 8 } 9 10 public interface IMvxVideoCapture<TPreviewView> : IMvxVideoCapture11 {12 bool StartPreview(IMvxVideoDisplay<TPreviewView> display);13 bool StopPreview();14 }
• プラットフォームに依存しないインタフェースを定義する
MvxVideoCapture
1 var session = new AVCaptureSession 2 { 3 SessionPreset = AVCaptureSession.PresetHigh 4 }; 5 var camera = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Video); 6 var mic = AVCaptureDevice.DefaultDeviceWithMediaType(AVMediaType.Audio); 7 var cameraInput = AVCaptureDeviceInput.FromDevice(camera); 8 session.AddInput(cameraInput); 9 var micInput = AVCaptureDeviceInput.FromDevice(mic);10 session.AddInput(micInput);
• Touch プロジェクト側で実装を書く • iOS AVFoundation クラスを C# からそのまま扱える
初期化からプレビュー開始まで(part1)
MvxVideoCapture
11 var queue = new DispatchQueue("CaptureBuffer");12 var videoOutput = new AVCaptureVideoDataOutput();13 session.AddOutput(videoOutput);14 videoOutput.SetSampleBufferDelegate(this, queue);15 var connection = videoOutput.ConnectionFromMediaType(AVMediaType.Video);16 connection.VideoOrientation = GetCaptureOrientation(_currentOrientation);17 var audioOutput = new AVCaptureAudioDataOutput();18 session.AddOutput(audioOutput);19 audioOutput.SetSampleBufferDelegateQueue(this, queue);20 var previewLayer = new AVCaptureVideoPreviewLayer(session);21 session.StartRunning();
• Touch プロジェクト側で実装を書く • iOS AVFoundation クラスを C# からそのまま扱える
初期化からプレビュー開始まで(part2)
MvxVideoCapture 1 var videoSettings = new NSMutableDictionary(); 2 videoSettings.Add(AVVideo.CodecKey, AVVideo.CodecH264); 3 videoSettings.Add(AVVideo.WidthKey, new NSNumber(640)); 4 videoSettings.Add(AVVideo.HeightKey, new NSNumber(480)); 5 var compressionSettings = new NSMutableDictionary(); 6 compressionSettings.Add(AVVideo.AverageBitRateKey, new NSNumber(2413000)); 7 videoSettings.Add(AVVideo.CompressionPropertiesKey, compressionSettings); 8 videoSettings.Add(AVVideo.ScalingModeKey, AVVideoScalingModeKey. 9 ResizeAspectFill);10 11 var videoInput = new AVAssetWriterInput(AVMediaType.Video, new 12 AVVideoSettingsCompressed(videoSettings));13 videoInput.ExpectsMediaDataInRealTime = true;14 writer.AddInput(videoInput);
• AVAssetWriterInput の引数を作成するために、NSMutableDictionary を C# から扱っています。
コーデックの設定から撮影開始部分(part1)
MvxVideoCapture
15 16 var audioSettings = new NSMutableDictionary();17 audioSettings.Add(AVAudioSettings.AVFormatIDKey, NSNumber.FromInt32((int)18 AudioFormatType.MPEG4AAC));19 audioSettings.Add(AVAudioSettings.AVNumberOfChannelsKey, NSNumber.FromInt16(2))20 ;21 audioSettings.Add(AVAudioSettings.AVSampleRateKey, NSNumber.FromFloat(44100.0f)22 );23 var audioInput = new AVAssetWriterInput(AVMediaType.Audio, new AudioSettings(24 audioSettings));25 writer.AddInput(audioInput);
• AVAssetWriterInput の引数を作成するために、NSMutableDictionary を C# から扱っています。
コーデックの設定から撮影開始部分(part2)
Core プロジェクトから MvxVideoCapture を使う
1 ... 2 var videoCapture = Mvx.Resolve<IMvxVideoCapture>(); 3 videoCapture.StartRecording(filePath); 4 5 ... 6 7 await videoCapture.StopRecording();
• MVVMCross プラグインとしてインタフェースとプラットフォーム固有の実装を分けたため、ViewModel 側の PCL プロジェクトへこれを記述できる
PlatformService 1 public class TouchPlatformService : IPlatformService 2 { 3 public IMediaFile GetMediaFile(string path) 4 { 5 return new TouchMediaFile(path); 6 } 7 8 public async Task Share(string shareText) 9 {10 var items = new NSObject[] {11 new NSString(shareText)12 };13 var activity = new UIActivityViewController(items, null);14 var vc = ...;15 ...16 await vc.PresentViewControllerAsync(activity, true);17 }18 19 public string VersionName()20 {21 return NSBundle.MainBundle.ObjectForInfoDictionary("CFBundleVersion").22 ToString();23 }24 25 ...
• プラットフォーム固有の機能を呼び出すユーティリティサービス
画面回転
画面回転
• 現在の iPhone の傾きや、傾きの変化を取得する • 動画撮影の縦、横切り替えに使用 • BeginGeneratingDeviceOrientationNotifications で判定する
• 画面ロック時には回転通知は発生しない
画面回転 1 public override void ViewWillAppear(bool animated) 2 { 3 base.ViewWillAppear(animated); 4 5 UIDevice.CurrentDevice.BeginGeneratingDeviceOrientationNotifications(); 6 var notificationAvailable = false; 7 _orientationObserverToken = NSNotificationCenter.DefaultCenter.AddObserver( 8 UIDevice.OrientationDidChangeNotification, notification => 9 {10 notificationAvailable = true; // 傾き取得成功11 ... // 傾きに応じた処理12 });13 14 Task.Run(async () =>15 {16 await Task.Delay(500);17 if(!notificationAvailable) {18 … // 傾きロックされている可能性がある。Portraitであるとみなして処理19 }20 }).Forget();21 }
画面回転
1 public override void ViewWillDisappear(bool animated) 2 3 { 4 base.ViewWillDisappear(animated); 5 UIDevice.CurrentDevice.EndGeneratingDeviceOrientationNotifications(); 6 if (_orientationObserverToken != null) 7 { 8 NSNotificationCenter.DefaultCenter.RemoveObserver(_orientationObserverT 9 oken);10 _orientationObserverToken = null;11 }12 }
Background Fetch
Background Fetch
• iOS7 以上で、バックグラウンドのアプリが一定期間ごとに起動し、なんらかの処理を実行できる機能 • 起動の間隔はOSが決定するため、指定することはできない • 30秒間のみ処理可能であり、できることは限られてくる • 主にAPIとの通信で、データ更新をチェックするための機能 • 30秒を過ぎるとプロセスが殺される
プロジェクトの設定
• iOSのプロジェクト設定から、Background fetchオプションを設定する
Background Fetch 1 public override bool FinishedLaunching(UIApplication app, NSDictionary options) 2 3 { 4 ... 5 6 // BackgrondFetchの呼び出し間隔指定 7 UIApplication.SharedApplication 8 .SetMinimumBackgroundFetchInterval( 9 UIApplication.BackgroundFetchIntervalMinimum);10 11 return true;12 }
• AppDelegateで、起動時に Background Fetch の間隔を設定
PerformFetchの実装 1 public override async void PerformFetch(UIApplication application, Action< 2 UIBackgroundFetchResult> 3 completionHandler) 4 5 { 6 var cancelToken = new CancellationTokenSource(); 7 try 8 { 9 // 25秒で強制中断10 cancelToken.CancelAfter(25000);11 12 // API通信などを実行13 … completionHandler(UIBackgroundFetchResult.NewData);14 }15 catch (OperationCanceledException)16 {17 completionHandler(UIBackgroundFetchResult.Failed);18 }19 }
• 保険として25秒制限をしているが、安全に対応するならBackground Transfer (NSURLSession Background mode)で対応する
• Xamarin はアップデートが早いため、OSの最新機能もすぐ使えるようになる • マイナーな機能は SDK にバグがあることもある
Jenkins ビルド
Jenkins ビルド
• 弊社ではすべてのプロダクトは Jenkins でビルドしています • 納品時のトラブルを防ぐ • エンジニア不在の時でも最新のアプリの動作確認を行える
• Xamarin.iOS x Jenkins は公式ガイドあり • http://developer.xamarin.com/guides/cross-platform/ci/jenkins_walkthrough/
• Jenkins は Mac 上で実行されている • Jenkins ビルドでの構成(定数)切り替えビルド対応
プロジェクト側の準備
• mdtool コマンドは定数切り替えに対応していない • 定数切り替え対応のため、Touchプロジェクトにシンボルを定義しておく
Jenkins 環境の準備
• Xamarin Studio をインストールする • ライセンス認証する • Stable の最新バージョンにアップデートしておく
• 開発チームとJenkinsで、「常にXamarin StudioのStable最新環境で作業する」という取り決めをしておく
• Xcode も一度は起動して使える状態にしておく(念のため) • プロビジョニングプロファイルと署名をインポートしておく
Jenkins 環境で作業します
ビルド構成を作成• フリースタイルビルド構成
• ビルド環境 > ビルドプロセスに環境変数をインジェクト (Environment Injector Plugin)
ビルド構成を作成• ビルド > シェルの実行 > シェルスクリプト
1 sed -i '' -Ee "s/__JENKINS__;/${Options};/g" MyProject.*/MyProject.*.csproj 2 4 /Applications/Xamarin\ Studio.app/Contents/MacOS/mdtool -v build -t:Build -c:$(BuildConfiguration}\|iPhone -p:MyProject.Touch MyProject/MyProject.sln 5 6 find MyProject/MyProject.Touch/bin/${BuildConfiguration} -name '*.ipa' -exec 7 mv {} MyProject/MyProject.Touch/bin/${ 8 BuildConfiguration}/MyProject-${ 9 BuildConfiguration}-${BUILD_ID}.ipa \;
まとめ
まとめ
• iOS寄りの各機能の紹介をコードレベルで紹介しました • PCLとプラットフォームのプロジェクトを分けて実装する
• パッケージマネージャなどの機能は揃っており、Xamarin は開発プラットフォームとしては強力
• プラットフォーム固有の機能へのアクセスもできるため、業務に耐えうる
• プラットフォームごとの対応の手間はかかる、罠も多い