2018年9月27日木曜日

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 Sweep方式ではメモリからクリアされるためだ。問題となるのは、ROOTにつながる参照である。
ROOTにつながる参照を探すには、ReferencesDepth カラムに着目する。Depthが小さくなるほどROOTに近い。そのためDepthの昇順でソートして(デフォルト)、上から順に展開していき、Depth 0 までの参照をたどる。

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

注意

static変数

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

WeakReference

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

this$0

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

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

2016年6月1日水曜日

設定からPermissionをOFFにされたアプリはいったん死ぬ

“The user can revoke the permissions at any time, by going to the app’s Settings screen.”
https://developer.android.com/training/permissions/requesting.html

タイトルが全て

以下は、読む必要なし。


Android 6.0から、設定画面の「アプリ」ってとこから権限(Permission)をON/OFFできるようになったよ。で、この画面でONにすると権限を付与できるんだ。

権限をOFFにしたときには、実行中のそのアプリのプロセスはKILLされるよ。メモリが足りなくなった時に(LowMemoryKillerによって)バックグラウンドアプリがKILLされた時と同じで、そのアプリがフォアグラウンドに戻ってきたタイミングでプロセスは復帰されるんだ。

権限がある!と思っていても、プロセス復帰したら権限なくなっているかもしれないってことだね!

権限がない場合を考慮していれば、問題にならないよね。プロセス復帰の対応がおろそかだと、ユーザーの操作で権限OFFにされた時に変なクラッシュするかもね〜

2016年5月30日月曜日

Intentを使ってカメラを起動するときにもCAMERA権限は必要になる?

これバグじゃないんだって

targetSdkVersion を 23 以上にしてますか?

Android 6.0 以降で Dangerous なパーミッションはRuntimeに権限要求する必要がでてきた。Androidユーザーとして嬉しい変更だ。
しかし開発者としては面倒だから targetSdkVersion を上げないでおいているアプリは多いだろう。

たとえば android.permission.CAMERA 権限は Dangerous なパーミッションとして分類されている。なのでカメラアプリを作るには、Runtimeに権限要求をする必要があるぞい。

あ、そうそうAndroid 6.0 以降で Intent を使ってカメラアプリを起動するときにも場合によっては、 android.permission.CAMERA の権限が必要になるぞい。

ということでバグ報告されております

これな https://code.google.com/p/android/issues/detail?id=188073

しかし、Googleの方は「え?バグじゃないよ」と回答。

  • この挙動はAndroidManifest.xmlにCAMERAの権限がある ときだけ起こる
  • ユーザーが設定画面でカメラ権限を無効化できるようにするため

だそう。なるほどね。なるほど……。

ライブラリプロジェクトに使っていないCAMERA権限がある場合は要注意!

実装方法

このように、ユーザーの利便性を考えて権限のON/OFFができるようにしたいって場合は、以下を使おう。

<uses-permission-sdk-23>
https://developer.android.com/guide/topics/manifest/uses-permission-sdk-23-element.html

これを使えば大丈夫ということだ!


ちなみに <uses-permission> には android:maxSdkVersion 属性があるから minSdkVersion もあるのかな〜と思ったらなかった。
https://developer.android.com/guide/topics/manifest/uses-permission-element.html

2016年3月17日木曜日

HyperCardの体験をiOS/Androidでできたらいいな

はじめまして。@DevMassive です。


HyperCardとは

(HyperCardが何なのかを知らない人がこのページにたどり着くことはないので略)

HyperCardがない

HyperCardっぽいものが世の中にないです。非常に悲しい。あなたもそう思っているのでしょう。間違いはないはずです。

プログラミングが身近にあった時代。
プログラミング言語が魔法だなんて幻想のなかった時代。

子供の頃のわたしに、つくることで自分を表現する楽しさを教えてくれたのは、HyperCardでした。

ぼーっと待っていれば、どこかの大人がまたあんなツールを作ってくれる。
そう思っていましたが、誰も作らず、気づけば僕も大人です。

しかも驚いたことに、プログラマになっていました。

つくったもの

iOS / Androidアプリ「ハナシカク」
http://door.develga.com/

当時のわたしはHyperCardを使いこなすには幼かったので、紙芝居とゲームの間のようなものをつくって遊んでいました。
2016年の現在、カンタンな紙芝居やゲームがつくりたいなら、Webサイトをつくればいいんです。Flashもあります(ありました)。Unityなんてものもありますね(なんと3Dです)。忘れちゃいけない、iOSアプリだってカンタンにつくれます(Appleがいうんだから間違いないですね)。

現代に必要なHyperCardは、それらよりもさらに作成のハードルが低いものだと考えました。

「ハナシカク」は絵本をつくるツールです。
ノートに描いたラクガキを、カメラに撮りながらつなぎます。
絵本のストーリーは分岐させることができます。
いまのところ、それだけです。

つくることで自分を表現する楽しさを、多くの人に伝えていきたい。
スマホ世代のHyperCardになるべく、これから可能性を探ります。

ぜひ使ってみてください。
そして、ご意見やご感想を聞かせてください。

このアプリや活動に興味を持たれた方のご連絡もお待ちしております。
Mail: support@develga.com
Twitter: @DevMassive


ビル・アトキンソンに感謝を込めて。

2016年2月11日木曜日

LibGDXで子Actorを切り抜くGroup

切り抜き

Group.setCullingArea() を使えば、矩形の範囲外にある子Actorを描画しなくなる。(カリング処理)
でも、少しでも矩形の中にはみ出てるActorはその全体が描画されてしまう。
だから、ある矩形で子Actor共々切り抜きたい時にはそぐわない。

そこで以下の様なGroupをつくっている。子ActorをaddActorして、setCullingArea()するとその矩形で切り抜いてくれる。
(batch.end()を使っているのでStageにたくさん置くと描画が遅くなるよ)
public class CullingGroup extends Group {
    private Rectangle mScissorBounds;
    public CullingGroup() {
        mScissorBounds = new Rectangle();
    }
    @Override
    public void draw(Batch batch, float parentAlpha) {
        getStage().calculateScissors(getCullingArea(), mScissorBounds);
        batch.end();
        batch.begin();
        if (ScissorStack.pushScissors(mScissorBounds)) {
            super.draw(batch, parentAlpha);
            batch.flush();
            ScissorStack.popScissors();
        }
    }
}
ちなみに、ScrollPaneは同じような処理を行なっている。

2016年2月3日水曜日

libGDXでフルスクリーンをやめる

フルスクリーンになりがち

libGDXはなんも設定しないとフルスクリーンになる。なんならImmersiveになる。なるべく大きくなろうとする。(Unityもそうだ)
でもそんなに大きくなられると困ることもある。
libGDXのバージョンによるだろうから、上から順にみていって怪しいところいじってみて欲しい。

Android

Theme

AndroidManifest.xmlをチェックすると
android:theme="@style/GdxTheme"
 というテーマになっているが、こいつのなかで
        <item name="android:windowFullscreen">true</item>
とある。これをfalseにする。

Config

AndroidLauncher.javaをチェックすると
AndroidApplicationConfigurationのインスタンスをデフォルトのまま使っている。これを

AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
config.useImmersiveMode = false;
config.hideStatusBar = false;
として、ImmersiveModeやステータスバーを非表示にする処理を抑制する。

Initialize

引き続きAndroidLauncher.javaをチェックすると、

initialize(app, config);
という処理がある。ここで様々なAndroidに関する初期化が行われるわけだ。しかしこのままだとまたステータスバーを非表示にする処理が走るので、
View view = initializeForView(app, config);
setContentView(view);
に変更する。

iOS

iOSは単純明快で、info.plist.xml の
<key>UIStatusBarHidden</key>
<true/>
をfalseにすればよい。