Diary

Diary

日々学んだことをアウトプットする場として初めてみました

android でメモリーリークを調べてみた

モリーリークとは

Java のランタイムの文脈においてメモリーリークとは、ガベージコレクション (GC) と強く関連しています。具体的には、必要無くなったオブジェクトの参照を持ち続け、GC が削除をいつまでも行わないことを指します。

すぐにバグを引き起こすわけではありませんが、リソースが限られている Android においては OOM (out of memory) crash を引き起こす可能性があります。

また、GC を挙動を知る必要もあるため、リークを検知するのも防ぐのも簡単ではありません。

[目次]

環境

- gradle:7.0.4
- kotlin-gradle-plugin:1.5.21
- targetSdk 31
- leakcanary-android:2.8.1'

android で検知する

android で簡単にメモリーリークを調べるには、LeakCanaryというライブラリを使用します。

依存関係を追加する

dependencies {
    ...
    // to detect memory leak
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
}

使い方

上で依存関係を導入したら、あとはアプリを debug モードでビルドするだけです。

ビルドが完了すると、作成中ののアプリとは別アプリとして、以下のアイコンのアプリがインストールされています。

f:id:kokoichi206:20220204000703p:plain

このアプリを開いてみると以下のような画面が表示されます。

f:id:kokoichi206:20220204002615p:plain

このままだと面白くないので、意図的にメモリーリークを発生させてみます。

モリーリーク発生時

モリーリークが検知されると、アプリ内に以下のような通知が出ます。

f:id:kokoichi206:20220204002823p:plain

より詳細ページを見てみます。

f:id:kokoichi206:20220204003004p:plain

f:id:kokoichi206:20220204003231p:plain

どこで発生したかや、どのくらいの量のメモリーリークが発生しているか、その詳細が記載されています。

使用できない!(インストールできない)

debug モードでビルドするだけなのですが、自分の場合は Logcat に以下のようなメッセージがでて使用できませんでした。

D/LeakCanary: LeakCanary is currently disabled: test class org.junit.Test was found in classpath.
D/LeakCanary: Setting up flushing for Thread[LeakCanary-Heap-Dump,5,main]

そこで、こちらのドキュメントに従い次の1文をres/values/strings.xmlに追加することで解決しました。

<resources>
    ...
    <!-- to detect memory leak -->
    <string name="leak_canary_test_class_name">assertk.Assert</string>
</resources>

この時は Logcat のメッセージが以下のようになっていて、検知する準備が整っていることがわかります。

D/LeakCanary: LeakCanary is running and ready to detect memory leaks.
D/LeakCanary: Setting up flushing for Thread[LeakCanary-Heap-Dump,5,main]

おわりに

ドキュメントにはリソースファイルをどこに追加するかが書かれてなかったので少し戸惑いましたが、今作ってるアプリにメモリーリークがなくてよかったです。

定期的に確認していきたいと思います(もしくは自動で確認するフローを作りたいです)

android studio のメソッドが真っ赤になった!

android studio のメソッドが真っ赤になった!

テストメソッドのほとんどのファイルで、多くのメソッドが真っ赤になる現象が発生しました。

import 文を見て、赤文字の対象となっていたものには以下のようなものがありました。

androidx.test.xxx
org.junit.Assert.*

試してみたこと

以下のようなことをしてみましたが、一向に解決しませんでした。

  • FIle > Invalidate Caches/Restart
  • Android Studio 再起動
  • .idea フォルダの削除
  • PC の再起動

解決方法

Build Variants を Release -> Debug にすると解決しました

テストクラスでのみエラーが出ていた時点で、もっと早くに気づくべきでした。

Jetpack Compose でタイマー処理を実装する

タイマーを Android で作りたいことがあり、その際 Jetpack Compose で定期ループみたいなもの実装しました。

Kotlin で実装を行うには Timer や Runnable を用いることが多いそうですが、Jetpack compose では以下のようにLaunchedEffectdelay を用いて簡単に実装できます

// 監視対象とする変数
var deadLine by remember {
    mutableStateOf(20000)
}
var cTime by remember {
    mutableStateOf(10000)
}

LaunchedEffect(key1 = deadLine, key2 = cTime) {
    if (cTime > 0) {
        // delay で指定秒数(ミリ秒)だけ間隔を開ける
        delay(100)
        // 時間の更新処理
        cTime -= 100
        // (必要なら)update 処理
    }
}

key1, key2 に指定したキーを監視し、値が変更されるとLaunchedEffectの中身が実行されます。(keyは1つでも構いません。)

LaunchedEffectの中身で監視対象であるcTimeの更新をおこなっているので、ブロック終了後に再度LaunchedEffectが呼ばれます

これが上記コードがタイマー処理として働く理由となります。

おわりに

今回自分も初めて使ってみましたが、思ったより簡単にかけました。 今後も機会があれば使っていきたいと思います。

androidTest が表示されなくなった!

android でリリースに向けて色々といじってた際、左上のタブでAndroidを選択している状態で androidTest が表示されなくなりました。

Android Studio の再起動や、『File > Invalidate Cache / Restart』をやってみても解決しませんでした。

最終的にはこちらの Stack Overflow に従い、app/build. gradle に下記内容を加えることで解決しました(その後に Invalidate Cache / Restart )

android {
    ...
    sourceSets {
        main { java.srcDirs = ['src/main/java'] }
        test { java.srcDirs = ['src/test/java'] }
        androidTest { java.srcDirs = ['src/androidTest/java'] }
    }
    ...
}

原因はわかってないですが、とりあえず解決したのでよしとします。

GitHub 上でコミットを確認する2つの方法

GitHub でコミットの確認をしようと思った際、「独立した Commits 確認ボタン」がなかったので探すのに少し時間がかかりました。

URL に打ち込む

特定のコミットを確認する

https://github.com/<name>/<repository-name>/commit/<commit-hash>

https://github.com/<name>/<repository-name>までは、いわゆるリポジトリまでの URL です。

過去コミット一覧を確認する

https://github.com/<name>/<repository-name>/commits/<branch>

<branch>の部分にはブランチ名 (main など) が入ります。

Code 欄から飛ぶ

GitHub の Code 欄をひらき、下図赤枠の部分を確認します。

f:id:kokoichi206:20220201082305p:plain

GitHub Actions 内で diff コマンドのエラー

エラー詳細

GitHub の Actions のなかで、次のようなステップを実行しました。

- name: Build
  run: |
    ...
    diff_license=`diff "${fileA}" "${fileB}"`
    ...

ところが次のようなエラーが発生し、次に進めませんでした。

Error: Process completed with exit code 1.

diff のコマンドの部分の終了コードが 1 を返すことが問題なようです。

diff コマンドの終了コード

diff コマンドの終了ステータスは以下のようになっています。

  • 0:差分がないとき
  • 1:差分があるとき
  • 2:ファイルが存在しないとき

(予想に反して?)差分がある場合 1 が返ってきて、これが問題になっているようです。

$ echo a > a
$ echo b > b

# 差分がない場合
$ diff a a
$ echo $?
0

# 差分がある場合
$ diff a b
1c1
< a
---
> b
$ echo $?
1

# ファイルが存在しない場合
$ diff a c
> diff: c: No such file or directory
$ echo $?
2

解決策

今回の場合、diff コマンドの内容がエラーと判定されて処理が中断されてしまってることが原因のようです。

一般に、クリプトの途中のエラーで実行を中断するのは良いことが多く、set -eなどをつけることが多いです。

GitHub Actions がよしなに付けてくれているものと思われますが、これを一旦外してあげることで解決しました(基本的にはset -eをつけておいた方がいいと考え、diffコマンド直後にもとに戻しています。)

- name: Build
  run: |
    ...
    set +e
    diff_license=`diff "${fileA}" "${fileB}"`
    set -e
    ...

おわりに

GitHub Actions は詰まるところが多いですが、その分色々と勉強になって楽しいです。

次は独自の Actions を作ってみたいと思います。

連続する文字を tr で削除する

tr-sオプションを利用すると、「連続するN文字を1文字にすること」が可能です

例を見てみます

基本的な使い方

$ echo pieeeeeen
pieeeeeen

# tr -s "変換を行いたい文字"
$ echo pieeeeeen | tr -s e
pien

無駄に多い改行を削除

また次の例では、入れすぎた改行を削除することを行なっています。

# 無駄な改行が多い
$ echo -e "pi\n\n\n\n\nen"
pi




en

# 連続する改行を1つにまとめる
$ echo -e "pi\n\n\n\n\nen" | tr -s "\n"
pi
en

アルファベット以外で連続するものは削除

tr でアルファベット一般を表すには、[:alpha:]とします。

$ echo -e "my\n\n\n\nknee" | tr -s "[:alpha:]"
my



kne 

これだとアルファベットのみが対象になってしまうので、アルファベット以外を表すために、-cオプション(complement set:補集合)をつけてあげます

$ echo -e "my\n\n\n\nknee" | tr -sc "[:alpha:]"
my
knee

今回アルファベット一般を表すために[:alpha:]という表現を用いましたが、そのほかの文字クラスについては以下を参考にしてください。

$ man tr
...
[:class:]  Represents all characters belonging to 
the defined character class.  Class names are:
    alnum        <alphanumeric characters>
    alpha        <alphabetic characters>
    blank        <whitespace characters>
    cntrl        <control characters>
    digit        <numeric characters>
    graph        <graphic characters>
    ideogram     <ideographic characters>
    lower        <lower-case alphabetic characters>
    phonogram    <phonographic characters>
    print        <printable characters>
    punct        <punctuation characters>
    rune         <valid characters>
    space        <space characters>
    special      <special characters>
    upper        <upper-case characters>
    xdigit       <hexadecimal characters>
...

たとえば、大文字を小文字に変換するには以下のようにします

$ echo pien | tr "[:lower:]" "[:upper:]"
PIEN

# この方法でも可能
$ echo pien | tr "[a-z]" "[A-Z]"
PIEN
# こっちの方が拡張性は高い
$ echo pien | tr "[a-e]" "[A-E]"
piEn

他にも参考になりそうな部分は多いので、[:class:]の部分は一度眺めておくといいかもしれません。

マニュアルページ

-sオプションの部分は一応読んでおきます

$ man tr
...
-s  Squeeze multiple occurrences of the characters 
    listed in the last operand (either string1 or string2) 
    in the input into a single instance of the character. 
    This occurs after all deletion and translation is completed.
...