iOSでアプリがバックグラウンドへ遷移してもタスクが終了するまではアプリのプロセスをkillさせないようにする
AndroidではAndroid Oからバックグラウンド処理の実行制限が厳しくなることが話題ですが、iOSでは以前から基本的にはバックグラウンド処理はアプリがバックグラウンドに回った時点で停止させられます。
実際にはiOS側の判断で止めるので、タスクがすぐにkillされる状況は少なくともiOS10の段階ではなかなか開発段階の状況では再現できなかったりします*1。しかし、少なくとも実装時はバックグラウンドに回ったタスクはすぐkillされても困らない前提で書く必要があります。
そして、音楽アプリでの再生処理や地図アプリ用の位置情報の取得、ファイルのダウンロードなどバックグラウンドになってもしばらくの間動き続けることが保証されていてほしいタスクはよくあります。
この場合の対処としてできることは3種類紹介されており、それぞれ概要としては、
- Foregroundで短いタスクを開始する場合は、アプリがバックグラウンドへ遷移するときにそのタスクが終了するまでの時間を要求することができる
- Foregroundでダウンロード処理を開始する場合は、ダウンロード中にアプリが停止ないし終了してもよいようにそのダウンロード処理の管理をシステム側へ手渡すことができる
- 特定の種類のタスク(音楽再生など)を支援するためにバックグラウンドで実行する必要がある場合は、その支援タスクに1つ以上のバックグラウンドでの実行モードを宣言することができる
となっています。
今回は、1. のケースについての実装についてメモします。
実装概要
beginBackgroundTaskWithExpirationHandler:
を使ってiOSのSystemへバックグラウンドのタスクを実行するための追加の実行時間をリクエストする。引数のブロックには追加の実行時間が終わっても処理が終了していなかった場合の後処理の内容を記述します。最低限書く必要がある内容は、後の項目の 3. に記載するiOSのSystemに対するタスクの終了通知です。- の際に、何のタスク用、あるいはコードのどこでリクエストしたのかの識別するための値として
UIBackgroundIdentifier
の値を受け取ります。
- の際に、何のタスク用、あるいはコードのどこでリクエストしたのかの識別するための値として
- GCDのキューなどにバックグラウンドで実行したいタスクを渡して実行します。タスクの最後で、 2. の値を引数に
endBackgroundTask:
を実行して、iOSのSystemに当該タスクは終了したため、そのために実行時間を取らなくていいことを伝えます。
サンプルコード
// https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html より UIBackgroundIdentifier *bgTask; - (void)applicationDidEnterBackground:(UIApplication *)application { bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ // リクエストした実行時間が切れてしまい、途中終了する場合に呼ばれる後処理を書く [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // バックグラウンドに入っても継続して実行したい処理を書く [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }); }
endBackgroundTask:
はUIスレッドからもバックグラウンドスレッドからも呼び出されていますが、ドキュメントによれば問題ありません。
最初、beginBackgroundTaskWithName:expirationHandler:
ないしbeginBackgroundTaskWithExpirationHandler:
は、公式やQiitaなどにあるサンプルコードの表面だけ見ると applicationDidEnterBackground:
メソッドなど、Backgroundに遷移する前のタイミングで一回呼び出せばいいようなバックグラウンドに入る前に一度呼び出すもののように見えました。
しかし、beginBackgroundTaskWithExpirationHandler:
によれば、Backgroundに遷移したら中断されたらユーザー体験に影響があるようなタスクのたびにこのメソッドを呼ぶべきだしアプリの実行中のどこで呼ぶこともできる、と書いてあるので、必要なタイミングで begin~
と end~
が対応するように呼んだほうがわかりやすいかもしれません。
なお、実行中のBackground処理の残り時間が必要な場合は backgroundTimeRemaining
プロパティにて確認可能です。
現場からは以上です。
参考
- [Objective-C] バックグラウンドで継続して処理を実行する
- Background Execution
- UIBackgroundTaskIdentifier
- beginBackgroundTaskWithExpirationHandler:
- backgroundTimeRemaining
*1:特に、実行時に他のアプリが動いていないシミュレータなどでは(苦笑)。
java.util.TimerをAndroidで使ってみた場合
以前気になったので素振りした時のメモ。AndroidのバージョンはAPI 21です。
まとめ
Timer.schedule(TimerTask task, long delay, long interval)
はdelay
秒後から、前回のタスクと今回のタスクの実行開始時間の間隔がなるべくintervalになるように定期的にtaskを実行する- Timerのコンストラクタで新しいスレッドが動き出し、定期的に実行されるタイマーのタスクは全てそのスレッドで実行される
Activity.onStop
が呼び出されてアプリがバックグラウンドへ隠れてもTimerの処理は止まらない
参考:
- http://techbooster.jpn.org/andriod/application/934/
- https://docs.oracle.com/javase/jp/6/api/java/util/Timer.html
簡単にコード書いてログで確認してみた
package com.example.woshidan.timertest; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.Timer; import java.util.TimerTask; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Timer timer = new Timer(); final Random random = new Random(); final List<Integer> list = new ArrayList<>(); final long startTime = System.currentTimeMillis(); timer.schedule(new TimerTask() { @Override public void run() { Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " start "); int wait = random.nextInt(1000); try { Thread.sleep(wait + 1000); // アップロード処理の終了の代わりに適当にウェイト置いてる } catch (InterruptedException e) { e.printStackTrace(); } Log.d(Thread.currentThread().getName(), (System.currentTimeMillis() - startTime) + " ms. " + "n = " + list.size() + " end "); list.add(list.size()); } }, 500, 3000); } @Override protected void onStop() { super.onStop(); Log.d("MainActivity", "onStop"); } @Override protected void onDestroy() { super.onDestroy(); Log.d("MainActivity", "onDestroy"); } }
// 定期的にリクエストされるタイマーのタスクは全て同じスレッドで実行される D/Timer-0: 500 ms. n = 0 start D/Timer-0: 2218 ms. n = 0 end D/Timer-0: 3500 ms. n = 1 start // 前回と今回の間のstartの間隔はなるべく3秒間隔 D/Timer-0: 4896 ms. n = 1 end D/Timer-0: 6500 ms. n = 2 start D/Timer-0: 7733 ms. n = 2 end D/Timer-0: 9500 ms. n = 3 start D/Timer-0: 11096 ms. n = 3 end D/MainActivity: onStop // バックグラウンドになっても止まらない D/Timer-0: 12500 ms. n = 4 start D/Timer-0: 14368 ms. n = 4 end D/Timer-0: 15530 ms. n = 5 start // 遅れることもある(前回分の実行に時間がかかったなど) D/Timer-0: 17289 ms. n = 5 end D/Timer-0: 18568 ms. n = 6 start // そうするとその次で「前回と今回の間のstartの間隔はなるべく3秒間隔」を守るために、次以降も時間がずれたままになる D/Timer-0: 19608 ms. n = 6 end D/Timer-0: 21608 ms. n = 7 start D/Timer-0: 23448 ms. n = 7 end D/Timer-0: 24645 ms. n = 8 start D/Timer-0: 26188 ms. n = 8 end D/Timer-0: 27678 ms. n = 9 start D/Timer-0: 29216 ms. n = 9 end D/Timer-0: 30718 ms. n = 10 start D/Timer-0: 32033 ms. n = 10 end D/Timer-0: 33744 ms. n = 11 start D/Timer-0: 34993 ms. n = 11 end
コードでボタンのStateによって色を変えるようにする
前回の続き的なノリで、コードでボタンのStateによって色を変えるように指定するコードを書いてみます。
API21以上の場合
API21以上の場合は、以下のように記述できます。
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setCornerRadius(roundRadius); int[][] states = new int[][] { new int[] {-android.R.attr.state_pressed}, // not pressed new int[] { android.R.attr.state_pressed} // pressed }; int [] colors = new int[] { Color.RED, Color.GREEN }; // API21以上対応 gradientDrawable.setColor(new ColorStateList(states, colors)); if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(gradientDrawable); } else { button.setBackgroundDrawable(gradientDrawable); }
API20以下も対応する場合
これで先ほどと同じ振る舞いを確保しようとした場合は下記のようになります。
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawableNotPressed = new GradientDrawable(); gradientDrawableNotPressed.setCornerRadius(roundRadius); gradientDrawableNotPressed.setColor(Color.RED); GradientDrawable gradientDrawablePressed = new GradientDrawable(); gradientDrawablePressed.setCornerRadius(roundRadius); gradientDrawablePressed.setColor(Color.GREEN); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[] {-android.R.attr.state_pressed}, gradientDrawableNotPressed); // not pressed stateListDrawable.addState(new int[] {android.R.attr.state_pressed}, gradientDrawablePressed); // pressed if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(stateListDrawable); } else { button.setBackgroundDrawable(stateListDrawable); }
現場からは以上です。
参考
ShapeDrawableのRoundRectShapeとGradientDrawableをそれぞれ使って角丸ボタン作ってみた
ShapeDrawableのRoundRectShape、あるいは、GradientDrawableを使うとコードからフチの余白や色を指定できる形で角丸四角形の背景を持つボタンが作ることができます。
もともとShapeDrawable
で書いていたのですが、こちらの記事でShapeDrawable
のリソースをGradientDrawable
にコンパイルされていることを知り、GradientDrawable
で書いてみたらスッキリ記述できたので書き直した、という話です。
どちらにせよ、リソースファイルから読み込んだほうが基本的に高速なので、あまり表示頻度が多くない箇所をカスタマイズして表示したいとか、そういった場合に使えば良いと思います。
ShapeDrawableのRoundRectShapeで作る場合
同じ指定をxmlで記述した場合
今回はxmlで記述した場合は下記のようになる指定をコードで実装することにします。
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <padding android:left="@dimen/button_padding_horizontal" /> <padding android:top="@dimen/button_padding_vertical" /> <padding android:right="@dimen/button_padding_horizontal" /> <padding android:bottom="@dimen/button_padding_vertical" /> <!-- 上記の指定はButton要素に下記属性を追加するのと同じ結果となる。 android:paddingLeft="@dimen/button_padding_horizontal" android:paddingTop="@dimen/button_padding_vertical" android:paddingRight="@dimen/button_padding_horizontal" android:paddingBottom="@dimen/button_padding_vertical" --> <!-- デフォルトのボタンではButton要素は paddingを設定しなくてもButton要素に余白を持っているが、それは android.R.drawable.btn_default_normal.xmlなどで paddingが指定されているため --> <solid android:color="#f00"/> <corners android:radius="@dimen/button_round_radius"/> </shape>
RoundRectShapeを利用してコードで記述した場合
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); ShapeDrawable shape = new ShapeDrawable(); shape.setShape(new RoundRectShape(new float[] {roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius, roundRadius}, null, // 背景のうち、真ん中の方をくりぬいた形にすることができるのですが、その領域の大きさをRectFで指定可能. 利用しない(=一色塗りつぶしにする)場合はnull null)); // 背景のうち、くりぬいた部分の長方形の角丸指定. 利用しない(=くりぬいた部分は長方形のまま)場合はnull // 余白の設定 int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_horizontal); int verticalPadding = getResources().getDimensionPixelSize(R.dimen.button_padding_vertical); shape.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); shape.getPaint().setColor(Color.RED); if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(shape); } else { button.setBackgroundDrawable(shape); }
<!-- レイアウト --> <Button android:id="@+id/button_bg_resource" android:layout_below="@id/some_element" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/test" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" /> <Button android:id="@+id/button_bg_code" android:layout_below="@id/button_bg_resource" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" />
<!-- dimenリソースの設定 --> <resources> <dimen name="button_round_radius">8dp</dimen> <!-- この辺の元ネタはこちら https://github.com/android/platform_frameworks_base/blob/c29fff50322599f53feadf9cf87df9956c9ac44e/core/res/res/values/dimens_material.xml#L109-L116 --> <dimen name="button_padding_horizontal">8dp</dimen> <dimen name="button_padding_vertical">4dp</dimen> </resources>
見た目はこんな感じです。上がdrawableリソースで背景を指定したボタンで下がコードで背景を指定したボタンになります。
GradientDrawableで作る場合
コードでの記述
Button button = (Button) findViewById(R.id.button_bg_code); // 角丸の設定 int roundRadius = getResources().getDimensionPixelSize(R.dimen.button_round_radius); GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setCornerRadius(roundRadius); gradientDrawable.setColor(Color.RED); // 余白の設定はButton要素の属性でやる if (BuildConfig.VERSION_CODE >= 16) { button.setBackground(gradientDrawable); } else { button.setBackgroundDrawable(gradientDrawable); }
<!-- レイアウト --> <Button android:id="@+id/button_bg_resource" android:layout_below="@id/some_element" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/test" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" /> <Button android:id="@+id/button_bg_code" android:layout_below="@id/button_bg_resource" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingLeft="@dimen/button_padding_horizontal" <!-- GradientDrawableの場合はpaddingを設定するメソッドはないので、 View側にpaddingの指定を置く --> android:paddingTop="@dimen/button_padding_vertical" android:paddingRight="@dimen/button_padding_horizontal" android:paddingBottom="@dimen/button_padding_vertical" android:layout_marginTop="8dp" <!-- 見やすくするためつけてる --> android:text="TestTestTestTest" />
見た目は変わってません。
参考
- https://developer.android.com/reference/android/graphics/drawable/ShapeDrawable.html
- http://qiita.com/guchio/items/bebc13db0d2c0ae03363
- https://developer.android.com/guide/topics/resources/drawable-resource.html#Shape
- https://developer.android.com/reference/android/graphics/drawable/GradientDrawable.html
AndroidのNDKやABIについてのメモ
はじめに
C系の言語で動いているクロスプラットフォームのブリッジを作成するには、そのコンパイルなどの過程でNDKに関する知識があるとトラブルシューティングやテストケースの設計に役立つことがあります。
なので、ここではその概要をまとめておきます。はじめに用語からまとめようと思ったのですが、
https://developer.android.com/ndk/guides/index.html?hl=ja
Native Development Kit(NDK)は、Android アプリで C および C++ コードを使用できるようにするツールのセットのことです。NDK を使用して独自のソースコードをビルドしたり、事前にビルドされた既存のライブラリを活用したりできます。
https://developer.android.com/ndk/guides/concepts.html?hl=ja
アプリケーション バイナリ インターフェース(ABI)はアプリのマシンコードが実行時にどのようにシステムと連携するかを正確に定義したものです。 NDK はこれらの定義に対して
.so
ファイルをビルドします。
と言われてもよくわからなかったので、NDKがABIを使ってC, C++のソースコードから.so
ファイルをビルドする周辺で何をしているかを説明します。
.soファイルとは
と書いたものの、書いた本人は.soファイルについての事前知識が調査開始時点でなかったため、簡単に説明します。
so
はshared object file
の略です。
.so
ファイルは、UNIX系の環境下での共通ライブラリファイルのことで、Windowsでいうdllにあたります。具体的には、複数の実行ファイル間で共有して利用される処理の定義がまとめられています。
中身は実行形式ファイルですが、あくまで処理を含んでいるだけでエントリポイントがなく単体で起動することはできません。
各々の実行ファイルに含まれず、実行ファイルを起動した時に動的にロードとリンクが行われるため、動的(ダイナミック)リンクライブラリと呼ばれることがあり、Android Developers GuideのNDKについての記述では主に動的リンクライブラリ、と呼称されています。
Androidでは、アプリのコンパイル時にリンクされている方が.a
(静的リンクライブラリ), アプリの起動時にロードされてリンクされる方が.so
で、NDKを使ってABIに従って作成される方が基本的に.so
という感じです。
自分が作成したライブラリが.so
か、.a
になるかはコンパイルの設定によりますが、Androidアプリのビルドの文脈だと、.so
ファイルの方しか、公式のドキュメントに記載がなかったので、こちらが多いかもしれません。
NDKを利用してビルドする際の流れ
https://developer.android.com/ndk/guides/concepts.html のフローをもとに適宜用語の説明を入れながら説明します。
- Androidプロジェクトを作成します
- Androidプロジェクト直下にjniディレクトリを作成し、ネイティブライブラリ(.aファイル, .soファイルなど.アーキテクチャごとにフォルダを用意したり…)とコンパイル対象のソースコード(CやC言語で書かれたソースコード)とそれらをコンパイルするモジュールにどう含むか記述していくAndroid.mkを配置
- 2のディレクトリに任意でターゲットABI, ツールチェーン, リリース/デバッグモード, STLを設定するApplication.mkファイルを作成(任意)
- デフォルト設定
- ABI: armeabi
- ツールチェーン: GCC 4.8(4.8 32bit / 4.9 64bit端末対応らしい)
- モード: リリース
- リリースモードだと変数宣言が最適化されていたりして、C言語側のステップ実行が困難になったりします
- STL: system
- NDKツールを適切に利用するために作成されたシェルのラッパーであるndk-buildを使用して、Cのソースコードをネイティブ(.so, .a. 主に.so?)ライブラリへコンパイルしたり、静的ライブラリをリンクしたり
- Javaのソースコードをビルドして、DVMで実行可能な.dexファイルを生成
- アプリの実行に必要な .so, .dex、その他ファイルをすべてapkファイルにパッケージ化
ところで
.soファイルやネイティブライブラリと.dexを一緒に処理していませんが、JNI用のヘッダとそれに対応した規則のメソッド名で作成していけば、JVM(DVM)上でメソッドテーブルが作成されると理解していて、それにしたがってJava<=>C,C++間でメソッドを利用し合うことが可能みたいです。
この辺を調べていると、CMakeといった言葉もでてきますが、ひとまずこの記事を書くにあたっての状況に限っては(Andorid Studioでビルドをしない場合は)関係なかったので省きます。
CMakeについては、また今度機会あればまとめます。
参考
https://developer.android.com/ndk/guides/concepts.html http://blog.katty.in/4347 http://www.peliphilo.net/archives/681 http://qiita.com/m1takahashi/items/3a3c9d2845e9b57aeda3 http://morado106.blog106.fc2.com/blog-entry-80.html
COUNT関数の引数にNULLが入る場合について
SQLで行数を数える時に利用する COUNT
関数で一番使い慣れているのは COUNT(*)
なのですが、 COUNT(col)
や、COUNT(1かNULLになる式)
といった形を見かけたのでメモ。
COUNT(*)
は行数をカウントするCOUNT(col)
はcolの値がNULLの行はカウントしない- resfs: http://nippondanji.blogspot.jp/2010/03/innodbcount.html?m=1
- フラグが立っているかどうかでカウントする条件を分ける、みたいなことをしたい場合はわかりやすさのために
COUNT(1かNULLになる式)
といった形で書くのかも
見かけた時は面白いなと思ったのに短い。。
iOSのProvisioning Profile周りについてざっくり確認するシートを作りました
iOSのProvisioning Profile周りについて毎回ハマっているような気がしていたので、どのファイルをいつ用意して用意した後どうすればいいのか、といったことを手短に確認するために
- 主になんて呼ばれているか
- 代表的なファイル名
- 主にいつ作成するか、どう作成するか
- ファイルをDLするか、DLした後どうするか
- XCode上でそのファイルに関連してどう設定するか
- そのほか備考
をまとめて、作成する際に必要、など関連のあるところの線を引いた図を作ったので貼ります。
上の図に千鳥足みたいなのありますが、一つのファイルに対して他方のファイルが複数存在するといった場合は複数存在する方が雑に鳥足みたいにしました。 なんかそういうツールや様式があった気がしますが、それは次の機会ということで。
参考
https://i-app-tec.com/ios/provisioning-profile.html http://macdays.hatenablog.com/entry/2013/10/11/172532 http://qiita.com/edo_m18/items/6f10e57f95b25d9dab4e http://qiita.com/fujisan3/items/d037e3c40a0acc46f618
注釈
※1 Apple Developer Centerに登録した1つのチームから複数の開発者を招待して、その開発者はそれ以外のチームに所属していないという想定。個人開発している人もいるのでいつも事実では無いですが会社アドレスのアカウントでは事実なこと多いのでは
※2 証明書周りがよくわからなくなってむやみやたらに再発行され各種ファイルの作り直しに励むのはよくあるので助けてください
※3 1つのアプリについてProductionとSand Boxというつもり。
気になるところあったらツッコミください。