unity crash 的快速定位
因为最近做的都是和unity作为一个库嵌入原生app的项目,一直以来饱受unity crash的折磨。中间碰到过很多崩溃,都是之前做游戏没有碰到过的,大多都是兼容性问题,还有一些是和原生层通信的方式不对导致的。不管是那种情况,都很让人崩溃,也由此熟练了crash日志定位的操作,今天在这里分享一下。首先untiy的日志分两种:
1.如果我们打包的时候勾选了 development build选项
那么崩溃的时候,我们基本上可以在日志里面直接看到unity的代码输出,这里不多做表述。
2.第二种情况就是我们最常见的情况了,因为我们上线的时候是不可能输出调试的包的,这时候崩溃我们看到的一堆地址,类似
第一反应就是 “什么鬼”。
OK,接下来我们就详细说下这种情况怎么处理
1.我们先说下如何导出这种日志,大多数时候崩溃是发生在测试的手机或者设备上面,我们不可能时刻观察他的日志输出,而且大多时候崩溃内容不会显示在控制台,通常会惯性几连发
测试同学:某某某,这个东西又崩溃了,赶紧过来看一下
你:你做了什么异常操作吗?
测试同学:没有
你:有错误输出吗?
测试同学:没有
你:。。。。。一脸懵逼 那看个毛,啥信息也没有
测试同学:可是崩溃了
你:。。。。。。无言以对
没错,此时的你束手无措,可是这就是你的锅,因为程序崩溃了,尤其是unity作为表现层,可能崩溃的是其他sdk的库或者是系统相关的,但是表现就是画面卡了,然后锅就是我们的,这可能就是作为前端开发的悲哀吧。好了,废话不多说,接下来我就来说下大家怎么甩锅吧
首先,使用adb命令把崩溃日志输出,命令如下:
adb shell dumpsys dropbox --print > log-crash.txt当然你要配置adb为环境变量,这是基本操作,不再赘述
2.这时候你就拿到了崩溃文件,通常操作是直接拉到文件最下方,文件通常是这样
大家如果看到unity或者包含il2cpp,不好意思,是你的锅没跑了,如果是下图这样
那么恭喜你,你可以甩锅了。。。
3.当我们清楚了是谁的锅之后,我们就可以根据文件内容定位问题了,那么首先请参考官方的两篇文章
还有一个补充的点就是,如果你打包的版本是il2cpp的,那么最新的unity(我用的是2018.4的版本)已经把符号表在打包工程的时候,自动打包成一个压缩包同时输出了,我们就不用费心的去找符号表了,如下图
解压就可以看到我们的符号表
通常到这一步,我们已经可以根据第一个链接的方法去根据符号表反射出unity的代码了,这时候就可以分析是什么原因了。
有好多人可能不知道 arm-linux-androideabi-addr2line这个东西是从哪里来的
就是这个东西,他是ndk里面包含的东西,大家下载后ndk之后解压就能找到了,这里给出ndk的下载地址
然后解压后找到路径 ,我的电脑上是在
这个路径在后面自动化输出的时候还会用到。我这里做一个最简单的演示
4.但是这时候有一个很恶心的问题,就是日志可能有几十行,如果我们要看完整的堆栈,我们就要一行一行输出,这当然不是一个程序员该做的事情,我们当然要自动化。我根据自己经常碰到的情况,写了个脚本,分别处理两种情况,一种是以libuntiy.so结尾的崩溃如图
还有一种就是libunity和il2cpp在中间的如图
OK,上代码
import sys
import os
def OutCrash(filename):
#addr2line 路径 (要替换成自己电脑的路径)
addr2linePath = r'''D:\android-ndk-r13b-windows-x86_64\android-ndk-r13b\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe -f -C -e '''
#libil2cpp.so.debug 路径 (要替换成自己电脑的路径)
il2cppdebugsoPath = r''' "E:\work\obexMainService-0.1-v1.symbols (2)\armeabi-v7a\libil2cpp.so.debug" '''
#unity.so.debug 路径 (要替换成自己电脑的路径)
unitydebugsoPath = r''' "C:\Program Files\Unity201841\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Symbols\armeabi-v7a\libunity.sym.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):
#找以libunity.so结尾的崩溃日志
startIndex = log.index(' pc ')
endflag = log.index(r' /data/')
addstr = log
print(addstr)
cmdstr = addr2linePath +debugsoPath+addstr
os.system(cmdstr)
else:
#查找 il2cpp和libunity 崩溃日志
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
addstr = addstr.replace(' ','')
cmdstr = addr2linePath + debugsoPath +addstr
print(addstr)
os.system(cmdstr)
OutCrash('test.txt')运行结果如下:
我们可以通过堆栈来分析我们的逻辑,避免和绕过崩溃了,当然不得不吐槽一下,unity的bug不是你找到问题就能解决问题的,o(╥﹏╥)o
代码是用python写的 ,改替换的变量已经在注释里了,我python不熟悉,希望大佬勿喷,天知道我为啥用python写,(捂脸。。),应该用c#写的。
更新补充:
鉴于python对大家不太友好,这里补充一个c#解析错误的版本,大家可以直接放入工程使用
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public enum PathType
{
unitySoPath=1,
il2cppSoPath,
addr2Line
}
public class AnalysisCrashWindow :EditorWindow
{
bool groupEnabled;
//path
string addr2linePath = string.Empty; //ndk解析工具路径
string il2cppdebugsoPath = string.Empty; //android 符号表路径
string unitydebugsoPath = string.Empty;//unity符合表路径
string MyCashPath = string.Empty;
//flag
string il2cppflag = "libil2cpp";
string unityflag = "libunity";
string crashEndFlag = "libunity.so";
string unityPath = @"\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Symbols\armeabi-v7a\libunity.sym.so";
bool isAnalysis = false;//文件是否解析
private void OnEnable()
{
GetPathByMemory();
}
static void Init()
{
AnalysisCrashWindow window = (AnalysisCrashWindow)EditorWindow.GetWindow(typeof(AnalysisCrashWindow));
window.Show();
}
void OnGUI()
{
groupEnabled = EditorGUILayout.BeginToggleGroup("基础设置", groupEnabled);
addr2linePath =EditorGUILayout.TextField("NDK工具路径(addr2linePath)", addr2linePath, GUILayout.Width(400));
unitydebugsoPath = EditorGUILayout.TextField("unity符号表(unitydebugsoPath)", unitydebugsoPath, GUILayout.MaxWidth(400));
il2cppdebugsoPath = EditorGUILayout.TextField("ill2cpp符合表(il2cppdebugsoPath)", il2cppdebugsoPath, GUILayout.MaxWidth(400));
MyCashPath = EditorGUILayout.TextField("崩溃日志文件路径", MyCashPath, GUILayout.MaxWidth(400));
EditorGUILayout.EndToggleGroup();
GUILayout.Label("检索内容", EditorStyles.boldLabel);
GetCrashByPath(MyCashPath);
}
/// <summary>
/// 从内存中获取存储的路径
/// </summary>
void GetPathByMemory()
{
addr2linePath = EditorPrefs.GetString(&#34;addr2linePath&#34;);
il2cppdebugsoPath = EditorPrefs.GetString(&#34;il2cppdebugsoPath&#34;);
unitydebugsoPath = EditorPrefs.GetString(&#34;unitydebugsoPath&#34;);
if (string.IsNullOrEmpty(unitydebugsoPath))
{
unitydebugsoPath = string.Concat(System.AppDomain.CurrentDomain.BaseDirectory, unityPath);
JudgePath(PathType.unitySoPath,unitydebugsoPath);
}
MyCashPath = EditorPrefs.GetString(&#34;MyCashPath&#34;, MyCashPath);
}
/// <summary>
/// 路径判断
/// </summary>
/// <param name=&#34;type&#34;>路径类型</param>
/// <param name=&#34;path&#34;></param>
bool JudgePath(PathType type, string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
bool temp = true;
if ((int)type == 1)
{
if (!path.EndsWith(&#34;libunity.sym.so&#34;))
{
path = string.Empty;
Debug.LogError(&#34;自动添加unity符合表路径出错,请手动添加&#34;);
temp = false;
}
else
{
if (!File.Exists(path))
{
temp = false;
Debug.LogErrorFormat(&#34;当前路径{0}unity符号表不存在&#34;, path);
}
}
}
else if ((int)type == 2)
{
if (!path.EndsWith(&#34;libil2cpp.so.debug&#34;))
{
temp = false;
}
else
{
if (!File.Exists(path))
{
temp = false;
}
}
}
else
{
if (!path.EndsWith(&#34;aarch64-linux-android-addr2line.exe&#34;))
{
temp = false;
}
else
{
if (!File.Exists(path))
{
temp = false;
}
}
}
return temp;
}
/// <summary>
/// 创建Button
/// </summary>
/// <param name=&#34;name&#34;></param>
/// <param name=&#34;path&#34;></param>
void CreatorButton(string name,string path)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextField(&#34;名称&#34;, name, GUILayout.MaxWidth(400));
GUILayout.Space(10);
if (GUILayout.Button(&#34;解析&#34;, GUILayout.Width(50)))
{
if (!JudgePath(PathType.addr2Line,addr2linePath))
{
Debug.LogError(&#34;Ndk解析路径出错&#34;);
return;
}
if (!JudgePath(PathType.unitySoPath, unitydebugsoPath) && !JudgePath(PathType.il2cppSoPath, il2cppdebugsoPath))
{
Debug.LogError(&#34;unity与il2cppSoPanth符合表路径出错&#34;);
return;
}
if (!JudgePath(PathType.il2cppSoPath, il2cppdebugsoPath))
{
Debug.LogError(&#34;il2cppSoPanth符合表路径出错&#34;);
}
OutCrash(name,path);
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 根据获取Crash文件的文件创建Button与显示框
/// </summary>
/// <param name=&#34;path&#34;></param>
void GetCrashByPath(string path)
{
if (Directory.Exists(path))
{
var dirctory = new DirectoryInfo(path);
var files = dirctory.GetFiles(&#34;*&#34;, SearchOption.AllDirectories);
foreach (var fi in files)
{
CreatorButton(fi.Name, path);
}
}
}
/// <summary>
/// 打开Crash
/// </summary>
void OutCrash(string filename,string path)
{
isAnalysis = false;
string filePath = string.Join(&#34;/&#34;,path,filename);
using (StreamReader sr =new StreamReader(filePath))
{
while (!sr.EndOfStream)
{
OutCmd(sr.ReadLine());
}
}
if (!isAnalysis)
{
Debug.LogError(&#34;无法解析当前cash文件,请检查文件是否为设备崩溃日志&#34;);
}
}
/// <summary>
/// 解析Crash
/// </summary>
void OutCmd(string log)
{
if (log==null)
{
return;
}
if (log.EndsWith(crashEndFlag))//找以libunity.so结尾的崩溃日志
{
if (log.Contains(&#34;pc&#34;))
{
int startIndex = log.IndexOf(&#34;pc&#34;) + 3;
if (log.Contains(&#34;/data/&#34;))
{
int endIndex = log.IndexOf(&#34;/data/&#34;);
string addStr = log.Substring(startIndex, endIndex - startIndex - 1);
string tempUnitySoPath = string.Format(&#34;\&#34;{0}\&#34;&#34;, unitydebugsoPath);
ExecuteCmd(tempUnitySoPath, addStr);
}
}
}
else//找 il2cpp和libunity 崩溃日志
{
if (log.Contains(il2cppflag) && JudgePath(PathType.il2cppSoPath,il2cppdebugsoPath))
{
string tempill2cppSoPath = string.Format(&#34;\&#34;{0}\&#34;&#34;, il2cppdebugsoPath);
FindMiddleCrash(log, il2cppflag, tempill2cppSoPath);
} else if(log.Contains(unityflag))
{
string tempUnitySoPath = string.Format(&#34;\&#34;{0}\&#34;&#34;, unitydebugsoPath);
FindMiddleCrash(log,unityflag, tempUnitySoPath);
}
}
}
/// <summary>
/// 找 il2cpp和libunity 崩溃日志
/// </summary>
/// <param name=&#34;log&#34;></param>
/// <param name=&#34;debugFlag&#34;>标志元素</param>
/// <param name=&#34;SoPath&#34;>符号表路径</param>
void FindMiddleCrash(string log,string debugFlag,string SoPath)
{
if (!string.IsNullOrEmpty(SoPath))
{
int startIndex = log.IndexOf(debugFlag);
startIndex = startIndex + debugFlag.Length + 1;
if (log.Contains(&#34;(&#34;))
{
int endIndex = log.IndexOf(&#34;(&#34;);
if (endIndex > 0)
{
string addStr = log.Substring(startIndex, endIndex - startIndex);
ExecuteCmd(SoPath, addStr);
}
}
}
else
{
Debug.LogErrorFormat(&#34;{0}的符号表路径为空&#34;,debugFlag);
}
}
/// <summary>
/// 执行CMD命令
/// </summary>
/// <param name=&#34;SoPath&#34;>符号表路径</param>
/// <param name=&#34;addStr&#34;>崩溃代码地址</param>
void ExecuteCmd(string soPath, string addStr)
{
string cmdStr = string.Join(&#34; &#34;, addr2linePath, &#34;-f&#34;, &#34;-C&#34;, &#34;-e&#34;, soPath, addStr);
CmdHandler.RunCmd(cmdStr, (str) =>
{
Debug.Log(string.Format(&#34;解析后{0}&#34;, ResultStr(str, addStr)));
isAnalysis = true;
});
}
/// <summary>
/// 对解析结果进行分析
/// </summary>
/// <param name=&#34;str&#34;></param>
/// <param name=&#34;addStr&#34;></param>
/// <returns></returns>
string ResultStr(string str,string addStr)
{
string tempStr = string.Empty;
if (!string.IsNullOrEmpty(str))
{
if (str.Contains(&#34;exit&#34;))
{
int startIndex = str.IndexOf(&#34;exit&#34;);
if (startIndex < str.Length)
{
tempStr = str.Substring(startIndex);
if (tempStr.Contains(&#34;)&#34;))
{
startIndex = tempStr.IndexOf(&#34;t&#34;) + 1;
int endIndex = tempStr.LastIndexOf(&#34;)&#34;);
tempStr = tempStr.Substring(startIndex, endIndex - startIndex + 1);
tempStr = string.Format(&#34;<color=red>[{0}]</color> :<color=yellow>{1}</color>&#34;, addStr, tempStr);
}
else
{
startIndex = tempStr.IndexOf(&#34;t&#34;) + 1;
tempStr = tempStr.Substring(startIndex);
tempStr = string.Format(&#34;<color=red>[{0}]</color> :<color=yellow>{1}</color>&#34;, addStr, tempStr);
}
}
}
else
{
Debug.LogErrorFormat(&#34;当前结果未执行cmd命令&#34;, str);
}
}
else
{
Debug.LogErrorFormat(&#34;执行cmd:{0}命令,返回值为空&#34;, str);
}
return tempStr;
}
private void OnDestroy()
{
EditorPrefs.SetString(&#34;addr2linePath&#34;, addr2linePath);
EditorPrefs.SetString(&#34;il2cppdebugsoPath&#34;, il2cppdebugsoPath);
EditorPrefs.SetString(&#34;unitydebugsoPath&#34;, unitydebugsoPath);
EditorPrefs.SetString(&#34;MyCashPath&#34;, MyCashPath);
}
}
命令执行类
using System;
using System.Collections.Generic;
using System.Diagnostics;
public class CmdHandler
{
private static string CmdPath = &#34;cmd.exe&#34;;
//C:\Windows\System32\cmd.exe
/// <summary>
/// 执行cmd命令 返回cmd窗口显示的信息
/// 多命令请使用批处理命令连接符:
/// <![CDATA[
/// &:同时执行两个命令
/// |:将上一个命令的输出,作为下一个命令的输入
/// &&:当&&前的命令成功时,才执行&&后的命令
/// ||:当||前的命令失败时,才执行||后的命令]]>
/// </summary>
/// <param name=&#34;cmd&#34;>执行的命令</param>
public static string RunCmd(string cmd,Action <string>act=null)
{
cmd = cmd.Trim().TrimEnd(&#39;&&#39;) + &#34;&exit&#34;;//说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态
using (Process p = new Process())
{
p.StartInfo.FileName = CmdPath;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true;//由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = true; //不显示程序窗口
p.Start();//启动程序
//向cmd窗口写入命令
p.StandardInput.WriteLine(cmd);
p.StandardInput.AutoFlush = true;
//获取cmd窗口的输出信息
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();//等待程序执行完退出进程
p.Close();
if (act!=null)
{
act(output);
}
return output;
}
}
/// <summary>
/// 执行多个cmd命令
/// </summary>
/// <param name=&#34;cmdList&#34;></param>
/// <param name=&#34;act&#34;></param>
public static void RunCmd(List<string> cmd, Action<string> act = null)
{
//cmd = cmd.Trim().TrimEnd(&#39;&&#39;) + &#34;&exit&#34;;//说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态
using (Process p = new Process())
{
p.StartInfo.FileName = CmdPath;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true;//由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = true; //不显示程序窗口
p.Start();//启动程序
//向cmd窗口写入命令
foreach (var cm in cmd)
{
p.StandardInput.WriteLine(cm);
p.StandardInput.WriteLine(&#34;exit&#34;);
p.StandardInput.AutoFlush = true;
//获取cmd窗口的输出信息
string output = p.StandardOutput.ReadToEnd();
if (act != null)
{
act(output);
}
p.Start();
}
p.WaitForExit();//等待程序执行完退出进程
p.Close();
}
}
}
效果如下:
点击解析
好了,各位,如果你觉得这篇文章对你有帮助,请不要吝惜你的鼓励,给我一个赞同吧!
页:
[1]