ここ最近読んだ技術書籍感想文。
雑多に読んでます。
リンクは書籍の公式サイトです。
黒帯エンジニアが教えるプロの技術 Android開発の教科書
SBクリエイティブ:黒帯エンジニアが教えるプロの技術 Android開発の教科書 (ヤフー黒帯シリーズ)
幅広いトピックを扱っているのだけれど、それゆえにどこかで読んだ本の内容を簡略化しているだけの章もあったり、開発以外のトピックの割合が多かったり、Andorid 開発の高度な技術トピックを期待すると中途半端かもしれないという感想でした。
同時期により初心者向けと思われる「基本からしっかり身につくAndroidアプリ開発入門 Android Studio 2.x対応」という本も出版されていたので、こちらのトピックでよかったんじゃない?という内容もちらほら。
また、アプリのグロース関連の章については、Firebase の登場で大きく変わった部分が多く、タイミングの悪い感じです。
具体的には、たとえばクラッシュレポートは Firebase Crash Reporting、AB テストは Rule Types and Variables、アプリケーションのプロモーションのチャネルに Firebase Invites が追加され、GCM は FCM になりました。
これらは単に置き換わったのではなく、Firebase プラットフォーム上のサービス間のシナジーが本質で、例えば「バグの対象機種で、最近起動率の減ったユーザーをトピックに、バージョンが上がって改善したことを Push 通知を使ってフォローする」といった面白い使い方ができるのです。
C#実践開発手法~デザインパターンとSOLID原則によるアジャイルなコーディング
Xamarin のために久々に C# やっていて、1年くらい積まれてた本をようやく消化したのですが、今まで読まなかったのを悔やむくらい、適応力の高いコードの書き方について解説している良書でした。
- 近年に重要になった、「変化に強い」デザインパターンを使ったコードの書き方
- デザインパターンを支える基礎である、重要なプログラミング原則群の解説
- 架空のストーリー仕立てで、プロダクトマネージャと開発者がいかに協調し、実際的にアダプティブにコードを作っていくか
という構成になっています。ただし、翻訳は結構眠くなる気がします。
モダンで C# 向けの CleanCode という感じの本です。
Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング
これからの Android 開発は、Kotlin か Java8 で頑張るかの二択になると思いますが、付け焼き刃の新言語を使うまでもなく Java8 で十分いける気がしてきました。
記述が冗長という欠点については、IntelliJ IDEA の超強力なサジェストがさくさく代筆してくれるので、あまり気にならないです。
この本は、
を読んだ人向けに、Java SE7 と SE8 との diff をざっくり理解するのには良い本です。翻訳者が同じなので。
ただ内容は非常にあっさりとしていて、それを補うためか章末問題が用意されています。この問題は一部が難解というか、この本の内容だけで解けるとは到底思えないものもあるので、かなり苦しむことになります。
しかも解答が載っていないのですが、この章末問題に挑戦している人は国内外にいっぱいいて、解法を GitHub などで共有していたりするので、自分が苦しんで得た解答を他人の解法と照らし合わせて、遥か高みにいるなーとか、またさらに他の人は別のアプローチで解いていて読んで唸らされたり、というのが本当に楽しいです。
そこまでやれば、Java8 実践レベルの知識を得たと自負していいはず。
Android Studio本格活用バイブル~効率的にコーディングするための使い方
「Android Studio 対応」を謳う入門書は多々ありますが、この本が他の Android Studio 本と一線を画しているのは、Android 開発をやりたい人向けではなく、IntelliJ IDEA での効率的なコーディング方法を知りたい人向けに特化しているということです。
正直、この本のタイトルは詐欺だと思いますが、いい意味で騙されました。
Eclipse ADT から Android Studio へ移行したとき、Darcula テーマのモダンさから「新しいクールな IDE になった」と捉える向きがありました。
この本はそうではなく、IntelliJ IDEA も Eclipse と同じくらい歴史のある古い IDE とみなしています。長い歴史のある IDE はしがらみのためにショートカットに癖があったり、あるやり方をするのに複数の方法があったり、洗練されていない部分もあることを認めつつ、しなしながらその歴史が育んだ効率的なコーディング方法をいかに手に馴染ませるかということを滔々と解説しています。IntelliJ 愛してるなーという感じです。
反面として、レイアウトエディタの詳細な使い方とか、メモリリークの調査方法とかそういう Android 開発をする上で実際的に必要になる使い方については書籍版では触れていないです。Android Studio のコーディングの操作量を上げたいという人に向けた一点突破の本です。
SwiftではじめるUI設計&プログラミング 「操作性」と「デザイン性」を兼ね備えたアプリの開発手法
この本は、Swift 1.X & iOS8 なので今から読むには微妙だと前置きします。
しかしながら、個人的には、AutoLayout や SizeClass に関する説明は割と分かりやすかった気がします。全編フルカラーで、UI カタログ的な章もありますので、iOS SDK にどういう UI コンポーネントがあるのかまだよく分かっていない人に向けていい本だと思いました。
評判の良いよくわかるAuto Layoutも買ったので読み比べる予定です。
スマートフォンの連絡帳の話。
純正の連絡帳で別に満足してるって人もいれば、オリジナルの連絡帳を入れてる人もいますし、そもそも連絡帳に何も登録されてない人もいるでしょう。
それはさておき、iOS や Android の OS 標準の連絡帳はユーザー視点で見るか、開発者視点で見るかで貧弱とも言えるし、高性能とも言えて面白いです。
連絡帳の基本思想
iOS / Android に共通して言えることとして、実用的ではないです。グルーピング表示すらできません。
その理由は、OS 標準の連絡帳は、端末内の利用出来る情報を活用しつつ、様々なアプリへ連絡帳情報を提供するためのデータベースとなるよう、割り切った実装になっているからです。
連絡帳の元データは、多種多様です。
Android であれば Google アカウント、iOS であれば iCloud アカウントに紐付いていることが多いと思います。
その他にも SIM カードやソーシャルアカウントなど複数のデータソースを持つ場合があります。連絡帳はそれらの情報を収集し、統合した連絡先を表示しています。
例えば適当な数字ですが、Skype のフレンドが5000人いて、Facebook の友達が5000人いて、Google アカウントに5000人登録されていたとします。連絡帳はそれらのアカウント情報の断片から名寄せを行い、統合された7500人のデータを表示します。「自分」が3人に分裂したりすることは、きっとありません。
このような処理はリアルタイムではできません。このため、事前にデータベースに蓄積しています。高速に取得するために、インデックスを貼り、非同期で行われる更新をトリガーに、情報の紐付けを更新しています。これだけ考えるとものすごい高性能です。
そしてグルーピング機能が貧弱なのも説明できます。API 的には iOS でも Android でもグループを設定することはできるのです。ただしデータソース元がグルーピング機能を持っているとは限りませんし、また統合された連絡先をグルーピングすると、異なるデータソース間をまとめてしまう可能性があるので GUI としての提供は限定的なのです。
テーブルから理解する連絡帳
データは SQLite で保持されています。なので SQL の民であれば、ER 図を見れば余裕で理解できるはずです。
Android 編
いっぱいテーブルがあるんですけれど、だいたい下の3つがわかってれば問題ないという噂。
contact
テーブルに統合された連絡先データがあります。
統合人格は名寄せされた複数のraw_contact
テーブルと関連付けられています。
実際の各データはdata
テーブルに入っています。このdata1
〜data15
という汎用的で素敵なレコード名で察せられるかと思いますが、名前の場合、「data1
に表示名、data7
に名前のフリガナ」、電話番号だったら「data1
に電話番号、data2
に種別(携帯か固定電話か)」みたいな感じです。
連絡帳のContent Provider
へ問い合わせをするときには、このテーブル構造を覚えておくと少しだけ便利です。ほとんどの場合、単体のテーブルではなく、JOIN
したテーブルに対する問い合わせになります。SQL を意識すれば無駄を省くことができます。
以下、いくつかポイントです。
lookup
contact
のプライマリキーは_id
列ですが、これはただの Android の作法で、実際の一意キーはlookup
です。これの使い方にも癖がありますけど、リファレンス読めば大丈夫。
sort_key
と sort_key_alt
日本のロケールで見たとき、連絡帳は「山田太郎」さんと「Jhon Doe」さんのどちらが上であるのが自然でしょうか?
単純な文字列のソートでは、数字、英語、ひらがな、漢字になります。sort_key
は読み仮名などを考慮した上で、それぞれの国のロケールに合わせたソート順で連絡帳データを並び替えるためのものです。連絡帳データは件数が予期できないので、複雑な条件でソートをするよりも事前にソート用の値を持っておいたほうが楽なわけですね。
sort_key_alt
はロケールに依存しないソートをしたい場合に利用します。
name_lookup
とphone_lookup
名前や電話番号は表記ブレがおきます。080-3123-2931と書いたり、(080)31232931と書いたり。
名前や電話番号を正規化して保持しているルックアップテーブルが存在します。氏名や電話番号から検索する場合などは、contact
テーブルから辿るよりも、こちらを使うほうが適切です。
iOS 編
iOS の連絡帳の構造はすごく単純です。外部キー制約も一切ないです。
統合された連絡先情報を、ABPerson
テーブルに保持しています。複数のデータを持つ場合(例えば電話番号が自宅と携帯と会社で3つあるなど)は、ABMultiValue
でラベルと値を保持しています。
Android の raw_contact
に相当するテーブルはなく、統合された結果だけを保持しています。
なので統合された情報が何に由来してきているのかを知る術があまりなさそうで、ABStore
から知ることができる情報も、Local
かExchange
かCardDAV
かみたいな超ざっくりとした情報しか持っていません。
iOS の場合、AddressbookUI/ContactsUI フレームワークが用意されているため、あまり気にすることもないでしょう。
Addressbook フレームワークはほぼ C 言語な感じで、Objective-C からでも使いづらいのに、Swift から使うのはチャレンジャーな感じがありましたが、iOS9 から Contact フレームワークに置き換わって楽になりました。
FirstSort
とLastSort
この中身は姓名でソートするために事前に計算されたキーです。
A
は)
、D
は=
みたいに文字を並び替えていっているみたいですね。FirstSortLanguageIndex
とLastSortLanguageIndex
という列もあって、これを組み合わせてロケール対応しているのでしょうか。
iOS の場合、テーブルへの問い合わせはNSPredicate
というブラックボックスを介するので、Android ほど SQL 感はありません。しかしユーザーコード上でソートするよりも、sortOrder
を適切に渡した方が何かと無難だとは思います。
NSUnit と NSMeasurement についての話。
iOS10.0 / OS X 10.12 / tvOS 10.0 / watchOS 3.0 で追加されたクラスです。(これをひとまとめにする呼び方がほしい)
NSUnit
は単位を表すクラスです。単位と double
の値を組み合わせて、NSMeasurement
、測定値を記述することができます。
NSMeasurement
は加算・減算のメソッドがあるほか、乗算・除算の演算子のオーバーロードがあるので、例えば時間の計算を以下の記述で行うことができます。
// Swift3 で書く場合には、`NS` が drop されるので、以下はそのように記述します let sec = Measurement(value: 1.0, unit: UnitDuration.seconds) let hour = Measurement(value: 1.0, unit: UnitDuration.hours) let time = 2 * hour + 3 * sec;
この結果は、7203 sec になります。2時間と3秒の合計なので、7203秒であってますね。
これを Objective-C で書こうとすると、
NSMeasurement<NSUnitDuration *> *hour = [[NSMeasurement alloc] initWithDoubleValue:1.0 unit:NSUnitDuration.hours]; NSMeasurement<NSUnitDuration *> *minute = [[NSMeasurement alloc] initWithDoubleValue:1.0 unit:NSUnitDuration.minutes]; NSMeasurement<NSUnitDuration *> *computed = [hour measurementByAddingMeasurement:minute];
こんな記述になる上に、乗算のメソッドがなくて詰みます。
Xcode8 beta1 において、Objective-C 側では未実装で、nil
が返ってきていました。Swift 側が先行して実装されており、Objective-C で書かれた Foundation
のエクスポートではない?という推察ができます。
現時点でも NSUnit
のサブクラスである NSDimension
にはかなりの数が用意されていて、角度、面積、速度、質量濃度、分散、電気量、電圧、電流、電気抵抗、熱量、周波数、燃費、光量、距離、重量、電力、圧力、音量、温度があるっぽいです。
更にそれぞれの NSDimension
は距離であれば「メートル」「ヤード」「ポンド」みたいな単位を持っています。単位に値を紐付けたものが、NSMeasurement
ですので、上に示したコードのように「時間」と「秒」ということなるディメンションを持つ測定値で四則演算や比較を行ったり、NSUnitConverter
で「ジュール」から「カロリー」に変換することができるというコンセプトなのだと思います。
さらに、NSMeasurementFormatter
によって、ローカライズされた文字列を表示できるのです。
以下、初期の Xcode8 beta1 におけるバグについて書いてありましたが、現在は修正されているので削除。
Javaと偽Javaの話。
これの話。ブコメに書こうとしたら4000字は入らなかった。
Microsoft Java VM
かつての WIndows には MS 製の Java VM が搭載されていました。
古代の Java は「Write once, run anywhere」を掲げていた通り、クライアントサイドで Java アプレットとして利用されるのが主流でした(サーバーサイドで動くようになって、真価を発揮した感じがあります)。
しかし Java VM の仕様は、パフォーマンスについての記述は曖昧になっており、OS ごとの実装の違いによって、実行速度に顕著な差がありました。
Windows の Sun 純正の Java VM は性能が悪かったため、MS は独自の Java VM を開発し、Internet Explorer にバンドルしました。調子に乗った MS は Windows GUI ライブラリを利用できる、Sun Java VM と互換性のない Java 統合開発環境 Visual J++ をリリースしたりしていました。
これが 1997年に「Java の互換性を破壊した」として訴訟に発展、2004年に和解に至りましたが、現在 MS の Java VM は頒布されなくなりました。
Visual Studio 2015 で Android がサポートされ、Java モドキを再び書けるようになったとき、J++ の面影を思い出した人は少なくないはずです。J# は知らんです。
Apache Harmony
Apache 財団が開発を進めていた、オープンソース、ライセンスフリーな Java 実装でした。
「自由な Java」を求めた IBM や Intel、GNU Classpath に関わってきたフリーソフトウェア開発者たちが一同に介して開発を進めた夢のプロジェクトでした。
プロジェクトの開始以来、多くの企業や団体がJava SEのオープンソース実装に対する賛同を示し、協力を申し出てきた。例えばIBMがコア・クラスやクラスライブラリ/VMのインタフェース、Eclipseプラグイン等の実装への協力を申し出ているほか、IBM developerworksは評価ライセンスでのJVMの提供に同意している。また、Intelはセキュリティや認証系、正規表現やRMIなどの実装に協力すると表明している。オープンソース団体ではクラスライブラリの実装を行っているGNU Classpathプロジェクトが、Apache Harmonyとの提携を開始している。その他多くの協力を得た結果、現在はプロジェクトの大部分において実際に動作するコードが集まっているという。
JavaOne というのは Sun が主催していた Java の祭典です。Sun のお膝元でプロジェクトの進捗が公表されていた Apache Harmony ですが、悲劇的な末路を辿ります。
Sun が JSPA(Java Specification Participation Agreement)の供与を拒否したため、Java テクノロジ互換キット (Technology Compatibility Kit) と呼ばれる「Java 互換を名乗る資格を得るためのテスト」を受けられませんでした。
Apache 財団は Sun に公開書簡を送りましたが、Sun からの返答はありませんでした。
やがて、Sun 自ら Java をオープンソース化する OpenJDK プロジェクトを始めると、それまで Apache Harmony に関わってきた開発者たちは撤退し、OpenJDK へ移行しはじめました。
IBM や Apple(当時の Apple は Cocoa Java Bridge など Java テクノロジに熱心だった)が OpenJDK への参加を表明したことで、Apache Harmony プロジェクトは事実上終結しました。
Android
Apache Harmony は、「Java 互換」を名乗ることができない偽 Java として天命を迎えるはずのプロジェクトでした。
その成果を利用したのが Google です。Android の当初の Java 標準ライブラリは Apache Harmony が採用されており、このため「本物の Java」と挙動が違いました。貧弱な端末で動作させるために機能を制約した Dalvik VM 上で動作していながら、JIT もなく性能面で大きく劣っていたりしました。
歴史の流れを見ると、Sun を買収した Oracle が「Java の互換性を破壊した」として Google と訴訟に至るのは疑問の方が強いです。なぜオープンソース指向へ変わりつつあった Sun の意思が失われたのか、また Google が MS の取った行動をなぞるかのごとくリスクを犯したのか。
API のフェアユースが認められた今回の裁判結果が一つの契機となってくれることを願うばかりです。
Android N 現在の Java 標準ライブラリは Apache Harmony を捨て、Open JDK を採用しています。Dalvik VM は ART ランタイム(Java のクラスファイルを、Dalvik VM 向けに変換して APK 化した後、更に端末上で AOT コンパイラが走り、ネイティブなコードに変換している)に置き換えられました。
その他の Java
Oracle Java VM は唯一絶対な Java VM ではありません。
たとえば、IBM の WebSphere で利用されている IBM J9 VM のように、公式にライセンス供与された Java VM が存在しています。
ガラケー時代に流行ったアプリは Java ME で書かれており、Sun とのライセンス契約が締結されていました。
これらの中には、MIDP(Mobile Information Device Profile)に従っていた EZアプリや S!アプリも存在していましたが、当時主流だった iアプリは DoJa プロファイルという互換性のない Java で動作していました。ただライセンス料金を徴収されていたので非互換の偽 Java ですが、正式なサブセットという扱いでした。
歴史との diff
Android 登場以前までは、業界関係者が皆協力しており、Oracle といえども JCP に則って仕様策定を進めていました。
MS のような Java の破壊者は Android 以前にもいました。
仕様を策定する際には、RI (Reference Implementation) (リファレンス実装) と TCK (Technology Compatibility Kit) (テスト群) も同時に用意します。これは、策定した仕様が現実的に実装可能であるかどうかを確認するため、および、第三者が仕様に則って実装をおこなったときにその実装の互換性を確認できるようにするためです。
API デザインの互換性を担保する上で、TCK を公開することの重要性は、「Practical API Design: Confessions of a Java Framework Architect(API デザインの極意)」などでも説かれていますが、Oracle は自社のビジネスの都合で TCK の利用に制約を掛け、「正当な Java」として認定する対象を選別しています。
それが Oracle のビジネスモデルだと思うので、しょうがないですね。
一方 Oracle は、JCP に則り、エキスパートを集めて Java API の仕様策定作業をしています。時間はかかりますが、Java の新バージョンで追加される言語仕様や API の設計が洗練されているのは、そういう理由です。
かつて OpenJDK を支えていた Apple も IBM も今は Swift を盛り上げる方向にいっていますし、Java の進化が遅くなったのは仕様策定が綿密というよりも、業界の巨人の Java 離れが進んだ結果なんじゃないかなーと個人的には思っているのですが、どうなんでしょう?
JCP についてはまったく詳しくないので、的外れなのかも。
Oracle 提供の JDK / JRE に比べ、Android の品質はどうでしょうか? こちらは、平気でゴミクズのような API が公式 API として追加されます。
Android の API の品質が悪いのは関係ないだろ!!!!
往年の HttpURLConnection とか SecureRandom とか酷かったですよね。Fragment も酷い(Fragment in Fragment の闇は深い)。
TCK のような互換性維持のためのテストセットもありませんから、String.indexOf(String,int) という基本 API に実装不具合があっても、それを事前に検出できずにリリースが行われることがあります。
現在の標準ライブラリの実装は OpenJDK ですのでご安心ください。N 以降だけですが!
「Android 新バージョンの発表があると思うから、Google 主催の次のイベントが楽しみ!」と思っている時点で、Google に振り回されていることに気付くべきです。
今では Google I/O や WWDC が盛り上がっていますが、かつては Java の祭典である JavaOne が活発でした。
ところで、あまり知られていない(?)ですが、JavaOne や WWDC は日本でも開催されていました。(WWDC は世界開発者会議の略称なので、日本語版は JDC ですが)
最後に開催された JDC の大きなトピックは、「Cocoa on Windows をライセンスフリーで提供」(実現しなかった)だったので、いかにバブルな時代だったかというのが伺えるでしょう。
つまるところ、Sun や Apple に振り回されていたのが、Google や Apple に振り回されるようになっただけですね。
Anyパターンについて考えてみる
「Swiftの標準ライブラリだと付属型を持ったプロトコルを変数に格納するため、慣例的にAny
なんとかってクラスを用意してる」というところまでが話の前提です。
Any
クラス実装者の責務
例によって、Pokemon
プロトコルで考えてみます。
ポケモンではすべての技(move)を使いきった場合、攻撃を選択しても「わるあがき」しかできなくなります。これはすべてのポケモン共通の挙動なので、Protocol Extensionで実装すると楽ですね。
protocol Pokemon { associatedtype PokemonType func attack(move:PokemonType) func struggle() } extension Pokemon { func struggle() { // 相手にダメージを与えて自分のHPを1/4削る } }
「わるあがき」は技タイプなしの特殊攻撃なので(ゴーストタイプにも当たる)、PokemonType
には依存しないものとして実装されるとします。
残念ながら、現世代までのポケモンで「わるあがき」で特殊な挙動をする個体はいないみたいですが、将来的に特殊な実装をされる可能性を考慮すると、適切に実装を退避させる必要があります。
class AnyPokemon <PokemonType>: Pokemon { private let _attack: ((PokemonType) -> Void) private let _struggle: (() -> Void) required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) { _attack = pokemon.attack _struggle = pokemon.struggle } func attack(type:PokemonType) { return _attack(type) } // 省略した場合はデフォルト実装が呼ばれてしまう! func struggle() { return _struggle() } }
省略するとデフォルト実装になってしまう、というのがポイントです。
Any
タイプを作る開発者は、Protocol Extensionによるデフォルト実装も含めた、そのプロトコルの全ての処理を知る必要がある のです。
AnySequence
を実現している、_SequenceBox
の場合どうなってるのかというと、必ずしも全ての処理が委譲されるわけではないらしく、たとえばdropFirst(_ n: Int)
は委譲されるのだけれど、dropFirst()
(引数なし版)は委譲されません。
dropFirst()
を変な処理でオーバーライドすると壊れます。やる人いないと思いますけど。
Protocol ExtensionでAny
パターンを壊す
SequenceType
にProtocol Extensionでメソッドを足してみます。
extension SequenceType { func hogehoge () { print("do") } } let sequence = AnySequence(["a", "b"]) sequence.hogehoge() // do
AnySequence
もSequenceType
に準拠しているので、Protocol Extensionの恩恵を受けます。ここまでは期待通りです。
ただし、独自のSequenceType
を作って、Protocol Extensionで追加したものと同名のメソッドを実装すると破綻します。
class MySequence : SequenceType { private var values: [String] = ["a", "b"] func generate() -> IndexingGenerator<[String]>{ return values.generate() } func hogehoge() { print("inherit") } } let original = MySequence() let boxed = AnySequence(original) original.hogehoge() // inherit boxed.hogehoge() // do original.dropFirst().hogehoge() // do
これは_SequenceBox
の実装を考えれば自明ですね。dropFirst()
でもデフォルト実装が呼び出されてしまうのは、dropFirst()
がAny
でラップする実装だからですね。
// 何も考えずに、Github上で master branch を見ていたので、 // Swift 2.2じゃなくて3.0のコードですが、大差ないので気にしない @warn_unused_result public func dropFirst() -> SubSequence { return dropFirst(1) } @warn_unused_result public func dropFirst(_ n: Int) -> AnySequence<Iterator.Element> { precondition(n >= 0, "Can't drop a negative number of elements from a sequence") if n == 0 { return AnySequence(self) } return AnySequence(_DropFirstSequence(_iterator: makeIterator(), limit: n)) }
結論
CollectionType
なんかの組み込み型に対してProtocol Extensionする場合、Any
で型消去されたときに、Protocol Extensionのデフォルト実装しか呼び出されない点について注意が必要?
これは言語仕様的に防御した方がいいような気がする…何のメリットもないし。
AnySequenceってなんなの?
AnySequence
ってなんなの?っていうのを調べようとしたメモです。
JavaのGenericsは本当に型情報を消し去るイレイジャなのでまず意識しなくていいし、C#(4.0以上)のGenericsは共変性・反変性を持っているので、Swiftだとなんでややこしいコードになるのかよくわからなかったのです。
まず、SequenceType
プロトコルに準拠した独自のクラスを作ることを考えます。
// 本 struct Book { var name: String } // 本棚 final class BookShelf: SequenceType { private var books: [Book] = [] func append(book: Book) { self.books.append(book) } // Swift 3.0 だと AnyIterator とかいうストレートな名前になるはず func generate() -> AnyGenerator<Book> { var index : Int = 0 return AnyGenerator<Book> { // index++ を Swift 3.0 準拠で書くとこうなる、微妙… defer { index = index.successor() } return index < self.books.count ? self.books[index] : .None } } }
SequenceType
に準拠することで、自作のクラスでもfor-in
構文を利用できます。
let bookShelf = BookShelf() bookShelf.append(Book(name: "ハイペリオン")) bookShelf.append(Book(name: "ハイペリオンの没落")) bookShelf.append(Book(name: "エンディミオン")) bookShelf.append(Book(name: "エンディミオンの覚醒")) for book in bookShelf { print(book.name) }
ここまでは簡単です。
問題は、「BookShelf
という実装詳細を隠したい場合はどうすればいいか?」と考えたときです。SequenceType
プロトコルとして扱うという素直なアプローチは使えません。
let sequence: SequenceType = bookShelf
以下のコンパイルエラーになります。
error: protocol 'SequenceType' can only be used as a generic constraint because it has Self or associated type requirements
これがなぜかというと、SequenceType
の定義を見ると分かるように、
public protocol SequenceType { associatedtype Generator : GeneratorType associatedtype SubSequence // 略
SequenceType
は付属型(associatedtype
になって意味がわかりやすくなりましたね!)を持っているためです。Swiftの仕様上、付属型を持つプロトコルはgenericの制約としてしか使えません。
GeneratorType
とSubSequence
を具体化したクラスが必要です。そこで、AnySequence
の出番です。
let sequence: AnySequence<Book> = AnySequence(bookShelf) for book in sequence { print(book.name) }
AnySequence
でラップすることで、BookShelf
の実装詳細を隠蔽することができました。
ただし同じElement
の型でまとめるのが限界です。AnySequence<AnyObject>
にAnySequence<Book>
を突っ込むみたいな融通は効かない(共変性とか反変性みたいなのはない)です。
ポケモン型消去
この辺の説明をポケモンを使ってすっごいわかりやすく説明したのが、ポケモン型消去の話です。
以下、Sequence
よりもポケモンの方が分かりやすそうなので、ポケモンで考えます。
この説明ですが、初見ではAnyPokemon
がattack
の実装をイニシャライザで退避させている理由がわかりませんでした。
class AnyPokemon <PokemonType>: Pokemon { private let _attack: ((PokemonType) -> Void) required init<U:Pokemon where U.PokemonType == PokemonType>(_ pokemon: U) { _attack = pokemon.attack } func attack(type:PokemonType) { return _attack(type) } }
これは実際に手を動かせば答えはすぐに出ました。PokemonType
はAnyPokemon
に渡すポケモンの型に束縛される―たとえばイニシャライザにAnyPokemon<Electric>
のピカチュウを渡して初めて、attack
の型がattack(type :Electric) -> ()
に決定される―からです。
先の説明の通り、付随型を持つプロトコルのままポケモンを保持することはできないので、束縛された後の実装を保持するようにしているのです。でもプロトコルのすべての実装を、イニシャライザで退避させるようなAny
クラスを作るのって正直エレガントだと思えません。
そこで、AnySequence
がどうやってSequenceType
の各処理を移譲しているのか調べようと考えたのです。
AnySequenceは言語仕様内で型安全と型消去を両立している
下調べしたとき、Swift 2.1のバグにちょうどそんなのがあったのでちょろいと思ったのですよ。
AnySequence
のイニシャライザでは、元のシーケンスをさらに_SequenceBox
にラップして処理を移譲していたのですが、S.Generator.Element
とS.SubSequence.Generator.Element
との型が別になるような、変なGenerator
を実装した場合に、移譲がうまくいかないのでイニシャライザに制約を加えたとのこと。
だから、_SequenceBox
の実装を探して終わりだと思ったんですけど、これは自動生成されているボイラーコードなので単純には見つかりませんでした。
自動生成されているコードを、ポケモンの例に置き換えるとたぶんこんな感じになるのだと思います。
繰り返しますがSwiftでは付随型を持つPokemon
プロトコルを型として持てません。
そこでまず付随型をジェネリクスにした_AnyPokemonBase
という抽象クラスを作り、型として保持できるようにします。もちろん、Swiftの言語仕様に抽象クラスがあるわけではないので、空実装を提供した「抽象クラスっぽい何か」です。
そしてその_AnyPokemonBase
を継承した_AnyPokemonBox
に実際のポケモンをラップします。実際のPokemon
プロトコルの処理はここから移譲します。
2段階ラップして、抽象クラス経由で操作することで、型消去したい型を保持する(日本語が迷走してる)、というようなことを実現しています。Pokemon
プロトコルに機能が増えた場合にはボイラーコードを書くことになることには変わりありませんし(StdLibはこの辺は自動化している)、素直にabstract
なクラスが言語仕様にあった方が幸せな気がするのですが、仕組みとしてはうまいと思いました。
AnySequence
の場合、内部にAnyGenerator
を持っているのでより複雑ですが、ほぼ同じ構造です。きっと。
なお、以下の記事を大変参考にしました。
ほぼ真似と言っても過言ではありません。こんなの自力で辿り着けないです…。
いまさら Android 6.0 の Permissions at Run Time について
ここに書いてある内容が正しいという保証が一切持てないので、けっして鵜呑みにしないでください。
とりあえず公式ドキュメントのRequesting Permissions at Run Timeをざっくり読みました。
ランタイムパーミッションの概要
Andorid 6.0(API level23)以降では、ユーザーはインストール時ではなく、アプリの実行中にパーミッションを付与します。
このアプローチにより、ユーザーはアプリのインストールやアップデート時に権限を付与する必要がなくなったため、インストールプロセスがスリムになります。
それだけでなくユーザーはアプリの機能単位で、より詳細な制御を行うことができます。
例えば、カメラアプリが「カメラ」と「位置情報」の機能を要求する場合、「カメラ」へのアクセスを許可し、「位置情報」には権限を与えないことができます。
ユーザーはアプリの設定画面へ行くことで、いつでも権限の取り消しを行うことができます。
今までのパーミッションシステムには以下のような問題がありました。
- 今まではインストール・アップデート時に必要な権限が長々と並んで、ユーザーが許諾しないとアプリを入れられない仕組みだったけれど、インストール時点で「何のためにその権限を要求しているのか」ということは分かりづらかった。
- 説明がユーザーにとっては直感的ではない上に、ネタみたいなのもあった。
android.permission.BRICK
(あなたの端末を文鎮化する可能性があります)とか。 - すべてのパーミッション要求を許諾しないとインストールできない仕組みだったため、アプリのコア機能に不要な付加的なパーミッションも許諾する必要があった。
- 開発者でもないとその権限で何ができるのかは分からないので、危険なパーミッションがそれと知らずに紛れ込んでいる可能性があった。
ただし全ての問題が解決したわけではないですし、実装する側にとっては面倒きわまりない話です。これはそんな話です。
パーミッションのON/OFFができる粒度
パーミッションには膨大な数がありますが、それぞれの一つ一つについて、ON/OFFの制御をすることは できません 。
上記の記事が非常に参考になるのですが、パーミッショングループの単位でのみ設定が可能です。
パーミッショングループ
パーミッショングループについて補足しておくと、これはもともとパーミッションをカテゴライズしておくことで、インストール時に整理された状態で表示する機能でした。
古のインストール確認画面はこんな感じで、「現在地」や「ネットワーク通信」のように分かりやすく分類し、優先度の高いものが上に並ぶように並べ替え、そして本当に危険なパーミッションを「その他」に追いやって分かりにくくするための機能でした。
パーミッショングループ単位での指定の問題
「より詳細な制御」はできない
例えば「カレンダー」というグループ単位でONにすると、「カレンダーデータの読み込み(android.permission.READ_CALENDAR
)」と「カレンダーデータへの書き込み(android.permission.WRITE_CALENDAR
)」の両方を許諾したことになります。
アプリから「電話」を掛けられるようにONをすると、android.permission.READ_PHONE_STATE
が許諾されます。「電話の状態の読み取り」という牧歌的な名前に反して、電話番号から端末の一意識別子まで抜ける超危険なパーミッションのひとつです。
もちろん、マニフェストに宣言されていないパーミッションまでは取得できません。要求する権限は最小限にしましょう。
また、ユーザー視点としては、Permissions at Run Timeがあっても安心せずに、アプリが要求するパーミッションを見定める必要があります。
許諾状態は常に変わる可能性がある
このパーミッショングループの分類について、将来的に変更される可能性があります。
例えば「カレンダーの読み込み」と「書き込み」が分離されたとして、その場合は一度「カレンダー」としてパーミッションの許諾を得ていたとしても、再度パーミッションを取得する処理を入れる必要があります。
また、ユーザーは任意のタイミングで許諾の取り消しを行うことができます。このため、常に最新の許諾状態を取得する必要があります。
例えばカーナビアプリが定期的にバックグラウンドで位置情報を取得している場合、設定から位置情報の許諾を取り消された場合ってどうすればいいんでしょう?
フォアグラウンドのみのアプリならonResume()
での確認でよいのではと思うのですが、Android Nのマルチウインドウで設定を変えられたら?
許諾状態は常に変わる可能性を踏まえて、パーミッションの確認、要求ダイアログの表示などを自前で実装する必要があります。
パーミッショングループと危険性には相関がない
もともとAndroidのパーミッションにはProtectionLevel
という概念があり、個々のパーミッションについてnormal
やdangerous
などの格付けがありました。
この部分は正直確証がないのですが、Android 6.0ではこの仕組みを投げ捨てていているように思います。
Normal Permission
とDangerous Permission
に大分し、前者はユーザーの許諾を必要としないパーミッション、後者は必要とする(=パーミッショングループに定義されている)パーミッションというように再構築されているような…。
その結果、パーミッショングループに含まれない、dangerous
なパーミッションは現状では許可を取る必要なかったり、整合性を取るためにandroid.permission.INTERNET
がdangerous
からnormal
に格下されたりと不穏な感じがします。
どう実装すればいいのか
ここからが本題です。
Android Nが出る時代に、targetSDKを22にしたときの話をしてもしょうがないので、targetSDKを23以上にして開発するものとします。
また、requestPermissions()
を投げて、onRequestPermissionsResult()
で結果を得る、という基本的な部分は理解しているものとします。この部分についてはいくらでも情報があるので、特に困ることはないでしょう。
パーミッションを得ているかどうか知る方法
checkSelfPermission()
これは、PackageManager
のcheckPermission(String permName, String pkgName)
のシンタックスシュガーです。
checkPermission()
メソッドはAPI Lv1からあり、指定したパッケージのアプリがパーミッションを宣言しているかどうかを知ることができました。
Android 6.0ではこの意味合いが変わり、「パーミッションを宣言しており、かつユーザーに許可されているかどうか」に変わりました。
checkSelfPermission()
ではパッケージ名の指定が不要で、アプリ自身のパーミッションの取得状態を知ることができます。
ただし、checkSelfPermission()
はAPI Lv23以降にしかありません。そこで、互換ライブラリのContextCompat.checkSelfPermission()
を使うことができます。
実装は単純で、単に自分のパッケージ名を明示的に指定して、checkPermission()
を呼び出しているだけです。
このAPIを使うことで、
を知ることができます。
ただしまだrequestPermissions()
を呼んでいない場合には、PERMISSION_DENIED
が返ってきます。(targetSDK23以上の場合。22以下ではインストール時点で自動的にパーミッションの承諾が行われるので、ユーザーが明示的に設定を変えない限りはPERMISSION_GRANTED
が返って来る)
PermissionChecker
サポートライブラリには、PermissionChecker
というクラスもあり、checkSelfPermission()
など同名のメソッドが用意されています。
これが何のために存在するのかというと、Android 4.3のAppOpsという隠し機能 のためです。
ご存知の方はみんな知っての通り、Android 6.0で追加されたPermissions at Run Timeの原型とも言える仕組みは、Android 4.3の時点から隠し機能として搭載されていました。
恐らくですが、
- targetSdkを23未満でビルドしたアプリを、Android 6.0で動作させて、パーミッションを剥奪した場合
- このときはPermissions at Run Timeではなく、AppOpsで動作している?
- Android 6.0未満の端末で、App Opsを用いて、パーミッションを剥奪した場合
こうした状況では、PermissionChecker
がより適切です。App Opsによって剥奪されたパーミッションは、通常のrequestPermissions()
ではPERMISSION_GRANTED
が返却されますが、PermissionChecker
ではAppOpsManager
に問い合わせて、PERMISSION_DENIED_APP_OP
という結果を返してくれます。
ContextCompat.checkSelfPermission()
とPermissionChecker.checkSelfPermission()
のどちらを使うべきなのでしょうか。PermissionChecker
を推されている記事もありますが、公式ドキュメントではContextCompat
を使うことを推奨しています。
個人的には、今後はtargetSdk 23以降が基本となり、App Opsに対応しなければならない状況はほとんどなくなると思いますので、PermissionChecker
の存在は忘れてしまっても支障はないと思います。
パーミッションのリクエストを得る方法
requestPermissions()
例によって、以下の二つのメソッドがあります。
Activity#requestPermissions()
ActiivtyCompat#requestPermissions()
引数は取得したいパーミッションの配列(Manifest.permission
以下のString
定数)と、リクエストコードです。
オリジナルのrequestPermissions()
メソッド、およびコールバックとなるonRequestPermissionsResult()
は当然ながら、API level23以降のActivity
にしかありません。
ActiivtyCompat#requestPermissions()
は内部でバージョン判定を行ってくれるのので便利なのですが、両者の仕組みはちょっとだけ違います。
ほとんどの場合、AppCompatActivity
やv4パッケージの偽Fragment
を使っていると思うのですが、ActiivtyCompat#requestPermissions()
でリクエストした場合、FragmentActivity
が実装しているActivityCompat.OnRequestPermissionsResultCallback
インターフェースで結果が処理されます。
このとき、コールバック先がFragment
またはActivity
かの振り分けを16bitのシフト演算で行っています。これはFragmentActivity#startActivityForResult()
の原理をご存知でしたらお馴染みですね。ActiivtyCompat
版を使う場合には、リクエストコードの値が16bitを超えないようにする必要があります。
ActiivtyCompat
版では、 Android 6.0未満の端末でrequestPermissions()
を呼んだ場合、即座にonRequestPermissionsResult()
コールバックメソッドが呼び出される という違いがあります。
つまりAndroid 6.0未満がターゲットに入っていても、パーミッションリクエストが存在するものとして書いてしまえば、同じ処理で書けるのでは?
www.slideshare.net
PERMISSION_GRANTED
のパーミッションに対して、requestPermissions()
を行った場合に、何度でもリクエストのダイアログが表示されるという仕様らしく、この方法はダメみたいですね…。
バーミッションが拒絶された場合
Activity#shouldShowRequestPermissionRationale()
AppCompat#shouldShowRequestPermissionRationale()
もう2つある意味の説明は不要ですよね?
さて、一番良く分かんないメソッドがこれです。メソッドの意味としては「リクエストの根拠を示すべきか?」です。
とりあえず原文にあるサンプルコードを訳してみます。
// 以下、thisActivityが現在のActivityであるとする if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { // パーミッションを必要とする説明を表示する必要があるか? if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)) { // ユーザーへの説明を*非同期*で表示 ―決してブロックしてはいけない― // このスレッドはユーザーの応答を待っています! // ユーザーが説明を見た後、再度許可の要求を行ってください // 註)原文中のexpanationはexplanationのtypoとみなしました } else { // 何の説明も必要がありません、パーミッションの要求をすることができます ActivityCompat.requestPermissions(thisActivity, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS はアプリが定義したint型の定数です // コールバックメソッドは要求の結果を得ることができます。 } }
まずパーミッションのチェックを行って、PERMISSION_DENIED
の場合に使うものみたいですね。
このメソッドについて理解するには、パーミッションダイアログの初回表示と2回目以降表示、そして「今後表示しない」が選択されたときの挙動を理解する必要があります。
分かりやすく表にしました
最初からこの表を用意すれば8000文字も書かなくて良かった気がしますね。
メソッド | Android 6.0未満 | 初回表示 | 2回目以降表示 | 今後表示しない |
---|---|---|---|---|
checkSelfPermission | GRANTED | DENIED | 前回選択に順ずる | DENIED |
requestPermissions | GRANTED | パーミッション許可 ダイアログ表示 |
パーミッション許可 ダイアログ表示 (「今後表示しない」付き) |
DENIED |
shouldShowRequest PermissionRationale |
false | false | true | false |
※全部AppCompat
でやっているものとします。
初回の問い合わせにおいて、checkSelfPermission()
はPERMISSION_DENIED
をshouldShowRequestPermissionRationale()
はfalse
を返します。そのままrequestPermissions()
を投げましょう。
一度リクエストが拒絶されたかどうかは、shouldShowRequestPermissionRationale()
を呼ぶことで知ることができます。
そこで、2度目のrequestPermissions()
の前に、なぜパーミッションを必要とするのか、その根拠を説明することができます。この処理は別途ダイアログを表示するなど、非同期で行う必要があります。もしユーザーが心変わりした場合だけ、requestPermissions()
を投げましょう。
「今後表示しない」が選択された場合、もはや弁解の余地はないのでshouldShowRequestPermissionRationale()
はfalse
を返しますし、requestPermissions()
を投げても即断でPERMISSION_DENIED
が返されます。
どうしても必要なパーミッションを拒絶された場合
もし「今後表示しない」が選ばれたパーミッションを、必ず要求するアプリの場合、どうすればいいのでしょうか?
アプリ設定を変更して貰うようなダイアログを表示するフローを用意するのが現実的です。しかしこれは「二度と表示するな」というユーザーの意思を尊重したものとはいえません。
shouldShowRequestPermissionRationale()
がtrue
を返したときに、「なぜこのアプリがパーミッションを要求するのか」という論理的な説明を行い、ユーザーに納得して貰うというのが最良の体験になるのではないでしょうか。
「今後表示しない」が選択されたことを知る方法
onRequestPermissionsResult()
コールバック内で、結果がPERMISSION_DENIED
かつ、shouldShowRequestPermissionRationale()
がfalse
を返した場合、「今後表示しない」が選択されたと判定することができます。
つまり、 必ずrequestPermissions()
を呼ぶ必要があります 。もし既に「今後表示しない」が選択されている場合に、そのパーミッションが必要なボタンをdisabled
にしておいたり、Snackbar
でさりげなく通知するといった気を利かせる方法はありません。
個人的にはこの仕様は、Permissions at Run Time の欠陥だと思っています。公式ドキュメントのサンプルコードだとこのケースについて何も考えてないので注意です。
ちなみにiOSの世界では
iOSでは、マイクや位置情報を利用するAPIの初回に利用許可を求めるダイアログをOSが自動的に表示します。
アプリケーションを再インストールしない限り、再度表示されることはありません。設定から変更して貰う必要があります。
requestPermissions()
のタイミングに注意しつつ、shouldShowRequestPermissionRationale()
は2回目以降ではtrue
を返す性質を利用すれば簡単にiOSと同じ挙動にできて便利ですね!
まとめ(rev2)
Permissions at Run Time は調べれば調べるほど闇が深い…。