Unity Android符号表解析崩溃日志

我们在将Unity打包成il2cpp+release模式的apk的之后,内部的代码是会被打包成so文件的,这个东西其实是linux的动态链接库的格式,对应的就是windows的dll或者Mac的dylib之类的东西。在android这边也可以叫做符号表,因为它内部会保存虚拟内存地址与汇编代码的的对应关系。

这样打包以后,会降低我们的包体积,而且有助于我们的代码保护不被抄袭,但是同时也会导致我们开发者看不懂报错信息,因为这些报错信息都是些虚拟内存地址,而不是函数的名字,那么对于线上发生的报错信息,我们怎么去阅读呢?

其实刚才我们就说过了,这个so文件不仅是动态链接库,内部还保存着虚拟内存地址与函数之间的映射关系,所以我们要读懂这些报错信息,还需要借助这个动态链接库。

接下来我们就讲一下,如何利用这so文件

打包过程中保存so文件

打包android项目,我们是可以选择直接导出为apk或者导出为gradle项目的,如果我们在打包位apk的时候,勾选打包设置Create Symbols.zip,就会在apk的同级目录下将所有的so文件保存下来

而如果选择的是导出为gradle项目,并且架构选择是arm64-v8a,那么我们自己的代码因为选择了il2cpp,就会将我们的代码打包为libil2cpp.so,作为动态链接库存放在gradle项目的的unityLibrary/src/main/jniLibs/arm64-v8a下面

这里解释一下这个路径,如果熟悉安卓开发的同学会对这个路径很敏感,这个路径基本就是普通安卓项目默认的动态链接库的路径,jniLibs,拆开是jni + libs,jni的意思是java native interface,就是java的原生方法的意思,后面那个文件夹是架构,在不同架构的手机上,会调用不同的文件夹下的动态链接库。如果只选择了arm64-v8a,那么这个apk就只能运行在改架构下的手机上。

除了我们自己代码会打包成libil2cpp.so,unity自身引擎代码,或者第三方包的代码,也会打包成各种so文件,如libunity.so, 但是这些文件是不在这个目录下的,我们有两个地方可以找到这些so文件,一个就是unityLibrary/src/symbols/arm64-v8a,另一种就是在Unity项目的/Temp/StagingArea/libs/下面,我们需要在打包完成之后把它们复制出来。

这里需要注意一点,如果是用editor来手动build,那么手动拷贝就好,但是如果是通过命令行batchmode的方式打包,就要注意,如果想要BuildPlayer方法运行完成以后再去复制,是获取不到的,因为一旦editor关闭,Temp文件夹就会被删除。

这个时候我们就需要用到PostProcessBuildAttribute这个hook,在Build完成之后,退出editor之前加上一段逻辑去保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using UnityEngine;
using System.Collections;
using UnityEditor.Callbacks;
using UnityEditor;
using System.IO;
using System;
public class PostProcessBuild
{
[PostProcessBuildAttribute()]
public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject)
{
if (target == BuildTarget.Android)
PostProcessAndroidBuild(pathToBuiltProject);
}

public static void PostProcessAndroidBuild(string pathToBuiltProject)
{
UnityEditor.ScriptingImplementation backend = UnityEditor.PlayerSettings.GetScriptingBackend(UnityEditor.BuildTargetGroup.Android);

if (backend == UnityEditor.ScriptingImplementation.IL2CPP)
{
CopyARMSymbols("armeabi-v7a");
CopyARMSymbols("arm64-v8a");
}
}


private static void CopyARMSymbols(string target)
{
const string libpath = "/Temp/StagingArea/libs/";
string sourcePath = Directory.GetCurrentDirectory() + libpath + $"{target}/";
string desPath = Directory.GetCurrentDirectory() + $"/Build/android/unityLibrary/symbols/{target}";
if (!Directory.Exists(desPath))
{
Directory.CreateDirectory(desPath);
}
CopyDirectory(sourcePath, desPath);
}

private static void CopyDirectory(string sourcePath, string targetPath)
{

if (System.IO.Directory.Exists(sourcePath))
{
string[] files = System.IO.Directory.GetFiles(sourcePath);

// Copy the files and overwrite destination files if they already exist.
foreach (string s in files)
{
// Use static Path methods to extract only the file name from the path.
var fileName = System.IO.Path.GetFileName(s);
var destFile = System.IO.Path.Combine(targetPath, fileName);
Debug.Log($"fileName: {fileName}, destFile: {destFile}");
System.IO.File.Copy(s, destFile, true);
}
}
else
{
Console.WriteLine("Source path does not exist!");
}
}

}

使用保存的so去解析日志

有了so之后,我们就可以解析全是地址的日志了。

因为这个其实是android的动态链接库,所以解析需要用到android的工具:addr2line,我是直接用了unity安装的android工具,而且根据架构不同,要用不同的工具,因为我的是arm64-v8a,我用到的是aarch64-linux-android-addr2line,具体路径每个人电脑不同,找到就好

我们可以直接在命令行运行:

1
aarch64-linux-android-addr2line -f -C -e libil2cpp.so 日志中的地址

当然也可以通过代码去做这件事,因为一行行解析很费劲,我这里只用了lib2cpp.so和libunity.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import sys
import os

def OutCrash(filename, version, target):

addr2linePath = "/Applications/Unity/Hub/Editor/2020.3.20f1/PlaybackEngines/AndroidPlayer/NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -f -C -e "
il2cppdebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libil2cpp.so "
unitydebugsoPath = "/Users/bytedance/Desktop/Unity/Android/symbols/" + version + "/" + target + "/libunity.so "

f = open(filename,'r')
logstr = f.readlines()
il2cppflag = 'libil2cpp'
unityflag = 'libunity'
crashEndFlag = 'libunity.so\n'
for log in logstr:
OutCmd(log,addr2linePath,crashEndFlag,unitydebugsoPath)
OutCmd(log,addr2linePath,il2cppflag,il2cppdebugsoPath)
OutCmd(log,addr2linePath,unityflag,unitydebugsoPath)

def OutCmd(log,addr2linePath,debugFlagStr,debugsoPath):

if log.endswith(debugFlagStr):
startIndex = log.index(' pc ')
endflag = log.index(r' /data/')
addstr = log[startIndex+4:endflag]
print(addstr)
cmdstr = addr2linePath +debugsoPath+addstr
os.system(cmdstr)
else:
unitystart = log.find(debugFlagStr)
if unitystart >= 0:
unitylen = log.index(debugFlagStr)
unitylen = unitylen + len(debugFlagStr) +1
endlen = log.find('(')
if endlen >= 0:
endIndex = log.index('(')
addstr = log[unitylen:endIndex]
addstr = addstr.replace(' ','')

cmdstr = addr2linePath + debugsoPath +addstr
print(addstr)
os.system(cmdstr)

OutCrash(sys.argv[1], sys.argv[2], sys.argv[3])