Skip to content

Instantly share code, notes, and snippets.

@sandin
Last active April 25, 2021 04:42
Show Gist options
  • Save sandin/43e09910ef118e19690441d74efc5bf3 to your computer and use it in GitHub Desktop.
Save sandin/43e09910ef118e19690441d74efc5bf3 to your computer and use it in GitHub Desktop.
native crash handler patch for unity 2019
package com.unity3d.player;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class UnityNativeCrashHandlerPatch {
public static void setupNativeCrashHandler() {
try {
Object unityPlayer = getUnityPlayer();
if (unityPlayer != null) {
Class<?> unityPlayerCls = unityPlayer.getClass();
// String originLaunchUrl = unityPlayer.getLaunchURL();
Method getLaunchURLMethod = unityPlayerCls.getDeclaredMethod("getLaunchURL");
getLaunchURLMethod.setAccessible(true);
String originLaunchUrl = (String) getLaunchURLMethod.invoke(unityPlayer);
// unityPlayer.nativeSetLaunchURL(originLaunchUrl);
Method nativeSetLaunchURLMethod = unityPlayerCls.getDeclaredMethod("nativeSetLaunchURL", String.class);
nativeSetLaunchURLMethod.setAccessible(true);
nativeSetLaunchURLMethod.invoke(unityPlayer, originLaunchUrl); // will invoke JNI_METHOD_IMPL -> sigaction() to setup native crash handler for unity
Log.d("UnityUtils", "nativeSetLaunchURL success, launchUrl=" + originLaunchUrl);
}
} catch (Throwable e) {
Log.e("UnityUtils", e.getMessage());
}
}
private static Object getUnityPlayer() {
try {
Object unityPlayerActivity = getUnityPlayerActivity();
if (unityPlayerActivity != null) {
Class<?> unityPlayerActivityCls = Class.forName("com.unity3d.player.UnityPlayerActivity");
if (unityPlayerActivityCls.isInstance(unityPlayerActivity)) {
Field unityPlayerField = unityPlayerActivityCls.getDeclaredField("mUnityPlayer");
unityPlayerField.setAccessible(true);
return unityPlayerField.get(unityPlayerActivity);
} else {
Log.e("UnityUtils", unityPlayerActivity + " is not a instanceof com.unity3d.player.UnityPlayerActivity, maybe it's not a Unity Application?");
}
}
} catch (Throwable e) {
Log.e("UnityUtils", e.getMessage());
}
return null;
}
private static Object getUnityPlayerActivity() {
try {
Class<?> unityPlayerCls = Class.forName("com.unity3d.player.UnityPlayer");
Field unityPlayerActivityField = unityPlayerCls.getField("currentActivity");
return unityPlayerActivityField.get(null);
} catch (Throwable e) {
Log.e("UnityUtils", e.getMessage());
}
return null;
}
}
@sandin
Copy link
Author

sandin commented Apr 12, 2021

风险说明

NOTE: 需要特别注意 的是如果今后升级Unity引擎,Unity新版本中涉及到的函数实现逻辑发生变化时,则可能引起副作用(包括 可能会引起启动崩溃 ),故升级引擎后需观察日志确认该补丁是否工作正常。

背景说明

很多Unity项目的native crash会显示 java.lang.Error ,这是因为Unity引擎会优先捕捉到native crash的信号量,并将其封装成一个Java异常抛出,因此其他崩溃信号处理器则会捕捉到一个Java异常,但其堆栈确是native堆栈,并且只有函数地址,并且因为此时捕捉到的已经被Unity封装过的Java异常了,并没有详细崩溃dump信息了,因此无法使用符号文件对函数堆栈再次解析符号。

在Unity 2019之前,Unity会在 UnityPlayerActivity.onCreate() 方法中实例化 new UnityPlayer() , 在该构造器中Unity会调用JNI方法,并注册崩溃信号处理器,因此在一般情况下,其他崩溃信号处理器只需要在其后注册,则可以先捕捉到崩溃信号,而如果让Unity先捕捉到,则Unity会将Native Crash包装成一个Java Exception抛出到上层进行捕捉,而在这个过程中,会使得其他崩溃信号处理器无法得到原始的崩溃dump信息,因此无法用符号文件去解析堆栈信息。

而在Unity 2019之后,Unity不会在 new UnityPlayer() 的构造器中注册崩溃信号处理器,而是在 UnityPlayerActivity.onResume() 函数中再通过handler异步去注册,因此无法准确掌握其注册时机,导致无法确保在其后面注册以便先捕捉到崩溃信号。因为C#的虚拟机也使用了崩溃信号来实现了C#的异常机制,因此必须在Unity注册之后,Mono注册之前注册信号处理器才不会引起副作用。

补丁说明

因此针对特定的Unity版本,需要在初始化崩溃处理器之前,主动通过反射来让Unity先注册崩溃信号处理器,以便绕过这个问题,但是该解决方案必须在对Unity注册信号处理的行为完全掌握的情况下(一般情况下需要拥有Unity源码),才能准确的通过Java反射来调用Unity的底层API来实现,并不产生任何的其他副作用。

因为Unity是在第一次调用JNI的native方法时注册信号处理器的,因此这里补丁的办法就是在 new UnityPlayer() 之后,主动通过JAVA反射来调用 UnityPlayer 的一个JNI native方法,这里选择的是 nativeSetLaunchURL 方法,该方法是用来设置 Application.absoluteURL 的值,在游戏启动的时候,该值默认为空,因此将其重新设置一下不会引起其他副作用的目的。

该补丁通过反射执行的Java代码如下:

String originLaunchUrl = unityPlayer.getLaunchURL();
unityPlayer.nativeSetLaunchURL(originLaunchUrl);

该代码的逻辑是先通过GET方法获取launchURL(此时为null),然后再通过SET方法来重新设置一遍launchURL,因为SET方法是native方法,因此会触发Unity引擎注册native崩溃信号处理器的逻辑。

因此这个补丁的唯一要求:

  1. UnityPlayer.java 中存在 getLaunchURL()nativeSetLaunchURL(String url) 这2个方法。(可通过反编译apk来确认这2个方法是否存在于该Unity版本中)

使用说明

UnityPatch.java 文件拷贝到Unity导出的Android工程中(默认可放在 com.unity3d.player 包名下,也可以修改源码第一行的package名称后放置在任何包名下),并在 UnityPlayerActivity.onCreate() 方法之后,初始化其他崩溃捕捉器(如Crasheye,Bugly等)之前调用该补丁即可。

NOTE:必须确保调用补丁代码的时机,必须在 new UnityPlayer() 之后,初始化其他崩溃捕捉器(如Crasheye,Bugly等)之间的时间段内才能有效。

代码示例:

public class MainActivity extends UnityPlayerActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        UnityNativeCrashHandlerPatch.setupNativeCrashHandler();
        Crasheye.initWithNativeHandle(this, "22979bc0");
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment