Skip to content

Commit 5b32662

Browse files
committed
perf(integrations): Use single lifecycle observer
1 parent 83d80d7 commit 5b32662

File tree

6 files changed

+251
-263
lines changed

6 files changed

+251
-263
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ static void loadDefaultAndMetadataOptions(
128128
options.setCacheDirPath(getCacheDir(context).getAbsolutePath());
129129

130130
readDefaultOptionValues(options, context, buildInfoProvider);
131+
AppState.getInstance().addLifecycleObserver(options);
131132
}
132133

133134
@TestOnly

sentry-android-core/src/main/java/io/sentry/android/core/AppLifecycleIntegration.java

Lines changed: 26 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
import androidx.lifecycle.ProcessLifecycleOwner;
66
import io.sentry.IScopes;
7+
import io.sentry.ISentryLifecycleToken;
78
import io.sentry.Integration;
89
import io.sentry.SentryLevel;
910
import io.sentry.SentryOptions;
1011
import io.sentry.android.core.internal.util.AndroidThreadChecker;
12+
import io.sentry.util.AutoClosableReentrantLock;
1113
import io.sentry.util.Objects;
1214
import java.io.Closeable;
1315
import java.io.IOException;
@@ -17,20 +19,11 @@
1719

1820
public final class AppLifecycleIntegration implements Integration, Closeable {
1921

22+
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
2023
@TestOnly @Nullable volatile LifecycleWatcher watcher;
2124

2225
private @Nullable SentryAndroidOptions options;
2326

24-
private final @NotNull MainLooperHandler handler;
25-
26-
public AppLifecycleIntegration() {
27-
this(new MainLooperHandler());
28-
}
29-
30-
AppLifecycleIntegration(final @NotNull MainLooperHandler handler) {
31-
this.handler = handler;
32-
}
33-
3427
@Override
3528
public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
3629
Objects.requireNonNull(scopes, "Scopes are required");
@@ -55,85 +48,47 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
5548

5649
if (this.options.isEnableAutoSessionTracking()
5750
|| this.options.isEnableAppLifecycleBreadcrumbs()) {
58-
try {
59-
Class.forName("androidx.lifecycle.DefaultLifecycleObserver");
60-
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
61-
if (AndroidThreadChecker.getInstance().isMainThread()) {
62-
addObserver(scopes);
63-
} else {
64-
// some versions of the androidx lifecycle-process require this to be executed on the main
65-
// thread.
66-
handler.post(() -> addObserver(scopes));
51+
try (final ISentryLifecycleToken ignored = lock.acquire()) {
52+
if (watcher != null) {
53+
return;
6754
}
68-
} catch (ClassNotFoundException e) {
69-
options
70-
.getLogger()
71-
.log(
72-
SentryLevel.WARNING,
73-
"androidx.lifecycle is not available, AppLifecycleIntegration won't be installed");
74-
} catch (IllegalStateException e) {
75-
options
76-
.getLogger()
77-
.log(SentryLevel.ERROR, "AppLifecycleIntegration could not be installed", e);
78-
}
79-
}
80-
}
8155

82-
private void addObserver(final @NotNull IScopes scopes) {
83-
// this should never happen, check added to avoid warnings from NullAway
84-
if (this.options == null) {
85-
return;
86-
}
56+
watcher =
57+
new LifecycleWatcher(
58+
scopes,
59+
this.options.getSessionTrackingIntervalMillis(),
60+
this.options.isEnableAutoSessionTracking(),
61+
this.options.isEnableAppLifecycleBreadcrumbs());
8762

88-
watcher =
89-
new LifecycleWatcher(
90-
scopes,
91-
this.options.getSessionTrackingIntervalMillis(),
92-
this.options.isEnableAutoSessionTracking(),
93-
this.options.isEnableAppLifecycleBreadcrumbs());
63+
AppState.getInstance().addAppStateListener(watcher);
64+
}
9465

95-
try {
96-
ProcessLifecycleOwner.get().getLifecycle().addObserver(watcher);
9766
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration installed.");
9867
addIntegrationToSdkVersion("AppLifecycle");
99-
} catch (Throwable e) {
100-
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
101-
// connection with conflicting dependencies of the androidx.lifecycle.
102-
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
103-
watcher = null;
104-
options
105-
.getLogger()
106-
.log(
107-
SentryLevel.ERROR,
108-
"AppLifecycleIntegration failed to get Lifecycle and could not be installed.",
109-
e);
11068
}
11169
}
11270

11371
private void removeObserver() {
114-
final @Nullable LifecycleWatcher watcherRef = watcher;
72+
final @Nullable LifecycleWatcher watcherRef;
73+
try (final ISentryLifecycleToken ignored = lock.acquire()) {
74+
watcherRef = watcher;
75+
watcher = null;
76+
}
77+
11578
if (watcherRef != null) {
116-
ProcessLifecycleOwner.get().getLifecycle().removeObserver(watcherRef);
79+
AppState.getInstance().removeAppStateListener(watcherRef);
11780
if (options != null) {
11881
options.getLogger().log(SentryLevel.DEBUG, "AppLifecycleIntegration removed.");
11982
}
12083
}
121-
watcher = null;
12284
}
12385

12486
@Override
12587
public void close() throws IOException {
126-
if (watcher == null) {
127-
return;
128-
}
129-
if (AndroidThreadChecker.getInstance().isMainThread()) {
130-
removeObserver();
131-
} else {
132-
// some versions of the androidx lifecycle-process require this to be executed on the main
133-
// thread.
134-
// avoid method refs on Android due to some issues with older AGP setups
135-
// noinspection Convert2MethodRef
136-
handler.post(() -> removeObserver());
137-
}
88+
removeObserver();
89+
// TODO: probably should move it to Scopes.close(), but that'd require a new interface and
90+
// different implementations for Java and Android. This is probably fine like this too, because
91+
// integrations are closed in the same place
92+
AppState.getInstance().removeLifecycleObserver();
13893
}
13994
}

sentry-android-core/src/main/java/io/sentry/android/core/AppState.java

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
11
package io.sentry.android.core;
22

3+
import androidx.annotation.NonNull;
4+
import androidx.lifecycle.DefaultLifecycleObserver;
5+
import androidx.lifecycle.Lifecycle;
6+
import androidx.lifecycle.LifecycleOwner;
7+
import androidx.lifecycle.ProcessLifecycleOwner;
8+
import io.sentry.ILogger;
39
import io.sentry.ISentryLifecycleToken;
10+
import io.sentry.NoOpLogger;
11+
import io.sentry.SentryLevel;
12+
import io.sentry.android.core.internal.util.AndroidThreadChecker;
413
import io.sentry.util.AutoClosableReentrantLock;
14+
import java.io.Closeable;
15+
import java.io.IOException;
16+
import java.util.List;
17+
import java.util.concurrent.CopyOnWriteArrayList;
518
import org.jetbrains.annotations.ApiStatus;
619
import org.jetbrains.annotations.NotNull;
720
import org.jetbrains.annotations.Nullable;
821
import org.jetbrains.annotations.TestOnly;
922

1023
/** AppState holds the state of the App, e.g. whether the app is in background/foreground, etc. */
1124
@ApiStatus.Internal
12-
public final class AppState {
25+
public final class AppState implements Closeable {
1326
private static @NotNull AppState instance = new AppState();
1427
private final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
28+
volatile LifecycleObserver lifecycleObserver;
29+
MainLooperHandler handler = new MainLooperHandler();
1530

1631
private AppState() {}
1732

1833
public static @NotNull AppState getInstance() {
1934
return instance;
2035
}
2136

22-
private @Nullable Boolean inBackground = null;
37+
private volatile @Nullable Boolean inBackground = null;
2338

2439
@TestOnly
2540
void resetInstance() {
@@ -31,8 +46,156 @@ void resetInstance() {
3146
}
3247

3348
void setInBackground(final boolean inBackground) {
49+
this.inBackground = inBackground;
50+
}
51+
52+
void addAppStateListener(final @NotNull AppStateListener listener) {
53+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
54+
ensureLifecycleObserver(NoOpLogger.getInstance());
55+
56+
lifecycleObserver.listeners.add(listener);
57+
}
58+
}
59+
60+
void removeAppStateListener(final @NotNull AppStateListener listener) {
61+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
62+
if (lifecycleObserver != null) {
63+
lifecycleObserver.listeners.remove(listener);
64+
}
65+
}
66+
}
67+
68+
void addLifecycleObserver(final @Nullable SentryAndroidOptions options) {
69+
if (lifecycleObserver != null) {
70+
return;
71+
}
72+
3473
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
35-
this.inBackground = inBackground;
74+
ensureLifecycleObserver(options != null ? options.getLogger() : NoOpLogger.getInstance());
75+
}
76+
}
77+
78+
private void ensureLifecycleObserver(final @NotNull ILogger logger) {
79+
if (lifecycleObserver != null) {
80+
return;
3681
}
82+
try {
83+
Class.forName("androidx.lifecycle.ProcessLifecycleOwner");
84+
// create it right away, so it's available in addAppStateListener in case it's posted to main thread
85+
lifecycleObserver = new LifecycleObserver();
86+
87+
if (AndroidThreadChecker.getInstance().isMainThread()) {
88+
addObserverInternal(logger);
89+
} else {
90+
// some versions of the androidx lifecycle-process require this to be executed on the main
91+
// thread.
92+
handler.post(() -> addObserverInternal(logger));
93+
}
94+
} catch (ClassNotFoundException e) {
95+
logger
96+
.log(
97+
SentryLevel.WARNING,
98+
"androidx.lifecycle is not available, some features might not be properly working,"
99+
+ "e.g. Session Tracking, Network and System Events breadcrumbs, etc.");
100+
} catch (Throwable e) {
101+
logger
102+
.log(
103+
SentryLevel.ERROR,
104+
"AppState could not register lifecycle observer",
105+
e);
106+
}
107+
}
108+
109+
private void addObserverInternal(final @NotNull ILogger logger) {
110+
final @Nullable LifecycleObserver observerRef = lifecycleObserver;
111+
try {
112+
// might already be unregistered/removed so we have to check for nullability
113+
if (observerRef != null) {
114+
ProcessLifecycleOwner.get().getLifecycle().addObserver(observerRef);
115+
}
116+
} catch (Throwable e) {
117+
// This is to handle a potential 'AbstractMethodError' gracefully. The error is triggered in
118+
// connection with conflicting dependencies of the androidx.lifecycle.
119+
// //See the issue here: https://github.com/getsentry/sentry-java/pull/2228
120+
lifecycleObserver = null;
121+
logger
122+
.log(
123+
SentryLevel.ERROR,
124+
"AppState failed to get Lifecycle and could not install lifecycle observer.",
125+
e);
126+
}
127+
}
128+
129+
void removeLifecycleObserver() {
130+
if (lifecycleObserver == null) {
131+
return;
132+
}
133+
134+
final @Nullable LifecycleObserver ref;
135+
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
136+
ref = lifecycleObserver;
137+
lifecycleObserver.listeners.clear();
138+
lifecycleObserver = null;
139+
}
140+
141+
if (AndroidThreadChecker.getInstance().isMainThread()) {
142+
removeObserverInternal(ref);
143+
} else {
144+
// some versions of the androidx lifecycle-process require this to be executed on the main
145+
// thread.
146+
// avoid method refs on Android due to some issues with older AGP setups
147+
// noinspection Convert2MethodRef
148+
handler.post(() -> removeObserverInternal(ref));
149+
}
150+
}
151+
152+
private void removeObserverInternal(final @Nullable LifecycleObserver ref) {
153+
if (ref != null) {
154+
ProcessLifecycleOwner.get().getLifecycle().removeObserver(ref);
155+
}
156+
}
157+
158+
@Override
159+
public void close() throws IOException {
160+
removeLifecycleObserver();
161+
}
162+
163+
static final class LifecycleObserver implements DefaultLifecycleObserver {
164+
final List<AppStateListener> listeners = new CopyOnWriteArrayList<AppStateListener>() {
165+
@Override
166+
public boolean add(AppStateListener appStateListener) {
167+
// notify the listeners immediately to let them "catch up" with the current state (mimics the behavior of androidx.lifecycle)
168+
Lifecycle.State currentState = ProcessLifecycleOwner.get().getLifecycle().getCurrentState();
169+
if (currentState.isAtLeast(Lifecycle.State.STARTED)) {
170+
appStateListener.onForeground();
171+
} else {
172+
appStateListener.onBackground();
173+
}
174+
return super.add(appStateListener);
175+
}
176+
};
177+
178+
@Override
179+
public void onStart(@NonNull LifecycleOwner owner) {
180+
for (AppStateListener listener : listeners) {
181+
listener.onForeground();
182+
}
183+
AppState.getInstance().setInBackground(false);
184+
}
185+
186+
@Override
187+
public void onStop(@NonNull LifecycleOwner owner) {
188+
for (AppStateListener listener : listeners) {
189+
listener.onBackground();
190+
}
191+
AppState.getInstance().setInBackground(true);
192+
}
193+
}
194+
195+
// If necessary, we can adjust this and add other callbacks in the future
196+
public interface AppStateListener {
197+
void onForeground();
198+
199+
void onBackground();
37200
}
38201
}

0 commit comments

Comments
 (0)