在解决iOS应用线上崩溃时,我们通常要分析崩溃日志来定位原因。线上崩溃日志一般是未符号或部分符号化的日志,是一堆十六进制的内存地址集合,可读性比较差,这对解决问题几乎没有帮助。所以,我们首先需要先对崩溃日志进行符号化——根据App出错的函数内存地址,在.dSYM文件中找到具体的文件名、函数名和行号信息。有了上述信息,我们就可以定位分析具体的崩溃原因了。
如果存在大量的崩溃日志,光符号化崩溃日志是远远不够的,我们不可能一条一条去分析大量崩溃日志,这时候就需要对符号化的崩溃日志进行聚类了。通过聚类,将相同或相近的一类崩溃归类在一起,进行统计分析,这在实际问题解决中具有重要的意义。本文将介绍基于NLP的崩溃日志聚类的实现。
本文提纲如下:
1. 崩溃符号化
1.1 崩溃日志结构
线上崩溃日志结构大致如下图所示。我们需要关心的信息主要是标注为红色的部分,具体说明详见表格的描述。
图1.1 崩溃日志片段
日志段 | 字段 | 意义 |
头部信息 | Identifier | 应用的bundle ID |
Version | 应用版本号 | |
Code Type | 设备CPU类型,例如ARM-64 | |
Date/Time | 崩溃时间 | |
OS Version | 崩溃设备及系统版本 | |
异常信息 | Exception Type | 崩溃类型 |
Exception Codes | 崩溃代码 | |
Crashed Thread | 崩溃线程号,根据线程号定位到具体的崩溃栈信息 | |
崩溃栈 | Thread xx: | 线程信息 |
Thread xx Crashed | 实际崩溃线程栈信息,也是符号化的关键信息 | |
崩溃线程状态 | Thread xx crashed with ARM-64 Thread State | 崩溃线程状态信息 |
Binary Images | yourAppProjectName | 你的工程名称 |
<xxxxxxxxxxxxxxxx> | 崩溃日志对应的dsYM文件的uuid,日志和dsYM的uuid要一一对应才可以符号化 |
通过分析崩溃日志的格式,我们很容易获取到应用的bundle ID、版本号、CPU类型、崩溃时间、崩溃设备及系统版本、崩溃类型、崩溃线程号,崩溃线程栈信息、崩溃日志对应dsYM文件的uuid。了解这些基础信息,我们就可以进行符号化了。
1.2 符号化方法
Xcode自带工具可以帮助我们来完成符号化的工作:symbolicatecrash、atos。
1.2.1 symbolicatecrash
symbolicatecrash
是一个将堆栈地址符号化的脚本,执行symbolicatecrash命令,可以得到一个符号化之后的输出文件,查看这个文件可以看到崩溃的位置。
1)准备工作
准备3个文件,放在同一个文件夹下:
I. dSYM文件;
II. 崩溃日志文件(注意文件格式为.crash,如果是其他格式,如txt,先将文件后缀改为.crash);
III.symbolicatecrash(symbolicatecrash的位置可以通过下面的指令来查找)
find /Applications/Xcode.app -name symbolicatecrash -type f
会输出所有的路径,我们只需要随便拷贝一个脚本和dSYM、崩溃日志放在一起即可。
find: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/private/var/mobile: Permission denied
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/iOSSupport/Library/PrivateFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/AppleTVSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/PrivateFrameworks/DVTFoundation.framework/symbolicatecrash
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
2)执行命令
在终端分别输入下面的两条命令,会生成YourApp.log文件,在该文件中可以产看具体的崩溃位置。
export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
./symbolicatecrash YourApp.crash YourApp.app.dSYM > YourApp.log
1.2.2 atos
atos
命令的特点是可以对单行堆栈进行符号化操作,通过输入本地地址(load address)和崩溃内存地址(address to symbolicate)就可以符号化。
1)命令使用
atos -arch <Binary Architecture> -o <Path to dSYM file>/Contents/Resources/DWARF/<binary image name> -l <load address> <address to symbolicate>
Binary Architecture:arm64、armv6、armv7armv7s, 根据应用的情况来写;
Path to dSYM file: dSYM文件的路径;
binary image name: 你工程的名字;
load address:基地址,如果我们的崩溃日志中没有这个信息,load address = address to symbolicate - offset;
address to symbolicate:当前方法的内存地址。
2)使用例子
参考图1.1的崩溃日志片段,我们通过顶部的崩溃信息找到崩溃线程的ID为23,然后查看线程号为23的崩溃栈信息,从圈红色部分找到本地地址和崩溃的内存地址。例如本地地址为:0x102104000,崩溃的内存地址:0x000000010338cee8。
图1.2 崩溃线程信息
执行指令即可符号化出崩溃日志。
atos -o yourProject.app.dSYM/Contents/Resources/DWARF/yourProjectName -arch arm64 -l 0x102104000 0x00000001032c60a4
1.2.3 符号化方法总结
symbolicatecrash:开发者不需要分析本地地址和崩溃内存地址,直接将整个崩溃文件符号化,指令会自动找到崩溃线程栈,并对崩溃关键信息进行符号化,操作简单
。
atos:需要开发者分析本地地址和崩溃内存地址才能进行符号化,优点是可以对
单行堆栈进行符号化。
在实际应用中,我们需要根据项目线上崩溃情况,灵活选择符号化的方法。例如在博主的项目中,下载的崩溃日志是已经格式化的csv崩溃日志,采用atos来符号化,便于批量操作,统计分析。
2.崩溃日志聚类
通过第1节内容,我们已经可以将线上大量的崩溃日志进行符号化了。实际项目中,光符号化崩溃日志远远不够的。面对大量的崩溃日志,我们不可能一条一条地去分析。因此,对符号化的崩溃日志进行分门别类,将相同或相近的一类崩溃归类在一起,统计分析每一类崩溃,在应用问题解决中十分有意义。本节将介绍基于NLP的崩溃日志聚类实现。
2.1 聚类流程
整个聚类过程分为崩溃日志输入、分词、特征提取、输出特征向量、计算相似度、聚类归并、输出统计结果七部分。
图2.1 聚类过程
2.2 聚类实现
1)崩溃日志输入:就是批量输入符号化的崩溃日志,初始化每条崩溃版本号、标题、崩溃log、崩溃次数等。
2)分词:采用jieba分词库,对崩溃log进行分词。
3)特征提取:将分词后的字符串作为每条崩溃 log的特征。
def getWordList(s):
cut = jieba.cut(s)
list_word = (','.join(cut)).split(',')
return list_word
4)生成特征向量:循环计算每两条日志的所有词、取并集、计算词频。
def getWordVector(list_word1, list_word2):
# 列出所有的词,取并集
key_word = list(set(list_word1 + list_word2))
# 给定形状和类型的用0填充的矩阵存储向量
word_vector1 = np.zeros(len(key_word))
word_vector2 = np.zeros(len(key_word))
# 计算词频
# 依次确定向量的每个位置的值
for i in range(len(key_word)):
# 遍历key_word中每个词在句子中的出现次数
for j in range(len(list_word1)):
if key_word[i] == list_word1[j]:
word_vector1[i] += 1
for k in range(len(list_word2)):
if key_word[i] == list_word2[k]:
word_vector2[i] += 1
return word_vector1, word_vector2
5)计算相似度:余弦距离是自然语言处理中用来计算两个句子的相似度的方法之一,在本文中,博主通过计算特征向量的余弦距离来判断两条日志的相似度。余弦距离越大,相似度越大。
def calcuCosDistance(vec1, vec2):
dist = float(np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)))
return dist
6)聚类归并:设定余弦距离阈值,当相似度大于等于余弦阈值时,则认为这两条日志是相似的,把它们归并在一起。余弦阈值选取多少,没有统一的标准,需要在项目中进行测试选择一个合适的值。
7)输出统计结果:遍历聚类归并全部崩溃日志之后,输出统计后的崩溃日志,例如保存到数据库或以csv格式输出。 下面是博主项目中输出的统计结果截图。
图2.2 崩溃日志聚类结果截图
3.参考文章
1.Apple Developer Documentation
2.jieba:https://github.com/fxsjy/jieba