アプリのAndroid 11対応で行ったこと

okhttpのバージョンを上げた

上げないとsetSocketFactoryできない
https://gist.github.com/tateisu/51d20ef6aa6c813fab5635f33880bdfb

ToastCompatを書き直した

Android 11でToastにカスタムビューを設定できる場所が制限され、またカスタムビューを設定しない限り Toast.getView() はnullを返す。
APIレベル25にはアプリのコードと関係ないところでToastがクラッシュするバグがあり、それを回避する ToastCompatというライブラリがある。
Toast.getView() が null を返す場合でもうまく動作するようToastCompatを書き直した。
投げたissueに代替コードも載せてある。
github.com

AndroidManifest.xmlに queries セクションを書いた

targetSdkVersion 30 以降で、PackageManager の resolveActivity() 、 queryIntentActivities()、 getInstalledApplications() 、 getInstalledPackages() などの他のパッケージを探索するAPIを使う際は AndroidManifest.xml に対象を明記しないといけない。

developer.android.com

例。 利用できるブラウザを確認する、Chrome Custom Tabs の有無を確認する、テキストを共有できるアプリの一覧を調べる、読み上げの利用、Simejiのマッシュルームプラグインの存在確認、など。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ... >
    <!--suppress AndroidElementNotAllowed -->
    <queries>

        <intent>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.BROWSABLE" />
        </intent>

        <intent>
            <action android:name="android.support.customtabs.action.CustomTabsService" />
        </intent>

        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="text/plain" />
        </intent>

        <intent>
            <action android:name="android.intent.action.TTS_SERVICE" />
        </intent>

        <intent>
            <action android:name="com.adamrocker.android.simeji.ACTION_INTERCEPT" />
            <category android:name="com.adamrocker.android.simeji.REPLACE"/>
        </intent>
    </queries>

    <application ... >
実行時権限の確認/取得でエラーがでた場合のメッセージを変更した

(targetSdkVersionに関わらず)Android 11は暫く使ってないアプリの実行時権限を削除する。
また実行時権限のリクエストをユーザが拒否したりすると権限ダイアログを表示しなくなる。
実行時権限のリクエストが拒否された際に、端末の設定画面から権限を与えるようにユーザを促すべき。
なおアプリの設定画面を開くインテントは中華系端末だと権限の設定ができない画面に飛ばされるので、今回はそういった誘導はしていない。

JobScheduler.schedule()が失敗したらlogcatに出力させた

(targetSdkVersionに関わらず) Android 11は JobScheduler.schedule() が呼び出された時にレートリミットを超えると実際には何もしないようになった。
アプリのマニフェストに debuggable 属性がセットされている場合、JobScheduler.schedule() はRESULT_FAILURE を返す。

WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS と FLAG_TRANSLUCENT_NAVIGATION

ステータスバーとナビゲーションバーを不透明にしたい場合、Android 11以降の端末では以下の呼び出しが不要になる。

if(Build.VERSION.SDK_INT < 30) {
  @Suppress("DEPRECATION")
  clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR と View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR

ステータスバーとナビゲーションバーのアイコン色を調整したい場合、端末のバージョンによって処理を変える。

activity.window?.apply {
	statusBarColor = c1 or Color.BLACK
	if(Build.VERSION.SDK_INT >= 30) {
		decorView.windowInsetsController?.run {
			val bit = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
			setSystemBarsAppearance(if(rgbToLab(c).first >= 50f) bit else 0, bit)
		}
	} else if(Build.VERSION.SDK_INT >= 23) {
		@Suppress("DEPRECATION")
		val bit = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
		@Suppress("DEPRECATION")
		decorView.systemUiVisibility =
			if(rgbToLab(c).first >= 50f) {
				//Dark Text to show up on your light status bar
				decorView.systemUiVisibility or bit
			} else {
				//Light Text to show up on your dark status bar
				decorView.systemUiVisibility and bit.inv()
			}
	}
	
	navigationBarColor = c2 or Color.BLACK
	if(Build.VERSION.SDK_INT >= 30) {
		decorView.windowInsetsController?.run {
			val bit = WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
			setSystemBarsAppearance(if(rgbToLab(c).first >= 50f) bit else 0, bit)
		}
	} else if(Build.VERSION.SDK_INT >= 26) {
		@Suppress("DEPRECATION")
		val bit = View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
		@Suppress("DEPRECATION")
		decorView.systemUiVisibility =
			if(rgbToLab(c).first >= 50f) {
				//Dark Text to show up on your light status bar
				decorView.systemUiVisibility or bit
			} else {
				//Light Text to show up on your dark status bar
				decorView.systemUiVisibility and bit.inv()
			}
	}
}



アプリによって対応が必要な範囲は異なると思うけど、とりあえず今回はここまで。

おまけ

developer.android.com

COVID-19関連の考慮事項により、Android 11(APIレベル30)以上を対象とし、MANAGE_EXTERNAL_STORAGE 許可を必要とするアプリは、2021年の初めまでGoogle Playにアップロードできません。

ファイル管理をがっちりやるアプリは MANAGE_EXTERNAL_STORAGE 権限が必要なんで、これは困るやつ。コロナを理由にすれば何でも通ると思ってるなGoogleさん…

Kotlin の apply{…} の速度

次のような単純なコードがあったとする。

// Something like HashMap<String,String>
class Holder {
    @Volatile private var value :String = ""

    operator fun set(@Suppress("UNUSED_PARAMETER") key:String, value:String){
       this.value = value
    }
}

class PrefKey(private val key: String) {
    fun put(holder: Holder, value: String) {
        holder[key] = value
    }
}

fun Holder.putA(pref: PrefKey, value: String): Holder {
    pref.put(this, value)
    return this
}

fun Holder.putB(pref: PrefKey, value: String): Holder =
    apply {
        pref.put(this, value)
    }

さてputAとputB、速いのはどちらだろうか?

バイトコード

IntelliJ IDEAで Search Everywhere から「Show Kotlin Bytecode」を開いてバイトコードを確認すると次のようになる。

putA

// access flags 0x19
public final static putA(Lbench/Holder;Lbench/PrefKey;Ljava/lang/String;)Lbench/Holder;
    @Lorg/jetbrains/annotations/NotNull;() // invisible
    // annotable parameter count: 3 (visible)
    // annotable parameter count: 3 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 2
L0
    ALOAD 0
    LDC "$this$putA"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 1
    LDC "pref"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 2
    LDC "value"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
    LINENUMBER 16 L1
    ALOAD 1
    ALOAD 0
    ALOAD 2
    INVOKEVIRTUAL bench/PrefKey.put (Lbench/Holder;Ljava/lang/String;)V
L2
    LINENUMBER 17 L2
    ALOAD 0
    ARETURN
L3
    LOCALVARIABLE $this$putA Lbench/Holder; L0 L3 0
    LOCALVARIABLE pref Lbench/PrefKey; L0 L3 1
    LOCALVARIABLE value Ljava/lang/String; L0 L3 2
    MAXSTACK = 3
    MAXLOCALS = 3
putB
// access flags 0x19
public final static putB(Lbench/Holder;Lbench/PrefKey;Ljava/lang/String;)Lbench/Holder;
    @Lorg/jetbrains/annotations/NotNull;() // invisible
    // annotable parameter count: 3 (visible)
    // annotable parameter count: 3 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 1
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 2
L0
    ALOAD 0
    LDC "$this$putB"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 1
    LDC "pref"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 2
    LDC "value"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
L1
    LINENUMBER 21 L1
    ALOAD 0
    ASTORE 3
L2
    ICONST_0
    ISTORE 4
L3
    ICONST_0
    ISTORE 5
L4
    ALOAD 3
    ASTORE 6
L5
    ICONST_0
    ISTORE 7
L6
    LINENUMBER 22 L6
    ALOAD 1
    ALOAD 6
    ALOAD 2
    INVOKEVIRTUAL bench/PrefKey.put (Lbench/Holder;Ljava/lang/String;)V
L7
    LINENUMBER 23 L7
L8
    NOP
L9
    LINENUMBER 21 L9
L10
    ALOAD 3
L11
    LINENUMBER 23 L11
    ARETURN
L12
    LOCALVARIABLE $this$apply Lbench/Holder; L5 L8 6
    LOCALVARIABLE $i$a$-apply-Test1Kt$putB$1 I L6 L8 7
    LOCALVARIABLE $this$putB Lbench/Holder; L0 L12 0
    LOCALVARIABLE pref Lbench/PrefKey; L0 L12 1
    LOCALVARIABLE value Ljava/lang/String; L0 L12 2
    MAXSTACK = 3
    MAXLOCALS = 8

コンパイル

putA
@NotNull
public static final Holder putA(@NotNull Holder $this$putA, @NotNull PrefKey pref, @NotNull String value) {
      Intrinsics.checkParameterIsNotNull($this$putA, "$this$putA");
      Intrinsics.checkParameterIsNotNull(pref, "pref");
      Intrinsics.checkParameterIsNotNull(value, "value");
      pref.put($this$putA, value);
      return $this$putA;
}
putB
@NotNull
public static final Holder putB(@NotNull Holder $this$putB, @NotNull PrefKey pref, @NotNull String value) {
      Intrinsics.checkParameterIsNotNull($this$putB, "$this$putB");
      Intrinsics.checkParameterIsNotNull(pref, "pref");
      Intrinsics.checkParameterIsNotNull(value, "value");
      boolean var4 = false;
      boolean var5 = false;
      int var7 = false;
      pref.put($this$putB, value);
      return $this$putB;
}

putBの方が無駄なローカル変数と無駄な代入、そしてなぜかNOPが含まれている。これはinline lambdaのオーバーヘッドという奴だろう。

間違ったベンチマーク

https://gist.github.com/tateisu/be6cdaee0a4e28186090fb3834899788 のようなコードで比較してみたら3倍の差がでて驚いたが、これは間違った測定だった。JVMではいくつかの理由で、ベンチマーク用のライブラリを使わないとコードの速度を正しく評価できない。たとえば呼び出し回数が一定以上になったメソッドに対してJITが働くのでウォームアップが必要になる。理由は他にもあるかもしれない。

JMHを使った計測

JMH https://openjdk.java.net/projects/code-tools/jmh/ で計測した結果がこちら。

6:58:09: Executing task 'jmh --stacktrace'...

> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin NO-SOURCE
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :compileJmhKotlin UP-TO-DATE
> Task :compileJmhJava NO-SOURCE
> Task :processJmhResources NO-SOURCE
> Task :jmhClasses UP-TO-DATE
> Task :jmhRunBytecodeGenerator UP-TO-DATE
> Task :jmhCompileGeneratedClasses UP-TO-DATE
> Task :jmhJar UP-TO-DATE
# Warmup Iteration   1: 197644398.040 ops/s
# Warmup Iteration   2: 
> Task :jmh
# JMH version: 1.22
# VM version: JDK 1.8.0_121, Java HotSpot(TM) 64-Bit Server VM, 25.121-b13
# VM invoker: C:\Java\jdk-x64-1.8\jre\bin\java.exe
# VM options: <none>
# Warmup: 15 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: bench.Test1.usePutA

# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1

198603045.551 ops/s
# Warmup Iteration   3: 198549682.052 ops/s
# Warmup Iteration   4: 194557723.411 ops/s
# Warmup Iteration   5: 198470775.716 ops/s
# Warmup Iteration   6: 196711343.247 ops/s
# Warmup Iteration   7: 198641168.014 ops/s
# Warmup Iteration   8: 199108411.601 ops/s
# Warmup Iteration   9: 198914004.326 ops/s
# Warmup Iteration  10: 198531682.261 ops/s
# Warmup Iteration  11: 198777787.476 ops/s
# Warmup Iteration  12: 198825547.011 ops/s
# Warmup Iteration  13: 197627450.659 ops/s
# Warmup Iteration  14: 195803620.165 ops/s
# Warmup Iteration  15: 198606598.039 ops/s
Iteration   1: 196452099.108 ops/s
Iteration   2: 198454517.763 ops/s
Iteration   3: 198752988.922 ops/s
Iteration   4: 199905451.165 ops/s
Iteration   5: 199674003.652 ops/s

> Task :jmh


Result "bench.Test1.usePutA":
  198647812.122 �}(99.9%) 5274224.588 ops/s [Average]
  (min, avg, max) = (196452099.108, 198647812.122, 199905451.165), stdev = 1369699.377
  CI (99.9%): [193373587.534, 203922036.710] (assumes normal distribution)


# JMH version: 1.22
# VM version: JDK 1.8.0_121, Java HotSpot(TM) 64-Bit Server VM, 25.121-b13
# VM invoker: C:\Java\jdk-x64-1.8\jre\bin\java.exe
# VM options: <none>
# Warmup: 15 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: bench.Test1.usePutB

# Run progress: 50.00% complete, ETA 00:00:20
# Fork: 1 of 1

# Warmup Iteration   1: 198942094.378 ops/s
# Warmup Iteration   2: 194860962.483 ops/s
# Warmup Iteration   3: 199178184.960 ops/s
# Warmup Iteration   4: 195436293.151 ops/s
# Warmup Iteration   5: 199607202.705 ops/s
# Warmup Iteration   6: 197020019.891 ops/s
# Warmup Iteration   7: 198460528.526 ops/s
# Warmup Iteration   8: 198985887.154 ops/s
# Warmup Iteration   9: 199731334.489 ops/s
# Warmup Iteration  10: 199309376.472 ops/s
# Warmup Iteration  11: 200102008.778 ops/s
# Warmup Iteration  12: 199003797.389 ops/s
# Warmup Iteration  13: 199735482.462 ops/s
# Warmup Iteration  14: 195999469.589 ops/s
# Warmup Iteration  15: 199425721.275 ops/s
Iteration   1: 198251071.528 ops/s
Iteration   2: 198490905.407 ops/s
Iteration   3: 199717039.707 ops/s
Iteration   4: 199911875.985 ops/s
Iteration   5: 199829906.354 ops/s

> Task :jmh


Result "bench.Test1.usePutB":
  199240159.796 �}(99.9%) 3084180.600 ops/s [Average]
  (min, avg, max) = (198251071.528, 199240159.796, 199911875.985), stdev = 800951.908
  CI (99.9%): [196155979.196, 202324340.396] (assumes normal distribution)


# Run complete. Total time: 00:00:40

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark       Mode  Cnt          Score         Error  Units
Test1.usePutA  thrpt    5  198647812.122 �} 5274224.588  ops/s
Test1.usePutB  thrpt    5  199240159.796 �} 3084180.600  ops/s

Benchmark result is saved to C:\kotlin\BenchmarkInlineFunction\build\reports\jmh\results.txt

BUILD SUCCESSFUL in 40s
5 actionable tasks: 1 executed, 4 up-to-date
6:58:50: Task execution finished 'jmh --stacktrace'.

なぜかputBの方が速い。ただし差は0.3%程度で、ほぼ無視して構わない量だ。

なお文字化けしてるのは"±"らしい。

おまけ。 melix/jmh-gradle-plugin を使ってベンチマークをとる

melix/jmh-gradle-plugin https://github.com/melix/jmh-gradle-plugin を使うとGradle から比較的簡単にJMHを利用できる。

jmh-gradle-plugin 0.5.0 は Gradle 5.5 を要求する。IntelliJ IDEA のプロジェクトのgradle/wrapper/gradle-wrapper.properties ファイルを編集する。

(略)
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip
(略)

次にbuild.gradleを編集する。 kotlinx.coroutines のベンチマーク https://github.com/Kotlin/kotlinx.coroutines/tree/master/benchmarks が参考になるだろう。

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.3.41'
    id "me.champeau.gradle.jmh" version "0.5.0"
}

group 'jp.juggler.BenchmarkInlineFunction'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

compileKotlin {
    kotlinOptions {
        jvmTarget = "1.8"

        // https://discuss.kotlinlang.org/t/run-time-null-checks-and-performance/2086/17
        freeCompilerArgs = [
                '-Xno-param-assertions',
                '-Xno-call-assertions',
                '-Xno-receiver-assertions',
                '-Xjvm-default=enable'
        ]
    }
}
compileTestKotlin {
    kotlinOptions {
        jvmTarget = "1.8"

        // https://discuss.kotlinlang.org/t/run-time-null-checks-and-performance/2086/17
        freeCompilerArgs +=[
                '-Xno-param-assertions',
                '-Xno-call-assertions',
                '-Xno-receiver-assertions'
        ]
    }
}

compileJmhKotlin {
    kotlinOptions {
        jvmTarget = "1.8"
        freeCompilerArgs += [
                '-Xjvm-default=enable',
                '-Xno-param-assertions',
                '-Xno-call-assertions',
                '-Xno-receiver-assertions'
        ]
    }
}

jmh {
    jmhVersion = '1.22'
    failOnError = true

    duplicateClassesStrategy DuplicatesStrategy.INCLUDE
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
    implementation "org.openjdk.jmh:jmh-core:1.22"

    jmh 'org.apache.commons:commons-lang3:3.6'
    jmh 'com.google.guava:guava:22.0'
}

ソースコードは src/jmh/java 以下に置く。また、パッケージ指定なしだJMHがエラーを出すので必ず何かしらのパッケージ名が必要になる。

src/jmh/java/bench/Test1.kt

package bench

import org.openjdk.jmh.annotations.*

class Holder {
    @Volatile private var value :String = ""

    operator fun set(@Suppress("UNUSED_PARAMETER") key:String, value:String){
       this.value = value
    }
}

class PrefKey(private val key: String) {
    fun put(holder: Holder, value: String) {
        holder[key] = value
    }
}

fun Holder.putA(pref: PrefKey, value: String): Holder {
    pref.put(this, value)
    return this
}

fun Holder.putB(pref: PrefKey, value: String): Holder =
    apply {
        pref.put(this, value)
    }

// Benchmark classes should not be final. // [jmh.bench.Test1]

@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 15, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
@Suppress("unused")
open class Test1 {

    private var holder= Holder()
    private var pref= PrefKey("foo")

    @Benchmark
    fun usePutA(){
        holder.putA(pref, "zap")
    }

    @Benchmark
    fun usePutB(){
        holder.putB(pref, "zap")
    }
}

IntelliJ IDEA のGradleペインから Tasks/jmh/jmh を実行するとベンチマークが行われる。

ただしWindows環境だと build/libs/に生成されるjarファイルをjava.exeプロセスが握ったままなので、再試行する度にIntelliJ IDEA を終了してjarファイルを削除しなければならない。でないと Error reading benchmark list や MANIFEST中にbenchmarkListがないなどのエラーを出す。

Debian 9のisc-dhcp-serverがNot configured to listen on any interfaces!というエラーを出す

自宅でDHCP鯖に使ってるRaspbianをjessie(8) から stretch(9)に上げたらisc-dhcp-server がエラーを出すようになった。/var/log/syslog から適当に抜粋するとこんな感じ。

Launching both IPv4 and IPv6 servers (please configure INTERFACES in /etc/default/isc-dhcp-server if you only want one
or the other).
No subnet declaration for eth0 (no IPv4 addresses).
No subnet6 declaration for wlan0 (****::****:****:****:****).
No subnet6 declaration for eth0 (no IPv6 addresses).
Not configured to listen on any interfaces!

/etc/init.d/isc-dhcp-server を眺めると $INTERFACES と $INTERFACESv4 と $INTERFACESv6 を参照していて、それぞれの定義の有無でdhcpdの起動を切り替えているらしい。ログをよく見るとIPv4用とIPv6用の2つのサーバプロセスを起動しようとしていた。

対応

(1) /etc/default/isc-dhcp-server に以下を追加。

INTERFACESv4="wlan0"

(2) 既存のdhcpdをkillしてpidファイルを削除。

(3) サービス起動。

service isc-dhcp-server start

okhttpのCacheControl.Builder.maxStale()

okhttpのCacheControl.Builder.maxStale()の挙動を勘違いしてた。

たとえばこんなコードを書いたとする。

val CACHE_CONTROL = CacheControl.Builder()
	.maxAge(5, TimeUnit.MINUTES)
	.maxStale( Integer.MAX_VALUE, TimeUnit.SECONDS)
	.build()

val call = okhttpClient.newCall(
	Request.Builder()
	.cacheControl(CACHE_CONTROL)
	.url(url)
)
続きを読む

Toastの再利用

Androidで、サービス等のUIを持たないコンポーネントから画面にテキストを表示するToastというAPIがある。

割と昔からあるものなので挙動も同じだろうと思っていたら、某社の端末(未発売)でトラブルがあった。
その端末のバグっぽい印象だったので割と邪道な対応で済ませたが、それについてはあまり詳しく書けない。

ついでに手持ちの他の端末でも簡単なテストを行ってみた。

  • トースト を表示して4秒経過したらトーストをキャンセルして、トーストへの参照をnullで上書きする。
  • トースト を前回表示してから4秒未満で再度トーストを表示した時に、以下のいずれかの処理を行う。
    • キャンセルした後に再利用(setTextして再度show)
    • キャンセルせずに再利用(setTextして再度show)
    • キャンセルした後に新しくmakeTextしてshow
    • キャンセルせずに新しくmakeTextしてshow

f:id:tateisu:20181109234422j:plain

結果

キャンセルして再利用
    • 4.4.2~9.0 でうまくいかない。文章は更新されるが、最初のトーストが表示されてから4秒で消えてしまう。
キャンセルせずに再利用
    • 4.4.2の端末ではOK。最後に文章を更新してshowしてから4秒でトーストが消える。
    • 7.1.1の端末ではNG。文章は更新されるが、最初のトーストが表示されてから4秒で消えてしまう。
    • 8.0.0の端末ではNG。文章は更新される(+トーストの表示がちらつく)が、最初のトーストが表示されてから4秒で消えてしまう。
    • 9.0.0の端末ではNG。文章は更新される(+トーストの表示がちらつく)が、連打するとすぐにトーストが消えてしまう。
キャンセルした後に新しくmakeTextしてshow
    • 特に問題は見られなかった
キャンセルせずに新しくmakeTextしてshow
    • 端末によってはトーストがキューに溜まり遅延が発生する

コード例

package jp.juggler.toastsample

import android.os.Bundle
import android.os.Handler
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.Toast

class MainActivity : AppCompatActivity() {
	
	companion object {
		private const val LOGTAG = "ToastSample"
	}
	
	lateinit var handler : Handler
	
	lateinit var btnCancel : View
	
	private var nCount = 0
	
	private var lastToast : Toast? = null
	
	override fun onCreate(savedInstanceState : Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)
		handler = Handler()
		
		btnCancel = findViewById<View>(R.id.btnCancel)
		btnCancel.setOnClickListener { toastRemover.run() }
		btnCancel.isEnabled = false
		
		
		findViewById<View>(R.id.btnShow1).setOnClickListener { showToast(1) }
		findViewById<View>(R.id.btnShow2).setOnClickListener { showToast(2) }
		findViewById<View>(R.id.btnShow3).setOnClickListener { showToast(3) }
		findViewById<View>(R.id.btnShow4).setOnClickListener { showToast(4) }
	}
	
	private val toastRemover : Runnable = object : Runnable {
		override fun run() {
			handler.removeCallbacks(this)
			lastToast?.cancel()
			lastToast = null
			btnCancel.isEnabled = false
		}
	}
	
	private fun showToast(type : Int) {
		try {
			val gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
			val text = "Toast ${++ nCount}"
			val duration = Toast.LENGTH_LONG
			var toast = lastToast
			if(toast != null) {
				
				when(type) {
					1 -> {
						// キャンセルして再利用
						toast.cancel()
						toast.setText(text)
						toast.duration = duration
					}
					
					2 -> {
						// キャンセルせずに再利用
						toast.setText(text)
						toast.duration = duration
					}
					
					3 -> {
						// キャンセルして新しく作成
						toast.cancel()
						toast = Toast.makeText(this, text, duration)
						lastToast = toast
					}
					
					4 -> {
						// キャンセルせず新しく作成
						toast = Toast.makeText(this, text, duration)
						lastToast = toast
					}
				}
				toast?.setGravity(gravity, 0, 0)
				toast?.show()
			} else {
				toast = Toast.makeText(this, text, duration)
				lastToast = toast
				toast.setGravity(gravity, 0, 0)
				toast.show()
			}
			handler.removeCallbacks(toastRemover)
			handler.postDelayed(toastRemover, 4000L)
			btnCancel.isEnabled = true
		} catch(ex : Throwable) {
			Log.e(LOGTAG, "showToast failed.", ex)
		}
	}
}

ToastCompat

API level 25 以上で BadTokenException が出る不具合は ToastCompat https://github.com/drakeet/ToastCompat を使うと改善する。

KotlinのCoroutineScopeのメモ

やっとexperimental が外れたのでボチボチ使っていきたい。

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.mdhttp://kotlinlang.org/docs/reference/coroutines/exception-handling.html を読みながら理解しようとしたが、サンプルを見ても「なぜこういう挙動になるのか」が分からないのでソースコードを読んでみた。

コルーチンとは何か

コルーチンは停止(yield)と再開(resume)が可能な実行単位である。スレッドと異なり停止中はリソースを占有しないように考慮されているし、Dispatcherが抽象化されているので、必要ならAndroidのメインスレッド上でコルーチンを動かすこともできる。

意外なことに、Coroutine そのものが名づけられた クラスやインタフェースは 存在しない。存在するのは Continuation インタフェースと AbstractCoroutine抽象クラス(後述)である。

コルーチンの開始。CoroutineStart.invoke() の内部から startCoroutine が呼ばれ、組み込み関数の createCoroutineなんたら() により 「サスペンド可能なラムダ式」が「Continuationインスタンス」に変わり、 拡張関数 resumeなんたら(Unit) が呼ばれてコルーチンの処理が開始する。Continuation がもし DispatchedContinuation なら resume 時にDispatcherを利用する。

コルーチンがどのスレッドで実行されるかはDispatcherによって異なる。 runBlocking{} は呼び出し元スレッドでイベントループを回す。 launch{},async{}はcontextに指定されたDispatcherがなければ Dispatchers.Default を使う。Dispatchersには数種類のDispatcherが定義されている。

Continuation の context プロパティについて。 JVM実装では createCoroutineFromSuspendFunction()
https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/coroutines/jvm/src/kotlin/coroutines/intrinsics/IntrinsicsJvm.kt#L155 によると completion 引数のcontext が使われるらしい。

CoroutineContext

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/index.html

コルーチン(Continuation)は実行に必要な情報をCoroutineContextに保持する。CoroutineContextは複数の属性を持つ。Job や Dispatcher などが設定される。

  • 属性のキーは CoroutineContext.Key である。参照アドレスでの比較が行われる。
  • 属性の値は CoroutineContext.Element である。ElementそのものもCoroutineContextである。
  • キーの定数はJobやContinuationInterceptorのコンパニオンオブジェクトが使われる
  • EventLoopBase は CoroutineDispatcher である。 CoroutineDispatcher は ContinuationInterceptor である。
  • CoroutineContext は合成できる。左辺の属性の一部が右辺の属性で上書きされる。
  • 派生クラス CombinedContextなど

kotlin.coroutines パッケージに coroutineContext という suspend property が定義されている。
coroutine か suspend function からのみ suspend property にアクセスできる。
suspend fun の内部から現在のCoroutineContextを取得することができる。

Job

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/

Jobはコルーチンの実行状態を示す。キャンセルや完了待機などの状態を持つ。

JobはCoroutineContextの一部として扱われるため、Job のベースクラスは CoroutineContext.Element と CoroutineContext である。Job どうしを合成することは可能だが、意味がないので合成結果は右辺そのものとなる。

Jobオブジェクトを扱う機会

  • キャンセルのための親ジョブを作る時
  • launch{} や async{} の戻り値

Jobオブジェクトへの操作

  • join(), joinAll(), cancel(), cancelAndJoin()

Jobには親子関係がある。
コード中のコメントから引用

coroutineContext内のJobインスタンスは、コルーチン自体を表します。
ジョブは親ジョブを持つことができます。
親を持つジョブは、その親が取り消されるとキャンセルされます。
親ジョブは すべての子プロセスが完了するまで_completing_ または _cancelling_ 状態で待機します。
_completing_状態は、ジョブの純粋な内部状態であることに注意してください。
_completing_ ジョブは外部から観察したらまだ _active_ なように見えますが、内部的には子たちのために待機しています。

ジョブの失敗と通常のキャンセルはキャンセル例外のcauseの種別により区別されます。
キャンセル例外のcauseが[CancellationException]の場合、ジョブ通常のキャンセルされたと解釈されます。
追加パラメータなしでキャンセルが呼び出された時にこれは発生します。
もしキャンセル例外のcauseがその他の例外だった場合、ジョブは失敗したと解釈されます。
ジョブのコードで問題が起きて例外が投げられた時にこれは発生します。

Jobインタフェースと派生した全てのインタフェースの関数はスレッドセーフであり
外部の同期なしで並行するコルーチンから安全に呼び出すことができます。

キャンセル時の例外ハンドリングではcauseも表示しておくべきか。

Job() ヘルパ関数は JobImpl(parent=null) を作成する。JobImpl はJobSupportである。
JobSupport クラスには cancelsParent プロパティがあり、キャンセル発生時に親をキャンセルするかどうかを決定する。
JobImpl cancelsParent=true, onCancelComplete=true, handlesException=false のプロパティがJobSupportと異なる。

コード中のコメントから引用

private fun cancelParent(cause: Throwable): Boolean {
// (causeが)CancellationExceptionなのは "通常"と解釈され、子供がそれを生成した場合は親はキャンセルされない。
// これにより、子がクラッシュしてその完了時に他の例外が生成されない限り、親は自身をキャンセルせずに子を(通常の)キャンセルすることができます。
...
}

Structured concurrency https://medium.com/@elizarov/structured-concurrency-722d765aa952 の正体は、Jobの親子関係と JobSupport の cancelsParent プロパティだった。

CoroutineScope

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/

CoroutineScope は通常、ラムダ式へのレシーバ変数として提供される。

  • coroutineContextプロパティへのアクセス
  • 拡張関数 actor, async, broadcast, launch, newCoroutineContext, plus, produce, promise
  • 派生クラス GlobalScope, ActorScope, ProducerScope
  • CoroutineScope + CoroutineContext でスコープを合成できる

スコープの絶縁

runBlocking,coroutineScope,launch,async等のスコープ作成関数で作成したスコープ(ジョブ)は、その内部で作成した子スコープ(ジョブ)が全て終了するまでは終了しない。これはこれで便利なのだが、ライフサイクル的には親子のスコープ(ジョブ)を絶縁したい場合がある。

間違った例 coroutineScope{}

coroutineScope{} はコード外側のコンテキストからJob以外を継承してJobを新しくしたスコープを作成してblockを実行する。内部のJobのキャンセルがcoroutineScopeよりも外側に影響しないので絶縁できたかのように思えるが、他のスコープ作成関数と同様、coroutineScope{}自体は内部で作成した子ジョブが全て終了するまでブロックしてしまう。

val producer = coroutineScope{ produce<Int>{  (1..3).forEach { channel.send(it) } } }

この例は外側のスコープとproducerを分離しようとしているが、coroutineScope{} は produceのコードブロックが完了するまでブロックする。この時点ではproducer変数からデータを読む処理は開始しないので、ここで永遠にブロックしてしまう。

正しい例1 GlobalScope

GlobalScopeはEmptyCoroutineContext を持つグローバル定数。JobもDispatcherも割り当てられていない。
CoroutineScope(context) と同様、親スコープと子ジョブを分離する際に使われる。

val producer = GlobalScope.produce<Int>{ (1..3).forEach { channel.send(it) } } 

この例ではGlobalScopeを親にすることで、コードの外側のスコープとproducerのスコープを完全に分離する。
実際には必要に応じてproduceの引数にDispatcherやJob等のコンテキストを追加する。

正しい例2 CoroutineScope(context) ヘルパ関数 と SupervisorJob()

https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/exception-handling.md#supervision

val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
	val producer = produce<Int>{ (1..3).forEach { channel.send(it) } }
}

CoroutineScope() はコードの外側のスコープの影響を受けない。
また、もし引数のコンテキストにJobが含まれなければ空のジョブを追加する。

スコープを絶縁した後の注意

スコープの絶縁はコルーチンをリークさせる可能性がある。上の例ではproducer 変数のチャネルはcapacity=0 で作成されるため、チャネルからデータが読み取られるまでsend() の部分でブロックする。作成したコルーチンのライフサイクルをプログラマが管理する必要がある。

AbstractCoroutine

https://github.com/Kotlin/kotlinx.coroutines/blob/master/common/kotlinx-coroutines-core-common/src/AbstractCoroutine.kt

@InternalCoroutinesApi なのでユーザがこのクラスを直接利用することはないが書いておく。

runBlocking などのレシーバ変数(is CoroutineScope) は実際には BlockingCoroutine 等の、AbstractCoroutine の派生クラスである。

AbstractCoroutine はJobSupportであり、Jobであり、Continuationであり、CoroutineScope である。
1つのクラスに複数の役割が持たせられている理由はおそらくオブジェクト確保を減らすためだろう。
Continuation 以外にコルーチン個別の実行制御に必要な情報は全て AbstractCoroutine に入っている訳だ。

AbstractCoroutine作成時の引数 parentContext の指定は runBlocking() 等ではデフォルト値 EmptyCoroutineContext となっている。AbstractCoroutineのJobSupportには cancelsParent=true が設定されているので、もし parentContextが存在すればブロック内部で発生したキャンセルは親に伝達される。

asyncとlaunch

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html

launchはコルーチンを開始してJobを返す。
asyncはコルーチンを開始してDeferredを返す。DeferredはJobであるが戻り値の扱いが多少異なる。

asyncとlaunchはどちらも newCoroutineContext() を使って外側のコンテキストを引き継ぎ、指定がなければディスパッチャをDispatchers.Defaultにする。親Jobがあればキャンセル発生時にキャンセルの伝達が起きる。
newCoroutineContext() は外側のコンテキストと引数(省略時はEmptyCoroutineContext)のコンテキストを合成し、ディスパッチャの指定がなければDispatchers.Defaultを補う。

asyncとlaunch はそっくりだが少しだけ異なる。

  • launch は StandaloneCoroutine か LazyStandaloneCoroutine を生成、startしてそれを返す
  • async は DeferredCoroutine かLazyDeferredCoroutine を生成、startしてそれを返す

そして StandaloneCoroutine と DeferredCoroutine もそっくりだ。

  • どちらもAbstractCoroutineである。cancelsParent=trueに設定される。
  • DeferredCoroutine はDeferred, SelectClause1 である。await() と onAwait と registerSelectClause1() が用意される。

CoroutineExceptionHandler

launch で非同期処理を行う際に CoroutineExceptionHandler というCoroutineContextを追加することで例外を補足できる。
しかし実際に試してみると Structured Concurrency と相性が悪く、親スコープが例外を処理してしまうとそこで例外が再送出されてしまう。
直接の親のジョブをSupervisorJobにするだけでは足りない。 GlobalScope、親ジョブのないCoroutineScope(SupervisorJob)、SupervidorScopeなどを使う。

val handler = CoroutineExceptionHandler { _, exception ->
    println("handler caught $exception")
    println("thread ${Thread.currentThread().name}")
}

val supervisor = SupervisorJob()

// NG
// coroutineScope {
//    launch(handler) {
//        error("inside launch1")
//    }
//    delay(1000)
// }

// NG
// runBlocking(supervisor) {
//    launch(handler) {
//        error("inside launch2")
//    }
//    delay(1000)
// }

// OK
GlobalScope.launch(handler) {
    error("inside launch3")
}
delay(1000)

// OK
with(CoroutineScope(supervisor)){
    launch(handler) { error("inside launch4") }
    delay(1000)
}

supervisorScope{
    launch(handler) { error("inside launch5") }
    delay(1000)
}

Docker構成のMastodonに組み込んだpgbouncerの監視をmuninに追加する

「Docker構成のMastodonに後からpgBouncerを組み込む http://d.hatena.ne.jp/tateisu/20170418/1492457904 」の続きです。

https://github.com/munin-monitoring/contrib/blob/master/plugins/postgresql/pgbouncer_ を使ってみたというただそれだけの記事です。

pgbouncer への接続に必要な情報を確認する

まず mastodonの.env.productionに指定ているDB接続情報を確認します

DB_HOST=db
DB_USER=bouncer
DB_NAME=postgres
DB_PASS=XXXXXXXXXXXXXXXXX
DB_PORT=5432
DB_POOL=20

この例では DB_HOSTが'db' になっています。
これはdockerが内部で管理するホスト名なのでdockerの外からはそのままではアクセスできません。
pgbounderを動かしてるコンテナのポートマッピングを確認します。

$ docker ps
(略)
XXXXXXXX gavinmroy/alpine-pgbouncer "/bin/sh -c /start.sh" (略) 172.17.0.1:5432->5432/tcp, 6432/tcp mastodon1_db_1
(略)

この例では 172.17.0.1:5432 に接続できそうです

pgbouncer に管理ユーザを追加する

pgboucer には管理用の疑似データベース pgbouncer があります。
そのデータベースに接続すると管理用コマンドを使えます。

pgbouncer の userlist.txt に下記の行を追加
"bouncer_admin" "XXXXXXXXXXXXXXXXXX"

パスワード部分は適当に生成するなりダイジェスト化するなりしてください

pgbouncerのpgbouncer.ini を編集
; comma-separated list of users, who are allowed to change settings
;admin_users = user2, someadmin, otheradmin
admin_users = bouncer_admin
pgbouncer にHUPシグナルを送って設定をリロード
pkill -HUP pgbouncer

接続プールの名前を確認する

psql -h 172.17.0.1 -p 5432 -U bouncer_admin pgbouncer -c 'show pools;'
(略)
 database  |   user    | cl_active | cl_waiting | sv_active | sv_idle | sv_used | sv_tested | sv_login | maxwait |  pool_mode  
-----------+-----------+-----------+------------+-----------+---------+---------+-----------+----------+---------+-------------
 pgbouncer | pgbouncer |         1 |          0 |         0 |       0 |       0 |         0 |        0 |       0 | statement
 postgres  | postgres  |        65 |          0 |         0 |       3 |       4 |         0 |        0 |       0 | transaction
(2 rows)

この例だと postgres が接続プールの名前になるようです

pgbouncer プラグインをインストール

https://github.com/munin-monitoring/contrib/blob/master/plugins/postgresql/pgbouncer_
をRAW表示して/etc/munin/plugins にコピーしてchmod 755 します。
また、ファイル名は pgbounder_(プール名) にしておきます

pgbouncer プラグインの設定

/etc/munin/plugin-conf.d/munin-node に以下のセクションを追加します

[pgbouncer_postgres]
env.pgbouncer_host 172.17.0.1
env.pgbouncer_port 5432
# env.pgbouncer_pool postgres
env.pgbouncer_user bouncer_admin
env.pgbouncer_pass XXXXXXXXXXXXXXXXXX

セクション名の部分は pgbouncer プラグイン のファイル名と揃えます。
接続情報の部分は 接続プールの名前を確認した時の情報と揃えます。
env.pgbouncer_pool はスクリプト名末尾と実際のプール名が異なる場合だけ指定するのが良いようです。

動作確認

以下のコマンドを実行してプラグインの動作を確認します。

munin-run pgbouncer_postgres config
munin-run pgbouncer_postgres

私の環境ではパッケージの追加インストールが必要でした。

apt install libdbd-pg-perl 

munin-nodeの再起動

sudo /etc/init.d/munin-node try-restart

今すぐグラフを生成する

su - munin --shell=/usr/bin/munin-cron

作成直後は数値がNaNになるみたいですが、5-10分ほど待ってリロードすると解決します