2018年7月13日金曜日

Android Studioでメモリリークを解決しちゃおう

Androidにおけるメモリリーク

Javaはメモリリークしない。(過去記事を参照)オブジェクトが相互に参照しあっていても Mark and Sweep によってメモリからクリアされるからだ。
よって、Java、そしてAndroidにおけるメモリリークとは、ライフサイクルの不一致のことを指す。

一般的に、リークするとまずいのは画像などであるが、Androidの場合はActivity(or Fragment)という「画面」も該当する。なぜなら、たいていの画面はたくさんの画像を保持しているからだ。
Androidでは気をつけないとActivityがリークする。今回はActivityのリークを調べる方法をご説明しよう。

Android Studioを使ってActivityのメモリリークを調べる

Android Studio の Run > Profile… をクリックすると、apkがdeviceにインストールされ、Android Profiler が立ち上がる。

Android Profiler に3つのグラフが表示される。これらはリアルタイムで更新される。MEMORY のグラフをクリックして、メモリ使用量のグラフのみを表示する。

Activity間の遷移を行ったり、Activityでの操作を行ったりしたあと、Backキーによってアプリを閉じる。

Android Profiler の左上にあるゴミ箱アイコンを何回か時間を置いてクリックする。この操作によって、参照がないオブジェクトはGCによってメモリから消える。メモリ使用量のグラフががくっと下がるのがわかるはずだ。ゴミ箱を押してもグラフに変動がなくなったら、次のステップに進もう。

ゴミ箱の隣の Dump Java Heap をクリックし、処理が終わるまでしばらく待とう。グラフの下にオブジェクトが列挙される。これらが、メモリに残っているものだ。

Activityは全て閉じた状態でGCを走らせた。そのため、Activityがメモリに残っている場合はリークしている。Activityをアプリのパッケージ名配下 com.example.ui に置いているならば、Arrange by class となっているプルダウンメニューを箇所を Arrange by package に変更しよう。ここで、com > example > ui とたどっていって、Activityクラスの名前があればリークしている。みつけたらクリックしよう。

Android Studio を使ってActivityのメモリリークを解決する

みつけたActivityクラスをクリックすると、 Instance View が開く。ここには、そのクラスのメモリ上にあるインスタンスが列挙される。3つあれば、そのActivityが3つリークしているということだ。そのうちの1つをクリックしよう。

References が開く。ここには、そのインスタンスへの参照が列挙される。何かがActivityへの参照を保持しているために、GCによってもメモリから削除されずにリークしている。解決するためには、この参照を切る必要がある。
しかし、Activityへの参照はたくさんある。なぜなら、Activity内で表示するViewは、ActivityをmContext のようなメンバ変数で保持しているためだ。これは大抵の場合は問題ではない。なぜならActivity-View間の相互参照であり、JavaのMark and Swipe方式ではメモリからクリアされるためだ。問題となるのは、ROOTにつながる参照である。
ROOTにつながる参照を探すには、ReferencesDepth カラムに着目する。Depthが小さくなるほどROOTに近い。そのためDepthの昇順でソートして(デフォルト)、上から順に展開していき、Depth 0 までの参照をたどる。

Depth 0 までの参照がわかれば、その参照を切ればよい。なぜそのような参照が残ってしまうのかは、一般的なバグ解決の手段である(がんばれ)。これでメモリリークは解決だ。

注意

Activityが全て閉じられても、プロセスが死ぬとは限らない。もっとも、Profilerで見れている以上はそのプロセスは生きている。プロセスが生きていれば、staticな変数などはメモリ上に残っていて正常だ。

WeakReference のインスタンスからの参照は、メモリリークにつながらない。原因は別の参照経路にある。なぜなら WeakReference は他からの参照がある場合のみ参照を保持し続けるという「弱参照」をするためのクラスであるからだ。

this$0 から参照されている場合、実際のコードを探してもそのような定義文は存在しない。これは、staticでないインナークラスが、アウタークラスへの参照を暗黙のうちに保持しているためだ。Android開発におけるあるあるメモリリークパターンである。

2018年2月22日木曜日

mitmproxy で Android の通信を確認したり置き換えたりしよう

Androidアプリの通信内容を確認したいときがある。
便利なライブラリや、仕方なく組み込んだSDKにより、Androidの実際の通信内容をすべて把握することは難しい。

そんなあなたに mitmproxy

Charlesと違い無料だ。使い放題しろ

インストール

brew install mitmproxy

http://docs.mitmproxy.org/en/latest/install.html

起動

PCで実行

mitmproxy

接続

Androidのプロキシの設定をする。

https://shnk38.com/android/how-to-android/f5321-wi-fi-proxy/

  • IPアドレスはPCのもの。 ifconfig コマンドで参照できるやつ
  • ポートは8080

証明書のインストール

プロキシの設定が正しく行えていれば、Androidのブラウザで mitm.it にアクセスすると、証明書のインストールができる。

Android 7 以降での注意点

Android Nougat changes how applications interact with user- and admin-supplied CAs. By default, apps that target API level 24 will—by design—not honor such CAs unless the app explicitly opts in.
https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html

ここで諸君に残念なお知らせだ。

Android 7 以降の端末では、巷にあふれるイカしたアプリのSSL/TLSを用いた通信を覗き見ることはできない。なぜなら上記でインストールした証明書を参照してくれないからだ。

自分のアプリの場合は、以下の手順で証明書を参照するように設定できる。

https://qiita.com/notona/items/8faf62872a032b2d728d

src/main/res/xml/network_security_config.xml を作成。

<network-security-config>
     <debug-overrides>
          <trust-anchors>
               <certificates src="user" />
          </trust-anchors>
     </debug-overrides>
</network-security-config>

AndroidManifest.xml に以下を追加。

     <application
        <!-- 追加 -->
        android:networkSecurityConfig="@xml/network_security_config"

よく使うコマンド

  • 中を見たい通信をえらんで enter。Response<->Request<->Detailタブの切替は tab
  • shift-f で追従モード切り替え
  • z で全部消す
  • f でフィルタ設定

レスポンスの変更

レスポンスをPCに用意したtxtファイルの中身に置き換えたい、というようなことはあると思う。しかし、Charles と違いUIでポチポチ設定できない。

例えば http://example.com/hoge?q=anything のレスポンス結果をクエリパラメータによらず hoge.txt の中身に置き換えたいとしよう。

hoge.py に以下を書く。( mitmproxy v2.0.2 )

class Replacer:
    def response(self, flow):
        if flow.request.pretty_url.startswith("http://example.com/hoge"):
            with open("hoge.txt", "r") as file:
                flow.response.text = file.read()
                
def start():
    return Replacer()

そして、起動時のパラメータに付与する。

mitmproxy -s hoge.py