Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android Leak Memory on Purchases.getOfferings() #687

Open
11 tasks done
meomap opened this issue May 11, 2023 · 3 comments
Open
11 tasks done

Android Leak Memory on Purchases.getOfferings() #687

meomap opened this issue May 11, 2023 · 3 comments
Labels
bug Something isn't working

Comments

@meomap
Copy link

meomap commented May 11, 2023

Environment

  • Output of flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.7.12, on macOS 12.6.3 21G419 darwin-x64)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.2)
[✓] VS Code (version 1.78.1)
[✓] Connected device (2 available)
[✓] HTTP Host Availability
  • Version of purchases-flutter
    4.12.0

  • Testing device version e.g.: Android API 28

  • How often the issue occurs - Problem is reproducible in dev

  • [Debug logs]

┬───
│ GC Root: Input or output parameters in native code
│
├─ android.os.MessageQueue instance
│    Leaking: NO (MessageQueue#mQuitting is false)
│    HandlerThread: "main"
│    ↓ MessageQueue[1]
│                  ~~~
├─ android.os.Message instance
│    Leaking: UNKNOWN
│    Retaining 547 B in 21 objects
│    Message.what = 0
│    Message.when = 26437502 (267 ms after heap dump)
│    Message.obj = null
│    Message.callback = instance @325425960 of com.android.billingclient.api.zzz
│    Message.target = instance @325426040 of android.os.Handler
│    ↓ Message.callback
│              ~~~~~~~~
├─ com.android.billingclient.api.zzz instance
│    Leaking: UNKNOWN
│    Retaining 462 B in 19 objects
│    ↓ zzz.zzb
│          ~~~
├─ com.android.billingclient.api.zzy instance
│    Leaking: UNKNOWN
│    Retaining 418 B in 17 objects
│    ↓ zzy.zza
│          ~~~
├─ com.revenuecat.purchases.google.BillingWrapper$$ExternalSyntheticLambda5
│  instance
│    Leaking: UNKNOWN
│    Retaining 406 B in 16 objects
│    ↓ BillingWrapper$$ExternalSyntheticLambda5.f$3
│                                               ~~~
├─ com.revenuecat.purchases.google.
│  BillingWrapper$querySkuDetailsAsync$1$1$$ExternalSyntheticLambda0 instance
│    Leaking: UNKNOWN
│    Retaining 297 B in 10 objects
│    ↓ BillingWrapper$querySkuDetailsAsync$1$1$$ExternalSyntheticLambda0.f$1
│                                                                        ~~~
├─ com.revenuecat.purchases.Purchases$getSkuDetails$1 instance
│    Leaking: UNKNOWN
│    Retaining 28 B in 1 objects
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ Purchases$getSkuDetails$1.$onCompleted
│                                ~~~~~~~~~~~~
├─ com.revenuecat.purchases.Purchases$fetchAndCacheOfferings$1$1 instance
│    Leaking: UNKNOWN
│    Retaining 24 B in 1 objects
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ Purchases$fetchAndCacheOfferings$1$1.$completion
│                                           ~~~~~~~~~~~
├─ com.revenuecat.purchases.ListenerConversionsKt$receiveOfferingsCallback$1
│  instance
│    Leaking: UNKNOWN
│    Retaining 56.4 kB in 1095 objects
│    Anonymous class implementing com.revenuecat.purchases.interfaces.
│    ReceiveOfferingsCallback
│    ↓ ListenerConversionsKt$receiveOfferingsCallback$1.$onError
│                                                       ~~~~~~~~
├─ com.revenuecat.purchases.hybridcommon.CommonKt$getOfferings$1 instance
│    Leaking: UNKNOWN
│    Retaining 16 B in 1 objects
│    Anonymous subclass of kotlin.jvm.internal.Lambda
│    ↓ CommonKt$getOfferings$1.$onResult
│                              ~~~~~~~~~
├─ com.revenuecat.purchases_flutter.PurchasesFlutterPlugin$4 instance
│    Leaking: UNKNOWN
│    Retaining 56.4 kB in 1092 objects
│    Anonymous class implementing com.revenuecat.purchases.hybridcommon.OnResult
│    ↓ PurchasesFlutterPlugin$4.val$result
│                               ~~~~~~~~~~
├─ io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler$1 instance
│    Leaking: UNKNOWN
│    Retaining 56.4 kB in 1091 objects
│    Anonymous class implementing io.flutter.plugin.common.MethodChannel$Result
│    ↓ MethodChannel$IncomingMethodCallHandler$1.val$reply
│                                                ~~~~~~~~~
├─ io.flutter.embedding.engine.dart.DartMessenger$Reply instance
│    Leaking: UNKNOWN
│    Retaining 32 B in 2 objects
│    ↓ DartMessenger$Reply.flutterJNI
│                          ~~~~~~~~~~
├─ io.flutter.embedding.engine.FlutterJNI instance
│    Leaking: UNKNOWN
│    Retaining 232 B in 15 objects
│    ↓ FlutterJNI.localizationPlugin
│                 ~~~~~~~~~~~~~~~~~~
├─ io.flutter.plugin.localization.LocalizationPlugin instance
│    Leaking: UNKNOWN
│    Retaining 32 B in 2 objects
│    context instance of com.my.app.MainActivity with mDestroyed
│    = true
│    ↓ LocalizationPlugin.context
│                         ~~~~~~~
╰→ com.my.app.MainActivity instance
​     Leaking: YES (ObjectWatcher was watching this because com.my.
​     app.MainActivity received Activity#onDestroy() callback and
​     Activity#mDestroyed is true)
​     Retaining 47.0 kB in 710 objects
​     key = 83b0362b-bc37-4734-aa21-79b3af57c8e4
​     watchDurationMillis = 5604
​     retainedDurationMillis = 582
​     mApplication instance of android.app.Application
​     mBase instance of android.app.ContextImpl
  • Steps to reproduce:
  1. Install leakcanary debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
  2. Fetch offering Offerings offerings = await Purchases.getOfferings()
  3. Open then close the app

Other information

Problem seems gone when I replaced getOnResult(result) with a dedicated Result var for getOffering that can be set null when engine detached

Describe the bug

Plugin leaked main activity context

@meomap meomap added the bug Something isn't working label May 11, 2023
@RCGitBot
Copy link
Contributor

RCGitBot commented May 11, 2023

👀 SDKONCALL-276 We've just linked this issue to our internal tracker and notified the team. Thank you for reporting, we're checking this out!

@vegaro
Copy link
Contributor

vegaro commented May 11, 2023

Thanks for reporting this issue. Can you please share a snippet with the changes you did to make the leak disappear? We are using Result in the same way in all our method calls so it's kinda weird this only happens for getOfferings. Are you accessing the activity in any way from your Flutter code?

@meomap
Copy link
Author

meomap commented May 12, 2023

@vegaro Sure here is my workaround

diff --git a/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java b/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java
index 42fe666..edaa1ca 100644
--- a/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java
+++ b/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java
@@ -52,6 +52,7 @@ public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler,
     @Nullable private Context applicationContext;
     @Nullable private MethodChannel channel;
     @Nullable private Activity activity;
+    @Nullable private Result offeringResult;
 
     private final Handler handler = new Handler(Looper.getMainLooper());
 
@@ -97,6 +98,7 @@ public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler,
         }
         this.channel = null;
         this.applicationContext = null;
+        this.offeringResult = null;
     }
 
     @Override
@@ -389,7 +391,28 @@ public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler,
     }
 
     private void getOfferings(final Result result) {
-        CommonKt.getOfferings(getOnResult(result));
+        offeringResult = result;
+        CommonKt.getOfferings(new OnResult() {
+            @Override
+            public void onReceived(Map<String, ?> map) {
+                synchronized(this) {
+                    if (offeringResult != null) {
+                        offeringResult.success(map);
+                    }
+                    offeringResult = null;
+                }
+            }
+
+            @Override
+            public void onError(ErrorContainer errorContainer) {
+                synchronized(this) {
+                    if (offeringResult != null) {
+                        reject(errorContainer, offeringResult);
+                    }
+                    offeringResult = null;
+                }
+            }
+        });
     }
 
     private void getProductInfo(ArrayList<String> productIDs, String type, final Result result) {

I use bunch of other plugins so I think they will also access the activity. I don't know but maybe the problem is at stream listener. It's weird that I don't see leak report anymore after applying this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants