转自:https://zhuanlan.zhihu.com/p/77984555
最近做的项目上线之后崩溃率比较高,测试中没发现问题
后台收到的崩溃日志大概这样,我是懵逼的,网上查询看到了上面知乎上大神的方式,可以通过符号表来编译这些错误信息,就可以定位到Crash的位置
大神列举的两个官方文章
https://support.unity3d.com/hc/en-us/articles/115000292166-Symbolicate-Android-crash
https://support.unity3d.com/hc/en-us/articles/115000292166-Symbolicate-Android-crash
下面是我的尝试
我们再打包的时候需要勾选下图Shmbols选项,这样打包的时候会同时生成符号表压缩包,我理解的打包中会做混淆,符号表相当于我们的密码本,我们用这个才能反射错误信息中的内容
下图为生成的压缩包
我们先找到arm-linux-androideabi-addr2line,找到自己的Ndk位置就好
或者通过下面的链接下载
https://developer.android.com/ndk/downloads?hl=en
我们首先尝试用cmd来尝试一下
命令行,需要根据自己的 路径修改
"C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\NDK\toolchains\aarch64-linux-android-4.9\prebuilt\windows-x86_64\bin\aarch64-linux-android-addr2line.exe" -f -C -e "D:\git\greedysnake\Snake3D\build\snake-0.1-v1.symbols\arm64-v8a\libil2cpp.sym.so" 0x4d3190
反射的结果
RuntimeInvoker_FalseIntPtr_t_RuntimeObject_IntPtr_t_IntPtr_t(void ()(), MethodInfo const, void, void*)
下面贴上大神的代码,做了一点点修改
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextField("名称", name, GUILayout.MaxWidth(400));
GUILayout.Space(10);
if (GUILayout.Button("解析", GUILayout.Width(50)))
{
if (!JudgePath(PathType.addr2Line,addr2linePath))
{
Debug.LogError("Ndk解析路径出错");
return;
}
if (!JudgePath(PathType.unitySoPath, unitydebugsoPath) && !JudgePath(PathType.il2cppSoPath, il2cppdebugsoPath))
{
Debug.LogError("unity与il2cppSoPanth符合表路径出错");
return;
}
if (!JudgePath(PathType.il2cppSoPath, il2cppdebugsoPath))
{
Debug.LogError("il2cppSoPanth符合表路径出错");
}
OutCrash(name,path);
}
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 根据获取Crash文件的文件创建Button与显示框
/// </summary>
/// <param name="path"></param>
void GetCrashByPath(string path)
{
if (Directory.Exists(path))
{
var dirctory = new DirectoryInfo(path);
var files = dirctory.GetFiles("*", 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("/",path,filename);
using (StreamReader sr =new StreamReader(filePath))
{
while (!sr.EndOfStream)
{
OutCmd(sr.ReadLine());
}
}
if (!isAnalysis)
{
Debug.LogError("无法解析当前cash文件,请检查文件是否为设备崩溃日志");
}
}
/// <summary>
/// 解析Crash
/// </summary>
void OutCmd(string log)
{
if (log==null)
{
return;
}
if (log.EndsWith(crashEndFlag))//找以libunity.so结尾的崩溃日志
{
if (log.Contains("pc"))
{
int startIndex = log.IndexOf("pc") + 3;
if (log.Contains("/data/"))
{
int endIndex = log.IndexOf("/data/");
string addStr = log.Substring(startIndex, endIndex - startIndex - 1);
string tempUnitySoPath = string.Format("\"{0}\"", unitydebugsoPath);
ExecuteCmd(tempUnitySoPath, addStr);
}
}
}
else//找 il2cpp和libunity 崩溃日志
{
if (log.Contains(il2cppflag) && JudgePath(PathType.il2cppSoPath,il2cppdebugsoPath))
{
string tempill2cppSoPath = string.Format("\"{0}\"", il2cppdebugsoPath);
FindMiddleCrash(log, il2cppflag, tempill2cppSoPath);
} else if(log.Contains(unityflag))
{
string tempUnitySoPath = string.Format("\"{0}\"", unitydebugsoPath);
FindMiddleCrash(log,unityflag, tempUnitySoPath);
}
}
}
/// <summary>
/// 找 il2cpp和libunity 崩溃日志
/// </summary>
/// <param name="log"></param>
/// <param name="debugFlag">标志元素</param>
/// <param name="SoPath">符号表路径</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("("))
{
int endIndex = log.IndexOf("(");
if (endIndex > 0)
{
string addStr = log.Substring(startIndex, endIndex - startIndex);
ExecuteCmd(SoPath, addStr);
}
}
}
else
{
Debug.LogErrorFormat("{0}的符号表路径为空",debugFlag);
}
}
/// <summary>
/// 执行CMD命令
/// </summary>
/// <param name="SoPath">符号表路径</param>
/// <param name="addStr">崩溃代码地址</param>
void ExecuteCmd(string soPath, string addStr)
{
string cmdStr = string.Join(" ", addr2linePath, "-f", "-C", "-e", soPath, addStr);
CmdHandler.RunCmd(cmdStr, (str) =>
{
Debug.Log(string.Format("解析后{0}", ResultStr(str, addStr)));
isAnalysis = true;
});
}
/// <summary>
/// 对解析结果进行分析
/// </summary>
/// <param name="str"></param>
/// <param name="addStr"></param>
/// <returns></returns>
string ResultStr(string str,string addStr)
{
string tempStr = string.Empty;
if (!string.IsNullOrEmpty(str))
{
if (str.Contains("exit"))
{
int startIndex = str.IndexOf("exit");
if (startIndex < str.Length)
{
tempStr = str.Substring(startIndex);
if (tempStr.Contains(")"))
{
startIndex = tempStr.IndexOf("t") + 1;
int endIndex = tempStr.LastIndexOf(")");
tempStr = tempStr.Substring(startIndex, endIndex - startIndex + 1);
tempStr = string.Format("<color=red>[{0}]</color> :<color=yellow>{1}</color>", addStr, tempStr);
}
else
{
startIndex = tempStr.IndexOf("t") + 1;
tempStr = tempStr.Substring(startIndex);
tempStr = string.Format("<color=red>[{0}]</color> :<color=yellow>{1}</color>", addStr, tempStr);
}
}
}
else
{
Debug.LogErrorFormat("当前结果未执行cmd命令", str);
}
}
else
{
Debug.LogErrorFormat("执行cmd:{0}命令,返回值为空", str);
}
return tempStr;
}
private void OnDestroy()
{
EditorPrefs.SetString("addr2linePath", addr2linePath);
EditorPrefs.SetString("il2cppdebugsoPath", il2cppdebugsoPath);
EditorPrefs.SetString("unitydebugsoPath", unitydebugsoPath);
EditorPrefs.SetString("MyCashPath", MyCashPath);
}
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
public class CmdHandler
{
private static string CmdPath = "cmd.exe";
//C:\Windows\System32\cmd.exe
/// <summary>
/// 执行cmd命令 返回cmd窗口显示的信息
/// 多命令请使用批处理命令连接符:
/// <![CDATA[
/// &:同时执行两个命令
/// |:将上一个命令的输出,作为下一个命令的输入
/// &&:当&&前的命令成功时,才执行&&后的命令
/// ||:当||前的命令失败时,才执行||后的命令]]>
/// </summary>
/// <param name="cmd">执行的命令</param>
public static string RunCmd(string cmd,Action <string>act=null)
{
cmd = cmd.Trim().TrimEnd('&') + "&exit";//说明:不管命令是否成功均执行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="cmdList"></param>
/// <param name="act"></param>
public static void RunCmd(List<string> cmd, Action<string> act = null)
{
//cmd = cmd.Trim().TrimEnd('&') + "&exit";//说明:不管命令是否成功均执行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("exit");
p.StandardInput.AutoFlush = true;
//获取cmd窗口的输出信息
string output = p.StandardOutput.ReadToEnd();
if (act != null)
{
act(output);
}
p.Start();
}
p.WaitForExit();//等待程序执行完退出进程
p.Close();
}
}
}
效果如下
确实定位到了代码中的问题,真的是非常感谢,这里抄一下以防以后忘掉