パフォーマンスの最適化(2.4)-メモリ漏れのピット

15942 ワード

メインディレクトリ参照:Androidの高度な進歩知識(これは総ディレクトリインデックス)[written by無心追求]

Activity内部クラス漏洩

  • Activity内部クラスが存在する場合、匿名内部クラス、または宣言された内部クラスにかかわらず、Activityメモリが漏洩する可能性があります.内部クラスはデフォルトでこのactivityの参照を直接持っているため、内部クラスのライフサイクルがactivityのライフサイクルよりも長い場合、activityが破棄されたときに内部クラスが存在し、activityの参照を持っている場合、activityは自然にgcされません.メモリ漏洩の原因となる
  • Activity内部Handler

    class MyHandler extends Handler {
            
            MyHandler() {
                
            }
    
            @Override
            public void handleMessage(Message msg) {
                // to do your job
            }
        }
    MyHandler myHandler = new MyHandler();
    

    以上のように、Activity内部でこのようなHandlerが宣言された場合、myHandlerはActivity参照をデフォルトで保持し、Activityが終了したと仮定しますが、myHandlerのタスクpostがこの時点である可能性があります.Activityは回収できません.以下の方法で解決できます.
    static class MyHandler extends Handler {
            WeakReference mActivityReference;
    
            MyHandler(Activity activity) {
                mActivityReference = new WeakReference(activity);
            }
    
            @Override
            public void handleMessage(Message msg) {
                final Activity activity = mActivityReference.get();
                if (activity != null) {
                    if (msg.what == 1 && isJumpToHomePage) {
                        Intent intent = new Intent(activity, HomePageActivity.class);
    //                    intent.putExtra("themeType", themeType);
    //                    LogUtil.d("themeType == " + themeType);
                        activity.startActivity(intent);
                        activity.finish();
                    }
                }
            }
        }
    

    このうちMyHandlerは内部静的クラスで、java仮想マシンに静的クラスがロードされると独立してメモリにロードされ、他のクラスに依存しません.また、activityを弱引用でMyHandlerに伝えます.静的MyHandlerクラスのオブジェクトはずっと存在していますが、activity弱引用を持っているので、gc回収時にactivityオブジェクトは回収できます.また、Handlerの使用についてsendEmptyMessageDelayed()がタスクの実行を遅らせる場合はActivityのonDestroyでHandlerのタスクをすべて削除したほうがいいです(removeCallback(null))activityは終了後、onDestroyメソッドでタスクをキャンセルし、クリーンアップの操作を行うべきです

    Activity内部スレッド

  • Activityでは、非同期操作を実現するためにタスクを実行するために単独でスレッドを開いたり、非同期のネットワークリクエストも単独でスレッドを開いて実行したりする場合があります.内部スレッドのライフサイクルがActivityのライフサイクルよりも長い場合、内部スレッドはActivityの参照をデフォルトで保持し、Activityオブジェクトが回収できないという問題があります.しかし、このスレッドが実行されると、Activityオブジェクトが回収に成功し、クラッシュのリスクをもたらし、スレッドの中にActivityの内部オブジェクトが呼び出される可能性がありますが、Activityが終了すると、これらのオブジェクトが回収されてnullになる可能性があります.nullの判断を行わないと、ポインタが異常になります.このスレッドがずっと走っていたら、Activityオブジェクトは回収されないため、activityが終了すると必ずクリーンアップ操作、スレッドの中断、ネットワークリクエストのキャンセルなど
  • を行う.

    Activity内部クラスコールバックリスニング

  • 符号化では、OnClickListenerをクリックして傍受するように、Activity内部に定義されたり、Activityオブジェクトを直接使用してこのインタフェースを実現したりする場合があります.傍受を設定するときにsetListener(innerListener)またはsetListener(this)を直接呼び出したりします.innerListenerはActivity内部で定義されています.thisはActivityオブジェクトです.問題が発生しました.コールバックリスニングは必ずしもすぐに戻るとは限らず、トリガ条件が満たされたときにのみコールバックされるため、この時間は確定できないため、Activityが終了したときに表示すべきコールバックリスニングをsetListener(null)から削除し、コールバックリスニング対象が占有するメモリを解放するとともに、コールバックリスニングがactivity参照を保持し続けることを避ける.内部クラスにはもう一つの解決策があり、内部Handlerと同様にstatic内部クラスとして定義され、Activityオブジェクトの弱い参照が伝達される.これにより、プロジェクトで遭遇した実際のシーンを挙げると、
  • である.
    private static class RecorderTimeListener implements TimeCallback {
    
            WeakReference target;
    
            RecorderTimeListener(ChatActivity activity) {
                target = new WeakReference<>(activity);
            }
    
            @Override
            public void onCountDown(final int time) {
                if (target == null || target.get() == null) {
                    return;
                }
                final ChatActivity activity = target.get();
                activity.runOnUiThread(new Runnable() {
    
                    @Override
                    public void run() {
                        activity.volumeView.setResetTime(time);
                    }
                });
            }
    
            @Override
            public void onMaxTime() {
                if (target == null || target.get() == null) {
                    return;
                }
                final ChatActivity activity = target.get();
                activity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        activity.isMaxTime = true;
                        activity.stopRecord();
                    }
                });
            }
        }
    
    private class StartRecorderListener implements StartCallback {
    
    
            @Override
            public boolean onWait() {
                cancelRecord();
                return true;
            }
    
            @Override
            public void onStarted() {
                if (playerManager.isPlaying()) {
                    playerManager.stop();
                }
                recordWaveView.setVisibility(View.VISIBLE);
                animation = (AnimationDrawable) recordWaveView.getBackground();
                animation.start();
    
                volumeView.showMoveCancelView();
                volumeDialog.show();
    
                viewHandler.postDelayed(volumeRunnable, 100);
            }
    
            @Override
            public void onFailed(int errorCode) {
                if (errorCode == RecorderManager.ERROR_START_FAIL) {
                    showHintDialog(R.string.chat_permission_dialog_title, R.string.chat_permission_dialog_message);
                }
            }
        }
    
    private void startRecord() {
            SystemDateUtil.init(this);
            LogUtil.i(ChatKey.TAG_INFO, "-------------------------- --------------------------");
            final long startSendTime = SystemDateUtil.getCurrentDate().getTime();
            sliceSender = dialogMsgService.createSliceSender(
                    AccountUtil.getCurrentFamilyChatDialogId(),
                    AccountUtil.getCurrentImAccountId(), new DialogMsgService.OnSendVoiceMsgListener() {
                        @Override
                        public void onSuccess() {
                            LogUtil.d(TAG, " ");
                            sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                    SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_SUCCESS);
                        }
    
                        @Override
                        public void onFailure() {
                            sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                    SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_FAILURE);
                            LogUtil.d(TAG, " ");
                        }
                    });
            RecorderManager.getInstance(this).startRecorder(sliceSender, new StartRecorderListener(), new RecorderTimeListener(this));
            LogUtil.i(ChatKey.TAG_INFO, "groupId:" + sliceSender.getGroupId());
        }
    

    以上のようにStartRecorderListenerは内部クラスであり、RecorderTimeListenerは静的内部クラスでありActivity弱リファレンスに転送され、StartRecorderListenerの実装をRecorderTimeListenerの実装に変更するとActivityメモリリークは存在しない

    アニメーションによるメモリ漏洩

  • Activityインタフェースに入った後、コントロールにバインドされたプロパティアニメーションがいくつか実行されている場合は、終了時にcancelがこれらのアニメーション
  • を削除することを覚えておいてください.
     ImageButton :
    public void start(float startAngle, float endAngle) {
            setStop(false);
    
            final AnimatorSet as = new AnimatorSet();
            final ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress",
                    startAngle, endAngle);
            oa.setDuration(duration);
            oa.setInterpolator(new DecelerateInterpolator(1.1f));
            oa.setRepeatCount(count);
    //      oa.setRepeatMode(ObjectAnimator.INFINITE);
            oa.addUpdateListener(new AnimatorUpdateListener() {
    
                @Override
                public void onAnimationUpdate(ValueAnimator animator) {
                    if (stop && as.isRunning()) {
                        as.cancel();
    //                    oa.removeAllListeners();
                    } else {
                        float p = (float) animator.getAnimatedValue();
                        setProgress(p);
                    }
                }
            });
            as.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                }
            });
            as.play(oa);
            as.start();
        }
        
        public void cancel() {
            setStop(true);
        }
    
        public void setStop(boolean stop) {
            this.stop = stop;
            if (stop) {
                setProgress(0.0f);
            }
        }
    

    以上のように属性アニメーションをキャンセルしないと常に実行され、コントロールのonDrawメソッドを実行し続けると、ImageButtonはActivityオブジェクトを持ち、属性アニメーションObjectAnimatorはImageButtonを持ち、ObjectAnimatorは常に実行され、Activityオブジェクトは解放されません
  • プロパティアニメーションのオブジェクトはできるだけstaticで修飾しないでください.static修飾と、このオブジェクトが作成されるとずっと存在します.プロパティアニメーションはstart後、ずっと実行されます.この場合、activityを終了したときでもcancelがアニメーションを削除してもactivity参照は保持されます.次の例のように、
  • private static ValueAnimator valueAnimator;
    
    private void startValueAnimator() {
            int displayTime2Show = displayTime - 1;
            if (displayTime2Show > 1) {
                valueAnimator = ValueAnimator.ofInt(displayTime2Show, 1);
                valueAnimator.setDuration(displayTime2Show * 1000);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        tvStartPageTime.setText(animation.getAnimatedValue().toString());
                    }
                });
                valueAnimator.start();
            }
    
        }
    
    protected void onPause() {
            if (valueAnimator != null && valueAnimator.isRunning()) {
                valueAnimator.cancel();
                valueAnimator = null;
            }
            super.onPause();
        }
    

    activityが終了した後もcancelがアニメーションを削除しても、activityは解放されません.なぜですか.valueAnimatorは静的であり、かつアニメーション属性が変更されたリスニングaddUpdateListenerが追加されているため、リスニングコールにはtvStartPageTime(TextView)コントロールがあり、デフォルトではActivityオブジェクトが保持されているため、Activityが終了してもアニメーションcancelが削除されても保持されている参照が解放されず、修正方法は2つあり、1つはvalueAnimatorのstatic修飾を削除することであり、もう1つは中国:
    protected void onPause() {
    valueAnimator.removeAllUpdateListeners();
            if (valueAnimator != null && valueAnimator.isRunning()) {
                valueAnimator.cancel();
                valueAnimator = null;
            }
            super.onPause();
        }
    

    リスナーの削除コードremoveAllUpdateListeners()を追加

    Contextパラメータの転送時にActivityオブジェクトを使用してメモリが漏れる

  • androidではContext環境変数がよく使われ、ActivityはContextを継承しているので、Contextに入るときにはthisすなわちActivity本オブジェクトがActivityに直接入ることがよくあります.これは比較的悪い習慣で、Activityオブジェクトを必ず渡すと規定されていない場合はできるだけグローバルなContextオブジェクト、すなわちApplicationContextをパラメータとして渡すようにします.ApplicationContextは、appが実行されている限り常に存在するため、オブジェクトが長期にわたって参照されていても、アプリケーションContextよりもライフサイクルが長くなることはないため、アプリケーションContextのメモリ漏洩は発生しません.アプリケーションContextは、アプリケーションが実行されている限り、
  • を回収できないためです.
  • Androidプログラムでは単例を慎重に使用し、単例がContextオブジェクトを転送する必要がある場合は慎重にする必要があります.単例でContextを保存すると、この単例が作成されるとずっと存在し、Activityオブジェクトが転送されると、Activityオブジェクト参照を常に保持してメモリが漏洩し、解決バージョンはApplicationContextオブジェクトが転送され、あるいはActivityが終了したときにこの単例オブジェクトを破棄し、単例がいつ使用されるか、1つのオブジェクトが頻繁に呼び出されない場合は、単例を使用する必要はありません.頻繁に呼び出される可能性のあるオブジェクトメソッドに対して単例を採用することで、オブジェクトとgcオブジェクトの繰り返し作成によるメモリのジッタを避けることができます.保存する必要があるグローバル変数についても、単一の例でカプセル化することができます.単一の例は作成される限り参照が存在するため、gcの
  • には使用されない.
  • 静的変数を使用してActivityオブジェクトを保存するのは非常に悪い符号化習慣であり、staticが修飾したコードフラグメント、変数またはクラスはappがロードされたときにメモリにロードされているため、単例と少し似ており、static変数もAPPが殺されたり表示されたりするまでActivityオブジェクトを持っています.

    Android 5.0以上のWebViewリーク

  • ActivityがWebViewコントロールを参照してWebページをロードするか、ローカルのWebページをロードする場合は、activityを終了した後にWeb Viewを呼び出しても構いません.destroy()メソッドは、activityに対するwebviewの参照を解放することもできません.理由とソリューションはAndroid 5を参照することができます.1のWebViewメモリの漏洩は、この記事で分析したようなソリューションが効果的で、親測定が利用できます.

  • サブスレッドにおけるLooperの不適切な使用prepare()とLooper.loop()メソッドによるメモリ漏洩

  • Looper.loop()は無限ループの方法であり、MessageQueueの中からMessageを取り出して対応するHandlerに配布して実行することを繰り返し、サブスレッドでLooperを呼び出すとprepare()とLooper.loop()メソッド、Looper.loop()はこのスレッドが死なず、ずっとここに詰まっているため、スレッドは実行を終了できません.Looper.prepare()とLooper.loop()間のすべてのオブジェクトは解放されません.解決策は、使用しないときにLooperをquitに
  • 落とすことです.

    EditText setTransformationMethodによるメモリリーク

  • この問題は4.0のandroidシステムでしか存在しないが、5.0以上のシステムではもう存在しない.Androidに属する欠陥の一つであるはずだ.
    loginPasswdEt.setTransformationMethod(PasswordTransformationMethod.getInstance());
    loginPasswdEt.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
    

    一方、P a s s s w o r d T r a n s formationMethodとH i d e R e t urnsTransformationMethodはそれぞれ1つの例です.
    private static PasswordTransformationMethod sInstance;
    
    private static HideReturnsTransformationMethod sInstance;
    
    PasswordTransformationMethod
    
    public CharSequence getTransformation(CharSequence source, View view) {
            if (source instanceof Spannable) {
                Spannable sp = (Spannable) source;
    
                /*
                 * Remove any references to other views that may still be
                 * attached.  This will happen when you flip the screen
                 * while a password field is showing; there will still
                 * be references to the old EditText in the text.
                 */
                ViewReference[] vr = sp.getSpans(0, sp.length(),
                                                 ViewReference.class);
                for (int i = 0; i < vr.length; i++) {
                    sp.removeSpan(vr[i]);
                }
    
                removeVisibleSpans(sp);
    
                sp.setSpan(new ViewReference(view), 0, 0,
                           Spannable.SPAN_POINT_POINT);
            }
    
            return new PasswordCharSequence(source);
        }
        
    private static class ViewReference extends WeakReference
                implements NoCopySpan {
            public ViewReference(View v) {
                super(v);
            }
        }
    

    上は5.0系のソースコードで、中はすでにViewReferenceでviewをパッケージしてSpannableに設定されているので、viewの弱い参照を転送しているのでgcで回収できるが、4.0 android系ではそうではない可能性が高いので、4.0系上はViewオブジェクトがPasswordTransformationMethodとHideReturnsTransformationMethodの単例で長期にわたって保有されているが、ViewはActivityオブジェクトを持っている.4.0システムの場合、この2つの単一のオブジェクトを解放するだけでいいです.
    private void releaseMemoryLeak() {
            int sdk = Build.VERSION.SDK_INT;
            if (sdk >= Build.VERSION_CODES.LOLLIPOP) {
                return;
            }
            try {
                Field field1 = PasswordTransformationMethod.class.getDeclaredField("sInstance");
                if (field1 != null) {
                    field1.setAccessible(true);
                    field1.set(PasswordTransformationMethod.class, null);
                }
                Field field2 = HideReturnsTransformationMethod.class.getDeclaredField("sInstance");
                if (field2 != null) {
                    field2.setAccessible(true);
                    field2.set(HideReturnsTransformationMethod.class, null);
                }
            } catch (NoSuchFieldException e) {
                SyncLogUtil.e(e);
            } catch (IllegalAccessException e) {
                SyncLogUtil.e(e);
            }
        }
    

    上記のコードを追加すると、メモリが漏れないことを確認し、完了します.

    コントロールのBackGroundによるメモリリーク(4.0 androidシステムで解決)

  • は、ピクチャの繰り返しのロードを避けるために、1回目のロード後のBitmapまたはDrawableを静的変数で保存する場合があるが、このような静的修飾されたピクチャオブジェクトをコントロールの背景に設定すると、
  • になる.
    private static Drawable sBackground;
    @Override
    protected void onCreate(Bundle state) {
      super.onCreate(state);
    
      TextView label = new TextView(this);
      label.setText("Leaks are bad");
    
      if (sBackground == null) {
        sBackground = getDrawable(R.drawable.large_bitmap);
      }
      label.setBackgroundDrawable(sBackground);
    
      setContentView(label);
    }
    

    ビューのsetBackgroundDrawableメソッドには次のようなものがあります.
    public void setBackgroundDrawable(Drawable background) {
    ...... 
    background.setCallback(this);
    mBackground = background;
    }
    

    DrawableオブジェクトはViewオブジェクトをコールバックとして保存していますが、4.0システム以降にコールバックを導入してViewオブジェクトを保存しているので、メモリリークの問題は発生しません.
    public final void setCallback(Callback cb) {
            mCallback = new WeakReference(cb);
        }
    

    ここでは、staticを不適切に使用して変数を修飾すると、オブジェクトが回収されない可能性が高いことを例に挙げます.