ここ数日、私はアプリケーションのメモリリークを追跡して修正してきました。私はそれらすべてを修正したと私が信じたポイントに達したとき、私は Android StrictModeとヒープダンプ に記載されているものと同様のフェイルラウドメカニズムを実装しましたエラーメッセージ、ヒープダンプ、アプリケーションの次回起動時に遭難信号を送信)。もちろん、すべてがデバッグビルドです。
すべてのアクティビティリークを修正したと信じていましたが、特定のアクティビティでは、画面の回転時に厳格モードのインスタンス違反警告が発生します。不思議なことに、これを行うのはアプリケーションのすべてではなく、一部だけです。
このような違反が発生したときに取られたヒープダンプを確認し、リークを探して問題のアクティビティのコードを確認しましたが、どこにも到達しません。
そのため、この時点で、可能な限り最小のテストケースを作成しようとしました。次のように、完全に空白のアクティビティ(レイアウトはありません)を作成します。
package com.example.app;
import Android.app.Activity;
import Android.os.Bundle;
import Android.os.StrictMode;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
StrictMode.setVmPolicy(
new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
StrictMode.setThreadPolicy(
new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.penaltyLog()
.build());
super.onCreate(savedInstanceState);
}
}
そして完全性のために、マニフェスト:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
package="com.example.app" >
<application
Android:allowBackup="true"
Android:icon="@drawable/ic_launcher"
Android:label="@string/app_name"
Android:theme="@style/AppTheme" >
<activity
Android:name="com.example.app.MainActivity"
Android:label="@string/app_name" >
<intent-filter>
<action Android:name="Android.intent.action.MAIN" />
<category Android:name="Android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
アクティビティを開きます(デバイスでポートレートを保持)。私は横向きに回転し、縦向きに戻します。その時点で、LogCatでStrictModeからこれを確認します。
01-15 17:24:23.248:E/StrictMode(13867):クラスcom.example.app.MainActivity;インスタンス= 2; limit = 1 01-15 17:24:23.248:E/StrictMode(13867):Android.os.StrictMode $ InstanceCountViolation:class com.example.app.MainActivity;インスタンス= 2; limit = 1 01-15 17:24:23.248:E/StrictMode(13867):at Android.os.StrictMode.setClassInstanceLimit(StrictMode.Java:1)
この時点で、DDMSを使用してヒープダンプを手動で取得します。 MATの2つのインスタンスは次のとおりです。
そして、これがリークされたものであり、そこからGCルートへのパスの最初の部分が含まれています。
縦向きと横向きの間を何度回転しても、実際のインスタンスの数は2を超えることはなく、予想されるインスタンスの数は1を超えることはありません。
誰でもリークを理解できますか?それは本当の漏れですか?そうである場合、それはおそらくAndroidバグです。そうでない場合、それがStrictModeのバグである可能性があることを確認できる他の唯一の事柄です。
私はStrictModeソースを調べて、そこに明らかに問題がないことを確認しました-期待どおり、追加のインスタンスを違反と見なす前にGCを強制します。
StrictModeソースを読み取るための適切なエントリポイントは次のとおりです。
私はこれらのテストを1つのデバイス、CyanogenMod 11スナップショットM2を実行するGalaxy S4でのみ行いました( http://download.cyanogenmod.org/get/jenkins/53833/cm-11-20140104-SNAPSHOT-M2- jfltexx.Zip )ですが、CyanogenModがアクティビティ管理の点でKitKatから逸脱することは想像できません。
私はドア2を使います:これは実際のリークではありません。
より具体的には、それはガベージコレクションにのみ関連しています。 3つの手がかり:
GCルートへのパスは FinalizerReference
で終わります。これはGCに密接に関連しています。これは基本的に、GCに適格なオブジェクトでfinalize()
メソッドの呼び出しを処理します。これには、GCの対象となるオブジェクト、つまり、InputEventReceiver
を拡張する_ViewRootImpl.WindowInputEventReceiver
_インスタンスがあります。 finalize()
メソッド。これが「実際の」メモリリークである場合、オブジェクトはGCに適格ではなく、GCルートへの他のパスが少なくとも1つ必要です。
少なくとも私のテストケースでは、GCbeforeがヒープスナップショットを取得するように強制すると、oneMainActivity
への参照(これを行わずにスナップショットを取得すると2つあります)。 DDMSからのGCの強制には、実際にはすべてのファイナライザの呼び出しが含まれているようです(おそらくこれらの参照をすべて解放するFinalizerReference.finalizeAllEnqueued()
を呼び出すことによって)。
Android 4.4.4のデバイスでは再現できますが、Android Lでは新しいGCアルゴリズムを使用していません(これは多くの場合状況証拠です) 、しかしそれは他のものと一貫しています)。
これはなぜ他のアクティビティではなく一部のアクティビティで発生するのですか?確かなことは言えませんが、「より複雑な」アクティビティを構築するとGCが起動する可能性があります(単にメモリを割り当てる必要があるため)が、このような単純なものは「一般的に」起動しません。しかし、これは可変である必要があります。
なぜStrictModeは別の方法で考えるのですか?
このケースについてのStrictMode
には広範なコメントがあります。decrementExpectedActivityCount()
のソースコードを確認してください。それにもかかわらず、意図したとおりに機能していないようです。
_ // Note: adding 1 here to give some breathing room during
// orientation changes. (shouldn't be necessary, though?)
limit = newExpected + 1;
...
// Quick check.
int actual = InstanceTracker.getInstanceCount(klass);
if (actual <= limit) {
return;
}
// Do a GC and explicit count to double-check.
// This is the work that we are trying to avoid by tracking the object instances
// explicity. Running an explicit GC can be expensive (80ms) and so can walking
// the heap to count instance (30ms). This extra work can make the system feel
// noticeably less responsive during orientation changes when activities are
// being restarted. Granted, it is only a problem when StrictMode is enabled
// but it is annoying.
Runtime.getRuntime().gc();
long instances = VMDebug.countInstancesOfClass(klass, false);
if (instances > limit) {
Throwable tr = new InstanceCountViolation(klass, instances, limit);
onVmPolicyViolation(tr.getMessage(), tr);
}
_
更新
実際、リフレクションを使用してStrictMode.incrementExpectedActivityCount()
を呼び出し、さらに多くのテストを実行しました。非常に奇妙な結果が見つかりましたが、答えは変わりません(まだ#2です)が、追加の手掛かりを提供すると思います。アクティビティの「予想される」インスタンスの数を増やした場合(たとえば、4に)、厳密なモード違反が発生します(5つのインスタンスが存在すると主張します)、4回の回転ごとに。
このことから、Runtime.getRuntime().gc()
の呼び出しは、これらのファイナライズ可能なオブジェクトを実際に解放するものであり、そのコードは、設定された制限を超えた後にのみ実行されると結論づけられました。
それを修正するために何らかのアクションを実行できる場合はどうなりますか?
100%間違いないわけではありませんが、アクティビティのSystem.gc()
でonCreate()
を呼び出すと、この問題が解消される可能性があります(私のテストでもそうでした)。ただし、Javaの仕様は、ガベージコレクションは強制できない(そしてこれは単なるヒントです)ことができないことを明確に示しているので、この「修正」があっても、死刑は免除されます...
リフレクションを呼び出して、アクティビティインスタンス数の制限を手動で増やすことと組み合わせることができます。しかし、それは本当に粗雑なハックのようです:
_Method m = StrictMode.class.getMethod("incrementExpectedActivityCount", Class.class);
m.invoke(null, MainActivity.class);
_
(注:これは、アプリケーションの起動時に1回だけ実行してください)。
他の開発者の安全な時間に対する最終的な回答を要約して提供したいと思います。
Android.os.StrictMode $ InstanceCountViolationに問題があるかどうか
これは実際の問題になる可能性があります。これが問題であるかどうかを確認するには、次の投稿をお勧めします: Detecting Leak Activities in Android 。 Android Frameworkに関連しない、このアクティビティへの参照を保持するオブジェクトがある場合は、問題が発生しているため、修正する必要があります。
Android Frameworkに関連しない、このアクティビティへの参照を保持するオブジェクトがない場合は、detectActivityLeaksチェックの実装方法に関連する問題が発生したことを意味します。また、指摘したようにこの場合、ガベージコレクションが役立つはずです。
detectActivityLeaksが正しく機能せず、すべてのデバイスで再現されない理由
DetectActivityLeaksのソースを見る場合:
// Note: adding 1 here to give some breathing room during
// orientation changes. (shouldn't be necessary, though?)
limit = newExpected + 1;
...
// Quick check.
int actual = InstanceTracker.getInstanceCount(klass);
if (actual <= limit) {
return;
}
// Do a GC and explicit count to double-check.
// This is the work that we are trying to avoid by tracking the object instances
// explicity. Running an explicit GC can be expensive (80ms) and so can walking
// the heap to count instance (30ms). This extra work can make the system feel
// noticeably less responsive during orientation changes when activities are
// being restarted. Granted, it is only a problem when StrictMode is enabled
// but it is annoying.
Runtime.getRuntime().gc();
long instances = VMDebug.countInstancesOfClass(klass, false);
if (instances > limit) {
Throwable tr = new InstanceCountViolation(klass, instances, limit);
onVmPolicyViolation(tr.getMessage(), tr);
}
ここでの問題は、インスタンスを正しくカウントするには、以前のインスタンスをすべてガベージコレクションする必要があるということです(少なくともロジックはこれを中継します)。そして、これがこの実装の主な問題です。 Javaでは、解放の準備ができているすべてのオブジェクトがGCによって収集されることを確認したい場合、GCの1回の往復では十分ではないためです。ほとんどの場合System.gc()を2回呼び出すか、この jlibs ライブラリに実装されているメソッドに似たものを使用するだけで十分です。
そのため、この問題は一部のデバイス(OSバージョン)で再現され、別のデバイス(OSバージョン)では再現されません。これはすべて、GCの実装方法に依存し、GCによってオブジェクトが確実に収集されるようにするには、1回の呼び出しで十分な場合があります。
リークされたアクティビティがない場合にこの問題を回避する方法私は単にSystem.gc()を実行します次の例のように、デバッグ構成でアクティビティを開始する前。これは、この問題を回避するのに役立ち、detectActivityLeaksチェックを引き続き使用できるようになります。
if (BuildConfig.DEBUG)
{
System.gc();
}
Intent intent = new Intent(context, SomeActivity.class);
this.startActivity(intent);
これにより、リリースビルドではガベージコレクションが強制されないことも保証されます。
私はAndroid開発者ではないので、これは暗闇の中のちょっとしたショットです。
おそらく他のアクティビティでは動作が一貫していませんが、それでも完全に些細な動作によって引き起こされるため、問題はonCreateが完了するまでの時間にかかっています。
古いアクティビティの一時停止/停止/破棄シーケンスが完了する前にonCreateが完了する(および最終的にはガベージコレクションが完了する必要はない)場合、実際には2つのインスタンスが存在します。
これを防ぐための簡単な解決策は、onCreateでループする他のことを行う前にチェックする静的なアトミックブール「準備」(最初はfalse)をアクティビティに持たせることです。
while(!ready.compareAndSet(false, true)) {
//sleep a bit
}
次に、ondestroyライフサイクルコールバックをオーバーライドして、ready.compareAndSet(true、false)を呼び出します。
このアプローチがまったくナイーブである場合はお詫びします。