Unity and ANR
Pavel
Posted on April 3, 2024
My story is about how, by using a closed component, in simple terms a "black box" in the form of an SDK, frameworks, or any other module in our product, we end up in a tricky circle because we are dependent on this "black box".
In my products, I encountered ANR issues, as I use Unity. Therefore, I will further discuss the problems specifically related to Unity within the Android ecosystem.
In 99% of Unity projects, when exporting an Android project, the default UnityPlayerActivity
is used. The proxy engine part is located in com.unity3d.player.UnityPlayer
. So, using Android Studio, we can inspect what's inside UnityPlayer
, let's go dipper.
public class UnityPlayer extends FrameLayout implements IUnityPlayerLifecycleEvents
and another 1500 lines of code.
Let's start with something interesting, an example of ANR from UnityPlayer:
Here is the code from Android Studio:
private void pauseUnity() {
this.reportSoftInputStr((String)null, 1, true);
if (this.mState.f()) {
if (m.c()) {
final Semaphore var1 = new Semaphore(0);
Runnable var2;
if (this.isFinishing()) {
var2 = new Runnable() {
public final void run() {
UnityPlayer.this.shutdown();
var1.release();
}
};
} else {
var2 = new Runnable() {
public final void run() {
if (UnityPlayer.this.nativePause()) {
UnityPlayer.this.mQuitting = true;
UnityPlayer.this.shutdown();
var1.release(2);
} else {
var1.release();
}
}
};
}
this.m_MainThread.a(var2);
try {
if (!var1.tryAcquire(4L, TimeUnit.SECONDS)) {
com.unity3d.player.f.Log(5, "Timeout while trying to pause the Unity Engine.");
}
} catch (InterruptedException var3) {
com.unity3d.player.f.Log(5, "UI thread got interrupted while trying to pause the Unity Engine.");
}
if (var1.drainPermits() > 0) {
this.destroy();
}
}
this.mState.d(false);
this.mState.b(true);
if (this.m_AddPhoneCallListener) {
this.m_TelephonyManager.listen(this.m_PhoneCallListener, 0);
}
}
}
1) The first thing that catches your eye is:
if (!var1.tryAcquire(4L, TimeUnit.SECONDS)) {
com.unity3d.player.f.Log(5, "Timeout while trying to pause the Unity Engine.");
}
The tryAcquire() method with a timeout of 4 seconds is a blocking call. If this operation takes longer than the specified timeout, it can cause an ANR.
2) If the destroy() method involves heavy operations, it can also cause an ANR.
if (var1.drainPermits() > 0) {
this.destroy();
}
3) In short, each line of code below performs a heavy operation on the main thread.
var2 = new Runnable() {
public final void run() {
if (UnityPlayer.this.nativePause()) {
UnityPlayer.this.mQuitting = true;
UnityPlayer.this.shutdown();
var1.release(2);
} else {
var1.release();
}
}
};
Conclusion:
As you may have noticed, the main issue in this code is the handling of events in the main thread and proxying calls from the main thread to native code. Please note, this is not about writing to a file or downloading data from the internet. The way to solve these issues can be through CountDownLatch
, Thread
, HandlerThread
, and many other built-in mechanisms.
Happy coding!
Thanks for reading!
If you enjoyed this article you can like it by clicking on the👏 button (up to 100 times!), also you can share this article to help others.
Have you any feedback, feel free to reach me on twitter.
Posted on April 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024
November 30, 2024