pichillilorenzo / flutter_inappwebview

A Flutter plugin that allows you to add an inline webview, to use a headless webview, and to open an in-app browser window.
https://inappwebview.dev
Apache License 2.0
3.28k stars 1.62k forks source link

After dispose a InAppWebViewKeepAlive using InAppWebViewController.disposeKeepAlive. NullPointerException is thrown when main activity enter destroyed state. #2025

Closed Murmurl912 closed 1 week ago

Murmurl912 commented 8 months ago

Environment

Technology Version
Flutter version 3.16.9
Plugin version flutter_inappwebview: ^6.0.0-beta.25
Android version 9~14

Device information:

This issue is device unrelated.

Description

When using InAppWebViewController.disposeKeepAlive to manually dispose a keepalive webview. A NullPointerException is thrown when app enter destoryed state and InAppWebViewController.dispose() is called. The nullptr is caused by InAppWebViewManager.disposeKeepAlive not update keepAliveWebViews correctly .
It simply using keepAliveWebViews.put(keepAliveId, null); to remove release webview.

  public void disposeKeepAlive(@NonNull String keepAliveId) {
    FlutterWebView flutterWebView = keepAliveWebViews.get(keepAliveId);
    if (flutterWebView != null) {
      flutterWebView.keepAliveId = null;
      // be sure to remove the view from the previous parent.
      View view = flutterWebView.getView();
      if (view != null) {
        ViewGroup parent = (ViewGroup) view.getParent();
        if (parent != null) {
          parent.removeView(view);
        }
      }
      flutterWebView.dispose();
    }
    // keepAliveWebViews contains null value entry
    if (keepAliveWebViews.containsKey(keepAliveId)) {
      keepAliveWebViews.put(keepAliveId, null);
    }
  }

And in dispose method null check is missing.

  @Override
  public void dispose() {
    super.dispose();
    Collection<FlutterWebView> flutterWebViews = keepAliveWebViews.values();
    // flutterWebViews contains null element
    for (FlutterWebView flutterWebView : flutterWebViews) {
      String keepAliveId = flutterWebView.keepAliveId;
      if (keepAliveId != null) {
        disposeKeepAlive(flutterWebView.keepAliveId);
      }
    }
    keepAliveWebViews.clear();
    windowWebViewMessages.clear();
    plugin = null;
  }

Expected behavior:

Add a null check in dispose method's disposeKeepAlive loop.

  @Override
  public void dispose() {
    super.dispose();
    Collection<FlutterWebView> flutterWebViews = keepAliveWebViews.values();
    for (FlutterWebView flutterWebView : flutterWebViews) {
      if (flutterWebView == null) {
        continue;
      }
      String keepAliveId = flutterWebView.keepAliveId;
      if (keepAliveId != null) {
        disposeKeepAlive(flutterWebView.keepAliveId);
      }
    }
    keepAliveWebViews.clear();
    windowWebViewMessages.clear();
    plugin = null;
  }

Or refactor disposeKeepAlive to handle ConcurrentModificationException by extract a new method to dispose FlutterWebView

  public void disposeKeepAlive(@NonNull String keepAliveId) {
    FlutterWebView flutterWebView = keepAliveWebViews.remove(keepAliveId);
    dispoeKeepAlive(flutterWebView);
  }

  public void dispoeKeepAlive(FlutterWebView flutterWebView) {
    if (flutterWebView.keepAliveId == null) {
      return;
    }
    flutterWebView.keepAliveId = null;
    View view = flutterWebView.getView();
    if (view != null) {
      ViewGroup parent = (ViewGroup) view.getParent();
      if (parent != null) {
        parent.removeView(view);
      }
    }
    flutterWebView.dispose();
  }

  @Override
  public void dispose() {
    super.dispose();
    Collection<FlutterWebView> flutterWebViews = keepAliveWebViews.values();
    for (FlutterWebView flutterWebView : flutterWebViews) {
      dispoeKeepAlive(flutterWebView);
    }
    keepAliveWebViews.clear();
    windowWebViewMessages.clear();
    plugin = null;
  }

Steps to reproduce

  1. create a InAppWebview with keepAlive
  2. dispose InAppWebview using InAppWebViewController.disposeKeepAlive
  3. press back to exit app and wait main activity destory
  4. app crashed.

Stacktrace/Logcat

Unable to destroy activity {xxx.MainActivity}: java.lang.NullPointerException: Attempt to read from field 'java.lang.String com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView.keepAliveId' on a null object reference in method 'void com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewManager.dispose()'

github-actions[bot] commented 8 months ago

👋 @Murmurl912

NOTE: This comment is auto-generated.

Are you sure you have already searched for the same problem?

Some people open new issues but they didn't search for something similar or for the same issue. Please, search for it using the GitHub issue search box or on the official inappwebview.dev website, or, also, using Google, StackOverflow, etc. before posting a new one. You may already find an answer to your problem!

If this is really a new issue, then thank you for raising it. I will investigate it and get back to you as soon as possible. Please, make sure you have given me as much context as possible! Also, if you didn't already, post a code example that can replicate this issue.

In the meantime, you can already search for some possible solutions online! Because this plugin uses native WebView, you can search online for the same issue adding android WebView [MY ERROR HERE] or ios WKWebView [MY ERROR HERE] keywords.

Following these steps can save you, me, and other people a lot of time, thanks!

Murmurl912 commented 8 months ago

Here is the demo code to reproduce this issue: inappwebview_dispose_keepalive_nullptr

namnh-0652 commented 3 months ago

@pichillilorenzo could you help check this?

malthee commented 1 month ago

Yes please, this is causing a lot of errors for us Edit: linked your PR here: #2031

pichillilorenzo commented 1 week ago

Thanks, yes I missed the flutterWebView null check. I simply added it.

Murmurl912 commented 3 days ago

Before the next release resolves this issue. Anyone using keepAlive should use the following code to safely dispose the InAppWebViewFlutterPlugin. Simply using a try-catch block will prevent the Flutter engine from completely destroy, which can lead to memory leaks and other issues.

It took me several weeks of investigation to discover that a SqliteException(5): while executing statement, database is locked error was actually caused by the Flutter engine not being disposed of properly.

override fun onDestroy() {
    try {
        super.onDestroy()
    } catch (e: Exception) {
        // no-op
    }
}

Instead, using to following code the remove null webview from inAppWebViewManager :

import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin

override fun onDestroy() {
    safeDispose()
    super.onDestroy()
}

private fun safeDispose() {
    kotlin.runCatching {
        val flutterEngine = flutterEngine ?: return
        val plugins = flutterEngine.plugins.get(InAppWebViewFlutterPlugin::class.java) as? InAppWebViewFlutterPlugin
            ?: return
        val manager = plugins.inAppWebViewManager ?: return
        val clone = manager.keepAliveWebViews.toMap()
        for (entry in clone) {
            if (entry.value == null) {
                manager.keepAliveWebViews.remove(entry.key)
            }
        }
    }
}