Android 5.1 WebView 메모리 누 출 문제 및 빠 른 해결 방법

문제 배경
프로젝트 의 메모리 누 출 을 조사 하 는 과정 에서 WebView 로 인 한 메모리 누 출 이 발견 되 었 으 며,테스트 를 통 해 이 부분의 누 출 은 안 드 로 이 드 5.1 이상 의 기종 에 만 나타 나 는 것 으로 밝 혀 졌 다.비록 프로젝트 가 WebView 를 사용 하 는 장면 은 많 지 않 지만 누설 도 놓 치지 않 는 정신 을 계승 하여 우 리 는 반드시 그것 을 해결 해 야 한다.
닥 친 문제
프로젝트 에서 WebView 를 사용 하 는 페이지 는 주로 FAQ 페이지 에 있 으 며,문제 도 여러 번 종료 에 들 어 갔 을 때 메모리 사용량 이 크 고 GC 가 빈번 한 것 으로 나 타 났 다.LeakCanary 를 사용 하여 관찰 한 결과 두 개의 메모리 누 출 이 빈번 한 것 을 발견 하 였 습 니 다. 


우 리 는 이 두 가지 누설 을 분석 해 보 자.
그림 1 에서 웹 뷰 의 ContentView Core 의 구성원 변수 인 mContainer View 가 Accessibility Manager 의 mAccessibility State Change Listeners 를 인용 하여 activity 가 회수 되 지 못 해 유출 된 것 을 알 수 있다.
인용 관계:mAccessibilityStateChangeListeners->ContentViewCore->WebView->SettingHelpActivity
그림 2 에서 인용 관 계 는 mComponentCallbacks->AwContents->WebView->Setting HelpActivity 임 을 알 수 있다.
문제 분석
mAccessibility State Change Listeners 와 mComponentCallbacks 가 언제 등록 되 었 는 지 찾 아 보 겠 습 니 다.mAccessibility State Change Listeners 부터 살 펴 보 겠 습 니 다.
AccessibilityManager.java

private final CopyOnWriteArrayList<AccessibilityStateChangeListener>
    mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<>();

/**
 * Registers an {@link AccessibilityStateChangeListener} for changes in
 * the global accessibility state of the system.
 *
 * @param listener The listener.
 * @return True if successfully registered.
 */
public boolean addAccessibilityStateChangeListener(
    @NonNull AccessibilityStateChangeListener listener) {
  // Final CopyOnWriteArrayList - no lock needed.
  return mAccessibilityStateChangeListeners.add(listener);
}

/**
 * Unregisters an {@link AccessibilityStateChangeListener}.
 *
 * @param listener The listener.
 * @return True if successfully unregistered.
 */
public boolean removeAccessibilityStateChangeListener(
    @NonNull AccessibilityStateChangeListener listener) {
  // Final CopyOnWriteArrayList - no lock needed.
  return mAccessibilityStateChangeListeners.remove(listener);
}
위의 몇 가지 방법 은 Accessibility Manager.class 에서 정 의 된 것 입 니 다.방법 에 따라 호출 하면 ViewRootImpl 초기 화 에서 addAccessibility State Change Listener 를 호출 하여 listener 를 추가 한 다음 dispatchDetached FromWindow 에서 이 listener 를 제거 합 니 다.
remove 가 있 는 이상 왜 계속 인용 하고 있 습 니까?우 리 는 잠시 후에 다시 분석 할 것 이다.
mComponent Callbacks 가 언제 등록 되 었 는 지 다시 한 번 볼 게 요.
Application.java

public void registerComponentCallbacks(ComponentCallbacks callback) {
  synchronized (mComponentCallbacks) {
    mComponentCallbacks.add(callback);
  }
}

public void unregisterComponentCallbacks(ComponentCallbacks callback) {
  synchronized (mComponentCallbacks) {
    mComponentCallbacks.remove(callback);
  }
}
위의 두 가지 방법 은 응용 프로그램 에서 정 의 된 것 으로 방법 에 따라 호출 하면 Context 기본 클래스 에서 호출 된 것 을 발견 할 수 있다.

/**
 * Add a new {@link ComponentCallbacks} to the base application of the
 * Context, which will be called at the same times as the ComponentCallbacks
 * methods of activities and other components are called. Note that you
 * <em>must</em> be sure to use {@link #unregisterComponentCallbacks} when
 * appropriate in the future; this will not be removed for you.
 *
 * @param callback The interface to call. This can be either a
 * {@link ComponentCallbacks} or {@link ComponentCallbacks2} interface.
 */
public void registerComponentCallbacks(ComponentCallbacks callback) {
  getApplicationContext().registerComponentCallbacks(callback);
}

/**
 * Remove a {@link ComponentCallbacks} object that was previously registered
 * with {@link #registerComponentCallbacks(ComponentCallbacks)}.
 */
public void unregisterComponentCallbacks(ComponentCallbacks callback) {
  getApplicationContext().unregisterComponentCallbacks(callback);
}
유출 경로 에 따 르 면 AwContents 에 mComponentCallbacks 가 등록 되 어 있 지 않 은 것 일 까?
chromium 소스 코드 를 봐 야 진정한 이 유 를 알 수 있 습 니 다.다행히 chromium 은 오픈 소스 입 니 다.우 리 는 안 드 로 이 드 5.1 Chromium 소스 코드 에서 우리 가 필요 로 하 는 AwContents(자체 사다리)를 찾 았 습 니 다.언제 등 록 했 는 지 보 세 요.
AwContents.java

@Override
    public void onAttachedToWindow() {
      if (isDestroyed()) return;
      if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
      }
      mIsAttachedToWindow = true;
      mContentViewCore.onAttachedToWindow();
      nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),
          mContainerView.getHeight());
      updateHardwareAcceleratedFeaturesToggle();
      if (mComponentCallbacks != null) return;
      mComponentCallbacks = new AwComponentCallbacks();
      mContext.registerComponentCallbacks(mComponentCallbacks);
    }
    @Override
    public void onDetachedFromWindow() {
      if (isDestroyed()) return;
      if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
      }
      mIsAttachedToWindow = false;
      hideAutofillPopup();
      nativeOnDetachedFromWindow(mNativeAwContents);
      mContentViewCore.onDetachedFromWindow();
      updateHardwareAcceleratedFeaturesToggle();
      if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
      }
      mScrollAccessibilityHelper.removePostedCallbacks();
      mNativeGLDelegate.detachGLFunctor();
    }
상기 두 가지 방법 에서 우 리 는 mComponent Callbacks 의 종적 을 발견 했다.
onAttached ToWindow 에서 mContext.registerComponentCallbacks(mComponentCallbacks)를 호출 하여 등록 합 니 다.
onDetached FromWindow 에 역 등록 합 니 다.
onDetached FromWindow 의 코드 를 자세히 보면
onDetached FromWindow 에서 isDestroyed 조건 이 성립 되면 직접 return 합 니 다.이 로 인해 mContext.unregister ComponentCallbacks(mComponentCallbacks)를 실행 할 수 없습니다.
또한 우리 의 첫 번 째 누설 을 초래 할 수 있 습 니 다.onDetached FromWindow 가 정상 적 인 절 차 를 수행 하지 못 하고 ViewRootImp 의 dispatchDetached FromWindow 방법 을 사용 하지 않 기 때 문 입 니 다.이 조건 이 언제 true 가 될 지 찾 아 보 겠 습 니 다.

/**

   * Destroys this object and deletes its native counterpart.

   */

  public void destroy() {

    mIsDestroyed = true;

    destroyNatives();

  }
destroy 에서 true 로 설 정 된 것 으로 밝 혀 졌 습 니 다.즉,destroy()를 실행 하면 등록 을 취소 할 수 없습니다.저 희 는 보통 activity 에서 webview 를 사용 할 때 onDestroy 방법 에서 mWebView.destroy()를 호출 합 니 다.웹 뷰 를 풀 어 줍 니 다.원본 코드 에 따 르 면 onDetached FromWindow 전에 destroy 를 호출 하면 제대로 등록 하지 못 하고 메모리 가 새 는 것 을 알 수 있 습 니 다.
문제 의 해결
우 리 는 이 유 를 알 게 된 후에 해결 이 비교적 쉬 워 집 니 다.바로 webview 를 없 애기 전에 반드시 onDetached FromWindow 를 사용 해 야 합 니 다.우 리 는 먼저 webview 를 부모 view 에서 제거 한 다음 에 destroy 방법 을 호출 합 니 다.코드 는 다음 과 같 습 니 다.

@Override
protected void onDestroy() {
  super.onDestroy();
  if (mWebView != null) {
   ViewParent parent = mWebView.getParent();
   if (parent != null) {
     ((ViewGroup) parent).removeView(mWebView);
   }
   mWebView.removeAllViews();
   mWebView.destroy();
   mWebView = null;
  }
}
또 하나의 문 제 는 5.1 이하 의 기종 에 메모리 가 새 지 않 는 이유 입 니 다.4.4 의 소스 코드 AwContents 를 살 펴 보 겠 습 니 다.

/**
 * @see android.view.View#onAttachedToWindow()
 *
 * Note that this is also called from receivePopupContents.
 */
public void onAttachedToWindow() {
  if (mNativeAwContents == 0) return;

  mIsAttachedToWindow = true;

  mContentViewCore.onAttachedToWindow();

  nativeOnAttachedToWindow(mNativeAwContents, mContainerView.getWidth(),

      mContainerView.getHeight());

  updateHardwareAcceleratedFeaturesToggle();

  if (mComponentCallbacks != null) return;
  mComponentCallbacks = new AwComponentCallbacks();
  mContainerView.getContext().registerComponentCallbacks(mComponentCallbacks);
}

/**
 * @see android.view.View#onDetachedFromWindow()
 */

public void onDetachedFromWindow() {
  mIsAttachedToWindow = false;

  hideAutofillPopup();

  if (mNativeAwContents != 0) {
    nativeOnDetachedFromWindow(mNativeAwContents);
  }
  mContentViewCore.onDetachedFromWindow();
  updateHardwareAcceleratedFeaturesToggle();

  if (mComponentCallbacks != null) {
    mContainerView.getContext().unregisterComponentCallbacks(mComponentCallbacks);
    mComponentCallbacks = null;
  }
  mScrollAccessibilityHelper.removePostedCallbacks();

  if (mPendingDetachCleanupReferences != null) {
    for (int i = 0; i < mPendingDetachCleanupReferences.size(); ++i) {
      mPendingDetachCleanupReferences.get(i).cleanupNow();
    }
    mPendingDetachCleanupReferences = null;
  }
}
onDetached FromWindow 방법 에 isDestroyed 라 는 판단 조건 이 없 는 것 을 볼 수 있 습 니 다.이것 은 바로 이 원인 으로 인 한 메모리 누 출 임 을 증명 합 니 다.
문제 의 총화
웹 뷰 를 사용 하면 메모리 누 출 이 발생 하기 쉬 우 며,올 바 르 게 방출 되 지 않 으 면 oom 이 발생 하기 쉽다.웹 뷰 사용 에 도 구덩이 가 많 으 니 테스트 를 많이 해 야 합 니 다.
이상 의 안 드 로 이 드 5.1 WebView 메모리 유출 문제 와 빠 른 해결 방법 은 바로 편집장 이 여러분 에 게 공유 한 모든 내용 입 니 다.여러분 에 게 참고 가 되 고 저희 도 많이 응원 해 주시 기 바 랍 니 다.

좋은 웹페이지 즐겨찾기