海田1 发表于 2021-1-28 09:33

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("addr2linePath");
      il2cppdebugsoPath = EditorPrefs.GetString("il2cppdebugsoPath");
      unitydebugsoPath = EditorPrefs.GetString("unitydebugsoPath");
      if (string.IsNullOrEmpty(unitydebugsoPath))
      {
            unitydebugsoPath = string.Concat(System.AppDomain.CurrentDomain.BaseDirectory, unityPath);
            JudgePath(PathType.unitySoPath,unitydebugsoPath);
      }
      MyCashPath = EditorPrefs.GetString("MyCashPath", MyCashPath);
    }

    /// <summary>
    /// 路径判断
    /// </summary>
    /// <param name="type">路径类型</param>
    /// <param name="path"></param>
    bool JudgePath(PathType type, string path)
    {
      if (string.IsNullOrEmpty(path))
      {
            return false;
      }
      bool temp = true;
      if ((int)type == 1)
      {
            if (!path.EndsWith("libunity.sym.so"))
            {
                path = string.Empty;
                Debug.LogError("自动添加unity符合表路径出错,请手动添加");
                temp = false;
            }
            else
            {
                if (!File.Exists(path))
                {
                  temp = false;
                  Debug.LogErrorFormat("当前路径{0}unity符号表不存在", path);
                }
            }
      }
      else if ((int)type == 2)
      {
            if (!path.EndsWith("libil2cpp.so.debug"))
            {
                temp = false;
            }
            else
            {
                if (!File.Exists(path))
                {
                  temp = false;
                }
            }
      }
      else
      {
            if (!path.EndsWith("aarch64-linux-android-addr2line.exe"))
            {
                temp = false;
            }
            else
            {
                if (!File.Exists(path))
                {
                  temp = false;
                }
            }
      }
      return temp;
    }

    /// <summary>
    /// 创建Button
    /// </summary>
    /// <param name="name"></param>
    /// <param name="path"></param>
    void CreatorButton(string name,string path)
    {
      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();
      }
    }
}



效果如下:


点击解析
好了,各位,如果你觉得这篇文章对你有帮助,请不要吝惜你的鼓励,给我一个赞同吧!
页: [1]
查看完整版本: unity crash 的快速定位