Unity and ANR

pavelb

Pavel

Posted on April 3, 2024

Unity and ANR

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:

Image description

Here is the code from Android Studio:
Image description

    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);
            }

        }
    }
Enter fullscreen mode Exit fullscreen mode

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.");
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
                            }
                        }
                    };
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
pavelb
Pavel

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