folks
Using leakcanary tool we have noticed recently memory leaks in activities that use webview. The issue just occurs in some models like Samsung devices with Android 13 (one ui 5). We were not able to reproduce the issue in Pixel 4 with Android 13. When the user closes the activity it still in memory even after onDestroy. We have noticed an increase in ANRs to the affected devices.
The Android code to reproduce the issue and leakcanary`s report are bellow. (We just have to inflate a layout that has webview and close the activity, so the memory leak happens)
AndroidManifest.xml
<activity
android:name=".HomeActivity"
android:exported="false"
android:theme="@style/AppTheme.NoActionBar">
</activity>
activity_home.xml (Here we have the tag for webview component)
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="br.com.gabba.myapp.presentation.WebViewActivity"
tools:viewBindingIgnore="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
android:background="@drawable/bg_header"
android:gravity="bottom"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:paddingStart="8dp"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/app_name"
app:titleTextAppearance="@style/Toolbar.TitleTextExterno" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
HomeActivity.kt - All we need to have the issue is inflate the layout with the webview.
package br.com.gabba.myapp.presentation
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import br.com.gabba.myapp.R
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
}
}
app/build.gradle
(...)
implementation 'androidx.activity:activity-ktx:1.6.1'
(...)
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
LeakCanary report with the issue:
│ GC Root: Global variable in native code
│
├─ J1 instance
│ Leaking: UNKNOWN
│ Retaining 137,3 kB in 1971 objects
│ ↓ WindowAndroid.p
│ ~
├─ b30 instance
│ Leaking: UNKNOWN
│ Retaining 136,2 kB in 1930 objects
│ ↓ b30.b
│ ~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: YES (Window#mDestroyed is true)
│ Retaining 136,2 kB in 1929 objects
│ mContext instance of br.com.gabba.myapp.presentation.HomeActivity with mDestroyed = true
│ mOnWindowDismissedCallback instance of br.com.gabba.myapp.presentation.HomeActivity with mDestroyed = true
│ ↓ Window.mContext
╰→ br.com.gabba.myapp.presentation.HomeActivity instance
Leaking: YES (ObjectWatcher was watching this because br.com.gabba.myapp.presentation.HomeActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 6,8 kB in 245 objects
key = afa4c248-0006-4b43-9660-d2131e74893a
watchDurationMillis = 8817
retainedDurationMillis = 3814
mApplication instance of br.com.gabba.myapp.myappApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
METADATA
Build.VERSION.SDK_INT: 33
Build.MANUFACTURER: samsung
LeakCanary version: 2.10
App process name: br.com.gabba.myapp
Class count: 29960
Instance count: 230083
Primitive array count: 152647
Object array count: 33977
Thread count: 72
Heap total bytes: 32257183
Bitmap count: 7
Bitmap total bytes: 5674439
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: closed /data/user/0/br.com.gabba.myapp/databases/google_app_measurement_local.db
Db 2: open /data/user/0/br.com.gabba.myapp/databases/leaks.db
Db 3: open /data/user/0/br.com.gabba.myapp/databases/com.microsoft.appcenter.persistence
Db 4: open /data/user/0/br.com.gabba.myapp/no_backup/androidx.work.workdb
Db 5: open /data/user/0/br.com.gabba.myapp/databases/com.google.android.datatransport.events
Stats: LruCache[maxSize=3000,hits=119796,misses=220169,hitRate=35%]
RandomAccess[bytes=11094589,reads=220169,travel=116557590219,range=37155332,size=47075038]
Analysis duration: 10730 ms
Thanks to @PaolinoLAngeletti comment we figure out that putting this line - - webView.destroy(); - on activity ondestroy method seems to solve the issue.
(...)
this.webView = findViewById(R.id.webview); //get a reference to webview
(...)
@Override
protected void onDestroy() {
try {
if (webView != null) {
webView.setWebViewClient(null);
webView.setWebChromeClient(null);
webView.clearHistory();
webView.clearCache(true);
webView.destroy(); //this was added
webview = null;
}
} catch (Exception e) {
Log.e(TAG, "onDestroy", e);
}
super.onDestroy();
}